Skip to content

feat: verify drive at startup, make UMS prep cancellable - #5

Open
teal-bauer wants to merge 1 commit into
mainfrom
cancellable-prep
Open

feat: verify drive at startup, make UMS prep cancellable#5
teal-bauer wants to merge 1 commit into
mainfrom
cancellable-prep

Conversation

@teal-bauer

@teal-bauer teal-bauer commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Drive is verified (fsck + size) at service startup, not lazily on first UMS entry
  • Virtual drive created as a sparse file via os.Truncate; 1GB creation drops from ~10s to ~1s (2.1MB physical on eMMC)
  • UMS prep is cancellable: a mode=normal request mid-prep cancels the in-flight op, flips {status,step} to idle immediately, and unwinds in the background
  • Left-brake-hold now works during prep, not just during active UMS

Background

Two shortcomings made UMS switching feel unresponsive:

  1. diskMgr.Initialize only checked whether the file existed. fsck.fat was deferred to Mount(), so the first UMS entry after corruption blocked the user behind a full drive recreation. And that recreation was a dd if=/dev/zero bs=1M count=1024, which takes >30 seconds on eMMC.
  2. handleModeChange held s.mu across the entire prep. A mode=normal request mid-prep queued behind mount, settings copy, diagnostics collection (journalctl 8h + 30s DBC SSH), and unmount. scootui kept showing "preparing storage" for tens of seconds after the user asked to exit.

Changes

pkg/disk/manager.go

  • Initialize runs new verifyOrRecreate: fsck on existing, recreate if missing, smaller than configured, or fsck fails. Larger-than-configured file is kept (don't destroy user data on size decrease).
  • createDriveFile uses os.Truncate for sparse allocation. mkfs.fat writes FAT metadata only, rest stays sparse.
  • Mount no longer runs fsck.
  • Mount/Unmount/CleanDrive take context.Context; subprocess calls use exec.CommandContext. Unmount intentionally ignores its context because a stale loopback mount blocks the next session.

Service lifecycle (internal/service/service.go)

  • New operation { target, ctx, cancel, done } type and single requestMode(target) dispatcher. All three callers (hash watcher, detach loop, brake-hold) route through it.
  • dispatchMu serializes transitions. s.mu is only held for short state updates, not across I/O.
  • Cancelling an in-flight op: cur.cancel() + publishIdle() via SetMany({status:idle, step:""}) + wait on cur.done.
  • UMS prep checks op.ctx.Err() between each step; on cancel, deferred teardown unmounts using context.Background().
  • Post-UMS processing runs under context.Background(). A Mender install mid-cancel is unsafe, so that phase is not cancellable by design.
  • Seeded currentOp as a stable normal state at startup so dispatcher logic has a defined starting point.

Context propagation

  • settings.{CopyToUSB, CopyFromUSB}
  • update.PrepareUSB
  • maps.PrepareUSB
  • wireguard.{PrepareUSB, CopyToUSB, SyncFromUSB}
  • diagnostics.CollectToUSB (biggest win: SSH to DBC and journalctl now get SIGKILL on cancel)
  • rpm.PrepareUSB, scripts.PrepareUSB

internal/service/brake_exit.go

  • Checks s.currentOp.target instead of usbCtrl.GetCurrentMode(), so exit works during prep.

Test plan

Tested on deep-blue:

  • Cold start with drive file deleted: service started + recreated drive in ~1s (vs. ~10s on main). File is 1.0G apparent, 2.1M physical.
  • Cold start with existing healthy drive: startup unchanged, no spurious recreation.
  • redis-cli HSET usb mode ums during prep, then HSET usb mode normal mid-diagnostics: status flips to idle immediately, background teardown completes cleanly, next UMS request succeeds.
  • Same but with left-brake-hold triggering the exit during prep.
  • ums-by-dbc two-disconnect flow still works.
  • Full round-trip with updates + wireguard changes on the drive, verify processing phase completes uninterrupted.

Two shortcomings made UMS mode switching feel unresponsive:

1. Drive integrity was only checked lazily inside Mount(), so corruption
   surfaced on the first UMS entry after boot, then blocked the user
   behind a full drive recreation. Creation itself was a 10-second
   dd-of-1GB.

2. handleModeChange held the service mutex across the entire prep, so
   a mode=normal request mid-prep queued behind a multi-second mount,
   copy, diagnostics collection (journalctl + 30s DBC SSH), and unmount.
   scootui kept showing "preparing storage" long after the user had
   asked to exit.

Changes:

- disk.Manager.Initialize now runs verifyOrRecreate at service start:
  fsck.fat on the existing file, recreate if missing/smaller than
  configured/corrupt. Drops fsck from Mount's hot path.
- createDriveFile uses os.Truncate instead of dd, producing a sparse
  1GB file. mkfs.fat writes only FAT metadata (~2.1MB on eMMC) so the
  file stays sparse until the host writes to it. Cold creation drops
  from ~10s to ~1s.
- Introduced operation { target, ctx, cancel, done } and a single
  requestMode dispatcher. Mode changes cancel any in-flight op, wait
  for teardown, then install a new op. Ops run in goroutines.
- Context threaded through settings, update, maps, wireguard,
  diagnostics, rpm, scripts Prepare/Copy calls and through disk
  Mount/Unmount/CleanDrive. exec.Command replaced with
  exec.CommandContext so SIGKILL propagates on cancel.
- On cancel, publisher.SetMany atomically writes {status:idle, step:""}
  before waiting for teardown, so scootui flips out of "preparing"
  immediately.
- Brake-hold handler checks for any in-flight UMS op, not just an
  active gadget mode, so exiting works during prep too.
- Post-UMS processing phase (Mender install, DBC transfers) keeps a
  Background context. Cancelling mid-install is unsafe.

Tested on deep-blue: startup drive recreation is ~1s, file is 2.1MB
physical / 1GB apparent.
@teal-bauer
teal-bauer requested a review from Zanooda April 22, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant