Files
simslim-mirror/slim.go
Interlap 8afc0f61f5 feat: auto-discover simulator device sets (parallel-testing clones) (#9)
simslim only ever talked to the default CoreSimulator device set, so
Xcode parallel-testing clones — which live in a separate set — were
invisible to `list` and any command against a clone UDID failed with
"Invalid device or device pair".

simctl cannot enumerate device sets, so simslim now probes the two
well-known locations automatically: the default set and Xcode's
parallel-testing clone set ("testing", ~/Library/Developer/XCTestDevices).

- `list` scans both sets and tags each row (and the JSON `set` field) so
  clones are visible with no extra flags.
- Per-UDID commands resolve which set a UDID lives in via findDevice and
  thread that set through every simctl call, so `on`/`boot`/etc. act on a
  clone with no flag needed. The set is passed explicitly (Device.Set),
  not held in a global.
- A global `--set <name|path>` (comma-separated) adds further sets to the
  scan for a device set at a custom path.
- Disk commands take the on-disk data directory straight from the
  dataPath simctl already reports, dropping the hand-rolled set-to-path
  reconstruction.

Fixes #6
2026-07-22 12:27:02 +02:00

121 lines
3.9 KiB
Go

package main
import (
"context"
"fmt"
"sort"
)
// reporter receives human-readable progress lines. A nil reporter is a no-op,
// so non-interactive callers can ignore progress entirely.
type reporter func(string)
func (r reporter) report(msg string) {
if r != nil {
r(msg)
}
}
// ensure brings the device to exactly the desired disabled state and boots it.
// The disabled overrides persist in the device's launchd DB, so once set a slim
// device comes up slim in a single boot; a reboot only happens when the state
// actually changes. A running device is required to read/mutate launchctl, so
// the device is booted first regardless. Each slow phase reports progress so the
// caller can show the user that a multi-minute reconfigure is still working.
func ensure(ctx context.Context, set, udid string, desired map[string]bool, report reporter) (changed bool, err error) {
report.report("Booting the simulator (a first boot can take up to a minute)...")
if err := bootAndWait(ctx, set, udid); err != nil {
return false, err
}
current, err := readDisabled(ctx, set, udid)
if err != nil {
return false, err
}
toDisable, toEnable := delta(current, desired, managedSet())
if len(toDisable) == 0 && len(toEnable) == 0 {
return false, nil
}
if len(toDisable) > 0 {
report.report(fmt.Sprintf("Disabling %d background services...", len(toDisable)))
}
if len(toEnable) > 0 {
report.report(fmt.Sprintf("Re-enabling %d background services...", len(toEnable)))
}
if err := applyDelta(ctx, set, udid, toDisable, toEnable, report); err != nil {
return true, err
}
report.report("Rebooting the simulator to apply the changes...")
if err := shutdown(ctx, set, udid); err != nil {
return true, fmt.Errorf("shutdown before reboot: %w", err)
}
if err := waitShutdown(ctx, set, udid, shutdownTimeout); err != nil {
return true, err
}
return true, bootAndWait(ctx, set, udid)
}
// enableSlim disables the profile's daemons and boots the device slim.
func enableSlim(ctx context.Context, set, udid string, p Profile, report reporter) (bool, error) {
return ensure(ctx, set, udid, p.desired(), report)
}
// disableSlim re-enables every managed daemon, returning the device to stock.
func disableSlim(ctx context.Context, set, udid string, report reporter) (bool, error) {
return ensure(ctx, set, udid, map[string]bool{}, report)
}
// Status describes how slim a device currently is.
type Status struct {
ManagedDisabled int `json:"managedDisabled"` // managed labels currently disabled
ManagedTotal int `json:"managedTotal"` // size of the managed universe
Booted bool `json:"booted"`
}
// status reports how slim a device is and returns the labels it currently has
// disabled (nil when the device is not booted).
func status(ctx context.Context, udid string) (Status, map[string]bool, error) {
d, err := findDevice(ctx, udid, "")
if err != nil {
return Status{}, nil, err
}
return statusForDevice(ctx, d)
}
func statusForDevice(ctx context.Context, d Device) (Status, map[string]bool, error) {
managed := slimmableSet()
st := Status{ManagedTotal: len(managed), Booted: d.State == "Booted"}
if !st.Booted {
return st, nil, fmt.Errorf("simulator must be booted to read its state (it is %s)", d.State)
}
disabled, err := readDisabled(ctx, d.Set, d.UDID)
if err != nil {
return st, nil, err
}
for l := range disabled {
if managed[l] {
st.ManagedDisabled++
}
}
return st, disabled, nil
}
// droppedCategories groups the disabled managed daemons by category, in category
// order, omitting categories with nothing disabled.
func droppedCategories(disabled map[string]bool) []DroppedCategory {
var out []DroppedCategory
for _, c := range Categories {
var labels []string
for _, l := range c.Labels {
if disabled[l] {
labels = append(labels, l)
}
}
if len(labels) == 0 {
continue
}
sort.Strings(labels)
out = append(out, DroppedCategory{ID: c.ID, Name: c.Name, Downside: c.Downside, Labels: labels})
}
return out
}