Skip to content

iOS slim writes the launchd overrides before one boot, reads them back, and destroy removes their folder #1196

Closed

Description

@V3RON

Part of #1192.

Scope

After this PR, a slim lease on any iOS runtime from 18.5 up gets a simulator whose slim services are off from its one boot, proven by a readback, and a destroyed simulator leaves no folder in /private/var/tmp. It replaces the in-simulator slim pass (F1) with ADR 0060: write the launchd overrides file while the simulator is shut down, boot once, read back. It re-syncs the label list to simslim v0.13.0 and supersedes #206.

One task: the file write, the readback, the estimate and the label list all change one step (makeReady's slim branch) and its one test block, and splitting them would ship a release with two slim mechanisms.

In short

A slim simulator now has its services turned off before it boots, so it boots once and needs no shell inside the runtime. After the boot Simlock checks which services are off, and that check decides the mode: if it disagrees, the lease gets a full device and the worker log says why. Deleting a simulator also deletes its folder in /private/var/tmp.

flowchart LR
  A[create or erase] --> B["write disabled.plist<br/>(changed)"] --> C[boot once<br/>(changed: was boot, pass, reboot)] --> D["read back<br/>(new)"] --> E[slim or full]
  F[destroy] --> G[simctl delete] --> H["remove /private/var/tmp folder<br/>(new)"]
Loading

New term: launchd overrides file, /private/var/tmp/com.apple.CoreSimulator.SimDevice.<udid>/disabled.plist, an XML property list mapping a launchd label to true (off) or false (on), e.g. com.apple.tipsd → true.

The slim-skipped log line (warn, from onSlimSkipped, F12) carries one of these details, word for word, <n>, <m>, <label> and <error> filled in:

  • <n> of <m> managed labels did not read back as written at boot; first: <label> (device granted full; <m> counts every managed label written, true and false)
  • could not write the launchd overrides file: <error> (device granted the mode the readback shows)
  • could not read back the launchd overrides: <error> (device granted slim)

One prepare reports at most one slim-skipped fact. When the write failed and the readback then disagrees or fails, the details are joined with ; , write first.

A failed folder removal on destroy logs warn could not remove the launchd overrides folder of <udid>: <error>.

If this goes wrong, a user sees slim leases granted full on runtimes that worked before.

Technical spec

Facts

Commit 019ba5c unless named.

# Fact Source
F1 Today's pass runs simctl spawn <udid> /bin/sh -c with launchctl disable system/<label> in chunks of 50, each limited to 60 s, then shuts down and boots again src/drivers/ios/index.ts:1053-1066, :162, :166, :899-917
F2 The iOS 26.4 and 27.0 runtimes on a maintainer Mac have only df and launchctl in RuntimeRoot/bin ls <runtime>/Contents/Resources/RuntimeRoot/bin printed df launchctl for both (Xcode 27.0, 2026-11-02)
F3 simctl erase and simctl delete leave /private/var/tmp/com.apple.CoreSimulator.SimDevice.<udid>/disabled.plist in place, in a private device set xcrun simctl --set <tmp> create/erase/delete on Xcode 27.0 printed after erase: disabled.plist and after delete: disabled.plist
F4 A disabled.plist written before a new iOS 27.0 simulator's first boot is honoured at that boot, and the file is not rewritten by it maintainer Mac, 2026-11-02: three labels written <true/>, one boot + bootstatus -b (25 s), launchctl print-disabled system printed "com.apple.tipsd" => disabled and the two others; the file afterwards held the same three entries
F5 The file is an XML plist, a flat dict of bare label (no system/) to <true/> (disabled) or <false/> (explicit enable); simslim merges, writes a temp file in the same folder and renames it, file mode 0644, folder mode 0700 simslim v0.13.0 (d594c230) disabled_store.go:22-30, :69-99, :107-125, :81
F6 launchctl print-disabled system prints one line per entry, "<label>" => disabled or => enabled; simslim also accepts true as disabled e2e/slow-ios-slim.test.ts:44-63; F4's output; simslim simctl.go:423-446
F7 makeReady boots, then for prepare + slim + 18.5+ runs the pass; recover, full mode, no slim config → no pass; below 18.5 or an unparsable runtime id → skipped with runtime-too-old / unknown-runtime src/drivers/ios/index.ts:792-813, :2301-2326, :2288-2294
F8 reclaim shuts down, erases and rewrites the mark; destroy and destroyLegacy shut down and run simctl delete; nothing in src/ touches /private/var/tmp src/drivers/ios/index.ts:1093-1101, :1115-1118, :1148-1153; grep -rn "var/tmp|disabled.plist" src/ → no hits
F9 The driver keeps slimSignature and slimMarkToken in driver data for idempotence, stored in nullable ios_device_data columns src/drivers/ios/index.ts:184-185, :979-994, :1009-1013; src/drivers/ios/device-data.ts:8-9, :43
F10 A slim spec's boot estimate is SLIM_BOOT_ESTIMATE_MS (150 000 ms), a full one COLD_BOOT_ESTIMATE_MS (60 000 ms) src/drivers/ios/index.ts:156, :100, :1228-1234
F11 The Filesystem port has mkdirp(path, {mode}), writeFileAtomic(path, contents, {mode}), readFile, and rm, which is recursive and forced; MemoryFilesystem implements all four src/ports/filesystem.ts:27-32, :52-76, :213-214, :451-457
F12 onSlimmed carries deviceId, address, categories, labelCount, durationMs, signature, unknownLabels; onSlimSkipped carries deviceId, reason ("runtime-too-old" | "unknown-runtime" | "apply-failed"), detail and the daemon logs it as a warning; onSlimmed is bridged to device.slimmed src/drivers/ios/index.ts:188-202; src/daemon/main.ts:1386, :1586
F13 slim-labels.ts copies simslim's profiles.go at 65bda5bd; at v0.13.0 navd is in widgets and assistantd, gamed and sharingd are AlwaysEnabled (never disabled); sharingd is not in Simlock's list src/drivers/ios/slim-labels.ts:3-9, :26-28, :34, :214; simslim v0.13.0 profiles.go:43-46, :85-90, :275-280, :317-321
F14 A device's mode is fixed for its life, and a request takes a warm device only of its own mode src/core/domain.ts:28-40; src/core/warm-pool/policy.ts:252-258
F15 The driver has an optional logger, used today only by the device input src/drivers/ios/index.ts:223-224
F16 The slow lane picks the first runtime the catalog pairs with the first paired model, leases a cold slim device on a default-slim worker and asserts every label in ALL_LABELS reads disabled and exactly one device.slimmed with labelCount equal to ALL_LABELS.size; a second test asserts a recovered slim device still reads every label disabled e2e/slow-ios-slim.test.ts:80-91, :155-190, :261
F17 The unit tests drive the driver with ScriptedProcessRunner and MemoryFilesystem src/drivers/ios/index.test.ts:22, :2884-2896

Modules touched

Searches: slimSignature, slimMarkToken, SLIM_, applySlim, sanitizeSlimLabels, labelsFor, destroy(, estimate(, slim in docs/ and website/.

flowchart LR
  core[core ManagedDeviceLifecycle] --> drv["drivers/ios/index.ts (changed)"]
  drv --> labels["drivers/ios/slim-labels.ts (changed)"]
  drv --> fs[ports/filesystem]
  drv --> simctl[xcrun simctl]
  daemon[daemon/main.ts bridge] -.callbacks.-> drv
Loading

Files: src/drivers/ios/index.ts, src/drivers/ios/slim-labels.ts, src/drivers/ios/index.test.ts, src/drivers/ios/slim-labels.test.ts, e2e/slow-ios-slim.test.ts, src/drivers/ios/AGENTS.md, docs/internal/EVENTS.md, website/src/content/docs/docs/worker/capacity.mdx, website/src/content/docs/docs/reference/configuration.mdx, website/src/content/docs/docs/using-devices/choosing-a-device.mdx. src/drivers/ios/device-data.ts keeps its columns (ADR 0060 §3).

sequenceDiagram
  participant C as core
  participant D as iOS driver
  participant F as /private/var/tmp/...<udid>
  participant S as simctl
  C->>D: makeReady(prepare, slim)
  D->>F: read, merge, write (atomic)
  alt write fails
    D-->>D: log reason, still boot
  end
  D->>S: boot, bootstatus -b, listapps
  D->>S: spawn launchctl print-disabled system
  alt every label as written
    D-->>C: mode slim (+ onSlimmed)
  else disagrees
    D-->>C: mode full (+ onSlimSkipped)
  else readback fails or times out
    D-->>C: mode slim (+ onSlimSkipped)
  end
Loading

Contract and event changes

None to the wire. device.slimmed keeps its fields (F12); its trigger in docs/internal/EVENTS.md becomes "after a successful write and a readback showing every managed label as written"; a failed write followed by a matching readback grants slim with a slim-skipped line and no event (decided in delivery, pending the maintainer). A write skipped because the file is already as wanted counts as successful: a matching readback grants slim and fires device.slimmed (decided in delivery, pending the maintainer). A recover boot writes nothing, reads nothing back and returns the mode as today (decided in delivery, pending the maintainer). labelCount is the number of labels written true; unknownLabels is the unknown category names from resolveSlimCategories; durationMs runs from the start of the file read to the end of the readback. signature stays slimSignature(categories). SlimSkippedFact.reason keeps its three values; the new details use apply-failed.

Managed labels: every label in SLIM_CATEGORIES plus the never-disable labels. A managed label in a configured category is written true, every other managed label false (ADR 0060 §1). Configured categories that resolve to none known: skipped and granted full, nothing written, as today (index.ts:870-878).

The file read from disk: a key is a label, the value true or false. An entry Simlock does not manage is kept as it is. A file that does not parse as a plist dict is left unchanged and counts as a failed write: logged, the device boots, and the readback decides its mode.

The readback (launchctl print-disabled system): a line "<label>" => <value>; disabled or true reads disabled, anything else or no line reads enabled. A label written true is as wanted when it reads disabled; one written false when it reads enabled.

Other code on the same state

Searches: disabled.plist, var/tmp, CoreSimulator.SimDevice in src/ (0 hits); launchctl in src/ (only F1's script); slimSignature|slimMarkToken (index.ts, device-data.ts).

  • launchd_sim (host process) reads the file at boot and rewrites it on launchctl disable/enable. Simlock writes only while the device is shut down (after create or reclaim), so they never write at once.
  • reclaim erases; the file survives (F3), so the next prepare finds it as wanted and only boots and reads back.
  • A lease holder running xcrun simctl spawn <udid> launchctl enable ... changes the file through launchd; the next prepare (after reclaim) writes it back.
  • device-data.ts still reads the two columns; the driver stops writing them.

Failure modes

Call Outcome What happens Limit, counted from
read existing file absent treated as empty none (local file)
read existing file not a plist dict slim skipped, could not write ..., file left as is, device boots, readback decides none
mkdirp folder 0700 + writeFileAtomic 0644 throws slim skipped, could not write ..., device boots, readback decides none
boot as today BootTimeoutError as today ios.slim.bootTimeoutMs, from simctl boot
simctl spawn <udid> launchctl print-disabled system ok, all as written slim, onSlimmed 120 s, from the spawn
same ok, any label not as written full, slim skipped with count and first label 120 s
same non-zero exit or timeout slim; slim skipped, could not read back ... 120 s
rm folder on destroy / destroyLegacy throws logged warn, destroy resolves none
simctl delete on destroy fails throws as today, folder not removed 30 s, as today

A restart between the write and the boot leaves the file written; the next prepare finds it as wanted. Two makeReady calls never run on one device at once (core holds the device).

Rules in play

  • ADR 0060 §1-§6: "writes the launchd overrides while the simulator is shut down" — the whole task.
  • ADR 0002 §4 (kept): "Runtimes older than iOS 18.5 are skipped" — gating unchanged.
  • ADR 0002 §6 crash recovery (kept): "a plain boot with no slim pass" — recover writes nothing.
  • ADR 0007: the registry is the only record of a device's mode — the driver returns mode; nothing new stored.
  • Safety rule 1: "only ... devices ... that exist in its own registry" — the folder removed is the destroyed registry device's own UDID.
  • Vendor rule (MIT): slim-labels.ts keeps simslim's notice, now naming v0.13.0.
  • Documentation rule 9: the website pages that say "boots twice" change in this PR.

Tests

Seam: IosSimctlDriver (makeReady, destroy, estimate) with ScriptedProcessRunner and MemoryFilesystem, the store root passed as a driver option; model on the slim mode block of src/drivers/ios/index.test.ts. Labels: src/drivers/ios/slim-labels.test.ts. Real boot: e2e/slow-ios-slim.test.ts.

  • The core prepares a slim device on iOS 18.5+: the overrides file holds every label of the configured categories true and every other managed label false before the one boot, and the device comes back slim with one onSlimmed — cases: no file yet; file with the runtime's own entries (kept); file already as wanted (not rewritten, still booted and read back, one onSlimmed); file slimmed under wider categories (dropped labels now false). The scripted runner shows one boot and no /bin/sh.
  • The core prepares a slim device and the readback disagrees or fails: the device comes back with ADR 0060 §2's mode and one onSlimSkipped with the detail above — cases: a true label reads enabled (full); a true label missing (full); a true label reads true (as wanted: slim, onSlimmed, no skip fact); a false label missing (as wanted: slim, onSlimmed, no skip fact); a false label reads disabled (full); readback exits non-zero (slim); readback times out (slim).
  • The core prepares a slim device and the file cannot be written: the device still boots, the readback decides its mode (ADR 0060 §2), and onSlimSkipped says the write failed — cases: writeFileAtomic throws, readback all as wanted (slim, no onSlimmed); mkdirp throws, readback disagrees (full, one fact with both details); write throws, readback fails (slim, one fact with both details); existing file not a plist dict (left unchanged, readback decides).
  • The core prepares a device that must not be slimmed: the overrides file is not touched and no readback runs — cases: full mode; purpose: "recover"; runtime 18.4; runtime id that does not parse; configured categories none known (full). Extends "a purpose: 'recover' boot never applies slim" (index.test.ts:2784).
  • The core destroys a simulator: its overrides folder is removed after simctl delete, and a failed removal is logged without failing the destroy — cases: removal succeeds; rm throws; simctl delete fails (folder kept, error thrown); destroyLegacy removes it too (extends index.test.ts:1626).
  • doctor asks for a slim spec's boot estimate: it gets COLD_BOOT_ESTIMATE_MS — cases: slim spec; full spec. Rewrites "estimates the boot cost with the slim reboot+apply budget" (index.test.ts:2884).
  • The label list matches simslim v0.13.0 — cases: navd in widgets; assistantd, gamed and sharingd in no category and in the never-disable list.
  • On a real Mac, a slim lease on the newest installed iOS runtime and a re-lease of the same released simulator are granted slim with every label disabled after one boot — cases: cold lease (extends the test at e2e/slow-ios-slim.test.ts:155; catalogModelAndOs picks the highest version the catalog pairs with the model, not the first); release, then lease the same device again.

Done when

  • pnpm exec vitest run --project unit src/drivers/ios passes.
  • grep -rn '/bin/sh' src/drivers/ios/index.ts finds nothing.
  • Slow lane, real simulator: e2e/slow-ios-slim.test.ts passes on a Mac whose newest iOS runtime is 27.0.
  • src/drivers/ios/AGENTS.md, docs/internal/EVENTS.md and the three website pages no longer say a slim device boots twice, that erase removes the overrides, or that narrowing ios.slim.categories never turns a service back on.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Nov 2, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Nov 2, 2026
  3. self-assigned this
    on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions