Files
simslim-mirror/main.go
Interlap cf7080eb2f refactor: parse CLI with urfave/cli/v3
Replace the hand-rolled argument handling with the urfave/cli/v3 command
tree (the project's only dependency; no runtime transitive deps). This
removes the manual command switch, parseInterspersedFlags, jsonOption, and
extractDeviceSets in favor of the library's dispatch and flag parsing.

The command surface is unchanged: same commands, flags, arguments, JSON
output, error format (simslim: <msg>, exit 1), and unknown-command/usage
handling (exit 2). Flags may still appear before or after positional args
(v3 parses flags anywhere by default). --set is now a global flag inherited
by every subcommand, registered in its flag Action with last-wins,
comma-separated semantics. main still owns version, help, and the macOS
guard; the root usage text is preserved verbatim.

The command tree lives in app.go (newApp); each subcommand's Action is a
cmd* function in main.go reading flags via cmd.Bool/String and positionals
via cmd.Args(). Unit tests for the removed helpers are replaced by
app_test.go, which drives newApp through Run.
2026-07-22 13:16:41 +02:00

697 lines
20 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/signal"
"runtime"
"sort"
"strings"
"time"
cli "github.com/urfave/cli/v3"
)
// version is overwritten at build time via -ldflags "-X main.version=...".
var version = "dev"
const (
shutdownTimeout = 30 * time.Second
bootTimeout = 6 * time.Minute // a first slim reconfigure boots twice
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "-v", "--version", "version":
fmt.Printf("simslim %s\n", version)
return
case "-h", "--help", "help":
usage()
return
}
if runtime.GOOS != "darwin" {
fatal("simslim only works on macOS (it drives Apple's iOS simulators).")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := newApp().Run(ctx, os.Args); err != nil {
fatal(err.Error())
}
}
func cmdList(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
if cmd.Args().Len() != 0 {
return fmt.Errorf("list takes no arguments")
}
devices, err := listDevices(ctx)
if err != nil {
return err
}
sort.Slice(devices, func(i, j int) bool {
if devices[i].OSVersion != devices[j].OSVersion {
return devices[i].OSVersion > devices[j].OSVersion
}
return devices[i].Name < devices[j].Name
})
managed := len(slimmableSet())
memoryByUDID := map[string]Measurement{}
memoryErrors := map[string]string{}
if jsonOutput {
bootedUDIDs := make([]string, 0)
for _, device := range devices {
if device.State == "Booted" {
bootedUDIDs = append(bootedUDIDs, device.UDID)
}
}
memoryByUDID, memoryErrors = measureMany(ctx, bootedUDIDs)
}
summaries := make([]DeviceSummary, 0, len(devices))
for _, d := range devices {
tag := "shutdown"
summary := DeviceSummary{Device: d, ManagedTotal: managed}
if d.State == "Booted" {
tag = "booted"
if measurement, ok := memoryByUDID[d.UDID]; ok {
measured := measurement
summary.Memory = &measured
}
summary.MemoryError = memoryErrors[d.UDID]
if st, _, err := statusForDevice(ctx, d); err == nil {
tag = fmt.Sprintf("booted · %d/%d slim", st.ManagedDisabled, managed)
disabled := st.ManagedDisabled
summary.ManagedDisabled = &disabled
} else {
summary.StatusError = err.Error()
}
}
summaries = append(summaries, summary)
if !jsonOutput {
line := fmt.Sprintf("%s %-22s iOS %-6s %s", d.UDID, truncate(d.Name, 22), d.OSVersion, tag)
if d.Set != "" && d.Set != "default" {
line += " (" + d.Set + ")"
}
fmt.Println(line)
}
}
if jsonOutput {
return writeJSON(summaries)
}
return nil
}
func cmdProfiles(_ context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
args := cmd.Args().Slice()
if len(args) > 1 {
return fmt.Errorf("profiles takes at most one category ID (see `simslim profiles`)")
}
if len(args) == 1 {
c, ok := categoryByID(args[0])
if !ok {
return fmt.Errorf("unknown category %q (see `simslim profiles`)", args[0])
}
if jsonOutput {
return writeJSON(c)
}
fmt.Printf("%-14s %s\n", c.ID, c.Name)
fmt.Printf(" %s\n", c.Description)
fmt.Printf(" %d daemons · ~%d MB idle footprint when enabled\n", len(c.Labels), c.ApproxMemoryMB)
fmt.Printf(" When disabled: %s\n", c.Downside)
fmt.Println("\nDaemons:")
for _, l := range c.Labels {
if desc := c.ServiceDescriptions[l]; desc != "" {
fmt.Printf(" %-44s %s\n", l, desc)
} else {
fmt.Printf(" %s\n", l)
}
}
for _, service := range c.AlwaysEnabled {
fmt.Printf(" %-44s Always on — %s\n", service.Label, service.Reason)
}
return nil
}
if jsonOutput {
return writeJSON(Categories)
}
for _, c := range Categories {
fmt.Printf("%-14s %s\n", c.ID, c.Name)
fmt.Printf(" %d daemons · ~%d MB idle footprint when enabled\n", len(c.Labels), c.ApproxMemoryMB)
fmt.Printf(" When disabled: %s\n", c.Downside)
for _, service := range c.AlwaysEnabled {
fmt.Printf(" Always on: %s — %s\n", service.Label, service.Reason)
}
}
fmt.Printf("\n%d daemons across %d categories. Core workflow and deadlock-prone daemons are never disabled.\n",
len(slimmableSet()), len(Categories))
fmt.Println("Memory estimates are iOS 26.5 clean-boot measurements; they vary by runtime and workload and are not additive.")
return nil
}
func cmdStatus(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
showDropped := cmd.Bool("dropped")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
st, disabled, err := status(ctx, udid)
if err != nil {
return err
}
verdict := "stock"
switch {
case st.ManagedDisabled == st.ManagedTotal:
verdict = "slim"
case st.ManagedDisabled > 0:
verdict = "partially slim"
}
var dropped []DroppedCategory
if showDropped {
dropped = droppedCategories(disabled)
}
if jsonOutput {
return writeJSON(StatusOutput{Status: st, Verdict: verdict, Dropped: dropped})
}
fmt.Printf("%s: %d/%d managed daemons disabled (%s)\n", udid, st.ManagedDisabled, st.ManagedTotal, verdict)
if showDropped {
if len(dropped) == 0 {
fmt.Println(" Nothing dropped; every managed daemon is enabled.")
}
for _, c := range dropped {
fmt.Printf(" %-14s %s — %s\n", c.ID, c.Name, c.Downside)
for _, l := range c.Labels {
fmt.Printf(" %s\n", l)
}
}
}
return nil
}
func cmdMeasure(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
m, err := measure(ctx, udid)
if err != nil {
return err
}
if jsonOutput {
return writeJSON(m)
}
fmt.Printf("%s: %d processes, %s memory footprint\n", udid, m.Processes, humanBytes(m.Bytes))
return nil
}
func cmdSize(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
measurement, err := deviceDiskUsage(tctx, udid)
if err != nil {
return err
}
if jsonOutput {
return writeJSON(measurement)
}
fmt.Printf("%s: %s on disk\n", udid, humanBytes(measurement.Bytes))
return nil
}
func cmdDiskCategories(_ context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
if cmd.Args().Len() != 0 {
return fmt.Errorf("disk-categories takes no arguments")
}
if jsonOutput {
return writeJSON(DiskCleanupCategories)
}
for _, category := range DiskCleanupCategories {
availability := "cleanable"
if !category.CanClean {
availability = "measured only"
}
fmt.Printf("%-22s %-28s %s · %s\n", category.ID, category.Name, category.Risk, availability)
fmt.Printf(" %s\n", category.Downside)
}
return nil
}
func cmdDiskPlan(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
plan, err := diskCleanupPlan(tctx, udid)
if err != nil {
return err
}
if jsonOutput {
return writeJSON(plan)
}
fmt.Printf("%s: %s currently cleanable\n", udid, humanBytes(plan.CleanableBytes))
for _, category := range plan.Categories {
availability := ""
if !category.CanClean {
availability = " (iOS-managed; measured only)"
}
fmt.Printf(" %-22s %8s%s\n", category.ID, humanBytes(category.Bytes), availability)
}
if len(plan.Storage) > 0 {
fmt.Println(" Stored data (read only)")
for _, storage := range plan.Storage {
fmt.Printf(" %-20s %8s\n", storage.ID, humanBytes(storage.Bytes))
}
}
return nil
}
func cmdDiskClean(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
if !cmd.Bool("confirm") {
return fmt.Errorf("disk-clean permanently deletes data; pass --confirm after reviewing `simslim disk-plan %s`", udid)
}
categoryIDs := splitList(cmd.String("categories"))
if _, err := validateDiskCleanupSelection(categoryIDs); err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
result, err := cleanDeviceDisk(tctx, udid, categoryIDs, cmd.Bool("preserve-boot-state"))
if err != nil {
return err
}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Cleaned %s and reclaimed %s. Generated contents stay deleted; on-demand assets may download again when needed.\n", udid, humanBytes(result.ReclaimedBytes))
return nil
}
func cmdClone(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
args := cmd.Args().Slice()
if len(args) != 2 {
return fmt.Errorf("clone expects a simulator UDID and a new name")
}
name, err := normalizeSimulatorName(args[1])
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
newUDID, err := cloneDevice(tctx, args[0], name)
if err != nil {
return err
}
result := SimulatorMutationOutput{Action: "clone", UDID: newUDID, Name: name, SourceUDID: args[0]}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Cloned %s as %q (%s).\n", args[0], name, newUDID)
return nil
}
func cmdRename(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
args := cmd.Args().Slice()
if len(args) != 2 {
return fmt.Errorf("rename expects a simulator UDID and a new name")
}
name, err := normalizeSimulatorName(args[1])
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
if err := renameDevice(tctx, args[0], name); err != nil {
return err
}
result := SimulatorMutationOutput{Action: "rename", UDID: args[0], Name: name}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Renamed %s to %q.\n", args[0], name)
return nil
}
func cmdErase(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
if err := eraseDevice(tctx, udid); err != nil {
return err
}
result := SimulatorMutationOutput{Action: "erase", UDID: udid}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Erased simulator %s.\n", udid)
return nil
}
func cmdDelete(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
if err := deleteDevice(tctx, udid); err != nil {
return err
}
result := SimulatorMutationOutput{Action: "delete", UDID: udid}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Deleted simulator %s.\n", udid)
return nil
}
func cmdBoot(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
// Resolve the exact device first so a simctl alias such as "all" can never
// enter the boot path, consistent with erase/delete.
device, err := findDevice(ctx, udid, "")
if err != nil {
return err
}
result := SimulatorMutationOutput{Action: "boot", UDID: udid}
if device.State == "Booted" {
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Simulator %s is already booted.\n", udid)
return nil
}
// Booting and waiting for services can take ~a minute; keep an interactive
// caller informed. Suppressed under --json so machine consumers that read a
// combined stdout/stderr stream get clean JSON.
if !jsonOutput {
fmt.Fprintf(os.Stderr, "Booting %s...\n", udid)
}
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
if err := bootAndWait(tctx, device.Set, udid); err != nil {
return err
}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Booted %s.\n", udid)
return nil
}
func cmdShutdown(ctx context.Context, cmd *cli.Command) error {
jsonOutput := cmd.Bool("json")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
// Resolve the exact device first so a simctl alias such as "all" can never
// enter the shutdown path, consistent with erase/delete.
device, err := findDevice(ctx, udid, "")
if err != nil {
return err
}
result := SimulatorMutationOutput{Action: "shutdown", UDID: udid}
if device.State != "Booted" {
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Simulator %s is already shut down.\n", udid)
return nil
}
tctx, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
if err := shutdown(tctx, device.Set, udid); err != nil {
return err
}
if err := waitShutdown(tctx, device.Set, udid, shutdownTimeout); err != nil {
return err
}
if jsonOutput {
return writeJSON(result)
}
fmt.Printf("Shut down %s.\n", udid)
return nil
}
func cmdOn(ctx context.Context, cmd *cli.Command) error {
preserveBootState := cmd.Bool("preserve-boot-state")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
// Resolve once: this pins the device's set (routing every simctl call below,
// including a parallel-testing clone), fails fast on an unknown UDID, and
// tells us whether to restore a shutdown state afterward.
device, err := findDevice(ctx, udid, "")
if err != nil {
return err
}
originallyShutdown := preserveBootState && device.State == "Shutdown"
p := Profile{ExceptCategories: map[string]bool{}, Keep: map[string]bool{}}
for _, id := range splitList(cmd.String("except")) {
if _, ok := categoryByID(id); !ok {
return fmt.Errorf("unknown category %q (see `simslim profiles`)", id)
}
p.ExceptCategories[id] = true
}
for _, l := range splitList(cmd.String("keep")) {
p.Keep[l] = true
}
fmt.Fprintf(os.Stderr, "Slimming %s: disabling %d background services. The simulator will reboot to apply the changes.\n", udid, len(p.desired()))
report := reporter(func(msg string) { fmt.Fprintln(os.Stderr, msg) })
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
changed, operationErr := enableSlim(tctx, device.Set, udid, p, report)
if originallyShutdown {
shutdownErr := returnToShutdown(ctx, device.Set, udid)
if operationErr != nil && shutdownErr != nil {
return fmt.Errorf("%v; additionally could not restore original shutdown state: %w", operationErr, shutdownErr)
}
if shutdownErr != nil {
return shutdownErr
}
}
if operationErr != nil {
return operationErr
}
if changed {
if originallyShutdown {
fmt.Println("Done. Simulator reconfigured slim and returned to shutdown.")
} else {
fmt.Println("Done. Simulator reconfigured and rebooted slim.")
}
} else {
if originallyShutdown {
fmt.Println("Already slim. Original shutdown state restored.")
} else {
fmt.Println("Already slim. Nothing to change.")
}
}
return nil
}
func cmdOff(ctx context.Context, cmd *cli.Command) error {
preserveBootState := cmd.Bool("preserve-boot-state")
udid, err := oneUDID(cmd.Args().Slice())
if err != nil {
return err
}
device, err := findDevice(ctx, udid, "")
if err != nil {
return err
}
originallyShutdown := preserveBootState && device.State == "Shutdown"
fmt.Fprintf(os.Stderr, "Restoring %s to stock. The simulator will reboot to apply the changes.\n", udid)
report := reporter(func(msg string) { fmt.Fprintln(os.Stderr, msg) })
tctx, cancel := context.WithTimeout(ctx, bootTimeout)
defer cancel()
changed, operationErr := disableSlim(tctx, device.Set, udid, report)
if originallyShutdown {
shutdownErr := returnToShutdown(ctx, device.Set, udid)
if operationErr != nil && shutdownErr != nil {
return fmt.Errorf("%v; additionally could not restore original shutdown state: %w", operationErr, shutdownErr)
}
if shutdownErr != nil {
return shutdownErr
}
}
if operationErr != nil {
return operationErr
}
if changed {
if originallyShutdown {
fmt.Println("Done. All daemons re-enabled and simulator returned to shutdown.")
} else {
fmt.Println("Done. All daemons re-enabled and simulator rebooted.")
}
} else {
if originallyShutdown {
fmt.Println("Already stock. Original shutdown state restored.")
} else {
fmt.Println("Already stock. Nothing to change.")
}
}
return nil
}
func returnToShutdown(ctx context.Context, set, udid string) error {
shutdownCtx, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
if err := shutdown(shutdownCtx, set, udid); err != nil {
return fmt.Errorf("restore original shutdown state: %w", err)
}
if err := waitShutdown(shutdownCtx, set, udid, shutdownTimeout); err != nil {
return fmt.Errorf("restore original shutdown state: %w", err)
}
return nil
}
func oneUDID(args []string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("expected exactly one simulator UDID (see `simslim list`)")
}
return args[0], nil
}
func splitList(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func truncate(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {
return s
}
if n <= 1 {
return "…"
}
return string(runes[:n-1]) + "…"
}
func humanBytes(b int64) string {
switch {
case b >= 1<<30:
return fmt.Sprintf("%.2f GB", float64(b)/(1<<30))
case b >= 1<<20:
return fmt.Sprintf("%.0f MB", float64(b)/(1<<20))
case b >= 1<<10:
return fmt.Sprintf("%.0f KB", float64(b)/(1<<10))
default:
return fmt.Sprintf("%d B", b)
}
}
func fatal(msg string) {
fmt.Fprintln(os.Stderr, "simslim: "+msg)
os.Exit(1)
}
func usage() {
fmt.Print(`simslim runs more iOS simulators on the same Mac by disabling the
background daemons a simulator does not need.
USAGE
simslim <command> [args]
GLOBAL OPTIONS
--set <name|path> Also scan these device sets (comma-separated). The default
and Xcode ` + "`testing`" + ` (parallel-testing) sets are always
scanned; use this to add a set at a custom path.
COMMANDS
list List available simulators and their slim status
profiles [id] Show the daemon categories a slim boot disables;
pass a category ID to list that category's daemons
on <udid> Slim a simulator (persist disables + reboot slim)
--except ids Leave these categories enabled (comma-separated)
--keep labels Keep these individual daemons running (comma-separated)
--preserve-boot-state
Return an initially shutdown simulator to shutdown
off <udid> Restore a simulator to stock (re-enable + reboot)
--preserve-boot-state
Return an initially shutdown simulator to shutdown
status <udid> Report how slim a booted simulator is
--dropped Also list the disabled daemons grouped by category
measure <udid> Report a booted simulator's memory footprint
size <udid> Report a simulator's allocated disk footprint
disk-categories Show cleanup and protected system-asset categories
disk-plan <udid> Measure reclaimable and stored data without deleting
disk-clean <udid> Permanently delete selected per-device data
--categories ids Comma-separated cleanable category IDs
--confirm Required acknowledgement of permanent deletion
--preserve-boot-state
Reboot a simulator that was booted before cleanup
clone <udid> <name> Clone a simulator, preserving its current boot state
rename <udid> <name> Rename a simulator
boot <udid> Boot a simulator and wait for its services
shutdown <udid> Shut down a booted simulator
erase <udid> Erase a simulator's apps, data, and settings
delete <udid> Permanently delete a simulator
--json Print machine-readable JSON (also supported by list,
profiles, status, and simulator-management commands)
version Print the version
Disabling is persistent (stored in the simulator's launchd overrides) and fully
reversible with ` + "`simslim off`" + `. Deadlock-prone daemons are never touched.
Cleanup permanently removes existing cache, log, diagnostic, and temporary-file
contents; Erase does not bring that history back, although new generated data
appears as iOS and apps run. Downloaded language data is opt-in and may return
when a feature needs it. Required Siri assets are informational only because iOS
restores them after deletion. simslim never modifies the shared iOS runtime.
https://github.com/mobai-app/simslim · by MobAI (https://mobai.run)
`)
}