App > Put with Files: restore a backup zip onto the connected device
Follows Ontrol's Workbench tool (BPutWithFilesTool / PutApp): the backup's app.sax is encoded for the device's kits and put as app.sab.writing, renamed to app.sab.stage; the platform service's deleteFile clears fonts/* and images/*, then images, fonts (from Sedona home's bank, in the device's cut) and the boot logo go. The zip is checked against the device first: same platform (Orion, Rion or P-ION, from the platform id), and kits the device can load. Falls back to the backup's app.sab, with its kits.scode if needed. - sab_app.encode_sax: .sax -> .sab, ported from Py_sab2sax with Java's value parsing (fromLiteral, nullbool/nullfloat, Integer.decode); byte-identical to sedonac on the saved backups - app_backup.native_image: port of OntOxImageUtil's png -> native .bin - sox.put_file / rename_file - Get with Files: Orion's boot logo is system/logo.jpg - A restart that goes unanswered is taken as the device going down Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import palette
|
||||
import properties
|
||||
import sax_file
|
||||
import schema
|
||||
import sox
|
||||
import undo
|
||||
import ox_page_view
|
||||
import user_service_view
|
||||
@@ -615,15 +616,22 @@ 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")
|
||||
try:
|
||||
ds.sox_client.invoke(0, slot["id"])
|
||||
app_state.set_status(f"Invoked '{slot_name}' on device")
|
||||
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()):
|
||||
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()
|
||||
elif slot_name in ("hibernate", "quit", "restart", "reboot"):
|
||||
elif goes_down:
|
||||
do_device_disconnect()
|
||||
return True
|
||||
|
||||
@@ -656,9 +664,28 @@ def do_get_with_file():
|
||||
if not path:
|
||||
return
|
||||
|
||||
def finish(result, error):
|
||||
if error is not None:
|
||||
app_state.set_status(f"Get with Files failed: {error}", warn=True)
|
||||
messagebox.showerror("Get with Files", f"The backup failed:{NL}{NL}{error}")
|
||||
return
|
||||
app_state.mark_clean() # the app was saved on the device first
|
||||
_report_backup(result)
|
||||
|
||||
_run_with_progress("Get with Files",
|
||||
lambda status: app_backup.get_with_file(ds.sox_client, path, status),
|
||||
finish)
|
||||
|
||||
|
||||
def _run_with_progress(title, work, finish):
|
||||
"""Runs work(status) off the main thread behind a small progress window.
|
||||
|
||||
status(text, fraction) updates the window; finish(result, error) then runs on the
|
||||
main thread, error being the exception work raised, or None.
|
||||
"""
|
||||
window = app_state.window
|
||||
dlg = tk.Toplevel(window)
|
||||
dlg.title("Get with Files")
|
||||
dlg.title(title)
|
||||
dlg.resizable(False, False)
|
||||
dlg.transient(window)
|
||||
dlg.protocol("WM_DELETE_WINDOW", lambda: None) # a transfer cannot be stopped midway
|
||||
@@ -681,42 +708,42 @@ def do_get_with_file():
|
||||
bar.config(value=fraction)
|
||||
window.after(0, show)
|
||||
|
||||
def finish(result, error):
|
||||
def done(result, error):
|
||||
dlg.grab_release()
|
||||
dlg.destroy()
|
||||
if error is not None:
|
||||
app_state.set_status(f"Get with Files failed: {error}", warn=True)
|
||||
messagebox.showerror("Get with Files", f"The backup failed:{NL}{NL}{error}")
|
||||
return
|
||||
app_state.mark_clean() # the app was saved on the device first
|
||||
_report_backup(result)
|
||||
finish(result, error)
|
||||
|
||||
def work():
|
||||
def run():
|
||||
try:
|
||||
result = app_backup.get_with_file(ds.sox_client, path, status)
|
||||
result = work(status)
|
||||
except Exception as e:
|
||||
window.after(0, lambda e=e: finish(None, e))
|
||||
window.after(0, lambda e=e: done(None, e))
|
||||
else:
|
||||
window.after(0, lambda: finish(result, None))
|
||||
window.after(0, lambda: done(result, None))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
|
||||
def _ask_backup_path(ds):
|
||||
"""The save dialog for the zip: <Sedona home>/store/apps, the name filled in.
|
||||
def _device_app_names(ds):
|
||||
"""(deviceName, appName) of the device's app, or None for each it does not give.
|
||||
|
||||
The name takes the app's deviceName and appName, read off the device now -- the
|
||||
app component's config is not loaded until something asks for it.
|
||||
Read off the device now -- the app component's config is not loaded until
|
||||
something asks for it.
|
||||
"""
|
||||
device_name, app_name = app_state.device_host, "app"
|
||||
try:
|
||||
comp = ds.sox_client.cache_get(0)
|
||||
if comp is not None:
|
||||
ds.sox_client.update([comp], device_session.CONFIG)
|
||||
device_name = comp.values.get("deviceName") or device_name
|
||||
app_name = comp.values.get("appName") or app_name
|
||||
return comp.values.get("deviceName") or None, comp.values.get("appName") or None
|
||||
except Exception as e:
|
||||
app_state.set_status(f"Could not read the app's names: {e}", warn=True)
|
||||
return None, None
|
||||
|
||||
|
||||
def _ask_backup_path(ds):
|
||||
"""The save dialog for the zip: <Sedona home>/store/apps, named after the app."""
|
||||
device_name, app_name = _device_app_names(ds)
|
||||
device_name, app_name = device_name or app_state.device_host, app_name or "app"
|
||||
|
||||
folder = app_backup.backup_folder()
|
||||
try:
|
||||
@@ -751,6 +778,109 @@ def _report_backup(result):
|
||||
(messagebox.showwarning if warn else messagebox.showinfo)("Get with Files", NL.join(lines))
|
||||
|
||||
|
||||
def do_put_with_files():
|
||||
"""App > Put with Files: puts a Get with Files zip back onto the connected device.
|
||||
|
||||
The zip is checked against the device first: the same platform, and an app the
|
||||
device's kits can run -- the backup's app.sax encoded for them, as Ontrol's tool
|
||||
does, or its app.sab. The app goes staged, so the device takes it up when
|
||||
restarted, which is offered once the files are in.
|
||||
"""
|
||||
ds = app_state.device_session
|
||||
if not app_state.live_mode or ds is None or not ds.is_connected:
|
||||
messagebox.showinfo("Put with Files", "Connect to a device first.")
|
||||
return
|
||||
if not app_state.SEDONA_HOME:
|
||||
messagebox.showerror("Put with Files", "The Sedona home folder is not set (Preferences).")
|
||||
return
|
||||
|
||||
folder = app_backup.backup_folder()
|
||||
path = filedialog.askopenfilename(
|
||||
title="Put with Files",
|
||||
initialdir=folder if os.path.isdir(folder) else app_state.SEDONA_HOME,
|
||||
filetypes=[("Sedona backup files", "*.zip"), ("All files", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
|
||||
try:
|
||||
target = app_backup.device_target(ds)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Put with Files", f"Could not read the device:{NL}{NL}{e}")
|
||||
return
|
||||
plan = app_backup.plan_put(path, target)
|
||||
if not plan.ok:
|
||||
app_state.set_status(f"{os.path.basename(path)} does not match the device", warn=True)
|
||||
messagebox.showerror(
|
||||
"Put with Files",
|
||||
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}" +
|
||||
NL.join(plan.problems))
|
||||
return
|
||||
if not messagebox.askokcancel("Put with Files", _describe_put(plan),
|
||||
icon=messagebox.WARNING, default=messagebox.CANCEL):
|
||||
return
|
||||
|
||||
def finish(_result, error):
|
||||
if error is not None:
|
||||
app_state.set_status(f"Put with Files failed: {error}", warn=True)
|
||||
messagebox.showerror(
|
||||
"Put with Files",
|
||||
f"The put failed:{NL}{NL}{error}{NL}{NL}" +
|
||||
("The new app is staged, but not all of its fonts and images are on the "
|
||||
"device. Put the backup again before restarting it."
|
||||
if plan.staged else
|
||||
"app.sab.stage was not written; the device keeps its current app."))
|
||||
return
|
||||
app_state.set_status(f"Put {os.path.basename(path)}; restart the device to take it up")
|
||||
if messagebox.askyesno(
|
||||
"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")
|
||||
|
||||
_run_with_progress("Put with Files",
|
||||
lambda status: app_backup.put_with_files(ds.sox_client, plan, target,
|
||||
status),
|
||||
finish)
|
||||
|
||||
|
||||
def _describe_put(plan):
|
||||
"""The confirmation for a put: what goes to the device, and what that replaces."""
|
||||
images = sum(f.device_uri.startswith("images/") for f in plan.media)
|
||||
fonts = [f for f in plan.media if f.device_uri.startswith("fonts/")]
|
||||
lines = [f"Put {os.path.basename(plan.path)} on {app_state.device_host}?", "",
|
||||
"The device's app is replaced; anything not saved on it is lost." +
|
||||
(" Its fonts and images are deleted and replaced by the backup's."
|
||||
if plan.has_screen else ""), "",
|
||||
f" app.sab, from {plan.app_source}"]
|
||||
lines += [f" {line}" for line in plan.reencoded]
|
||||
if plan.kit_changes:
|
||||
lines += [" kits.scode, since the device's kits cannot load the app:"]
|
||||
lines += [f" {line}" for line in plan.kit_changes]
|
||||
lines += [" The device's firmware must support these kits, or the app will not "
|
||||
"start."]
|
||||
if not plan.has_screen:
|
||||
return NL.join(lines + ([""] + plan.warnings if plan.warnings else []))
|
||||
lines.append(f" {images} images" +
|
||||
(f" ({plan.converted} made from the backup's image bank)"
|
||||
if plan.converted else ""))
|
||||
lines.append(f" {len(fonts)} fonts" +
|
||||
(f" ({len(plan.fonts_from_zip)} from the backup, the rest from Sedona home)"
|
||||
if plan.fonts_from_zip else " from Sedona home" if fonts else ""))
|
||||
if any(f.device_uri.startswith("system/") for f in plan.media):
|
||||
lines.append(" boot logo")
|
||||
for title, items in (("The backup has no usable copy of these images:",
|
||||
plan.images_missing),
|
||||
("Neither Sedona home nor the backup has these fonts:",
|
||||
plan.fonts_missing)):
|
||||
if items:
|
||||
lines += ["", title] + [f" {item}" for item in items]
|
||||
if plan.warnings:
|
||||
lines += [""] + plan.warnings
|
||||
return NL.join(lines)
|
||||
|
||||
|
||||
NL = chr(10) # newline, for the multi-line message boxes
|
||||
|
||||
|
||||
@@ -2653,6 +2783,7 @@ def _on_connected(ds, root_comp):
|
||||
menu_device.entryconfig("Disconnect", state="normal")
|
||||
menu_device.entryconfig("Download to SAX...", state="normal")
|
||||
menu_app_info.entryconfig("Get with Files...", state="normal")
|
||||
menu_app_info.entryconfig("Put with Files...", state="normal")
|
||||
app_state.set_status(f"Connected to {app_state.device_host}:{app_state.device_port}")
|
||||
|
||||
sax_root = ds.build_dom_tree(root_comp)
|
||||
@@ -2720,6 +2851,7 @@ def do_device_disconnect():
|
||||
menu_device.entryconfig("Disconnect", state="disabled")
|
||||
menu_device.entryconfig("Download to SAX...", state="disabled")
|
||||
menu_app_info.entryconfig("Get with Files...", state="disabled")
|
||||
menu_app_info.entryconfig("Put with Files...", state="disabled")
|
||||
app_state.set_status("Disconnected")
|
||||
|
||||
sax_file.reset_to_startup_state()
|
||||
@@ -3008,6 +3140,7 @@ menu_app_info = tk.Menu(menu_bar, tearoff=0)
|
||||
menu_app_info.add_command(label="App Info", command=show_app_info)
|
||||
menu_app_info.add_separator()
|
||||
menu_app_info.add_command(label="Get with Files...", command=do_get_with_file, state="disabled")
|
||||
menu_app_info.add_command(label="Put with Files...", command=do_put_with_files, state="disabled")
|
||||
menu_bar.add_cascade(label="App", menu=menu_app_info)
|
||||
|
||||
menu_prefs = tk.Menu(menu_bar, tearoff=0)
|
||||
|
||||
Reference in New Issue
Block a user