SSE の切断を 5 秒以内に検出してスレッドを片付ける
これまではタブを閉じた接続に気付くのが次の ping (最大20秒後) の書き込み 失敗時だった。受信側の EOF を select で見て、待ち受けの合間に確認する。 ping は経過時間で送るようにしたので、間隔 (20秒) は変わらない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,7 @@ python chktime.py
|
|||||||
動かしながら開いておく用途向けです。オン/オフの状態はブラウザに記憶されます(既定はオン)。
|
動かしながら開いておく用途向けです。オン/オフの状態はブラウザに記憶されます(既定はオン)。
|
||||||
|
|
||||||
- サーバが `--interval` 秒ごとに jsonl を stat して変化を検出し、SSE(`/api/watch`)でブラウザに通知します。ブラウザ側は 1.2 秒まとめてから読み直します。
|
- サーバが `--interval` 秒ごとに jsonl を stat して変化を検出し、SSE(`/api/watch`)でブラウザに通知します。ブラウザ側は 1.2 秒まとめてから読み直します。
|
||||||
|
- タブを閉じた接続は 5 秒以内に検出して片付けます(20 秒ごとの ping を待ちません)。
|
||||||
- 更新時も**選択中のプロジェクト・タスク・スクロール位置・絞り込み・開いているタブは保たれます**。やりとりを末尾まで見ていた場合は、追記された行に追従して末尾に留まります。
|
- 更新時も**選択中のプロジェクト・タスク・スクロール位置・絞り込み・開いているタブは保たれます**。やりとりを末尾まで見ていた場合は、追記された行に追従して末尾に留まります。
|
||||||
- ヘッダ右の表示が状態です。`● 追従中` → 変化を検出すると `● 更新あり…` → 反映後に `● 更新 hh:mm:ss`。切断されると `● 切断(再接続待ち)` となり、自動で再接続します。
|
- ヘッダ右の表示が状態です。`● 追従中` → 変化を検出すると `● 更新あり…` → 反映後に `● 更新 hh:mm:ss`。切断されると `● 切断(再接続待ち)` となり、自動で再接続します。
|
||||||
- オフの間に増えたぶんは、次にオンにした時点でまとめて取り込みます。
|
- オフの間に増えたぶんは、次にオンにした時点でまとめて取り込みます。
|
||||||
|
|||||||
+24
-4
@@ -20,6 +20,8 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -38,6 +40,9 @@ TAG_RE = re.compile(r"<([a-z-]+)>(.*?)</\1>", re.S)
|
|||||||
PROMPT_LIMIT = 4000
|
PROMPT_LIMIT = 4000
|
||||||
SUMMARY_LIMIT = 200
|
SUMMARY_LIMIT = 200
|
||||||
|
|
||||||
|
WATCH_POLL = 5.0 # SSE: 切断を見に行く間隔(秒)
|
||||||
|
PING_INTERVAL = 20.0 # SSE: 接続維持のコメントを送る間隔(秒)
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 解析ロジック
|
# ---------------------------------------------------------------- 解析ロジック
|
||||||
|
|
||||||
|
|
||||||
@@ -912,7 +917,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
raise FileNotFoundError(route)
|
raise FileNotFoundError(route)
|
||||||
|
|
||||||
def watch(self):
|
def watch(self):
|
||||||
"""SSE。ログに変更があるまでブロックし、あれば change イベントを流す。"""
|
"""SSE。ログに変更があるまでブロックし、あれば change イベントを流す。
|
||||||
|
|
||||||
|
タブを閉じた接続はソケットの EOF で気付いて抜ける。書き込みが失敗する
|
||||||
|
まで (ping 最大20秒ぶん) スレッドを抱えたままにしないため。
|
||||||
|
"""
|
||||||
watcher = self.server.watcher
|
watcher = self.server.watcher
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
@@ -920,18 +929,29 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("X-Accel-Buffering", "no")
|
self.send_header("X-Accel-Buffering", "no")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
rev = watcher.rev
|
rev = watcher.rev
|
||||||
|
last_ping = time.monotonic()
|
||||||
try:
|
try:
|
||||||
self.stream(f"retry: 3000\n\nevent: hello\ndata: {json.dumps(watcher.info())}\n\n")
|
self.stream(f"retry: 3000\n\nevent: hello\ndata: {json.dumps(watcher.info())}\n\n")
|
||||||
while True:
|
while not self.disconnected():
|
||||||
current = watcher.wait_change(rev, 20)
|
current = watcher.wait_change(rev, WATCH_POLL)
|
||||||
if current != rev:
|
if current != rev:
|
||||||
rev = current
|
rev = current
|
||||||
self.stream(f"event: change\ndata: {json.dumps(watcher.info())}\n\n")
|
self.stream(f"event: change\ndata: {json.dumps(watcher.info())}\n\n")
|
||||||
else:
|
last_ping = time.monotonic()
|
||||||
|
elif time.monotonic() - last_ping >= PING_INTERVAL:
|
||||||
self.stream(": ping\n\n") # 接続維持
|
self.stream(": ping\n\n") # 接続維持
|
||||||
|
last_ping = time.monotonic()
|
||||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError, ValueError):
|
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError, ValueError):
|
||||||
return # タブを閉じた等
|
return # タブを閉じた等
|
||||||
|
|
||||||
|
def disconnected(self):
|
||||||
|
"""クライアントが切断済みかを見る (受信側の EOF で判定)。"""
|
||||||
|
try:
|
||||||
|
ready, _, _ = select.select([self.connection], [], [], 0)
|
||||||
|
return bool(ready) and not self.connection.recv(1, socket.MSG_PEEK)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return True
|
||||||
|
|
||||||
def stream(self, text):
|
def stream(self, text):
|
||||||
self.wfile.write(text.encode("utf-8"))
|
self.wfile.write(text.encode("utf-8"))
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
|
|||||||
Reference in New Issue
Block a user