revert: restore _poll_task and dir_size/get_md5 to 0.2.2 state
All changes since 0.2.2 to _poll_task, dir_size, and get_md5 (window_timeout, _poll_oneshot, start_and_poll_immediately) are reverted. The 0.2.2 behaviour worked reliably for small directories and is the last known-good baseline. The remaining known limitation (occasional 599 on large directories) is documented in SPEC.md. Retry the operation as a workaround. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,15 +69,13 @@ def register_filestation(
|
||||
) -> tuple[bool, dict[str, Any] | str]:
|
||||
"""Poll a DSM async task until finished or timeout.
|
||||
|
||||
For tasks that return intermediate ``finished=False`` status while
|
||||
running (CopyMove, Delete, Compress, Extract, Search). Use
|
||||
``_poll_oneshot`` for DirSize and MD5.
|
||||
|
||||
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
|
||||
@@ -88,6 +86,7 @@ def register_filestation(
|
||||
delay = 0.2
|
||||
elapsed = initial_delay
|
||||
timeout = 60.0
|
||||
consecutive_599 = 0
|
||||
|
||||
if initial_delay > 0:
|
||||
await asyncio.sleep(initial_delay)
|
||||
@@ -100,9 +99,14 @@ def register_filestation(
|
||||
version=version,
|
||||
params={"taskid": taskid},
|
||||
)
|
||||
consecutive_599 = 0
|
||||
except _SynologyError as e:
|
||||
if e.code == 599:
|
||||
pass # task not yet visible — keep polling
|
||||
# 599 can be transient (task just started, not yet available).
|
||||
# Retry up to 5 times before giving up.
|
||||
consecutive_599 += 1
|
||||
if consecutive_599 >= 5:
|
||||
return False, f"Error: {e}"
|
||||
else:
|
||||
return False, f"Error: {e}"
|
||||
else:
|
||||
@@ -119,89 +123,6 @@ def register_filestation(
|
||||
elapsed += delay
|
||||
delay = min(delay * 2, 2.0)
|
||||
|
||||
async def _poll_oneshot(
|
||||
api: str,
|
||||
version: int,
|
||||
taskid: str,
|
||||
first_status: dict[str, Any] | None,
|
||||
) -> tuple[bool, dict[str, Any] | str]:
|
||||
"""Continue polling a one-shot DSM task after the first status poll.
|
||||
|
||||
Called after ``client.start_and_poll_immediately`` has already made
|
||||
the first status request. Handles three outcomes for ``first_status``:
|
||||
|
||||
* ``finished=True`` — return immediately (task done on first poll).
|
||||
* ``finished=False`` — task confirmed running; enter Phase 2
|
||||
(exponential backoff until ``finished=True`` or 60 s timeout).
|
||||
* ``None`` (first poll returned 599) — burst-retry 10× at 10 ms,
|
||||
then enter Phase 2 regardless (large directories will eventually
|
||||
return ``finished=False``; a 599 after the task was seen alive
|
||||
means the window closed — fail fast with a retry message).
|
||||
|
||||
Returns:
|
||||
``(True, status_dict)`` on success, or ``(False, "Error: …")``
|
||||
on DSM error or timeout.
|
||||
"""
|
||||
from mcp_synology_filestation.client import SynologyError as _SynologyError
|
||||
|
||||
seen_alive = False
|
||||
|
||||
if first_status is not None:
|
||||
if first_status.get("finished"):
|
||||
return True, first_status
|
||||
seen_alive = True # finished=False: task is running
|
||||
else:
|
||||
# 599 on the immediate poll: burst-retry (10×, 10 ms apart)
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0.01)
|
||||
try:
|
||||
s = await client.request(
|
||||
api, "status", version=version, params={"taskid": taskid}
|
||||
)
|
||||
except _SynologyError as e:
|
||||
if e.code == 599:
|
||||
continue
|
||||
return False, f"Error: {e}"
|
||||
if s.get("finished"):
|
||||
return True, s
|
||||
seen_alive = True
|
||||
break # finished=False: enter Phase 2
|
||||
|
||||
# ── Phase 2: exponential backoff until finished or 60 s timeout ──
|
||||
delay = 0.2
|
||||
elapsed = 0.0
|
||||
timeout = 60.0
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(delay)
|
||||
elapsed += delay
|
||||
delay = min(delay * 2, 2.0)
|
||||
|
||||
try:
|
||||
s = await client.request(api, "status", version=version, params={"taskid": taskid})
|
||||
except _SynologyError as e:
|
||||
if e.code == 599:
|
||||
if seen_alive:
|
||||
# Task was running but the one-shot window closed before we read it
|
||||
return (
|
||||
False,
|
||||
"Error: Could not read task result — the operation finished"
|
||||
" before the result was polled. Please retry.",
|
||||
)
|
||||
# Not yet seen alive: large dir still initialising, keep polling
|
||||
else:
|
||||
return False, f"Error: {e}"
|
||||
else:
|
||||
seen_alive = True
|
||||
if s.get("finished"):
|
||||
return True, s
|
||||
|
||||
if elapsed >= timeout:
|
||||
return (
|
||||
False,
|
||||
"Error: Operation timed out after 60 seconds — check NAS manually.",
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
async def list_shares():
|
||||
"""List all shared folders. Returns name/path/volume-usage table."""
|
||||
@@ -890,16 +811,20 @@ def register_filestation(
|
||||
return "Error: no path provided."
|
||||
|
||||
try:
|
||||
taskid, first_status = await client.start_and_poll_immediately(
|
||||
start_data = await client.request(
|
||||
"SYNO.FileStation.DirSize",
|
||||
start_params={"path": json.dumps(paths)},
|
||||
poll_version=1,
|
||||
start_version=2,
|
||||
"start",
|
||||
version=2,
|
||||
params={"path": json.dumps(paths)},
|
||||
)
|
||||
except SynologyError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
ok, result = await _poll_oneshot("SYNO.FileStation.DirSize", 1, taskid, first_status)
|
||||
taskid: str = start_data.get("taskid", "")
|
||||
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)
|
||||
if not ok:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
@@ -947,16 +872,20 @@ def register_filestation(
|
||||
from mcp_synology_filestation.client import SynologyError
|
||||
|
||||
try:
|
||||
taskid, first_status = await client.start_and_poll_immediately(
|
||||
start_data = await client.request(
|
||||
"SYNO.FileStation.MD5",
|
||||
start_params={"file_path": json.dumps(path)},
|
||||
poll_version=1,
|
||||
start_version=2,
|
||||
"start",
|
||||
version=2,
|
||||
params={"file_path": json.dumps(path)},
|
||||
)
|
||||
except SynologyError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
ok, result = await _poll_oneshot("SYNO.FileStation.MD5", 1, taskid, first_status)
|
||||
taskid: str = start_data.get("taskid", "")
|
||||
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)
|
||||
if not ok:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user