From f2b18923b7b6ac04bd1dfd1e64f91e02f0d97140 Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Sun, 11 Oct 2026 10:42:36 +0300 Subject: [PATCH] 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 --- app_state.py | 2 +- main.py | 23 ++++++++++++++++------- sox.py | 18 +++++++++--------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app_state.py b/app_state.py index ff6b27a..2335a34 100644 --- a/app_state.py +++ b/app_state.py @@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it. import configparser 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") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/main.py b/main.py index 2ae40e3..1d4ddec 100644 --- a/main.py +++ b/main.py @@ -617,27 +617,35 @@ def _invoke_app_action(slot_name): if slot is None: app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True) return False - goes_down = slot_name in ("hibernate", "quit", "restart", "reboot") + goes_down = slot_name in _GOES_DOWN try: - ds.sox_client.invoke(0, slot["id"]) - app_state.set_status(f"Invoked '{slot_name}' on device") + # A device going down often does so before it answers: give the request a few + # 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: - # A P-ION restarts before it answers: no reply is what a restart looks like. - if not (goes_down and "timeout" in str(e).lower()): + refused = "timeout" not in str(e).lower() and "closed" not in str(e).lower() + if not goes_down or refused: app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True) return False - app_state.set_status(f"Invoked '{slot_name}'; the device went down without replying") except Exception as e: app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True) return False if slot_name == "save": app_state.mark_clean() + app_state.set_status(f"Invoked '{slot_name}' on device") elif goes_down: + host = app_state.device_host 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 _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(): @@ -871,7 +879,8 @@ def do_put_with_files(): f"{len(plan.app_files) + len(plan.media)} files written to the device." f"{NL}{NL}Restart the device now to complete provisioning?"): 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) diff --git a/sox.py b/sox.py index 0faf3cc..8b39574 100644 --- a/sox.py +++ b/sox.py @@ -667,13 +667,13 @@ class SoxClient: # ── 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.write_u2(comp_id) req.write_u1(slot_id) if arg is not None and type_id is not None: encode_value(req, type_id, arg) - res = self.request(req) + res = self.request(req, timeout) res.check_response('I') # ── Add Component ─────────────────────────────────────────── @@ -1062,17 +1062,17 @@ class SoxClient: # ── Networking ────────────────────────────────────────────── - def request(self, req): - results = self.requests([req]) + def request(self, req, timeout=30.0): + results = self.requests([req], timeout) return results[0] - def requests(self, reqs): + def requests(self, reqs, timeout=30.0): """Sends the batch and waits for every reply.""" if self.is_closed: raise SoxException("SoxClient closed") if len(reqs) > 255: - return self._chunk_requests(reqs) + return self._chunk_requests(reqs, timeout) with self._lock: for i, req in enumerate(reqs): @@ -1085,7 +1085,7 @@ class SoxClient: for req in reqs: self._session.send(req.to_bytes()) - deadline = time.monotonic() + 30.0 + deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() if remaining <= 0: @@ -1107,11 +1107,11 @@ class SoxClient: self._responses = None raise SoxException("Request timeout") - def _chunk_requests(self, reqs): + def _chunk_requests(self, reqs, timeout=30.0): results = [] for i in range(0, len(reqs), 255): chunk = reqs[i:i + 255] - results.extend(self.requests(chunk)) + results.extend(self.requests(chunk, timeout)) return results # called by receiver thread