Compare commits

..
2 Commits
Author SHA1 Message Date
ArdaandClaude Opus 5.5 f2b18923b7 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>
2026-10-11 10:42:36 +03:00
ArdaandClaude Opus 5.5 e070bd0f1e Put with Files: choose kits, as Manage Kits does, and rebuild kits.scode
A kits step after the zip is chosen, laid out as the Manage Kits panel:
installed kits (what the app needs, greyed), installed but not used, kits
available on the computer, not supported; the Action cell offers Keep at /
Upgrade to / Downgrade to / Add at / REMOVE; Upgrade all kits to latest
version. Kits not in the backup can be added, with their dependencies.

The ticked kits make app.sab's schema (encode_sax takes the kit list, as
Ontrol's PutApp rewrites it). When they are not the device's kits, or
Rebuild kits.scode is ticked, kits.scode is built for them with Ontrol's
sedonac from the Niagara install, after checking the natives the kits call
against the device's platform manifest (PutApp.checkNativeCompatibility).

- kit_build.py: platform manifest lookup, native check, sedonac run
- palette.kit_versions: a checksum released more than once stands for its
  highest version (ontrolPlatform 1.0.1, not 1.0.0)
- palette.unsupported_kits takes the kit list to judge against
- sab_app: a type from a kit the app does not list is a clear error

