mirror of
https://github.com/MobAI-App/simslim.git
synced 2026-10-13 04:49:07 +02:00
* feat: watch slims parallel-testing clones as they boot xcodebuild parallel testing creates a stock clone per worker in its own device set and deletes them per run; disable overrides do not survive simctl clone, so there is no device to pre-slim. watch polls the device sets and applies a no-reboot slim to each simulator once it boots, then leaves it alone, reusing EnableSlimNoReboot from #42. Answers the ephemeral-clone case raised in #41. * fix: watch re-slims rebooted clones and retries failed slims Review fixes on the watch scan loop: - Drop a UDID from seen when a scan finds it non-Booted. Below iOS 18.5 the no-reboot slim ends with the boot session, so a clone that booted again came back stock and was skipped for the rest of the run. - Drop a UDID from seen when its slim fails, so the next scan retries it instead of leaving that clone stock behind one stderr line. - Wait for the in-flight slim goroutines before Watch returns, so an importer holds no goroutine past the call. - Pass the slim a Reporter that prefixes each line with the UDID, so the per-device progress reaches the caller. seen is now mutated by the scan loop and by the slim goroutines, so it moves into a watcher behind a mutex. go test -race is green.
146 lines
6.4 KiB
Go
146 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
cli "github.com/urfave/cli/v3"
|
|
|
|
"github.com/mobai-app/simslim"
|
|
)
|
|
|
|
// newApp builds the command tree. It handles subcommand dispatch, the global
|
|
// --set flag, and per-command flag parsing.
|
|
func newApp() *cli.Command {
|
|
jsonFlag := func() cli.Flag {
|
|
return &cli.BoolFlag{Name: "json", Usage: "print machine-readable JSON", OnlyOnce: true}
|
|
}
|
|
preserveBootStateFlag := func(usage string) cli.Flag {
|
|
return &cli.BoolFlag{Name: "preserve-boot-state", Usage: usage}
|
|
}
|
|
|
|
commands := []*cli.Command{
|
|
{Name: "list", Flags: []cli.Flag{jsonFlag(), &cli.BoolFlag{Name: "booted", Usage: "only list booted simulators"}}, Action: cmdList},
|
|
{Name: "profiles", Flags: []cli.Flag{jsonFlag()}, Action: cmdProfiles},
|
|
{Name: "profile", Action: cmdNewProfile},
|
|
{Name: "status", Flags: []cli.Flag{jsonFlag(), &cli.BoolFlag{Name: "dropped", Usage: "list the disabled launchd labels grouped by category"}}, Action: cmdStatus},
|
|
{Name: "verify", Flags: []cli.Flag{
|
|
jsonFlag(),
|
|
&cli.StringFlag{Name: "profile", Usage: "verify against a JSON profile file (mutually exclusive with --except/--keep)"},
|
|
&cli.StringFlag{Name: "except", Usage: "comma-separated category IDs the profile leaves fully enabled (see `simslim profiles`)"},
|
|
&cli.StringFlag{Name: "keep", Usage: "comma-separated launchd labels the profile keeps running"},
|
|
}, Action: cmdVerify},
|
|
{Name: "doctor", Flags: []cli.Flag{
|
|
jsonFlag(),
|
|
&cli.StringFlag{Name: "requires", Usage: "comma-separated feature IDs the simulator must support (see `simslim doctor --list`)"},
|
|
&cli.BoolFlag{Name: "list", Usage: "list every checkable feature and its backing daemons"},
|
|
}, Action: cmdDoctor},
|
|
{Name: "measure", Flags: []cli.Flag{jsonFlag()}, Action: cmdMeasure},
|
|
{Name: "top", Flags: []cli.Flag{jsonFlag()}, Action: cmdTop},
|
|
{Name: "size", Flags: []cli.Flag{jsonFlag()}, Action: cmdSize},
|
|
{Name: "disk-categories", Flags: []cli.Flag{jsonFlag()}, Action: cmdDiskCategories},
|
|
{Name: "disk-plan", Flags: []cli.Flag{jsonFlag()}, Action: cmdDiskPlan},
|
|
{Name: "disk-clean", Flags: []cli.Flag{
|
|
jsonFlag(),
|
|
&cli.StringFlag{Name: "categories", Usage: "comma-separated cleanable disk category IDs"},
|
|
&cli.BoolFlag{Name: "confirm", Usage: "confirm permanent deletion"},
|
|
preserveBootStateFlag("reboot a simulator that was booted before cleanup"),
|
|
}, Action: cmdDiskClean},
|
|
{Name: "clone", Flags: []cli.Flag{jsonFlag()}, Action: cmdClone},
|
|
{Name: "repair-clone", Flags: []cli.Flag{jsonFlag()}, Action: cmdRepairClone},
|
|
{Name: "rename", Flags: []cli.Flag{jsonFlag()}, Action: cmdRename},
|
|
{Name: "boot", Flags: []cli.Flag{jsonFlag()}, Action: cmdBoot},
|
|
{Name: "shutdown", Flags: []cli.Flag{jsonFlag()}, Action: cmdShutdown},
|
|
{Name: "erase", Flags: []cli.Flag{jsonFlag()}, Action: cmdErase},
|
|
{Name: "delete", Flags: []cli.Flag{jsonFlag()}, Action: cmdDelete},
|
|
{Name: "on", Flags: []cli.Flag{
|
|
&cli.StringFlag{Name: "profile", Usage: "apply a JSON profile file (mutually exclusive with --except/--keep)"},
|
|
&cli.StringFlag{Name: "except", Usage: "comma-separated category IDs to leave fully enabled (see `simslim profiles`)"},
|
|
&cli.StringFlag{Name: "keep", Usage: "comma-separated launchd labels to keep running"},
|
|
&cli.BoolFlag{Name: "no-reboot", Usage: "stop the daemons in the current boot session without a reboot (works on every runtime; lost at the next reboot on iOS < 18.5)"},
|
|
preserveBootStateFlag("return an initially shutdown simulator to shutdown after reconfiguration"),
|
|
}, Action: cmdOn},
|
|
{Name: "off", Flags: []cli.Flag{
|
|
preserveBootStateFlag("return an initially shutdown simulator to shutdown after reconfiguration"),
|
|
}, Action: cmdOff},
|
|
{Name: "watch", Flags: []cli.Flag{
|
|
&cli.StringFlag{Name: "profile", Usage: "apply a JSON profile file (mutually exclusive with --except/--keep)"},
|
|
&cli.StringFlag{Name: "except", Usage: "comma-separated category IDs to leave fully enabled (see `simslim profiles`)"},
|
|
&cli.StringFlag{Name: "keep", Usage: "comma-separated launchd labels to keep running"},
|
|
&cli.DurationFlag{Name: "interval", Usage: "how often to rescan for booting simulators", Value: simslim.DefaultWatchInterval},
|
|
}, Action: cmdWatch},
|
|
}
|
|
|
|
onUsageError := func(_ context.Context, _ *cli.Command, err error, _ bool) error {
|
|
if err != nil && strings.Contains(err.Error(), "flag needs an argument: --set") {
|
|
return fmt.Errorf("--set requires a device set name (such as `testing`) or a path")
|
|
}
|
|
return err
|
|
}
|
|
for _, c := range commands {
|
|
c.OnUsageError = onUsageError
|
|
}
|
|
|
|
return &cli.Command{
|
|
Name: "simslim",
|
|
HideHelp: true, // root help/usage is handled by main's usage()
|
|
HideVersion: true, // version output is handled by main
|
|
OnUsageError: onUsageError,
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "set",
|
|
Usage: "also scan these device sets (comma-separated name|path)",
|
|
Action: func(_ context.Context, _ *cli.Command, value string) error {
|
|
sets := simslim.SplitList(value)
|
|
if len(sets) == 0 {
|
|
return fmt.Errorf("--set requires a device set name (such as `testing`) or a path")
|
|
}
|
|
for _, set := range sets {
|
|
simslim.RegisterDeviceSet(set)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
&cli.DurationFlag{
|
|
Name: "boot-timeout",
|
|
Usage: "max time for a simulator to boot and reconfigure (e.g. 10m, 15m); raise it for slow CI runners",
|
|
Sources: cli.EnvVars("SIMSLIM_BOOT_TIMEOUT"),
|
|
Value: simslim.BootTimeout,
|
|
Action: func(_ context.Context, _ *cli.Command, d time.Duration) error {
|
|
if d <= 0 {
|
|
return fmt.Errorf("--boot-timeout must be a positive duration (such as `15m`)")
|
|
}
|
|
simslim.BootTimeout = d
|
|
return nil
|
|
},
|
|
},
|
|
&cli.DurationFlag{
|
|
Name: "spawn-timeout",
|
|
Usage: "max time for a single launchctl transition inside the simulator (e.g. 2m, 5m); raise it for slow hosts",
|
|
Sources: cli.EnvVars("SIMSLIM_SPAWN_TIMEOUT"),
|
|
Value: simslim.SpawnTimeout,
|
|
Action: func(_ context.Context, _ *cli.Command, d time.Duration) error {
|
|
if d <= 0 {
|
|
return fmt.Errorf("--spawn-timeout must be a positive duration (such as `2m`)")
|
|
}
|
|
simslim.SpawnTimeout = d
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
Action: func(_ context.Context, cmd *cli.Command) error {
|
|
if args := cmd.Args().Slice(); len(args) > 0 {
|
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", args[0])
|
|
}
|
|
usage()
|
|
os.Exit(2)
|
|
return nil
|
|
},
|
|
ExitErrHandler: func(_ context.Context, _ *cli.Command, _ error) {},
|
|
Commands: commands,
|
|
}
|
|
}
|