|
| 1 | +# Mac Simulator Manager (Go) |
| 2 | + |
| 3 | +This is a Go translation of the Swift Mac Simulator Manager located in `../swift/`. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +The simulator manager is an HTTP server that manages iOS/macOS simulator instances for testing. It provides lease-based access to simulators with the following features: |
| 8 | + |
| 9 | +- **Lease Management**: PIDs can lease simulators (exclusively or shared) |
| 10 | +- **Automatic Cleanup**: Simulators are automatically deleted when idle |
| 11 | +- **Persistence**: Leases are persisted to disk so daemon restarts don't lose state |
| 12 | +- **Base + Clone Pattern**: Creates base simulators and clones them for efficiency |
| 13 | +- **Child Process Management**: Can launch and monitor startup processes |
| 14 | + |
| 15 | +## Key Components |
| 16 | + |
| 17 | +### Main Application (`main.go`) |
| 18 | +Entry point that parses command-line arguments and starts the server. |
| 19 | + |
| 20 | +### HTTP Server (`http_server.go`) |
| 21 | +Serves requests on a Unix domain socket with endpoints: |
| 22 | +- `POST /simulator/<pid>` - Lease a simulator |
| 23 | +- `DELETE /simulator/<pid>` - Release a simulator |
| 24 | +- `GET /version` - Get manager version |
| 25 | +- `GET /leases` - Get count of live leases |
| 26 | +- `POST /shutdown` - Shutdown the server |
| 27 | + |
| 28 | +### Simulator Manager (`simulator_manager.go`) |
| 29 | +Core orchestration logic: |
| 30 | +- Manages simulator slots per configuration |
| 31 | +- Handles lease/release operations |
| 32 | +- Tracks reference counts |
| 33 | +- Implements automatic deletion with LRU eviction |
| 34 | + |
| 35 | +### Simulator Control (`simulator_control.go`) |
| 36 | +Low-level interface to `simctl`: |
| 37 | +- Creates base simulators |
| 38 | +- Clones simulators |
| 39 | +- Boots and shuts down simulators |
| 40 | +- Deletes simulators |
| 41 | + |
| 42 | +### Lease Store (`lease_store.go`) |
| 43 | +Persists leases to disk in JSON format with atomic writes. Includes process start time to detect PID reuse. |
| 44 | + |
| 45 | +### Supporting Components |
| 46 | +- `lru_set.go` - LRU cache for tracking recently used configurations |
| 47 | +- `pty.go` - PTY creation for child process I/O |
| 48 | +- `logger.go` - Structured logging setup |
| 49 | + |
| 50 | +## Translation Notes |
| 51 | + |
| 52 | +### Differences from Swift |
| 53 | + |
| 54 | +These are concrete, verified behavioral or structural differences between the |
| 55 | +two implementations -- not just "different language, same thing" restatements. |
| 56 | + |
| 57 | +1. **Concurrency model**: |
| 58 | + - Swift's `SimulatorManager` and `SimulatorControl` are actors, so the |
| 59 | + compiler serializes access and only allows suspension at explicit |
| 60 | + `await` points. Go has no equivalent, so the same invariants are |
| 61 | + enforced by hand with `sync.Mutex`, manually unlocking around any |
| 62 | + blocking call (channel receive, `simctl` invocation) and re-locking |
| 63 | + afterward -- easy to get subtly wrong in a future change, unlike the |
| 64 | + actor version. |
| 65 | + - Coalescing concurrent callers onto one in-flight operation (`getBase`, |
| 66 | + `createBase`, `clone`) uses Swift's `Task<T, Error>`, which is directly |
| 67 | + awaitable and memoizes its result. Go instead uses a buffered channel |
| 68 | + (`chan taskResult`) that every caller receives from. |
| 69 | + - `SimulatorDeleteOrExistenceMutex`, which serializes delete/existence |
| 70 | + checks for a given device name, is a hand-rolled actor with an explicit |
| 71 | + waiter queue in Swift; Go uses a plain `sync.Mutex`. |
| 72 | + |
| 73 | +2. **Leaser-exit detection is slower in Go**: |
| 74 | + - Swift watches for a leaser's exit with `DispatchSource.makeProcessSource` |
| 75 | + (kqueue `EVFILT_PROC`), so `release` runs as soon as the kernel reports |
| 76 | + the exit. |
| 77 | + - Go's `registerReleaseOnExit` instead polls once a second with |
| 78 | + `time.Ticker` + `kill(pid, 0)`, so reclaiming a dead leaser's simulator |
| 79 | + can lag up to ~1s behind Swift. (The separate idle-deletion timer in |
| 80 | + `pendingDeletion` already polls once a second in *both* implementations, |
| 81 | + so that part carried over unchanged.) |
| 82 | + |
| 83 | +3. **`POST /shutdown` is abrupt in Go, graceful in Swift**: |
| 84 | + - Swift's only shutdown path is `POST /shutdown`, which triggers |
| 85 | + SwiftNIO's `ServerQuiescingHelper`: stop accepting connections, let |
| 86 | + in-flight ones finish, then fall through to the cleanup code that |
| 87 | + removes the socket and PID files. |
| 88 | + - Go additionally handles SIGINT/SIGTERM by calling `http.Server.Shutdown` |
| 89 | + (a comparable graceful drain), but its `POST /shutdown` handler just |
| 90 | + calls `os.Exit(0)` from a goroutine after writing the response. That |
| 91 | + skips draining any other in-flight requests and skips the deferred |
| 92 | + socket/PID-file cleanup in `HTTPServer.Run`, which only runs after |
| 93 | + `server.Serve()` returns -- which `os.Exit` prevents. An HTTP-triggered |
| 94 | + shutdown in the Go version can leave stale socket/PID files behind. |
| 95 | + |
| 96 | +4. **Post-boot script failures lose detail in Go**: |
| 97 | + - Swift runs the post-boot script through the same `subprocess`/`shellOut` |
| 98 | + helper used for every `simctl` call, so a failure comes back as a |
| 99 | + `ProcessError` with captured stdout/stderr. |
| 100 | + - Go's `Clone` invokes the post-boot script directly with `exec.Command` |
| 101 | + instead of the shared `subprocess`/`simctl` helper: stdout/stderr are |
| 102 | + discarded rather than captured, and a failure surfaces as a wrapped |
| 103 | + `*exec.ExitError` rather than a `*ProcessError`. |
| 104 | + |
| 105 | +5. **`--post-boot` required vs. optional**: |
| 106 | + - Swift declares `postBoot` as a non-optional `String` with no default, so |
| 107 | + `swift-argument-parser` requires the flag and fails startup without it. |
| 108 | + - Go's flag defaults to `""` and is treated as "skip the post-boot step", |
| 109 | + so it's optional. |
| 110 | + |
| 111 | +6. **Debug logging is unreachable in Go**: |
| 112 | + - Go's `slog` handler is hardcoded to `slog.LevelInfo`, so every |
| 113 | + `Debug`-level call (reference-count changes, existing-simulator lookups) |
| 114 | + is compiled but never emitted, and there's no flag to raise the level. |
| 115 | + - Swift's `os.Logger` calls at `.debug` are still captured live by the |
| 116 | + unified logging system (visible via `log stream`/Console), just not |
| 117 | + persisted long-term. |
| 118 | + |
| 119 | +7. **HTTP framework**: |
| 120 | + - Swift hand-rolls the server on SwiftNIO: a channel pipeline of |
| 121 | + `AccumulatedHTTPHandler` (buffers head/body/end into one in-memory |
| 122 | + request) followed by `SimulatorManagerHTTPHandler` (parses |
| 123 | + method/path/query), with one task per connection. |
| 124 | + - Go uses the standard library's `net/http` + `http.ServeMux`, which |
| 125 | + already delivers a fully parsed, buffered `*http.Request` per call, so |
| 126 | + there's no need for an equivalent two-stage accumulate/parse pipeline. |
| 127 | + |
| 128 | +8. **Child-process output capture**: |
| 129 | + - Swift watches each PTY with `DispatchSource.makeReadSource` |
| 130 | + (event-driven, woken by the runloop on readability). |
| 131 | + - Go uses pipes instead of PTYs (to avoid CGO) and parks a dedicated |
| 132 | + goroutine in a blocking `Read` loop per pipe. This works identically |
| 133 | + for line-buffered output capture but wouldn't preserve interactive |
| 134 | + terminal behavior if child processes expected a real TTY. |
| 135 | + |
| 136 | +9. **JSON serialization**: both use built-in JSON support with struct tags; |
| 137 | + no behavioral difference here. |
| 138 | + |
| 139 | +### Build |
| 140 | + |
| 141 | +```bash |
| 142 | +# With Bazel |
| 143 | +bazel build //experiments/yannic/macsimulatormanager/go:macsimulatormanager |
| 144 | + |
| 145 | +# With Go |
| 146 | +cd experiments/yannic/macsimulatormanager/go |
| 147 | +go build -o macsimulatormanager |
| 148 | +``` |
| 149 | + |
| 150 | +### Usage |
| 151 | + |
| 152 | +```bash |
| 153 | +./macsimulatormanager \ |
| 154 | + --version="1.0.0" \ |
| 155 | + --pid-path="/tmp/simulator-manager.pid" \ |
| 156 | + --unix-socket-path="/tmp/simulator-manager.sock" \ |
| 157 | + --delete-recently-used-idle-after=300 \ |
| 158 | + --delete-idle-after=60 \ |
| 159 | + --recently-used-capacity=1 \ |
| 160 | + --post-boot="/path/to/post-boot-script.sh" \ |
| 161 | + --lease-path="/tmp/leases.json" |
| 162 | +``` |
0 commit comments