Files
b0hemia e0bc4785e9 diskOS installer: initial public beta
Flashes the diskOS custom UI onto the FiiO Snowsky Disc over Ingenic mask-ROM USB,
building the image from your own stock firmware. Runs from source via install.sh.
2026-08-26 15:26:14 +10:00

221 lines
9.6 KiB
Python

"""diskOS installer CLI.
Commands:
doctor Report host, bundled tools, device presence, and saved state.
install Build a diskOS image from YOUR stock firmware and flash it.
restore-stock Deactivate diskOS: reflash your saved stock-rootfs image (leaves /usr/data files).
remove Delete everything this tool created (its full uninstall footprint).
Design: the whole tool is Python; the bricking-sensitive steps delegate to bundled
proven native binaries. Nothing is installed system-wide on Linux/macOS.
"""
import argparse
import fcntl
import os
import sys
import tempfile
from diskos_installer import (__version__, bundle, errors, flasher, imagebuild,
platform_probe, service, state, ui)
from diskos_installer.reporter import CLIReporter
# --- single-run lock so two installers can't touch the ONE physical device at once. It must be
# SHARED across all users on the host (a GUI as your user and a CLI under sudo must not both flash),
# so it lives at a FIXED /tmp path - NOT per-user. Advisory flock (auto-released on death -> no
# stale-lock hazard). Opened READ-ONLY so any user can take the flock on a 0644 file, and O_NOFOLLOW
# so a planted symlink can't redirect the open (we bail instead of following it). ---
class _Lock:
def __init__(self):
# A FIXED absolute path, not tempfile.gettempdir(): $TMPDIR differs between a normal run and a
# sudo run (and per-user on macOS), which would hand them SEPARATE locks and defeat the whole
# point. /tmp is world-accessible on every supported host (Linux/macOS); Windows is unsupported.
self.path = "/tmp/diskos-installer.lock"
self.fd = None
def __enter__(self):
import stat as _stat
flags = os.O_CREAT | os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
try:
self.fd = os.open(self.path, flags, 0o644)
except OSError as e:
raise SystemExit(ui.red(
f"could not open the run-lock at {self.path} ({e}). If it exists as a symlink, "
"remove it and retry."))
# A planted symlink is refused by O_NOFOLLOW above; also refuse a non-regular file.
if not _stat.S_ISREG(os.fstat(self.fd).st_mode):
os.close(self.fd); self.fd = None
raise SystemExit(ui.red(f"run-lock {self.path} is not a regular file - refusing."))
try:
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
os.close(self.fd)
self.fd = None
raise SystemExit(ui.red(
"another diskOS installer is already running. Close it and retry "
"(only one may touch the device at a time)."))
return self
def __exit__(self, *exc):
try:
if self.fd is not None:
fcntl.flock(self.fd, fcntl.LOCK_UN)
os.close(self.fd)
except OSError:
pass
return False
def _set_phase(st, phase):
st["phase"] = phase
state.save(st)
def cmd_doctor(args):
ui.step("diskOS installer - doctor")
o, a = platform_probe.host()
ui.info(f"version : {__version__}")
ui.info(f"host : {o}-{a} ({'supported' if platform_probe.is_supported() else 'UNSUPPORTED'})")
ui.info(f"state dir : {state.state_dir()}")
ui.info("bundled tools:")
all_ok = True
for name in ("usbboot", "mksquashfs", "unsquashfs", "my_write5_dram.bin", "disc_spl_lpddr3.bin"):
p = bundle.native(name, required=False)
(ui.ok if p else ui.err)(f" {name}: {p or 'MISSING'}")
all_ok = all_ok and bool(p)
for name in ("mq_ui", "S97diskos_install", "S99usbserial", "diskos-debug.sh", "dropbearmulti"):
p = bundle.data(name, required=False)
(ui.ok if p else ui.err)(f" {name}: {p or 'MISSING'}")
all_ok = all_ok and bool(p) # payload files are required to build a flashable image
n = platform_probe.maskrom_count()
if n < 0:
ui.warn("device: cannot enumerate USB (pyusb/lsusb unavailable)")
elif n == 0:
ui.info("device: none in mask-ROM mode (normal unless you're about to flash)")
else:
(ui.ok if n == 1 else ui.warn)(f"device: {n} in mask-ROM mode")
st = state.load()
if st.get("installed"):
ui.info(f"diskOS installed via this tool: yes (variant={st.get('variant')}, at {st.get('installed_at')})")
else:
ui.info("diskOS installed via this tool: no record")
ui.info(f"saved bone-stock image (for restore): {'yes' if state.have_stock_image() else 'no'}")
return 0 if all_ok else 1
def _cli_confirm(yes):
"""Build a service `confirm` callback for the CLI (prints the summary, then
y/N - or auto-yes with --yes)."""
def confirm(summary):
ui.step("Ready - please confirm")
for k in ("action", "variant", "image", "duration", "consequence"):
if summary.get(k):
ui.info(f" {k}: {summary[k]}")
if yes:
return True
return ui.confirm("Proceed?", default=False)
return confirm
def cmd_install(args):
if not platform_probe.is_supported():
ui.err(f"[E101] unsupported host {platform_probe.host_tag()} - Linux/macOS only for now")
return 2
if not args.firmware and not args.stock:
ui.err("[E140] need --firmware <FiiO official update .zip> (or --stock <rootfs.squashfs>).")
return 2
params = {"firmware": args.firmware, "stock": args.stock,
"ui_binary": args.ui, "variant": args.variant}
r = service.do_install(params, CLIReporter(), _cli_confirm(args.yes))
if r.get("ok"):
ui.info("The UI is embedded in the flashed image - just reboot the device and it installs")
ui.info("diskOS automatically on first boot (no microSD step needed).")
ui.info("To deactivate diskOS later (reflash your saved stock rootfs): diskos-installer restore-stock")
return 0
return 3 if r.get("aborted") else 1
def cmd_restore_stock(args):
r = service.do_restore({"firmware": args.firmware}, CLIReporter(), _cli_confirm(args.yes))
return 0 if r.get("ok") else (3 if r.get("aborted") else 1)
def cmd_remove(args):
def confirm(summary):
ui.warn(summary.get("consequence", ""))
return args.yes or ui.confirm("Delete the installer's files anyway?", default=False)
r = service.do_remove({"force": args.force}, CLIReporter(), confirm)
if r.get("ok"):
if getattr(sys, "frozen", False):
ui.info(f"To finish: delete this executable ({sys.executable}).")
else:
ui.info(f"To finish: delete the installer folder ({bundle.resource_root()}).")
ui.info("Nothing was installed system-wide, so there is nothing else to clean up.")
return 0
if r.get("errors"):
return 1 # partial-removal errors already reported by the service
ui.info("aborted; nothing removed.")
return 3
def build_parser():
p = argparse.ArgumentParser(prog="diskos-installer",
description="Standalone diskOS installer for the FiiO Snowsky Disc.")
p.add_argument("--version", action="version", version=f"diskos-installer {__version__}")
# no subcommand -> GUI (running ./diskos-installer with no arguments); subcommands = CLI
sub = p.add_subparsers(dest="cmd", required=False)
sub.add_parser("gui", help="launch the graphical installer (also the default with no arguments)")
d = sub.add_parser("doctor", help="report host, bundled tools, device, and state")
d.set_defaults(func=cmd_doctor)
i = sub.add_parser("install", help="build from YOUR stock firmware and flash diskOS")
i.add_argument("--firmware", help="FiiO official update .zip (your stock firmware)")
i.add_argument("--stock", help="pre-extracted stock rootfs.squashfs (instead of --firmware)")
i.add_argument("--ui", help="diskOS UI binary (default: bundled mq_ui)")
i.add_argument("--variant", choices=["public", "dev"], default="public",
help="public (no always-on shell; Debug Mode enables SSH on demand) or dev (adds an ALWAYS-ON PASSWORDLESS ROOT SHELL over "
"USB - anyone with physical access gets root every boot; dev devices only)")
i.add_argument("-y", "--yes", action="store_true", help="don't prompt before flashing")
i.set_defaults(func=cmd_install)
r = sub.add_parser("restore-stock", help="deactivate diskOS (reflash your saved stock rootfs)")
r.add_argument("--firmware", help="FiiO update .zip (only needed if no stock image is saved)")
r.add_argument("-y", "--yes", action="store_true", help="don't prompt before flashing")
r.set_defaults(func=cmd_restore_stock)
rm = sub.add_parser("remove", help="delete the installer and everything it created")
rm.add_argument("-y", "--yes", action="store_true", help="don't prompt")
rm.add_argument("--force", action="store_true", help="remove even if diskOS is still on the device")
rm.set_defaults(func=cmd_remove)
return p
def main(argv=None):
args = build_parser().parse_args(argv)
# no subcommand, or 'gui' -> launch the graphical installer (under the lock)
if getattr(args, "cmd", None) in (None, "gui"):
from diskos_installer import gui
with _Lock():
return gui.main()
try:
if args.cmd == "doctor": # doctor is read-only; no lock needed
return args.func(args)
with _Lock():
return args.func(args)
except errors.DiskOSError as e: # any coded installer error (Build/Flash/Preflight)
ui.err(str(e))
return 1
except KeyboardInterrupt:
ui.err("interrupted.")
return 130
if __name__ == "__main__":
sys.exit(main())