fix: window_timeout for one-shot tasks, drop debug stderr logging

_poll_task now accepts window_timeout. For DirSize and MD5 (one-shot result
windows), if only 599 errors arrive for window_timeout seconds without ever
seeing the task alive (finished=False), return a fast "result window missed —
please retry" error instead of waiting the full 60 s. Tasks that return
finished=False at least once (large dirs, large files) are unaffected.

Also removes the stale [dsm] debug stderr.write left in client.request().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 13:30:20 +02:00
parent 4bf655236d
commit 6510493930
5 changed files with 39 additions and 13 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""MCP server for Synology FileStation."""
__version__ = "0.2.4"
__version__ = "0.2.5"
-2
View File
@@ -247,8 +247,6 @@ class FileStationClient:
Raises:
SynologyError: On API errors.
"""
sys.stderr.write(f"[dsm] request: {api}/{method}\n")
sys.stderr.flush()
if not self._initializing:
await self._ensure_initialized()
http = self._get_http()
@@ -66,6 +66,7 @@ def register_filestation(
version: int,
taskid: str,
initial_delay: float = 0.2,
window_timeout: float | None = None,
) -> tuple[bool, dict[str, Any] | str]:
"""Poll a DSM async task until finished or timeout.
@@ -76,6 +77,11 @@ def register_filestation(
initial_delay: Seconds to wait before the first status poll.
Set to 0.0 for tasks that may finish before the first poll
interval (e.g. DirSize on small directories, MD5 on small files).
window_timeout: For one-shot tasks (DirSize, MD5) whose result is
available exactly once: if we receive nothing but 599 errors for
this many seconds without ever seeing the task running
(``finished=False``), the result window was missed — return an
error immediately instead of waiting for the full 60 s timeout.
Returns:
``(True, status_dict)`` on success, or ``(False, "Error: …")`` on
@@ -86,6 +92,7 @@ def register_filestation(
delay = 0.2
elapsed = initial_delay
timeout = 60.0
seen_task_alive = False # True once we receive any non-599 status response
if initial_delay > 0:
await asyncio.sleep(initial_delay)
@@ -100,13 +107,25 @@ def register_filestation(
)
except _SynologyError as e:
if e.code == 599:
# DSM returns 599 while the async task is still initialising
# or running (task-not-yet-available). Treat it the same as
# finished=False and keep polling until the 60 s timeout.
pass
# DSM 599 = task not found. For one-shot tasks (DirSize, MD5)
# this means either the task hasn't started yet or the result
# window has already closed. If we've never seen the task
# running and window_timeout has elapsed, the window is gone —
# fail fast so the caller can retry rather than wait 60 s.
if (
window_timeout is not None
and not seen_task_alive
and elapsed >= window_timeout
):
return (
False,
"Error: Could not read task result — the operation finished"
" before the first successful poll. Please retry.",
)
else:
return False, f"Error: {e}"
else:
seen_task_alive = True
if status_data.get("finished"):
return True, status_data
@@ -821,7 +840,9 @@ def register_filestation(
if not taskid:
return "Error: DSM did not return a task ID."
ok, result = await _poll_task("SYNO.FileStation.DirSize", 1, taskid, initial_delay=0.0)
ok, result = await _poll_task(
"SYNO.FileStation.DirSize", 1, taskid, initial_delay=0.0, window_timeout=3.0
)
if not ok:
return result # type: ignore[return-value]
@@ -882,7 +903,9 @@ def register_filestation(
if not taskid:
return "Error: DSM did not return a task ID."
ok, result = await _poll_task("SYNO.FileStation.MD5", 1, taskid, initial_delay=0.0)
ok, result = await _poll_task(
"SYNO.FileStation.MD5", 1, taskid, initial_delay=0.0, window_timeout=3.0
)
if not ok:
return result # type: ignore[return-value]