Skip to content

Commit 8ce6bb5

Browse files
committed
more
1 parent 947b596 commit 8ce6bb5

18 files changed

Lines changed: 2117 additions & 18 deletions

‎MODULE.bazel‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ go_sdk.download(version = GO_VERSION)
9999

100100
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
101101
go_deps.from_file(go_mod = "//:go.mod")
102-
use_repo(go_deps, "com_github_google_go_cmp")
102+
use_repo(go_deps, "com_github_google_go_cmp", "org_golang_x_sys")
103103

104104
bazel_dep(name = "rules_java", version = "9.6.1")
105105

‎go.mod‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module github.com/EngFlow/example
22

3-
go 1.21.2
3+
go 1.25.0
44

55
require github.com/google/go-cmp v0.6.0
6+
7+
require golang.org/x/sys v0.47.0

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
22
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
4+
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

‎tools/simulator_manager/BUILD‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ sh_binary(
4949
data = [
5050
":install_post_boot_script",
5151
":prepare_simulator",
52-
":simulator_manager",
52+
"//tools/simulator_manager/go",
5353
],
5454
visibility = ["//visibility:public"],
5555
deps = ["@bazel_tools//tools/bash/runfiles"],

‎tools/simulator_manager/Sources/ORPHAN_SIMULATOR_FIX.md‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,34 @@ See the next section for what addresses this.
172172

173173
## Verification status
174174

175-
There is no `BUILD` file or `Package.swift` for this target yet, so it isn't
176-
wired into Bazel and can't be built with `swift build` either. This change
177-
has been verified by code review against the existing invariants (see
178-
`LEASE_LIFECYCLE.md`), not by compiling or running it. Before relying on
179-
this in production:
175+
`bazel build //experiments/yannic/macsimulatormanager/swift:macsimulatormanager`
176+
now succeeds for real (see [`README.md`](README.md#building-it)), and
177+
`bazel-bin/experiments/yannic/macsimulatormanager/swift/macsimulatormanager
178+
--help` runs and prints the expected flags, including
179+
`--reap-interval-seconds`. That confirms the code compiles and links against
180+
`ShellOut`/`ArgumentParser`/SwiftNIO correctly, but not the runtime behavior
181+
described above — there's no CoreSimulator runtime installed on the machine
182+
this was built on (`xcrun simctl list runtimes` is empty there), so the
183+
lease/reaper/escalation logic itself is still verified only by code review
184+
against the invariants in `LEASE_LIFECYCLE.md`, not by exercising it.
185+
Getting the toolchain wired up also required one unplanned fix: this repo's
186+
`common:clang` config enforces `-Werror=sign-compare` globally (including for
187+
host tools), which `rules_swift`'s own bundled `tools/common/process.cc`
188+
doesn't build clean under — exempted via a `--per_file_copt` in `.bazelrc`
189+
rather than patching upstream, mirroring the existing `.pb.cc` exemption
190+
right above it.
191+
192+
Before relying on this in production:
180193

181194
1. Confirm this branch is actually the source for whatever customers run —
182-
given the missing build target and the `DO NOT MERGE` history noted
183-
above, that's not yet established. If it is, push this branch and get it
184-
merged; if there's a separate deploy path, find it.
185-
2. Set up a build target (Bazel or an ad hoc `Package.swift`) so this code
186-
compiles and can be typechecked — it currently depends on `ShellOut`,
187-
`ArgumentParser`, and SwiftNIO, none of which are vendored here.
188-
3. Manually exercise the restart-orphan path: lease a config, `kill -9` the
195+
given the `DO NOT MERGE` history noted above, that's not yet established.
196+
If it is, push this branch and get it merged; if there's a separate
197+
deploy path, find it.
198+
2. Manually exercise the restart-orphan path: lease a config, `kill -9` the
189199
daemon, start a new instance with a short `--reap-interval-seconds`, and
190200
confirm the orphaned clone disappears from `xcrun simctl list devices`
191201
within two sweep intervals without touching unrelated simulators.
192-
4. Manually exercise the wedged-device escalation path: lease a config, then
202+
3. Manually exercise the wedged-device escalation path: lease a config, then
193203
independently hang or otherwise make its clone's `simctl shutdown`/`delete`
194204
fail out-of-band, and confirm the reaper force-kills its `launchd_sim`
195205
after 3 failed sweeps rather than retrying forever.

‎tools/simulator_manager/Sources/README.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,23 @@ slow and resource-hungry.
99
The daemon speaks HTTP over a Unix domain socket rather than a TCP port,
1010
since it is only ever talked to by processes on the same machine.
1111

12+
## Building it
13+
14+
```bash
15+
bazel build //experiments/yannic/macsimulatormanager/swift:macsimulatormanager
16+
```
17+
18+
Third-party dependencies (`ShellOut`, `swift-argument-parser`, SwiftNIO) are
19+
resolved via [`rules_swift_package_manager`](https://github.com/cgrindel/rules_swift_package_manager)
20+
from the `Package.swift` / `Package.resolved` pair in this directory, not
21+
hand-vendored. If you change `Package.swift`, regenerate `Package.resolved`
22+
with `swift package resolve` and run `bazel mod tidy` at the repo root to
23+
pick up any new or changed external repos in `MODULE.bazel`.
24+
1225
## Running it
1326

14-
`Main.swift` is a `swift-argument-parser` command. Notable options:
27+
`Main.swift` is a `swift-argument-parser` command; `bazel run` works the same
28+
way, with arguments after `--`. Notable options:
1529

1630
- `--pid-path` / `--unix-socket-path` — where the daemon writes its PID file
1731
and creates the listening socket.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "go_lib",
5+
srcs = [
6+
"http_server.go",
7+
"lease_store.go",
8+
"logger.go",
9+
"lru_set.go",
10+
"main.go",
11+
"pty.go",
12+
"simulator_control.go",
13+
"simulator_manager.go",
14+
"simulator_request_handler.go",
15+
],
16+
importpath = "github.com/EngFlow/example/tools/simulator_manager/go",
17+
visibility = ["//visibility:private"],
18+
deps = ["@org_golang_x_sys//unix"],
19+
)
20+
21+
go_binary(
22+
name = "go",
23+
embed = [":go_lib"],
24+
visibility = ["//visibility:public"],
25+
)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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

Comments
 (0)