Repository navigation
Description
Request: #264
Problem
In the reporter's words (#264): "The Android driver's startup sweep announces every console port in 5586–5682 to this instance's adb server, as ADR 0001's correction prescribes. It assumes the range belongs to one instance. With two Simlock instances on one machine, which docs/CLI.md and docs/CONFIGURATION.md document, each instance attaches the other's emulators to its own adb server."
One worker per user is already the rule (ADR 0034), but nothing enforces it once SIMLOCK_HOME points a second worker at another folder. The website still suggests it for "a separate install". Two workers with real drivers then share the emulator ports, the emulators and the iOS runtimes. An agent leased from the second worker sees, and can drive, emulators leased to agents of the first:
$ adb devices # agent-2, leased from the second worker (adb server 5039)
emulator-5586 device # the first worker's emulator, leased to agent-1
emulator-5590 device # agent-2's own
The slow e2e lane hits the same thing: it starts its own real-driver worker under a temporary SIMLOCK_HOME, next to the maintainer's own worker.
Who it is for
A user on one Mac who starts a second Simlock worker with another SIMLOCK_HOME, by mistake or by following the old docs. And the maintainer, who runs the slow e2e lane on a Mac where their own worker runs.
Outcome
A second worker with real drivers for the same user refuses to start, and says which worker holds the machine and how to stop it. It never touches the first worker's emulators. Test workers with fake drivers still run side by side, as many as the suite wants.
How it works for the user
flowchart TD
A["simlock daemon start (SIMLOCK_HOME=~/.simlock-b)"] --> B{"another worker of this user with real drivers alive?"}
B -- no --> C["starts as today"]
B -- "yes, same folder" --> D["'already running', as today"]
B -- "yes, other folder" --> E["refuses, naming pid and folder; exit 1"]
F["scripts/slow-e2e.sh"] --> G{"a worker holds the lock?"}
G -- yes --> H["exits 69 (not fit), naming it and how to stop it"]
G -- no --> I["runs as today"]
Before:
$ SIMLOCK_HOME=~/.simlock-b simlock daemon start
Daemon running (pid 5150)
After:
$ SIMLOCK_HOME=~/.simlock-b simlock daemon start
The daemon could not start: another Simlock worker runs for this user (pid 4242, folder /Users/me/.simlock, since 2026-11-02T09:12:03.000Z); stop it first: SIMLOCK_HOME=/Users/me/.simlock simlock daemon stop
$ echo $?
1
Examples
| Situation | Today | After |
|---|---|---|
| No other worker | starts | starts |
| Same folder, worker alive | "already running" | "already running" |
| Other folder, worker alive, real drivers | starts, attaches the other's emulators | refuses, exit 1 |
| Other folder, its worker crashed (pid gone) | starts | starts (takes the stale lock over) |
Other folder, fake drivers (SIMLOCK_DRIVERS_MODULE) |
starts | starts |
| A gateway next to the worker | starts | starts |
| Slow lane while your own worker runs | runs, may fail on your emulators | exits 69, names your worker and how to stop it |
What could go wrong
- A worker crashes and its pid is reused by another program: the next start refuses until that program ends. Unlikely; a reboot clears it.
- A worker that cannot create its lock folder in
/tmprefuses to start; the message names the folder. - Two OS users on one Mac can still each run a worker (known gap, unchanged).
Words used
- worker: the Simlock daemon that owns devices, e.g. the one
simlock daemon startruns in~/.simlock. - real drivers: the iOS and Android drivers a worker finds on the machine; the opposite is a fake driver set from
SIMLOCK_DRIVERS_MODULE, used by tests. - worker lock: the folder holding
owner.json(pid, folder, start time):/tmp/simlock-<uid>/worker.lock/on macOS and Linux, e.g./tmp/simlock-501/worker.lock/owner.json;<os.tmpdir()>\simlock\worker.lock\on Windows. - stale lock: a worker lock whose pid is no longer running.
- folder: a worker's data directory,
~/.simlockor$SIMLOCK_HOME.
Non-goals
- Two OS users on one Mac (ADR 0034 keeps it a known gap).
- Attaching only this worker's own emulators in the sweep: with one worker per user it is not needed.
- Removing
SIMLOCK_HOMEfrom every user page (ADR 0034's own work); only the "separate install" passage goes here. - Checking a reused pid's command line.
Completion conditions
- A second real-driver worker of the same user with another folder refuses to start with the message above, exit 1, and starts no driver.
- The first worker's stop frees the lock; a crashed worker's lock is taken over at the next start.
pnpm test:e2estill runs its files in parallel and passes.scripts/slow-e2e.shexits 69 naming the holder while a worker outside the lane holds the lock.worker/running-the-daemon.mdxno longer offers a separate install withSIMLOCK_HOME.
Open questions
Decisions
- ADR 0059 — A worker with real drivers holds a per-user lock (PR docs(adr): 0059 a worker with real drivers holds a per-user lock (#1189) #1190): who takes the lock, where it lives, who wins, when it is released, and the slow lane's exit 69.
Shared values
none
Behaviours
| Behaviour (invoker, action, what it sees) | Seam | Cases | Task |
|---|---|---|---|
| A worker with real drivers takes the worker lock before driver discovery, writing its pid, folder and start time | startDaemon (src/daemon/main.ts) with the lock path pointed at a temp folder; the adapter |
no drivers passed and no SIMLOCK_DRIVERS_MODULE → taken before any command; drivers passed or module set → not read; no lock; stale lock (pid gone); unreadable owner.json; parent not ours or not 0700 |
#1191 |
| A second worker whose lock is held by a live pid with another folder fails to start with the refusal message, before any driver call | startDaemon |
other folder alive → refused; same folder alive → goes on to the socket claim | #1191 |
| A gateway never takes the lock, even when it is held | startDaemon |
gateway role with a held lock | #1191 |
The daemon and slow-e2e.sh agree on the default worker lock path |
defaultWorkerLockPath; the script's text |
darwin, linux (uid 501), win32; the script's default for the current uid | #1191 |
A stopping worker removes the lock, after its drivers are disposed, only when owner.json is still the one it wrote; a start that fails after taking it removes it too |
startDaemon then stop() |
still its own; taken over by another meanwhile; start fails after the lock | #1191 |
scripts/slow-e2e.sh check and a run exit 69 while a live pid holds the worker lock, naming it |
scripts/slow-e2e.sh with its test overrides |
live holder; stale holder → runs | #1191 |
Tasks
- A second real-driver worker of the same user refuses to start, naming the first #1191 A second real-driver worker of the same user refuses to start — a user's second worker refuses and names the first; the slow lane refuses to run and says to stop the worker. Risk: a worker refuses to start with no other worker running (a stale lock not taken over).
Every Completion condition is delivered by #1191.
Open work in the same area: bugs #1168 and #1169 change Simlock's adb server, not driver discovery or the daemon start; no Depends on. ADR 0034's issue #700 is closed.
Written by an agent.
Activity
V3RON commentedon Nov 2, 2026
Spec updated: Problem, Who it is for, Outcome, How it works, Examples, Behaviours, Decisions (ADR 0059, PR #1190) and Tasks (#1191) added.
Written by an agent.
docs(adr): accept 0059 (#1189)
docs(adr): 0059 same-folder race and failed-start release; index narr…
docs(adr): 0059 the claim winner makes sure it holds the lock (#1189)
docs(adr): 0059 a worker with real drivers holds a per-user lock (#1189…
github-actions commentedon Nov 2, 2026
Closing: all 1 sub-issues are closed. The PR that closed the last one walks the completion conditions (delivery rule 9); reopen this if one does not hold on
main. Flip its ADRs to Accepted if delivery has not.V3RON commentedon Nov 2, 2026
Delivery run
Merged: #1190 (ADR 0059), #1193 (#1191)
Parked: none
Waiting: none
Decided: #1191 — two same-folder starts racing for the worker lock → the socket claim's winner makes sure it holds the lock (re-makes it, or rewrites
owner.json, or fails on a live holder in another folder); a failed start removes only a lock it wrote. Pending the maintainer; see the## Decision takencomment on #1190.Written by an agent.