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 秒まとめてから読み直します。
|
||||
- タブを閉じた接続は 5 秒以内に検出して片付けます(20 秒ごとの ping を待ちません)。
|
||||
- 更新時も**選択中のプロジェクト・タスク・スクロール位置・絞り込み・開いているタブは保たれます**。やりとりを末尾まで見ていた場合は、追記された行に追従して末尾に留まります。
|
||||
- ヘッダ右の表示が状態です。`● 追従中` → 変化を検出すると `● 更新あり…` → 反映後に `● 更新 hh:mm:ss`。切断されると `● 切断(再接続待ち)` となり、自動で再接続します。
|
||||
- オフの間に増えたぶんは、次にオンにした時点でまとめて取り込みます。
|
||||
|
||||
+24
-4
@@ -20,6 +20,8 @@ import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
@@ -38,6 +40,9 @@ TAG_RE = re.compile(r"<([a-z-]+)>(.*?)</\1>", re.S)
|
||||
PROMPT_LIMIT = 4000
|
||||
SUMMARY_LIMIT = 200
|
||||
|
||||
WATCH_POLL = 5.0 # SSE: 切断を見に行く間隔(秒)
|
||||
PING_INTERVAL = 20.0 # SSE: 接続維持のコメントを送る間隔(秒)
|
||||
|
||||
# ---------------------------------------------------------------- 解析ロジック
|
||||
|
||||
|
||||
@@ -912,7 +917,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
raise FileNotFoundError(route)
|
||||
|
||||
def watch(self):
|
||||
"""SSE。ログに変更があるまでブロックし、あれば change イベントを流す。"""
|
||||
"""SSE。ログに変更があるまでブロックし、あれば change イベントを流す。
|
||||
|
||||
タブを閉じた接続はソケットの EOF で気付いて抜ける。書き込みが失敗する
|
||||
まで (ping 最大20秒ぶん) スレッドを抱えたままにしないため。
|
||||
"""
|
||||
watcher = self.server.watcher
|
||||
self.send_response(200)
|
||||
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.end_headers()
|
||||
rev = watcher.rev
|
||||
last_ping = time.monotonic()
|
||||
try:
|
||||
self.stream(f"retry: 3000\n\nevent: hello\ndata: {json.dumps(watcher.info())}\n\n")
|
||||
while True:
|
||||
current = watcher.wait_change(rev, 20)
|
||||
while not self.disconnected():
|
||||
current = watcher.wait_change(rev, WATCH_POLL)
|
||||
if current != rev:
|
||||
rev = current
|
||||
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") # 接続維持
|
||||
last_ping = time.monotonic()
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError, ValueError):
|
||||
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):
|
||||
self.wfile.write(text.encode("utf-8"))
|
||||
self.wfile.flush()
|
||||
|
||||
Reference in New Issue
Block a user