Disconnect at once after restart, reboot, quit or hibernate

The device goes down without answering, so the editor waited out the 30 s
Sox timeout before dropping the session. The invoke now waits 3 s (time for
DASP to resend it once) and the editor disconnects either way; an error
reply from the device is still reported and keeps the connection. Covers the
App right-click actions and the restart Put with Files offers.

- sox: request/requests/invoke take a timeout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 10:42:36 +03:00
co-authored by Claude Opus 5.5
parent e070bd0f1e
commit f2b18923b7
3 changed files with 26 additions and 17 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
import configparser import configparser
import os import os
VERSION = "0.0.0.167" VERSION = "0.0.0.168"
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties") PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
DEFAULT_GRID_SIZE = 10 DEFAULT_GRID_SIZE = 10
DEFAULT_ADD_TIMESTAMP = True DEFAULT_ADD_TIMESTAMP = True
+16 -7
View File
@@ -617,27 +617,35 @@ def _invoke_app_action(slot_name):
if slot is None: if slot is None:
app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True) app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True)
return False return False
goes_down = slot_name in ("hibernate", "quit", "restart", "reboot") goes_down = slot_name in _GOES_DOWN
try: try:
ds.sox_client.invoke(0, slot["id"]) # A device going down often does so before it answers: give the request a few
app_state.set_status(f"Invoked '{slot_name}' on device") # seconds (time for DASP to resend it once), then disconnect either way rather
# than wait for the session to drop.
ds.sox_client.invoke(0, slot["id"], timeout=_GOES_DOWN_WAIT if goes_down else 30.0)
except sox.SoxException as e: except sox.SoxException as e:
# A P-ION restarts before it answers: no reply is what a restart looks like. refused = "timeout" not in str(e).lower() and "closed" not in str(e).lower()
if not (goes_down and "timeout" in str(e).lower()): if not goes_down or refused:
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True) app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
return False return False
app_state.set_status(f"Invoked '{slot_name}'; the device went down without replying")
except Exception as e: except Exception as e:
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True) app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
return False return False
if slot_name == "save": if slot_name == "save":
app_state.mark_clean() app_state.mark_clean()
app_state.set_status(f"Invoked '{slot_name}' on device")
elif goes_down: elif goes_down:
host = app_state.device_host
do_device_disconnect() do_device_disconnect()
app_state.set_status(f"Sent '{slot_name}' to {host} and disconnected")
else:
app_state.set_status(f"Invoked '{slot_name}' on device")
return True return True
_APP_ACTIONS = ["save", "hibernate", "quit", "restart", "reboot"] _APP_ACTIONS = ["save", "hibernate", "quit", "restart", "reboot"]
_GOES_DOWN = ("hibernate", "quit", "restart", "reboot") # the device stops answering
_GOES_DOWN_WAIT = 3.0 # seconds to wait for its reply before disconnecting
def do_get_with_file(): def do_get_with_file():
@@ -871,7 +879,8 @@ def do_put_with_files():
f"{len(plan.app_files) + len(plan.media)} files written to the device." f"{len(plan.app_files) + len(plan.media)} files written to the device."
f"{NL}{NL}Restart the device now to complete provisioning?"): f"{NL}{NL}Restart the device now to complete provisioning?"):
if _invoke_app_action("restart"): if _invoke_app_action("restart"):
app_state.set_status(f"Put {os.path.basename(path)}; the device is restarting") app_state.set_status(f"Put {os.path.basename(path)}; the device is "
"restarting, disconnected")
_run_with_progress("Put with Files", prepare, planned) _run_with_progress("Put with Files", prepare, planned)
+9 -9
View File
@@ -667,13 +667,13 @@ class SoxClient:
# ── Invoke Action ─────────────────────────────────────────── # ── Invoke Action ───────────────────────────────────────────
def invoke(self, comp_id, slot_id, type_id=None, arg=None): def invoke(self, comp_id, slot_id, type_id=None, arg=None, timeout=30.0):
req = _prepare_request(ord('i')) req = _prepare_request(ord('i'))
req.write_u2(comp_id) req.write_u2(comp_id)
req.write_u1(slot_id) req.write_u1(slot_id)
if arg is not None and type_id is not None: if arg is not None and type_id is not None:
encode_value(req, type_id, arg) encode_value(req, type_id, arg)
res = self.request(req) res = self.request(req, timeout)
res.check_response('I') res.check_response('I')
# ── Add Component ─────────────────────────────────────────── # ── Add Component ───────────────────────────────────────────
@@ -1062,17 +1062,17 @@ class SoxClient:
# ── Networking ────────────────────────────────────────────── # ── Networking ──────────────────────────────────────────────
def request(self, req): def request(self, req, timeout=30.0):
results = self.requests([req]) results = self.requests([req], timeout)
return results[0] return results[0]
def requests(self, reqs): def requests(self, reqs, timeout=30.0):
"""Sends the batch and waits for every reply.""" """Sends the batch and waits for every reply."""
if self.is_closed: if self.is_closed:
raise SoxException("SoxClient closed") raise SoxException("SoxClient closed")
if len(reqs) > 255: if len(reqs) > 255:
return self._chunk_requests(reqs) return self._chunk_requests(reqs, timeout)
with self._lock: with self._lock:
for i, req in enumerate(reqs): for i, req in enumerate(reqs):
@@ -1085,7 +1085,7 @@ class SoxClient:
for req in reqs: for req in reqs:
self._session.send(req.to_bytes()) self._session.send(req.to_bytes())
deadline = time.monotonic() + 30.0 deadline = time.monotonic() + timeout
while True: while True:
remaining = deadline - time.monotonic() remaining = deadline - time.monotonic()
if remaining <= 0: if remaining <= 0:
@@ -1107,11 +1107,11 @@ class SoxClient:
self._responses = None self._responses = None
raise SoxException("Request timeout") raise SoxException("Request timeout")
def _chunk_requests(self, reqs): def _chunk_requests(self, reqs, timeout=30.0):
results = [] results = []
for i in range(0, len(reqs), 255): for i in range(0, len(reqs), 255):
chunk = reqs[i:i + 255] chunk = reqs[i:i + 255]
results.extend(self.requests(chunk)) results.extend(self.requests(chunk, timeout))
return results return results
# called by receiver thread # called by receiver thread