fix: correct DirSize/MD5 polling — initial_delay=0 and MD5 status v1

Live NAS investigation (test_dirsize_md5.py) revealed two bugs:

1. _poll_task() always slept 200ms before the first status call.
   DirSize and MD5 complete near-instantly on small data, so the result
   window closes before the first poll. Fix: add initial_delay parameter
   (default 0.2s for CopyMove/Delete/Compress/Extract); DirSize and MD5
   pass initial_delay=0.0 to poll immediately after start.

2. get_md5 used status version=2, but the NAS only serves the result on
   status v1 (v2 always returns 599 regardless of timing). Fix: change
   _poll_task version to 1 for SYNO.FileStation.MD5.

MD5 is one-shot: the result is consumed on the first successful status
read. Polling at 0ms ensures we catch it before it expires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:35:43 +02:00
parent 04caaef003
commit 4145d929a6
2 changed files with 194 additions and 81 deletions
@@ -61,13 +61,21 @@ def register_filestation(
# ── internal polling helper ───────────────────────────────────────────
async def _poll_task(api: str, version: int, taskid: str) -> tuple[bool, dict[str, Any] | str]:
"""Poll a DSM async task (CopyMove / Delete) until finished or timeout.
async def _poll_task(
api: str,
version: int,
taskid: str,
initial_delay: float = 0.2,
) -> tuple[bool, dict[str, Any] | str]:
"""Poll a DSM async task until finished or timeout.
Args:
api: DSM API name (e.g. "SYNO.FileStation.CopyMove").
version: API version to use for the status call.
taskid: Task ID returned by the corresponding start method.
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).
Returns:
``(True, status_dict)`` on success, or ``(False, "Error: …")`` on
@@ -76,13 +84,13 @@ def register_filestation(
from mcp_synology_filestation.client import SynologyError as _SynologyError
delay = 0.2
elapsed = 0.0
elapsed = initial_delay
timeout = 60.0
while True:
await asyncio.sleep(delay)
elapsed += delay
if initial_delay > 0:
await asyncio.sleep(initial_delay)
while True:
try:
status_data = await client.request(
api,
@@ -102,6 +110,8 @@ def register_filestation(
"Error: Operation timed out after 60 seconds — check NAS manually.",
)
await asyncio.sleep(delay)
elapsed += delay
delay = min(delay * 2, 2.0)
@mcp.tool()
@@ -805,7 +815,7 @@ 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)
ok, result = await _poll_task("SYNO.FileStation.DirSize", 1, taskid, initial_delay=0.0)
if not ok:
return result # type: ignore[return-value]
@@ -866,7 +876,7 @@ def register_filestation(
if not taskid:
return "Error: DSM did not return a task ID."
ok, result = await _poll_task("SYNO.FileStation.MD5", 2, taskid)
ok, result = await _poll_task("SYNO.FileStation.MD5", 1, taskid, initial_delay=0.0)
if not ok:
return result # type: ignore[return-value]