Tested on a P-ION: a rebuild for its own kits is byte-identical to the one
it runs; adding and then removing the logic kit both come back running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:37:08 +03:00
8 changed files with 801 additions and 135 deletions
+265 -83
View File
@@ -26,7 +26,10 @@ import re
import zipfile import zipfile
import app_state import app_state
import kit_build
import palette
import sab_app import sab_app
import schema
import sox import sox
from ox_widget_properties import IMAGE_EXTENSIONS, ORION_KIT, ORION_PLATFORM_TYPE, RION_KIT from ox_widget_properties import IMAGE_EXTENSIONS, ORION_KIT, ORION_PLATFORM_TYPE, RION_KIT
@@ -64,8 +67,16 @@ _PLATFORMS = (_ORION, _RION, _PION)
def platform_of(app): def platform_of(app):
kits = app.kit_names() return platform_of_kits(app.kit_names())
return next((p for p in _PLATFORMS if p.kit in kits), None)
def platform_of_kits(kit_names):
kit_names = set(kit_names)
return next((p for p in _PLATFORMS if p.kit in kit_names), None)
def platform_label(platform):
return platform.name if platform else "no platform this editor knows"
def backup_folder(): def backup_folder():
@@ -278,7 +289,8 @@ def get_with_file(client, path, status=None):
# --- Put with Files --------------------------------------------------------------------- # --- Put with Files ---------------------------------------------------------------------
# #
# Follows Ontrol's Workbench tool (ontrolSedonaOx BPutWithFilesTool, ontrolSedonanet # Follows Ontrol's Workbench tool (ontrolSedonaOx BPutWithFilesTool, ontrolSedonanet
# PutApp): the backup's app.sax is encoded against the device's kits; app.sab goes as # PutApp): the backup's app.sax is encoded for the chosen kit builds, kits.scode built
# for them with sedonac when they are not the device's (kit_build); both go as
# '.writing' renamed to '.stage'; the platform service's deleteFile clears fonts/* and # '.writing' renamed to '.stage'; the platform service's deleteFile clears fonts/* and
# images/*; then the images, the fonts and the boot logo go. Fonts come from Sedona # images/*; then the images, the fonts and the boot logo go. Fonts come from Sedona
# home's bank in the cut the device draws with, images in the form its screen takes. # home's bank in the cut the device draws with, images in the form its screen takes.
@@ -302,25 +314,13 @@ def kits_fit(app_kits, device_kits):
return all(any(d == (n, _checksum(c)) for d in device) for n, c in app_kits) return all(any(d == (n, _checksum(c)) for d in device) for n, c in app_kits)
def kit_differences(app_kits, device_kits):
"""Lines telling why an app's kits do not fit the device's."""
on_device = {n: _checksum(c) for n, c in device_kits}
lines = []
for name, cks in app_kits:
cks = _checksum(cks)
if name not in on_device:
lines.append(f"{name}: not on the device")
elif on_device[name] != cks:
lines.append(f"{name}: {on_device[name]:08x} on the device, {cks:08x} in the backup")
return lines or ["The same kits, in a different order"]
class DeviceTarget: class DeviceTarget:
"""What a put needs to know of the connected device.""" """What a put needs to know of the connected device."""
def __init__(self, platform, kits, name=None, portrait=False, def __init__(self, platform, kits, name=None, portrait=False,
platform_comp_id=None, delete_slot_id=None): platform_comp_id=None, delete_slot_id=None, platform_id=""):
self.platform = platform # one of _PLATFORMS, or None self.platform = platform # one of _PLATFORMS, or None
self.platform_id = platform_id # e.g. "ontrol-pion-b427a5b9"
self.kits = kits # [(name, checksum)] in kit-id order self.kits = kits # [(name, checksum)] in kit-id order
self.name = name # the app's deviceName self.name = name # the app's deviceName
self.portrait = portrait # Orion's screenOrientation self.portrait = portrait # Orion's screenOrientation
@@ -353,7 +353,7 @@ def device_target(ds):
platform = (next((p for p in _PLATFORMS if platform_id.startswith(p.platform_id)), None) platform = (next((p for p in _PLATFORMS if platform_id.startswith(p.platform_id)), None)
or next((p for p in _PLATFORMS if p.kit in kit_names), None)) or next((p for p in _PLATFORMS if p.kit in kit_names), None))
target = DeviceTarget(platform, kits) target = DeviceTarget(platform, kits, platform_id=client.read_version().platform_id)
comp = client.cache_get(0) comp = client.cache_get(0)
if comp is not None: if comp is not None:
client.update([comp], sox.CONFIG) client.update([comp], sox.CONFIG)
@@ -450,8 +450,8 @@ class PutPlan:
self.problems = [] # any of these stops the put self.problems = [] # any of these stops the put
self.warnings = [] # worth confirming, but the put can go on self.warnings = [] # worth confirming, but the put can go on
self.app_source = "" # how app.sab was come by, for the summary self.app_source = "" # how app.sab was come by, for the summary
self.reencoded = [] # kits app.sax was encoded against other builds of self.kit_changes = [] # each chosen kit build that is not the device's
self.kit_changes = [] # set when the backup's kits.scode has to go too self.scode_source = "" # where kits.scode comes from, when it goes
self.app_files = [] # PutFile to '<name>.writing', renamed to '.stage' self.app_files = [] # PutFile to '<name>.writing', renamed to '.stage'
self.media = [] # PutFile: images, fonts, logo self.media = [] # PutFile: images, fonts, logo
self.converted = 0 # images made from the backup's image bank self.converted = 0 # images made from the backup's image bank
@@ -466,39 +466,176 @@ class PutPlan:
return not self.problems return not self.problems
def _label(platform):
return platform.name if platform else "no platform this editor knows"
def _app_portrait(app): def _app_portrait(app):
comp = next((c for c in app.comps.values() if c["type"] == ORION_PLATFORM_TYPE), None) comp = next((c for c in app.comps.values() if c["type"] == ORION_PLATFORM_TYPE), None)
return bool(comp and app.prop(comp, "screenOrientation", False)) return bool(comp and app.prop(comp, "screenOrientation", False))
def _sax_kits(sax): class KitRow:
"""One kit in the kits step of a put: the device's build, the backup's, and what to put.
`included` rows are what goes: into app.sab's schema and, when it is rebuilt, into
kits.scode. A `required` row -- a kit the app's components name, or one an included
kit depends on -- is always included.
"""
def __init__(self, name, device, backup, builds, used=False, locked=False,
unsupported=False):
self.name = name
self.device = device # checksum hex the device runs, or None
self.backup = backup # checksum hex the backup's schema names, or None
self.builds = builds # [(version, checksum hex)] Sedona home has, newest first
self.used = used # the app's components name a type of it
self.locked = locked # the backup holds only app.sab: nothing can change
self.unsupported = unsupported # cannot run on the device's platform
self.required = used
self.included = used or (device is not None and not unsupported) or \
(backup is not None and not unsupported)
self.use = self._default() # the chosen checksum hex
def _default(self):
"""The device's build, when Sedona home can describe it; then the backup's; then
the newest."""
known = {c for _, c in self.builds}
if self.locked:
return self.backup
for checksum in (self.device, self.backup):
if checksum in known:
return checksum
return self.builds[0][1] if self.builds else self.device or self.backup
@property
def latest(self):
return self.builds[0][1] if self.builds else None
def _hex(checksum):
return f"{_checksum(checksum):08x}"
def _app_source(z, names):
"""(sax entry or None, sab entry or None) of the backup's app."""
sax = "app.sax" if "app.sax" in names else next(
(n for n in sorted(names) if n.endswith(".sax") and "/" not in n), None)
sab = "device/" + APP_SAB
return sax, sab if sab in names else None
def _backup_kits(path):
"""([(kit, checksum or None)] the backup's schema names, {kits its components use},
whether it holds only app.sab). Raises ValueError when the zip holds no app."""
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
schema_elem = ET.fromstring(sax).find("schema") with zipfile.ZipFile(path) as z:
return [] if schema_elem is None else [k.get("name") for k in schema_elem.findall("kit")] names = {n for n in z.namelist() if not n.endswith("/")}
sax_name, sab_name = _app_source(z, names)
if sax_name:
root = ET.fromstring(z.read(sax_name))
schema_elem = root.find("schema")
kits = [(k.get("name"), k.get("checksum"))
for k in (schema_elem.findall("kit") if schema_elem is not None else ())]
used = {c.get("type", "").split("::")[0] for c in root.iter("comp")}
return kits, used | {"sys"}, False
if sab_name:
app = sab_app.SabApp(z.read(sab_name))
used = {c["type"].split("::")[0] for c in app.comps.values()}
return app.kit_parts, used, True
raise ValueError("The zip holds no app: neither app.sax nor device/app.sab.")
def plan_put(path, target): def kit_rows(path, target):
"""Reads the zip at `path` and checks it against the device `target`.""" """Every kit the kits step lists, in Manage Kits' groups (see KitRow.group).
The kits the backup's app needs, those its schema or the device holds without the
app needing them, and every other kit Sedona home has. Each defaults to the device's
build. Raises ValueError when the zip holds no readable app.
"""
kits, used, locked = _backup_kits(path)
device = {n: _hex(c) for n, c in target.kits}
backup = {n: _hex(c) if c else None for n, c in kits}
names = list(dict.fromkeys([n for n, _ in kits] + list(device) + palette.list_kits()))
unsupported = palette.unsupported_kits(set(backup) | set(device))
rows = []
for name in names:
builds = [(v, _hex(c)) for v, c in palette.kit_builds(name)]
if locked and name not in backup:
continue # nothing can be added to a bare app.sab
rows.append(KitRow(name, device.get(name), backup.get(name), builds,
used=name in used, locked=locked,
unsupported=name in unsupported and name not in used))
settle_required(rows)
return rows
def settle_required(rows):
"""Marks every kit an included kit depends on as required -- and so included, at its
default build -- all the way down, as Manage Kits counts dependencies."""
by_name = {r.name: r for r in rows}
for row in rows:
row.required = row.used
pending = [r for r in rows if r.included or r.used]
while pending:
row = pending.pop()
for dep in schema.read_kit_dependencies(row.name, row.use):
dep_row = by_name.get(dep)
if dep_row is not None and not dep_row.required:
dep_row.required = dep_row.included = True
pending.append(dep_row)
for row in rows:
if row.used:
row.included = True
KIT_GROUPS = ("Installed kits", "Installed but not used kits",
"Kits available on the computer", "Not supported")
def group(row):
"""The Manage Kits section a row is listed under: the app needs it; the device or
the backup holds it without the app needing it; Sedona home has it; or it cannot run
on the device."""
if row.unsupported:
return KIT_GROUPS[3]
if row.required:
return KIT_GROUPS[0]
if row.device or row.backup:
return KIT_GROUPS[1]
return KIT_GROUPS[2]
def chosen_kits(rows):
"""[(name, checksum hex)] of the included rows."""
return [(r.name, r.use) for r in rows if r.included]
def plan_put(path, target, rows=None, rebuild=False):
"""Reads the zip at `path` and checks it against the device `target`.
rows are kit_rows() as the kits step left them; None takes the defaults. The
included kits make app.sab's schema; kits.scode is built for them when they are not
the device's kits, or when `rebuild` asks for it anyway.
"""
plan = PutPlan(path) plan = PutPlan(path)
try: try:
z = zipfile.ZipFile(path) z = zipfile.ZipFile(path)
except (OSError, zipfile.BadZipFile) as e: except (OSError, zipfile.BadZipFile) as e:
plan.problems.append(f"Not a readable zip: {e}") plan.problems.append(f"Not a readable zip: {e}")
return plan return plan
if rows is None:
try:
rows = kit_rows(path, target)
except Exception as e:
plan.problems.append(str(e))
return plan
with z: with z:
names = {n for n in z.namelist() if not n.endswith("/")} names = {n for n in z.namelist() if not n.endswith("/")}
app, backup_portrait = _plan_app(plan, z, names, target) app, backup_portrait = _plan_app(plan, z, names, target, rows, rebuild)
if app is None: if app is None:
return plan return plan
platform = platform_of(app) platform = platform_of(app)
if platform is None or platform is not target.platform: if platform is None or platform is not target.platform:
plan.problems.append(f"The backup is from {_label(platform)}; the device is " plan.problems.append(f"The backup is from {platform_label(platform)}; the device "
f"{_label(target.platform)}.") f"is {platform_label(target.platform)}.")
return plan return plan
zip_name = app.prop(app.app, "deviceName", "") or "" zip_name = app.prop(app.app, "deviceName", "") or ""
@@ -524,63 +661,108 @@ def plan_put(path, target):
return plan return plan
def _plan_app(plan, z, names, target): def _plan_app(plan, z, names, target, rows, rebuild):
"""Settles app.sab, Ontrol's way first: the backup's app.sax encoded against the """Settles app.sab and kits.scode, as Ontrol's PutApp does.
device's kits. Failing that, the backup's own app.sab when the device's kits can
load it, or with the backup's kits.scode. Returns (decoded app, its portrait)."""
sax_name = "app.sax" if "app.sax" in names else next(
(n for n in sorted(names) if n.endswith(".sax") and "/" not in n), None)
sab_name, scode_name = "device/" + APP_SAB, "device/" + KITS_SCODE
device_cks = {n: f"{_checksum(c):08x}" for n, c in target.kits}
sax_problem = None
if sax_name: The backup's app.sax is encoded for the chosen kit builds (a backup with only
sax = z.read(sax_name) device/app.sab goes as it is). When the device's kits.scode cannot load that app, or
a rebuild is asked for, kits.scode is built for the chosen builds with sedonac after
a check of their natives against the device's VM; without sedonac, the backup's own
kits.scode can stand in when the chosen builds are the backup's. Returns (decoded
app, whether the backup came from a portrait screen).
"""
sax_name, sab_name = _app_source(z, names)
chosen = chosen_kits(rows)
original = None
if sab_name:
try: try:
missing = [k for k in _sax_kits(sax) if k not in device_cks] original = sab_app.SabApp(z.read(sab_name))
if missing:
raise sab_app.SabError("the device lacks the kits " + ", ".join(missing))
sab = sab_app.encode_sax(sax, device_cks)
app = sab_app.SabApp(sab)
except Exception as e: except Exception as e:
sax_problem = f"{sax_name} cannot be encoded for this device: {e}" if not sax_name:
else:
original = sab_app.SabApp(z.read(sab_name)) if sab_name in names else None
if original is not None:
plan.reencoded = [line for line in kit_differences(original.kit_parts,
target.kits)
if "on the device," in line]
plan.app_source = f"{sax_name}, encoded for the device's kits"
plan.app_files.append(PutFile(APP_SAB, sab, sax_name))
return app, _app_portrait(original or app)
if sab_name not in names:
plan.problems.append(sax_problem or "The zip holds no app: neither app.sax nor "
f"{sab_name}.")
return None, False
sab = z.read(sab_name)
try:
app = sab_app.SabApp(sab)
except Exception as e:
plan.problems += [p for p in (sax_problem,) if p]
plan.problems.append(f"{sab_name} cannot be read: {e}") plan.problems.append(f"{sab_name} cannot be read: {e}")
return None, False return None, False
if kits_fit(app.kit_parts, target.kits):
plan.app_source = sab_name if sax_name:
elif scode_name in names: try:
plan.kit_changes = kit_differences(app.kit_parts, target.kits) sab = sab_app.encode_sax(z.read(sax_name), kits=chosen)
plan.app_source = f"{sab_name}, with the backup's {KITS_SCODE}" except Exception as e:
plan.app_files.append(PutFile(KITS_SCODE, z.read(scode_name), scode_name)) plan.problems.append(f"{sax_name} cannot be encoded for the chosen kits: {e}")
else:
plan.problems += [p for p in (sax_problem,) if p]
plan.problems.append(f"The device's kits cannot load {sab_name}, and the backup "
f"has no {scode_name}:")
plan.problems += [" " + line for line in kit_differences(app.kit_parts, target.kits)]
return None, False return None, False
if sax_problem: plan.app_source = f"{sax_name}, encoded for the chosen kits"
plan.warnings.append(sax_problem) source = sax_name
plan.app_files.append(PutFile(APP_SAB, sab, sab_name)) else:
return app, _app_portrait(app) sab = z.read(sab_name)
plan.app_source = sab_name
source = sab_name
app = sab_app.SabApp(sab)
app_kits = app.kit_parts
plan.kit_changes = [line for line in map(kit_change, rows) if line]
device_kits = {(n, _checksum(c)) for n, c in target.kits}
if sax_name:
same = {(n, _checksum(c)) for n, c in app_kits} == device_kits
else:
same = kits_fit(app_kits, target.kits)
if rebuild or not same:
scode = _plan_scode(plan, z, names, target, app_kits)
if scode is None:
return None, False
plan.app_files.append(scode)
plan.app_files.append(PutFile(APP_SAB, sab, source))
return app, _app_portrait(original or app)
def kit_change(row):
"""How a row changes the device's kits, or None."""
def label(checksum):
version = palette.version_for_checksum(row.name, checksum)
return f"{version} ({checksum})" if version else checksum
if row.device is None:
return f"{row.name}: added at {label(row.use)}" if row.included else None
if not row.included:
return f"{row.name}: removed"
if row.use != row.device:
return f"{row.name}: {label(row.device)} -> {label(row.use)}"
return None
def _plan_scode(plan, z, names, target, app_kits):
"""kits.scode for the app's kits: built by sedonac, or the backup's own."""
manifest = kit_build.platform_manifest(target.platform_id)
backup_scode = "device/" + KITS_SCODE
backup_kits = None
if "device/" + APP_SAB in names:
try:
backup_kits = sab_app.SabApp(z.read("device/" + APP_SAB)).kit_parts
except Exception:
pass
why = None
if manifest is None:
why = (f"Sedona home has no platform manifest for {target.platform_id or 'the device'}"
" (platforms/db), so kits.scode cannot be built or checked for it.")
else:
problems = kit_build.native_problems(app_kits, manifest)
if problems:
plan.problems.append("The chosen kits call native methods the device's VM "
"does not have:")
plan.problems += [" " + p for p in problems]
return None
try:
data = kit_build.build_kits_scode(app_kits, manifest)
except kit_build.BuildError as e:
why = str(e)
else:
plan.scode_source = "built by sedonac for the chosen kits"
return PutFile(KITS_SCODE, data, "sedonac")
if backup_kits is not None and backup_scode in names and \
[(n, _checksum(c)) for n, c in backup_kits] == \
[(n, _checksum(c)) for n, c in app_kits]:
plan.warnings.append(why + " The backup's own kits.scode goes instead.")
plan.scode_source = "the backup's, unchecked against the device's VM"
return PutFile(KITS_SCODE, z.read(backup_scode), backup_scode)
plan.problems.append(why)
return None
def _plan_images(plan, z, names, platform, target, same_screen): def _plan_images(plan, z, names, platform, target, same_screen):
+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.166" 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
+190
View File
@@ -0,0 +1,190 @@
"""Building a device's kits.scode, as Ontrol's PutApp does it with sedonac.
The editor carries no Sedona compiler: Ontrol's own sedonac is run through Java, from
the Niagara install next to Sedona home (Niagara 4's ontrolSedonanet jars, or Niagara
AX's sedona/sedonac jars -- both build the same image). Its input is a kits.xml naming
each kit at its version, with the target platform's endian, block and ref sizes taken
from the platform manifest in Sedona home's platform db.
Before a build the kits' calls into native code are checked against the platform's
SVM, as PutApp.checkNativeCompatibility does: an scode calling a native the device's
VM lacks would not start.
"""
import glob
import os
import re
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET
import zipfile
import app_state
import palette
class BuildError(Exception):
pass
def _home():
return app_state.SEDONA_HOME
# --- kits -------------------------------------------------------------------------------
def kit_file(name, checksum):
"""(path, version) of the .kit for a kit build; the highest version when one checksum
was released more than once. None when Sedona home has no such .kit."""
want = int(checksum, 16) if isinstance(checksum, str) else checksum & 0xFFFFFFFF
pattern = re.compile(rf"^{re.escape(name)}-([0-9a-fA-F]+)-(.+)\.kit$")
found = []
for path in glob.glob(os.path.join(_home(), "kits", name, f"{name}-*.kit")):
match = pattern.match(os.path.basename(path))
if match and int(match.group(1), 16) == want:
found.append((palette.version_sort_key(match.group(2)), path, match.group(2)))
if not found:
return None
_, path, version = max(found)
return path, version
# --- the target platform ----------------------------------------------------------------
def platform_manifest(platform_id):
"""The platformManifest.xml in Sedona home's platform db for a platform id, or None."""
if not platform_id:
return None
# os.walk, not glob: the manifests sit in hidden .par folders glob skips.
for folder, _, files in os.walk(os.path.join(_home(), "platforms", "db")):
if "platformManifest.xml" not in files:
continue
path = os.path.join(folder, "platformManifest.xml")
try:
if ET.parse(path).getroot().get("platformId") == platform_id:
return path
except ET.ParseError:
continue
return None
_CLASS = re.compile(r"^\s*(?:[a-z]+\s+)*class\s+(\w+)", re.M)
_NATIVE = re.compile(r"\bnative\b[^=\n]*?\b(\w+)\s*\([^)]*\)\s*=\s*(\d+::\d+)")
_CALL = re.compile(r"\bCallNative\w*\s+(\w+::\w+\.\w+)")
def native_problems(kits, manifest_path):
"""Lines naming each native the kits call that the platform's SVM does not provide.
kits is [(name, checksum)]. Read from the kits' .ir files: a native's id from its
declaration, the natives in use from the CallNative ops.
"""
svm = {e.get("id"): e.get("qname")
for e in ET.parse(manifest_path).getroot().iter("nativeMethod")}
declared, called = {}, set()
for name, checksum in kits:
found = kit_file(name, checksum)
if found is None:
continue # reported by the build
with zipfile.ZipFile(found[0]) as z:
for entry in z.namelist():
if not entry.endswith(".ir"):
continue
text = z.read(entry).decode("utf-8", "replace")
classes = [(m.start(), m.group(1)) for m in _CLASS.finditer(text)]
for m in _NATIVE.finditer(text):
owner = [c for pos, c in classes if pos < m.start()]
if owner:
declared[f"{name}::{owner[-1]}.{m.group(1)}"] = m.group(2)
called.update(_CALL.findall(text))
problems = []
for qname in sorted(called):
nid = declared.get(qname)
if nid is None or nid not in svm:
problems.append(f"{qname} ({nid or 'not declared'}) is not in the device's VM")
elif svm[nid] != qname:
problems.append(f"{qname} ({nid}) is {svm[nid]} in the device's VM")
return problems
# --- the compiler -----------------------------------------------------------------------
def _java():
found = shutil.which("java")
if found:
return found
candidates = []
if os.environ.get("JAVA_HOME"):
candidates.append(os.path.join(os.environ["JAVA_HOME"], "bin", "java.exe"))
niagara_root = os.path.dirname(os.path.abspath(_home()))
candidates += sorted(glob.glob(os.path.join(niagara_root, "Niagara-*", "jre", "bin",
"java.exe")), reverse=True)
for root in (os.environ.get("ProgramFiles", r"C:\Program Files"),):
candidates += sorted(glob.glob(os.path.join(root, "*", "*", "bin", "java.exe")),
reverse=True)
return next((c for c in candidates if os.path.isfile(c)), None)
def _classpath():
"""sedonac and the sedona runtime from the Niagara install beside Sedona home."""
niagara_root = os.path.dirname(os.path.abspath(_home()))
for niagara in sorted(glob.glob(os.path.join(niagara_root, "Niagara-*")), reverse=True):
modules = os.path.join(niagara, "modules")
for jars in (("ontrolSedonanet-rt.jar", "ontrolSedonanet-wb.jar"),
("sedona.jar", "sedonac.jar")):
paths = [os.path.join(modules, j) for j in jars]
if all(os.path.isfile(p) for p in paths):
return os.pathsep.join(paths)
return None
def toolchain():
"""(java, classpath) for running sedonac; raises BuildError saying what is missing."""
java, classpath = _java(), _classpath()
if not java:
raise BuildError("No Java runtime was found (java on the PATH, JAVA_HOME, or "
"Niagara's jre).")
if not classpath:
raise BuildError("sedonac was not found: no Niagara install beside Sedona home "
f"({os.path.dirname(os.path.abspath(_home()))}) has Ontrol's "
"ontrolSedonanet jars or sedona.jar + sedonac.jar.")
return java, classpath
def build_kits_scode(kits, manifest_path):
"""Compiles kits.scode for [(name, checksum)] on the platform; returns its bytes."""
plat = ET.parse(manifest_path).getroot()
lines = ["<sedonaCode"]
for attr in ("endian", "blockSize", "refSize", "debug", "test", "armDouble"):
lines.append(f' {attr}="{plat.get(attr)}"')
lines += [' main="sys::Sys.main"', ">"]
missing = []
for name, checksum in kits:
found = kit_file(name, checksum)
if found is None:
missing.append(f"{name} {checksum}")
else:
lines.append(f' <depend on="{name} {found[1]}=" />')
if missing:
raise BuildError("Sedona home has no .kit file for " + ", ".join(missing))
lines.append("</sedonaCode>")
java, classpath = toolchain()
with tempfile.TemporaryDirectory(prefix="sedona_editor_scode_") as work:
with open(os.path.join(work, "kits.xml"), "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
try:
run = subprocess.run(
[java, f"-Dsedona.home={_home()}", "-cp", classpath, "sedonac.Main",
"kits.xml"],
cwd=work, capture_output=True, text=True, timeout=180,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
except (OSError, subprocess.TimeoutExpired) as e:
raise BuildError(f"sedonac could not run: {e}") from e
scode = os.path.join(work, "kits.scode")
if run.returncode != 0 or not os.path.isfile(scode):
tail = "\n".join((run.stdout + run.stderr).strip().splitlines()[-12:])
raise BuildError(f"sedonac failed:\n{tail}")
with open(scode, "rb") as f:
return f.read()
+62 -25
View File
@@ -17,6 +17,7 @@ import live_hooks
import manage_kits import manage_kits
import palette import palette
import properties import properties
import put_kits_dialog
import sax_file import sax_file
import schema import schema
import sox import sox
@@ -616,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():
@@ -781,10 +790,11 @@ def _report_backup(result):
def do_put_with_files(): def do_put_with_files():
"""App > Put with Files: puts a Get with Files zip back onto the connected device. """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 As Ontrol's tool does: the kits the app needs are listed to choose builds for (the
device's kits can run -- the backup's app.sax encoded for them, as Ontrol's tool device's by default), the backup's app.sax is encoded for them, and kits.scode is
does, or its app.sab. The app goes staged, so the device takes it up when rebuilt with sedonac when they are not the device's. The zip is checked against the
restarted, which is offered once the files are in. device on the way. 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 ds = app_state.device_session
if not app_state.live_mode or ds is None or not ds.is_connected: if not app_state.live_mode or ds is None or not ds.is_connected:
@@ -808,19 +818,51 @@ def do_put_with_files():
except Exception as e: except Exception as e:
messagebox.showerror("Put with Files", f"Could not read the device:{NL}{NL}{e}") messagebox.showerror("Put with Files", f"Could not read the device:{NL}{NL}{e}")
return return
plan = app_backup.plan_put(path, target) try:
rows = app_backup.kit_rows(path, target)
except Exception as e:
messagebox.showerror("Put with Files", f"{os.path.basename(path)}:{NL}{NL}{e}")
return
backup_platform = app_backup.platform_of_kits(r.name for r in rows if r.backup or r.used)
if backup_platform is None or backup_platform is not target.platform:
messagebox.showerror(
"Put with Files",
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}"
f"The backup is from {app_backup.platform_label(backup_platform)}; the device "
f"is {app_backup.platform_label(target.platform)}.")
return
rebuild = put_kits_dialog.ask(app_state.window, rows)
if rebuild is None:
return
def planned(plan, error):
if error is not None:
messagebox.showerror("Put with Files", f"Preparing the put failed:{NL}{NL}{error}")
return
if not plan.ok: if not plan.ok:
app_state.set_status(f"{os.path.basename(path)} does not match the device", warn=True) app_state.set_status(f"{os.path.basename(path)} cannot be put on the device",
warn=True)
messagebox.showerror( messagebox.showerror(
"Put with Files", "Put with Files",
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}" + f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}" +
NL.join(plan.problems)) NL.join(plan.problems))
return return
if not messagebox.askokcancel("Put with Files", _describe_put(plan), if messagebox.askokcancel("Put with Files", _describe_put(plan),
icon=messagebox.WARNING, default=messagebox.CANCEL): icon=messagebox.WARNING, default=messagebox.CANCEL):
return _put(plan)
def finish(_result, error): def prepare(status):
status("Preparing the app" + (" and building kits.scode" if rebuild or any(
map(app_backup.kit_change, rows)) else "") + " ...", None)
return app_backup.plan_put(path, target, rows, rebuild)
def _put(plan):
_run_with_progress("Put with Files",
lambda status: app_backup.put_with_files(ds.sox_client, plan,
target, status),
lambda result, error: finish(plan, result, error))
def finish(plan, _result, error):
if error is not None: if error is not None:
app_state.set_status(f"Put with Files failed: {error}", warn=True) app_state.set_status(f"Put with Files failed: {error}", warn=True)
messagebox.showerror( messagebox.showerror(
@@ -837,12 +879,10 @@ 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", _run_with_progress("Put with Files", prepare, planned)
lambda status: app_backup.put_with_files(ds.sox_client, plan, target,
status),
finish)
def _describe_put(plan): def _describe_put(plan):
@@ -854,12 +894,9 @@ def _describe_put(plan):
(" Its fonts and images are deleted and replaced by the backup's." (" Its fonts and images are deleted and replaced by the backup's."
if plan.has_screen else ""), "", if plan.has_screen else ""), "",
f" app.sab, from {plan.app_source}"] f" app.sab, from {plan.app_source}"]
lines += [f" {line}" for line in plan.reencoded] if plan.scode_source:
if plan.kit_changes: lines.append(f" kits.scode, {plan.scode_source}")
lines += [" kits.scode, since the device's kits cannot load the app:"]
lines += [f" {line}" for line in plan.kit_changes] 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: if not plan.has_screen:
return NL.join(lines + ([""] + plan.warnings if plan.warnings else [])) return NL.join(lines + ([""] + plan.warnings if plan.warnings else []))
lines.append(f" {images} images" + lines.append(f" {images} images" +
+16 -7
View File
@@ -67,7 +67,12 @@ def kit_versions(kit_name):
for entry in os.listdir(kit_dir): for entry in os.listdir(kit_dir):
match = pattern.match(entry) match = pattern.match(entry)
if match: if match:
versions[_norm_checksum(match.group(1))] = match.group(2) # A checksum released more than once (ontrolPlatform 1.0.0 and 1.0.1)
# stands for its highest version, as the devices report it.
key, version = _norm_checksum(match.group(1)), match.group(2)
if (key not in versions or
version_sort_key(version) > version_sort_key(versions[key])):
versions[key] = version
_versions_cache[kit_name] = versions _versions_cache[kit_name] = versions
return versions return versions
@@ -269,7 +274,7 @@ def _platforms_db():
return os.path.join(app_state.SEDONA_HOME, "platforms", "db") return os.path.join(app_state.SEDONA_HOME, "platforms", "db")
def _platform_native_kits(): def _platform_native_kits(sax_names=None):
"""Return (matching, all_platforms) — two sets of kit names. """Return (matching, all_platforms) — two sets of kit names.
*matching* is the union of nativeKit names from platform builds that include *matching* is the union of nativeKit names from platform builds that include
@@ -278,8 +283,10 @@ def _platform_native_kits():
A kit that appears in *all_platforms* but not in *matching* has native code A kit that appears in *all_platforms* but not in *matching* has native code
compiled for a different device and cannot work with this SAX. compiled for a different device and cannot work with this SAX.
sax_names stands in for the open file's kits when given.
""" """
sax_names = set(app_state.active_schema_kits.keys()) sax_names = set(app_state.active_schema_kits.keys() if sax_names is None else sax_names)
db = _platforms_db() db = _platforms_db()
if not os.path.isdir(db): if not os.path.isdir(db):
return None, None return None, None
@@ -335,8 +342,9 @@ def _platform_native_kits():
return (matching if found else None), all_platforms return (matching if found else None), all_platforms
def unsupported_kits(): def unsupported_kits(sax_names=None):
"""Kit names that cannot work with the open file's platform. """Kit names that cannot work with the open file's platform -- or with the platform
of the kits in `sax_names`, when given.
A kit is unsupported when: A kit is unsupported when:
- it has native code (hasNatives or listed as nativeKit on some platform) - it has native code (hasNatives or listed as nativeKit on some platform)
@@ -345,11 +353,12 @@ def unsupported_kits():
Kits that depend on an unsupported kit are also flagged. Kits that depend on an unsupported kit are also flagged.
""" """
sax_names = frozenset(app_state.active_schema_kits.keys()) sax_names = frozenset(app_state.active_schema_kits.keys() if sax_names is None
else sax_names)
if sax_names in _unsupported_cache: if sax_names in _unsupported_cache:
return _unsupported_cache[sax_names] return _unsupported_cache[sax_names]
matching_natives, all_natives = _platform_native_kits() matching_natives, all_natives = _platform_native_kits(sax_names)
if matching_natives is None: if matching_natives is None:
_unsupported_cache[sax_names] = set() _unsupported_cache[sax_names] = set()
return set() return set()
+242
View File
@@ -0,0 +1,242 @@
"""The kits step of App > Put with Files, laid out as the Manage Kits panel is.
The same groups (kits the app needs, kits declared without being used, kits available,
kits not supported), the same tick column and the same Action cell: "Keep at",
"Upgrade to", "Downgrade to", "Add at", "REMOVE". Installed is what the device runs;
Backup is what the backup's app was saved with.
What is ticked goes: into app.sab's schema and, when that is not the device's kits,
into a kits.scode built for them (Ontrol's PutApp does the same). "Rebuild kits.scode"
forces the build when nothing changed.
"""
import tkinter as tk
from tkinter import messagebox, ttk
import app_backup
import manage_kits
from manage_kits import NOT_DECLARED, SECTION_TAG, column_label, make_checkbox_image
REMOVE_ACTION = "REMOVE from device"
def _options(row):
"""The Action choices for a row, as (label, checksum) pairs."""
if row.device:
return [(label, checksum or row.device) for label, checksum in
manage_kits.action_options(row.name, row.device, True, row.builds)]
return [(f"Add at {manage_kits.build_label(v, c)}", c) for v, c in row.builds]
def _build_text(row, checksum):
if not checksum:
return NOT_DECLARED
version = next((v for v, c in row.builds if c == checksum), "")
return column_label(version, checksum)
class _Dialog:
def __init__(self, parent, rows, title):
self.rows = rows
self.result = None
self.original = [(r.included, r.use) for r in rows]
self.top = top = tk.Toplevel(parent)
top.title(f"{title} - Kits")
top.transient(parent)
top.grab_set()
top.geometry(f"+{parent.winfo_x() + 60}+{parent.winfo_y() + 40}")
frame = ttk.Frame(top, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
columns = ("name", "installed", "backup", "latest", "action")
table_frame = ttk.Frame(frame)
table_frame.pack(fill=tk.BOTH, expand=True)
self.table = ttk.Treeview(table_frame, columns=columns, show="tree headings",
selectmode="browse", height=20)
self.table.heading("#0", text="")
self.table.column("#0", width=32, minwidth=32, stretch=False, anchor="center")
for column, heading, width in [("name", "Kit name", 190),
("installed", "Installed", 105),
("backup", "Backup", 105),
("latest", "Latest", 105),
("action", "Action", 215)]:
self.table.heading(column, text=heading, anchor="w")
self.table.column(column, width=width, minwidth=width, anchor="w", stretch=False)
vscroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.table.yview)
self.table.configure(yscrollcommand=vscroll.set)
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
self.table.pack(fill=tk.BOTH, expand=True)
self.table.tag_configure(SECTION_TAG, background="#dcdcdc")
self.boxes = {(checked, enabled): make_checkbox_image(checked, enabled)
for checked in (True, False) for enabled in (True, False)}
self.cross_box = make_checkbox_image(False, False, cross=True)
locked = any(r.locked for r in rows)
hint = ("The backup holds no app.sax, so its app goes with the kits it was saved "
"with." if locked else
"A tick means the kit goes to the device. Grey ticks are needed by the app "
"and cannot be removed; untick another to remove it, tick one to add it. "
"Click the Action cell to choose a build. Installed is the device's build. "
"Kits other than the device's mean a new kits.scode.")
ttk.Label(frame, text=hint, foreground="#666666", wraplength=780
).pack(anchor="w", pady=(8, 0))
self.rebuild = tk.BooleanVar(value=False)
self.rebuild_check = ttk.Checkbutton(frame, text="Rebuild kits.scode",
variable=self.rebuild)
self.rebuild_check.pack(anchor="w", pady=(6, 0))
button_row = ttk.Frame(frame)
button_row.pack(fill=tk.X, pady=(10, 0))
upgrade = ttk.Button(button_row, text="Upgrade all kits to latest version",
command=self._upgrade_all)
upgrade.pack(side=tk.LEFT)
if locked:
upgrade.state(["disabled"])
ttk.Button(button_row, text="Cancel", command=lambda: self._close(False)
).pack(side=tk.RIGHT)
ttk.Button(button_row, text="Next", command=lambda: self._close(True)
).pack(side=tk.RIGHT, padx=6)
self.row_by_item = {}
self._editor = None
self.table.bind("<Button-1>", self._on_click)
self.table.bind("<MouseWheel>", lambda _e: self._close_editor(), add="+")
top.protocol("WM_DELETE_WINDOW", lambda: self._close(False))
self._rebuild_table()
# --- the table ---
def _action_text(self, row):
if row.unsupported:
return "Not supported"
if not row.included:
return REMOVE_ACTION if row.device else NOT_DECLARED
return next((label for label, c in _options(row) if c == row.use),
_build_text(row, row.use))
def _values(self, row):
latest = row.builds[0] if row.builds else None
return (row.name, _build_text(row, row.device), _build_text(row, row.backup),
column_label(*latest) if latest else NOT_DECLARED, self._action_text(row))
def _image(self, row):
if row.unsupported:
return self.cross_box
return self.boxes[(row.included, not (row.required or row.locked))]
def _rebuild_table(self):
self._close_editor()
top = self.table.yview()[0]
self.table.delete(*self.table.get_children())
self.row_by_item.clear()
for title in app_backup.KIT_GROUPS:
members = [r for r in self.rows if app_backup.group(r) == title]
if not members:
continue
self.table.insert("", "end", values=(title, "", "", "", ""), tags=(SECTION_TAG,))
for row in members:
item = self.table.insert("", "end", image=self._image(row),
values=self._values(row))
self.row_by_item[item] = row
self.table.yview_moveto(top)
# Kits other than the device's cannot run on its kits.scode.
changed = any(app_backup.kit_change(r) for r in self.rows)
if changed:
self.rebuild.set(True)
self.rebuild_check.state(["disabled"])
else:
self.rebuild_check.state(["!disabled"])
def _changed(self):
app_backup.settle_required(self.rows)
self._rebuild_table()
# --- editing ---
def _close_editor(self):
if self._editor is not None:
self._editor.destroy()
self._editor = None
def _on_click(self, event):
self._close_editor()
if self.table.identify_region(event.x, event.y) not in ("tree", "cell"):
return
item = self.table.identify_row(event.y)
row = self.row_by_item.get(item)
if row is None or row.locked or row.unsupported:
return
column = self.table.identify_column(event.x)
if column == "#0":
self._toggle(row)
elif column == "#5":
self._edit_action(item, row)
def _toggle(self, row):
if row.required:
return
if not row.included and not row.builds:
messagebox.showwarning("Put with Files", f"Sedona home holds no build of "
f"'{row.name}' to add it at.", parent=self.top)
return
row.included = not row.included
if row.included and not row.use:
row.use = row.latest
self._changed()
def _edit_action(self, item, row):
options = _options(row)
if not options:
return
cell = self.table.bbox(item, "action")
if not cell:
return
labels = [label for label, _ in options]
variable = tk.StringVar(value=self._action_text(row))
combo = ttk.Combobox(self.table, textvariable=variable, values=labels,
state="readonly")
combo.place(x=cell[0], y=cell[1], width=cell[2], height=cell[3])
self._editor = combo
def commit(_event=None):
if variable.get() in labels:
row.use = options[labels.index(variable.get())][1]
row.included = True
self._close_editor()
self._changed()
combo.bind("<<ComboboxSelected>>", commit)
combo.bind("<Escape>", lambda _e: self._close_editor())
combo.focus_set()
combo.after_idle(lambda: combo.event_generate("<Down>"))
def _upgrade_all(self):
for row in self.rows:
if row.included and row.latest and not row.locked:
row.use = row.latest
self._changed()
def _close(self, ok):
self._close_editor()
if ok:
self.result = bool(self.rebuild.get())
else:
for row, (included, use) in zip(self.rows, self.original):
row.included, row.use = included, use
app_backup.settle_required(self.rows)
self.top.grab_release()
self.top.destroy()
def ask(parent, rows, title="Put with Files"):
"""Shows the kits step; returns whether to rebuild kits.scode, or None when cancelled.
The rows are updated in place with what was chosen.
"""
dialog = _Dialog(parent, rows, title)
dialog.top.wait_window()
return dialog.result
+12 -6
View File
@@ -80,6 +80,9 @@ def _resolve_slots(qname, types_by_q, cache):
"""The type's slots, base chain first, indexed by slot id.""" """The type's slots, base chain first, indexed by slot id."""
if qname in cache: if qname in cache:
return cache[qname] return cache[qname]
if qname not in types_by_q:
raise SabError(f"type {qname} is not in the app's kits -- a kit build that "
f"needs kit '{qname.split('::')[0]}', which the app does not list?")
t = types_by_q[qname] t = types_by_q[qname]
base = t.get("base") base = t.get("base")
out = list(_resolve_slots(base, types_by_q, cache)) if base else [] out = list(_resolve_slots(base, types_by_q, cache)) if base else []
@@ -505,21 +508,24 @@ def _encode_value(out, slot, raw):
raise SabError(f"unhandled slot type {vtype}") raise SabError(f"unhandled slot type {vtype}")
def encode_sax(sax, checksums=None): def encode_sax(sax, checksums=None, kits=None):
"""app.sax (text or bytes) -> app.sab bytes, as `sedonac app.sax` writes it. """app.sax (text or bytes) -> app.sab bytes, as `sedonac app.sax` writes it.
checksums, {kit name: checksum hex}, stands in for the sax's own -- how an app goes checksums, {kit name: checksum hex}, stands in for the sax's own -- how an app goes
to a device whose kits are other builds than the ones it was saved with. Every to a device whose kits are other builds than the ones it was saved with. kits,
kit's manifest must be in Sedona home under the checksum used. [(name, checksum hex)], replaces the schema altogether, as Ontrol's PutApp rewrites
it for the kits chosen to put. Every kit's manifest must be in Sedona home under
the checksum used.
""" """
root = ET.fromstring(sax) root = ET.fromstring(sax)
schema_elem, app_elem = root.find("schema"), root.find("app") schema_elem, app_elem = root.find("schema"), root.find("app")
if schema_elem is None or app_elem is None: if schema_elem is None or app_elem is None:
raise SabError("not a Sedona app: no <schema> or <app>") raise SabError("not a Sedona app: no <schema> or <app>")
if kits is None:
kits = [(k.get("name"), (checksums or {}).get(k.get("name")) or k.get("checksum"))
for k in schema_elem.findall("kit")]
parts = [] parts = []
for k in schema_elem.findall("kit"): for name, cks in kits:
name = k.get("name")
cks = (checksums or {}).get(name) or k.get("checksum")
if not cks: if not cks:
raise SabError(f"no checksum for kit '{name}'") raise SabError(f"no checksum for kit '{name}'")
parts.append((name, f"{int(cks, 16):08x}")) parts.append((name, f"{int(cks, 16):08x}"))
+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