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 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
+16 -7
View File
@@ -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)
+9 -9
View File
@@ -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