From 0c312afaa53624fee9515f4c606d665ce5c69eec Mon Sep 17 00:00:00 2001 From: yoshikazu Date: Tue, 1 Sep 2026 09:48:37 +0900 Subject: [PATCH] =?UTF-8?q?SSE=20=E3=81=AE=E5=88=87=E6=96=AD=E3=82=92=205?= =?UTF-8?q?=20=E7=A7=92=E4=BB=A5=E5=86=85=E3=81=AB=E6=A4=9C=E5=87=BA?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=82=B9=E3=83=AC=E3=83=83=E3=83=89=E3=82=92?= =?UTF-8?q?=E7=89=87=E4=BB=98=E3=81=91=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit これまではタブを閉じた接続に気付くのが次の ping (最大20秒後) の書き込み 失敗時だった。受信側の EOF を select で見て、待ち受けの合間に確認する。 ping は経過時間で送るようにしたので、間隔 (20秒) は変わらない。 Co-Authored-By: Claude Opus 5 --- README.md | 1 + chktime.py | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4e54188..2550b5b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ python chktime.py 動かしながら開いておく用途向けです。オン/オフの状態はブラウザに記憶されます(既定はオン)。 - サーバが `--interval` 秒ごとに jsonl を stat して変化を検出し、SSE(`/api/watch`)でブラウザに通知します。ブラウザ側は 1.2 秒まとめてから読み直します。 +- タブを閉じた接続は 5 秒以内に検出して片付けます(20 秒ごとの ping を待ちません)。 - 更新時も**選択中のプロジェクト・タスク・スクロール位置・絞り込み・開いているタブは保たれます**。やりとりを末尾まで見ていた場合は、追記された行に追従して末尾に留まります。 - ヘッダ右の表示が状態です。`● 追従中` → 変化を検出すると `● 更新あり…` → 反映後に `● 更新 hh:mm:ss`。切断されると `● 切断(再接続待ち)` となり、自動で再接続します。 - オフの間に増えたぶんは、次にオンにした時点でまとめて取り込みます。 diff --git a/chktime.py b/chktime.py index b17a52b..8b52599 100644 --- a/chktime.py +++ b/chktime.py @@ -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-]+)>(.*?)", 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()