Files
Johannes Plunien 4ae1659a0c feat: slim a shutdown simulator by writing its launchd overrides offline (#49)
Slimming a simulator cost a boot, one `launchctl` spawn per label, and a
reboot to apply them — 2m14s for the default 170-label profile. A device
that is already shut down does not need any of that: launchd_sim is a host
process, and it reads its disable overrides at startup from a per-device
host directory keyed only by UDID, next to the device's launchd.log. Writing
them there before the boot makes the simulator come up slim the first time.

Measured on an iPhone 17 Pro / iOS 26.5: a first slim goes from 2m14s to
33s, nearly all of it the boot itself, and `on --preserve-boot-state`
from a cold device takes 15s. `verify` reports 170/170 and 78 daemons
remain, matching a simulator slimmed the old way.

The path is an undocumented CoreSimulator detail, so it is best-effort
rather than trusted. Writes merge, leaving both the runtime's own
explicit-enable entries and any unmanaged label untouched. The booted
device is always read back with `print-disabled`, and an unwritable store
or a runtime that ignores it falls back to the `launchctl` path instead of
failing or reporting a slim simulator that isn't one.
2026-09-19 06:44:39 +02:00

170 lines
5.0 KiB
Go

package simslim
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
)
// launchd_sim is a host process, so a simulator's launchd disable overrides are
// not in the device's data directory: they live in the same per-device host
// directory as its launchd.log, keyed only by UDID. launchd_sim reads the file
// when it starts and rewrites it on every `launchctl disable`/`enable`, which is
// why the overrides survive `simctl erase` but not `simctl clone`.
//
// disabledStoreRoot is a var so tests can redirect it away from the real host path.
var disabledStoreRoot = "/private/var/tmp"
func disabledStoreDir(udid string) string {
return filepath.Join(disabledStoreRoot, "com.apple.CoreSimulator.SimDevice."+udid)
}
func disabledStorePath(udid string) string {
return filepath.Join(disabledStoreDir(udid), "disabled.plist")
}
// readDisabledStore returns the labels the store marks disabled, mirroring
// readDisabled's contract: a label absent from the result is enabled. A device
// that has never booted has no store, which is the same thing as nothing
// disabled, so a missing file is not an error.
func readDisabledStore(udid string) (map[string]bool, error) {
entries, err := readStoreEntries(udid)
if err != nil {
return nil, err
}
disabled := map[string]bool{}
for label, off := range entries {
if off {
disabled[label] = true
}
}
return disabled, nil
}
// readStoreEntries returns every entry verbatim, including the explicit
// false (enabled) entries the runtime writes for itself, which a merge must
// preserve.
func readStoreEntries(udid string) (map[string]bool, error) {
f, err := os.Open(disabledStorePath(udid))
if errors.Is(err, fs.ErrNotExist) {
return map[string]bool{}, nil
}
if err != nil {
return nil, err
}
defer f.Close()
return parseStoreEntries(f)
}
// writeDisabledStore merges the transitions into the store, leaving every other
// entry exactly as it was: the runtime writes its own explicit-enable entries
// here, and labels outside the managed set are never simslim's business. The
// write is atomic so launchd_sim can never read a half-written store.
func writeDisabledStore(udid string, toDisable, toEnable []string) error {
entries, err := readStoreEntries(udid)
if err != nil {
return err
}
for _, label := range toDisable {
entries[label] = true
}
for _, label := range toEnable {
entries[label] = false
}
dir := disabledStoreDir(udid)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create launchd disable store: %w", err)
}
temp, err := os.CreateTemp(dir, "disabled.plist.*")
if err != nil {
return fmt.Errorf("write launchd disable store: %w", err)
}
defer os.Remove(temp.Name())
if _, err := temp.Write(encodeStoreEntries(entries)); err != nil {
temp.Close()
return fmt.Errorf("write launchd disable store: %w", err)
}
if err := temp.Close(); err != nil {
return fmt.Errorf("write launchd disable store: %w", err)
}
if err := os.Chmod(temp.Name(), 0o644); err != nil {
return fmt.Errorf("write launchd disable store: %w", err)
}
if err := os.Rename(temp.Name(), disabledStorePath(udid)); err != nil {
return fmt.Errorf("write launchd disable store: %w", err)
}
return nil
}
// encodeStoreEntries renders the store in the same XML plist shape launchd_sim
// writes, sorted so a rewrite that changes nothing produces an identical file.
func encodeStoreEntries(entries map[string]bool) []byte {
var b bytes.Buffer
b.WriteString(xml.Header)
b.WriteString("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n")
b.WriteString("<plist version=\"1.0\">\n<dict>\n")
for _, label := range sortedKeys(entries) {
b.WriteString("\t<key>")
xml.EscapeText(&b, []byte(label))
b.WriteString("</key>\n\t<")
if entries[label] {
b.WriteString("true")
} else {
b.WriteString("false")
}
b.WriteString("/>\n")
}
b.WriteString("</dict>\n</plist>\n")
return b.Bytes()
}
func sortedKeys(entries map[string]bool) []string {
labels := make([]string, 0, len(entries))
for label := range entries {
labels = append(labels, label)
}
sort.Strings(labels)
return labels
}
// parseStoreEntries reads the flat <key>label</key><true/>|<false/> dict that
// launchd_sim writes. Anything else in the file is ignored rather than rejected,
// so an unexpected value type cannot make the store unreadable.
func parseStoreEntries(r io.Reader) (map[string]bool, error) {
entries := map[string]bool{}
decoder := xml.NewDecoder(r)
label := ""
for {
token, err := decoder.Token()
if errors.Is(err, io.EOF) {
return entries, nil
}
if err != nil {
return nil, fmt.Errorf("parse launchd disable store: %w", err)
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
switch start.Name.Local {
case "key":
var text string
if err := decoder.DecodeElement(&text, &start); err != nil {
return nil, fmt.Errorf("parse launchd disable store: %w", err)
}
label = text
case "true", "false":
if label != "" {
entries[label] = start.Name.Local == "true"
label = ""
}
}
}
}