Add App > Get with File: full app backup zip from the device
Reads app.sab and kits.scode over Sox file transfer, decodes app.sab to app.sax (byte-identical to sedonac), and on Orion/Rion also fetches the images and fonts the app names, with their sources from Sedona home's banks. Offers to save the app on the device first when it is dirty, and asks where to save with <Sedona home>/store/apps and app-<device>-<app>-<timestamp>.zip filled in. DASP trace lines are now printed by their own thread: printing from the network threads to an unread stdout pipe blocked the receiver and made file transfers time out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,11 @@ class SoxClient:
|
||||
# event callback: called on main thread via queue
|
||||
self.on_event = None # fn(comp_id, what_char)
|
||||
|
||||
# file transfer: one at a time; its chunks arrive unasked, so the receiver
|
||||
# hands them to whichever transfer is open
|
||||
self._xfer_lock = threading.Lock()
|
||||
self._xfer = None # _FileGet while a get is open
|
||||
|
||||
# ── Connect / Disconnect ────────────────────────────────────
|
||||
|
||||
def connect(self):
|
||||
@@ -902,6 +907,83 @@ class SoxClient:
|
||||
def _get_slots(self, kit_id, type_id_val):
|
||||
return self.slot_registry.get((kit_id, type_id_val))
|
||||
|
||||
# ── File transfer ───────────────────────────────────────────
|
||||
|
||||
def get_file(self, uri, progress=None):
|
||||
"""Reads a file off the device ('app.sab', 'kits.scode', 'images/x.bin').
|
||||
|
||||
Ported from FileTransfer.getFile: an 'f' request opens the transfer, the
|
||||
device then pushes the file as unsolicited 'k' chunks, and a 'z' closes it.
|
||||
A file the device does not have comes back as a SoxException from the open.
|
||||
progress(done_bytes, total_bytes) is called from this thread as chunks land.
|
||||
"""
|
||||
if self.is_closed:
|
||||
raise SoxException("SoxClient closed")
|
||||
with self._xfer_lock:
|
||||
chunk_size = max(16, self._session.ideal_max - 18)
|
||||
req = _prepare_request(ord('f'))
|
||||
req.write_str("g")
|
||||
req.write_str(uri)
|
||||
req.write_i4(0) # fileSize: unknown on a get
|
||||
req.write_u2(chunk_size)
|
||||
req.write_u1(0) # end of headers
|
||||
|
||||
# Set before the open goes out: the first chunks can beat its reply.
|
||||
xfer = _FileGet()
|
||||
self._xfer = xfer
|
||||
opened = closed = False
|
||||
try:
|
||||
try:
|
||||
res = self.request(req)
|
||||
except SoxException as e:
|
||||
# A P-ION on firmware 1.32 never answers a file get -- it drops off
|
||||
# the network for ~20 s, as a restart would -- even for a file it
|
||||
# does not have.
|
||||
raise SoxException(
|
||||
f"The device did not answer the request for {uri} ({e}). Its "
|
||||
f"firmware may not support reading files over Sox; some devices "
|
||||
f"stop responding, or restart, when asked.") from e
|
||||
res.check_response('F')
|
||||
opened = True
|
||||
file_size = res.i4()
|
||||
chunk_size = res.u2()
|
||||
while res.remaining > 0: # response headers -- nothing we use
|
||||
if not res.str():
|
||||
break
|
||||
res.str()
|
||||
num_chunks = -(-file_size // chunk_size) if chunk_size else 0
|
||||
|
||||
timeout = getattr(self._session, "receive_timeout", 30.0) or 30.0
|
||||
while not xfer.done(num_chunks):
|
||||
if self.is_closed:
|
||||
raise SoxException(f"Session closed while reading {uri}")
|
||||
if time.monotonic() - xfer.last_receive > timeout:
|
||||
raise SoxException(f"Timed out reading {uri}")
|
||||
xfer.wait(0.5)
|
||||
if progress:
|
||||
progress(min(file_size, xfer.count() * chunk_size), file_size)
|
||||
|
||||
close = self.request(_prepare_request(ord('z')))
|
||||
closed = True
|
||||
close.check_response('Z')
|
||||
finally:
|
||||
self._xfer = None
|
||||
# Left open, the transfer keeps the device answering "busy" to the next.
|
||||
if opened and not closed and not self.is_closed:
|
||||
try:
|
||||
self.request(_prepare_request(ord('z')))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
data = bytearray(file_size)
|
||||
for num, chunk in xfer.chunks.items():
|
||||
if num < num_chunks:
|
||||
start = num * chunk_size
|
||||
data[start:start + len(chunk)] = chunk
|
||||
if progress:
|
||||
progress(file_size, file_size)
|
||||
return bytes(data)
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────
|
||||
|
||||
def request(self, req):
|
||||
@@ -986,6 +1068,39 @@ class SoxClient:
|
||||
pass
|
||||
|
||||
|
||||
class _FileGet:
|
||||
"""The chunks of the get in progress, filled by the receiver thread."""
|
||||
|
||||
def __init__(self):
|
||||
self.chunks = {}
|
||||
self.last_receive = time.monotonic()
|
||||
self._event = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def receive_chunk(self, msg):
|
||||
msg.u1() # 'k'
|
||||
msg.u1() # replyNum
|
||||
num = msg.u2()
|
||||
size = msg.u2()
|
||||
data = msg.read_bytes(size)
|
||||
with self._lock:
|
||||
self.chunks[num] = data
|
||||
self.last_receive = time.monotonic()
|
||||
self._event.set()
|
||||
|
||||
def count(self):
|
||||
with self._lock:
|
||||
return len(self.chunks)
|
||||
|
||||
def done(self, num_chunks):
|
||||
with self._lock:
|
||||
return all(n in self.chunks for n in range(num_chunks))
|
||||
|
||||
def wait(self, seconds):
|
||||
self._event.wait(seconds)
|
||||
self._event.clear()
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# SoxReceiver — daemon thread
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
@@ -1013,8 +1128,12 @@ class _SoxReceiver(threading.Thread):
|
||||
cmd = payload[0]
|
||||
if cmd == ord('e'):
|
||||
client._receive_event(msg)
|
||||
elif cmd == ord('k') or cmd == ord('z'):
|
||||
pass # file transfer — not needed for editor
|
||||
elif cmd == ord('k'):
|
||||
xfer = client._xfer
|
||||
if xfer is not None:
|
||||
xfer.receive_chunk(msg)
|
||||
elif cmd == ord('z'):
|
||||
pass # close of a put -- the editor only gets files
|
||||
else:
|
||||
client._receive_response(msg)
|
||||
except DaspException:
|
||||
|
||||
Reference in New Issue
Block a user