Gateway: remote agents lease and reach physical devices on workers
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 8/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- android, ios, typescript
Research direction
Start in src/gateway/routing/pipeline.ts (runStages and the appliesToPhysical flag), then src/gateway/routing/serviceability.ts (assess), and src/gateway/routing/request-match.ts (physicalMatch). The design depends on fitsPhysical in core and on the worker view's devices list. Done means the e2e fake-driver path grants a physical lease through the gateway and forwards devicectl or adb to the right worker, and the UNKNOWN_MODEL and NO_CAPACITY rows in the table behave as specified.
Written by the indexing model from the issue text.
Description
Part of #428.
Scope
After this PR, a remote agent can lease a physical device attached to a worker through a gateway, and run devicectl (iOS) or adb (Android) on it, the same way it leases and drives a simulator. The gateway matches a physical request against the enrolled devices each worker reports. It fails at once when no enrolled device in the fleet could match. It sends the request only to a worker with a matching device that is ready and that the worker did not last read as not present. This replaces the interim from #438, where the gateway passed physical requests through to the workers and left them to the workers' own checks. Virtual requests route as today.
In short
The gateway learns to read the physical devices in each worker's view. A physical request is a lease request with --physical, e.g. simlock lease --platform ios --physical --os ">=18 <19". An enrolled match is a physical device in a worker's view, in any state but deleted, that the request matches: e.g. an iPhone 15 Pro on iOS 18.4, leased, for that request. A device's latest presence is the last presence read the worker's watcher (#439) made of it: present, or not present with a reason (locked, untrusted or not-present). This task puts it in the worker view, as presence on each physical device: e.g. an unplugged iPhone shows presence: {present: false, reason: "not-present"} after the watcher's next read.
A physical request with no enrolled match in the fleet fails at once. With an enrolled match that is ready and not last read as not present, it goes to that worker. Otherwise it waits in the gateway's queue. A worker that answers NO_CAPACITY (for example, the device read not present at grant) is not asked again for that request until its view changes, as for a virtual request. A device plugged back in changes its presence, so the request is asked again at the first view refresh after the watcher reads it present: at most one health.probeIntervalMs of the worker plus one 30 s view refresh. device.exec already forwards any tool name to the worker that holds the lease. The worker's driver aims devicectl or adb at the leased device (#440, #441). This task proves that path end to end.
flowchart TD
R["lease --physical (gateway)"] --> T{"enrolled match on a known worker? (changed)"}
T -->|no| U["UNKNOWN_MODEL at once (changed)"]
T -->|"yes, only on drained or disconnected workers"| N["NO_CAPACITY at once, as for virtual"]
T -->|yes| S{"a ready enrolled match, not last read not present, on a worker that takes requests? (changed)"}
S -->|yes| W["sent to that worker (noWait); the worker's own check decides"]
S -->|no| Q["waits in the gateway queue"]
W -->|granted| G["lease; device.exec devicectl / adb forwarded as today"]
W -->|"NO_CAPACITY (e.g. not present): worker kept out until its view changes"| Q
Q -->|"every view refresh: the 30 s tick, or a device.* / lease.* event; presence is in the view (changed)"| S
Session on a gateway with two workers. worker-b has an iPhone 15 Pro enrolled. worker-a has only simulators. Wording is illustrative.
# before (interim from #438): the gateway does not match physical requests; each worker's own check answers
# after
$ simlock lease --platform ios --physical --class tablet
{"error":{"code":"UNKNOWN_MODEL","message":"no enrolled physical device can match ios class tablet"}}
$ simlock lease --platform ios --physical
Lease wrk_b.lse_9: iPhone 15 Pro 00008110-001A2B3C4D5E (physical) on worker-b
$ curl -N -X POST $GW/v1/leases/wrk_b.lse_9/exec -H "authorization: Bearer $T" \
-d '{"tool":"devicectl","args":["device","info","apps"]}'
event: output ...
event: exit
data: {"exitCode":0}
$ simlock lease --platform ios
Lease wrk_a.lse_3: iPhone 16 simulator ... (unchanged: virtual only)
| Input, through a gateway | Before (interim, #438) | After |
|---|---|---|
lease --platform ios --physical, one worker has a ready enrolled iPhone |
the workers' own checks decide | granted that iPhone, on that worker |
| same, but every enrolled match is leased, absent, quarantined or reclaiming | the workers' own checks decide | waits in the gateway queue; granted when one is ready |
| same, but the only ready match was last read not present (unplugged) | the workers' own checks decide | waits; sent to its worker once the worker's view shows it present again |
same, with --no-wait |
the workers' own checks decide | NO_CAPACITY |
lease --platform ios --physical --class tablet, only iPhones enrolled |
the workers' own checks decide | UNKNOWN_MODEL at once |
lease --platform android --physical, no Android device enrolled on any worker |
the workers' own checks decide | UNKNOWN_MODEL at once |
lease --platform ios --physical, the only match is on a drained or disconnected worker |
the workers' own checks decide | NO_CAPACITY at once, as for a virtual request |
a waiting physical request whose last enrolled match is removed (simlock device remove <id>, run on the worker) |
the workers' own checks decide | fails then: UNKNOWN_MODEL |
lease --platform ios (no --physical) |
a simulator | a simulator, unchanged |
POST /v1/leases/{id}/exec devicectl on a physical iOS lease |
forwarded to the worker | forwarded to the worker, which runs it aimed at the leased UDID |
simlock adb --lease <id> ... on a physical Android lease |
forwarded to the worker | forwarded to the worker, which runs it with -s <serial> |
devicectl on a virtual lease, simctl on a physical lease |
forwarded; the worker refuses | unchanged: the worker refuses |
If this goes wrong, a user would see a physical request through a gateway wait for ever, fail UNKNOWN_MODEL though a worker has a matching device, or be sent to a worker without one. A request that is never sent again after its device is plugged back in, or one sent again and again to a worker whose device is unplugged, would also show it. So would a virtual request landing on a physical device.
Technical spec
Modules touched
flowchart LR
FC["gateway/fleet-coordinator: per-pass claims *"] --> RT["gateway/routing (policy)"]
FC --> VS["routing/view-state: viewLoadKey (covers presence)"]
RT --> PL["routing/pipeline: runStages *"]
RT --> SV["routing/serviceability: assess *"]
PL --> ST["stages: can-serve *, warm-hit *, free-slot / ram-budget / free-capacity *"]
ST --> RM["routing/request-match *"]
SV --> RM
RM --> CORE["core: fitsPhysical, noPhysicalMatch (#438)"]
FC --> WR["gateway/worker-registry: routingViews *"]
FC --> BUS["bus: request.dispatched *"]
WR --> K["contract: device entry presence *"]
DD["daemon/dispatcher: #decorateDevice *"] --> K
DD --> WAT["core/physical-presence: latest read *"]
FK["e2e/fake-driver *"]
sequenceDiagram
participant A as Remote agent
participant G as Gateway
participant B as Worker B (iPhone enrolled)
A->>G: lease --platform ios --physical --class tablet
G-->>A: UNKNOWN_MODEL at once (no enrolled match in any view)
A->>G: lease --platform ios --physical
G->>B: lease.request {physical, noWait, fleetRequestId}
B-->>G: NO_CAPACITY (iPhone read not present at grant)
Note over G: request waits; B kept out while its view's load key is unchanged (ADR 0009 §5)
Note over B: watcher reads the iPhone not present, later present again (cable back in)
G->>B: view refresh (30 s tick, or a device.* / lease.* event)
B-->>G: devices: iPhone ready, presence present (load key changed)
G->>B: lease.request {physical, noWait, fleetRequestId}
B-->>A: granted (physical)
A->>G: device.exec {tool: devicectl, args}
G->>B: device.exec, tool and args as sent
B-->>A: output pushes, exit code (or the worker's refusal)
src/gateway/routing/pipeline.ts:RoutableRequest(:11) already carriesphysical(#438).FilterStageandRankStagegain a requiredappliesToPhysical: boolean.runStages(:56) skips every stage whoseappliesToPhysicalisfalsewhen the request is physical. This is the one place a stage is skipped.
src/gateway/routing.ts: #438's interim is removed: thephysical-interimrank stage and the separate walkselect(:51) takes for a physical request. A physical request walkswarm-then-freelike any other, with the skips above.src/gateway/routing/request-match.ts: the one function that decides whether a worker can serve a request (ADR 0009 §3) gains a physical branch.- A new export,
physicalMatch(worker, request, which), says whether the view lists a device withphysical: truethat the request matches. Withwhich"enrolled", the device may be in any state butdeleted. Withwhich"pickable", the device isreadyand itspresenceis not a not-present read: present, or no read yet. It reads onlydevicesfrom the view, never the catalog. - The match itself is the core's
fitsPhysicalthat the worker's planner uses (#438): platform, model ignoring case, class (from the device entry, as #436 put it there; none means phone asrequestedClasssays), and OS, exact or a range (API level on Android). The gateway calls it; it never copies it. matchRequest(:31) anddeviceFit(:48) returnundefinedfor a physical request.hasPlatformis unchanged.
- A new export,
src/gateway/routing/stages/:can-serve.ts: keeps a worker with an"enrolled"match for a physical request; as today for a virtual one.appliesToPhysical: true.warm-hit.ts: for a physical request, scores 1 for a worker with a"pickable"match, and does not apply the mode rule. As today for a virtual one.appliesToPhysical: true. A device its worker last read as not present is skipped, so the request is not sent to a worker whose only match is unplugged. A physical pick'sreasonis thereforewarm-hit, which matcheslease.granted.sourcewarm(ADR 0022 §9) and needs no new value (ADR 0009 §8).takes-requests.ts,healthy.ts:appliesToPhysical: true, logic unchanged.idle-queue.ts:appliesToPhysical: false. It readsqueueDepth, the worker's virtual waiters, for a virtual request only. Physical waiters are counted inphysicalQueueDepthand matched per model by the worker itself (#438 adds the field and this rule).free-slot.ts,ram-budget.ts,free-capacity.ts:appliesToPhysical: false. A physical device takes no running slot and no RAM.
src/gateway/routing/serviceability.tsassess(:45): a physical request walks this table, in order:- no worker takes requests:
NO_CAPACITY,no-worker(as today); - no known worker has the platform in its catalog:
NO_DRIVER(as today); - no known worker has an
"enrolled"match:UNKNOWN_MODEL,unresolvable-spec, with the message anddetailsa worker gives the same request (ADR 0022 §6). Both come from #438'snoPhysicalMatch(request):detailsis{ platform, model?, class?, osVersion? }, from the request; - the
RUNTIME_MISSINGrow does not apply to a physical request; - a known worker has an
"enrolled"match, but none that takes requests has one:NO_CAPACITY,no-worker(as today's row 5); - otherwise: route or wait.
"Known" is as today: notincompatible.presenceplays no part in the table: a device last read not present still makes the request wait, not fail.
- no worker takes requests:
src/gateway/worker-registry.tsroutingViews(:162): a view that answeredstartingcarries the physical devices last read from that worker, as it carries the last catalog (ADR 0009 §4). A worker that restarts therefore stays known for its enrolled devices. A new map beside#lastCatalog(:120) holds them.refresh(:345) sets it when a refresh carriesdevices, and#forget(:557) deletes it.views()is unchanged, so the operator's view still lists none for a starting worker.- Presence in the worker view:
src/core/physical-presence.ts(#438'sreadPhysicalPresence, the one place every presence read goes through): it also keeps each enrolled device's latest answer in memory,{ present: true }or{ present: false, reason }, and the core's index exposes it, e.g.latestPresence(deviceId). Every read updates it: the watcher's (#439), the grant's (#438), the read after a failed reclaim and a quarantine retry's. So a grant that reads a device not present changes the worker's view too, and the gateway sees the device come back on the next read that says present. A device has none before its first read. A read that rejects leaves it as it was.src/contract/schemas.ts:statusDeviceSchema(:145) anddeviceRecordSchema(:82) gain an optionalpresence:{ present: true }or{ present: false, reason: "locked" | "untrusted" | "not-present" }. It is set only on a physical device that has been read at least once. The worker view's devices arestatusDeviceSchema(src/contract/worker-view.ts:70), so a gateway readspresencefromlist.getwith no change there, and a worker's ownworker.listview gets it the same way.src/daemon/dispatcher.ts#decorateDevice(:843) addspresencefrom the latest presence read, forstatus.get(:356) andlist.get(:650). It reads it through a new optionalpresenceoption ofDispatcher, wired insrc/daemon/main.tsbesidequeue: leasing(:437) and passed bybuildDispatcher(src/daemon/server.ts:303).src/gateway/routing/view-state.tsviewLoadKey(:9-17) needs no change: it keeps every device field buttransitionAgeMs(:12), sopresenceis in the key. A device whose latest read flips changes its worker's key at the next view refresh.
src/gateway/fleet-coordinator.ts:request.dispatched(:909) carriesphysical: truefor a physical request.- Refusals. A worker's
NO_CAPACITYto a physical request is remembered as for a virtual one (ADR 0009 §5):#rememberRefusal(:1102), read by#refused(:1111) at :815, held while the worker's view load key is unchanged. That code does not change. Because the view now carriespresence, a device that read not present at grant and is plugged back in changes the key: the watcher reads it present, and the next view refresh carries that. This ends #438's interim, where such a request waited for an unrelated change of the view. - The worst case after a device is plugged back in is one
health.probeIntervalMsof the worker (the watcher's next read) plus one view refresh. A refresh comes on the 30 s tick (src/gateway/service.ts:262-275,WORKER_VIEW_REFRESH_INTERVAL_MS), or sooner on anydevice.*orlease.*event the worker relays (src/gateway/worker-link.ts:552). Every refresh runs a dispatch pass:WorkerRegistry#refreshnotifies (src/gateway/worker-registry.ts:362), and#onViewsChanged(:1340, subscribed at :202) runs#dispatch. - This does not loop. The refresh the refusal asks for (
#staleView:1081) re-reads the view. If the refusal came with no new presence read, the view is unchanged, so the key holds and the request is not sent straight back. If a presence read (the grant's own, or the watcher's) said not present, the key changes, but the pick skips that device (warm-hitabove). - The gateway runs no timer of its own for this and reads no
health.*key.healthstays a worker-only key. - Per-pass claims. Today
#dispatchPass(:779) keeps oneclaimedThisPassset (:780), read at :815 and written at :819: one dispatch per worker per pass. #438 splits it per line: one set for virtual picks, and one for physical picks keyed by worker. This task keys the physical set by worker and the request's match: platform, model, class and OS, compared as the core's physical match compares them (#438'sfitsPhysical): a missing class counts asphone, and a model compares as the match does. A physical pick then holds the worker only for other physical waiters with the same match. A virtual pick holds it only for virtual waiters, as #438 left it. So in one pass a worker can be sent one virtual request and one physical request per match. - A
noWaitphysical request refusedNO_CAPACITYgets its one more walk without that worker (:990-997), as today, and then fails. - Nothing else changes.
routable(:1393), which carriesphysical,forwardedModel(:1372), which sends a physical request's model as typed, and the forward (:933-966) stay as #438 left them. The cannot-serve memo andexec(:432) need no change.
src/bus/index.tsrequest.dispatched(:366):physical?: true.e2e/fake-driver/: for a physical lease, its passthrough answersdevicectl(iOS) andadb(Android). The argv it prints names the leased device's ID. It refusesdevicectlon a virtual lease andsimctlon a physical one, as ADR 0022 §5 has the real drivers do. This task adds it: #440 only renamed the fake's tool list topassthroughTools.
Contract and event changes
- Contract shapes:
statusDeviceSchemaanddeviceRecordSchemagain an optionalpresence(above). It reaches the status device entry,list.get's device records and the worker view's devices. Protocol +1 at merge. Nothing else undersrc/contract/changes; the worker view already carriesphysicalandclasson each device (#436). request.dispatchedgainsphysical: truewhen the request asked for a physical device; absent otherwise. Additive (events rule 6).reasonkeeps its two values; a physical pick iswarm-hitwith stagewarm-hit. BothEVENTS.mdfiles say so.
Other code on the same state
The state: the device lists in worker views (now with each physical device's latest presence), the latest presence reads on each worker, the gateway's fleet queue, each waiter's refusal memos, and the per-pass claim sets.
src/gateway/worker-link.ts:552-555refreshes a view after everydevice.*andlease.*event a worker relays (changesCapacityOrLeases, :586-588). That coversdevice.enrolled,device.deleted,device.absence-detected,device.returnedanddevice.reclaimed. Each refresh runs a dispatch pass. So a waiting physical request is granted when its device becomesready, and fails when its last enrolled match is unenrolled. Areadydevice that reads present again emits no event (#439 wakes only the worker's own queue). Its newpresencereaches the gateway on the 30 s refresh tick (src/gateway/service.ts:262-275), which also runs a pass.- The worker's own match and grant-time presence check (#438), and its watcher (#439): the worker wins. The gateway's pick is a guess from a view that may be stale. The grant reads presence at grant time; the watcher reads it every
health.probeIntervalMs. A device last read present but unplugged since is still sent the request, and the worker refuses itNO_CAPACITY. A device last read not present but plugged in since is not sent it until the watcher's next read and the next refresh. A worker that answers a physical requestNO_CAPACITYis kept out while its view's load key is unchanged, as for a virtual request. A worker that answersUNKNOWN_MODEL(device unenrolled after the view was read) is left out for that request for good (fleet-coordinator.ts:1005,CANNOT_SERVE_CODES:92). The request then fails at once if no other worker has an enrolled match. As today. src/gateway/worker-registry.tsretention sweep (pruneExpired, :525-541): it forgets only a disconnected worker, and removing a view removes its devices. While that worker is disconnected, a physical request only it could match failsNO_CAPACITYno-workerat once (row 5), so none of them is still waiting at the sweep. After the sweep, the same request failsUNKNOWN_MODEL(row 3). This is the same rule as the catalog today.WorkerRegistry#incompatible: keeps the devices of an earlier session on the view.assessdoes not count anincompatibleworker as known, so they never match.- The virtual stages and the warm-hit virtual branch: #436 made them ignore physical devices. This task does not change the virtual paths. A change of a physical device's
presencealso changes the load key a virtual request's refusal is held against, so that worker can be asked once more for it; that costs one dispatch and never a wrong device. - #438's per-line claim sets in
#dispatchPassand its per-line counts of the fleet queue: this task changes only the physical set's key, and not the order the walk takes waiters in. src/gateway/aggregate.ts(status),lease-index.ts,owner-routed-facts.ts,WorkerRegistry#grantedDevices: read devices and leases, not routing. The gateway's status copies each device entry from the view,presenceincluded. A physical grant is forwarded verbatim, as today.src/gateway/dispatcher.ts:505#deviceExecandFleetLeaseCoordinator#exec(:432): forwardtoolandargsunchanged to the lease's worker. The gateway keeps no tool list. The worker's driver resolves and refuses (#440, #441).FleetLeaseCoordinator#waitingRequests(:218): lists a waiting physical request with the sharedLeaseRequestBook#waiting, as #438 left it.
Rules in play
architecture.mdrule 2:devicectlandadbstay driver knowledge. The gateway forwards tool names it does not read.architecture.mdrule 10: one physical match (the core's, from #438) for worker and gateway; one place skips stages (runStages); one place builds theUNKNOWN_MODELmessage and details (noPhysicalMatch); one place fillspresence(#decorateDevice, from the latest presence read) and one place on the gateway reads it (physicalMatch).architecture.mdrule 16: routing stays a pure function over views.safety.mdrule 10:presencefrom a worker is a claim. The contract's parse of the view's devices bounds it to the shapes above, and a wrong one costs one refused dispatch, never a wrong device.events.mdrules 6, 8: the additive field and both catalogs.testing.mdrules 1, 4: the stage-skip test checks every stage in the list, not only the ones this task touches.
Tests
Seam: a gateway daemon's CLI and HTTP API with fake-driver workers dialling it, scripted with enrolled physical devices as #437 allows. Models: e2e/gateway-fast-fail.test.ts and e2e/gateway-fleet.test.ts; e2e/gateway-smoke.test.ts for exec through a gateway (and e2e/device-exec.test.ts for reading the SSE stream). Rules the e2e cannot reach cheaply use createRoutingPolicy("warm-then-free") over view fixtures (model src/gateway/routing.test.ts, src/gateway/routing/serviceability.test.ts), src/gateway/routing/view-state.test.ts, src/gateway/worker-registry.test.ts, src/gateway/fleet-coordinator.test.ts and src/gateway/service.test.ts. The worker's side uses the watcher's test with a FakeClock (#439's src/leasing/physical-device-watcher.test.ts), src/daemon/dispatcher.test.ts and src/contract/schemas.test.ts.
- (e2e)
--physicalthrough a gateway is granted the enrolled iPhone on the worker that has it, not the worker with free simulator capacity and none enrolled - (e2e)
--physical --class tabletwith only iPhones enrolled fails at once withUNKNOWN_MODEL, exit 12, and the worker's message - (e2e)
--platform android --physicalwith no Android device enrolled on any worker fails at once withUNKNOWN_MODEL - (e2e)
--physical --device "iPhone 15 Pro" --os ">=18 <19"is granted the device matching both; with--os ">=17 <18", and with--os 18, and only an iOS 18.4 device enrolled, it fails at once withUNKNOWN_MODEL, and the--os 18message names the range form - (e2e)
--physical --no-waitwhile the only enrolled match is leased fails withNO_CAPACITY, exit 11 - (e2e) a second
--physicalrequest for the only enrolled match waits, and is granted after the first lease is released and the device reclaimed - (e2e) a waiting
--physicalrequest fails withUNKNOWN_MODELoncesimlock device remove <id>, run on the worker, unenrolls its last enrolled match - (e2e)
--physicalwhose only enrolled match is on a drained worker fails at once withNO_CAPACITY - (e2e) a request without
--physicalon a gateway whose only worker has a ready enrolled iPhone is granted a simulator - (e2e) through a gateway,
simlock adb --lease <id>on a physical Android lease runs on the worker, aimed at the leased serial, and exits with the tool's code - (e2e) through a gateway,
POST /v1/leases/{id}/execwithdevicectlon a physical iOS lease streams output aimed at the leased UDID and its exit code - (e2e) through a gateway,
devicectlon a virtual lease andsimctlon a physical lease are refused with the worker's code and HTTP status, and nothing runs - (routing)
assessroutes or waits a physical request whose only enrolled match isready,leased,reclaiming,absentorquarantined, one case per state - (routing)
assessroutes or waits a physical request whose only enrolled match isreadywithpresencenot present - (routing)
assessfails a physical request withUNKNOWN_MODELwhen its only match isdeleted, with the message and thedetails{ platform, model, class, osVersion }thatnoPhysicalMatchgives for the same request, and withoutmodel,classandosVersionwhen the request names none - (routing)
assessroutes or waits a physical request whose worker's catalog lists no model or runtime for it but has an enrolled match (noRUNTIME_MISSINGrow) - (routing)
assessanswers a physical requestNO_DRIVERwhen no known worker's catalog has the platform - (routing)
assessfails a physical request withUNKNOWN_MODELwhen only anincompatibleworker lists a match, and withNO_CAPACITYno-workerwhen only a disconnected worker does - (routing) an iPad entry (class
tablet) matches--class tablet, and not a physical request naming neither class nor model - (routing)
selectpicks, for a physical request, the worker with areadyenrolled match over one with a warm simulator of the same model and more free capacity;reasonandstagearewarm-hit - (routing)
selectgives no pick for a physical request when no taking worker has areadyenrolled match - (routing)
selectgives no pick for a physical request whose onlyreadyenrolled match haspresencenot present, one case per reason; it picks that worker when the match'spresenceis present, and when the match has nopresence - (routing)
selectpicks, for a physical request, a worker with no free running slot and at its RAM budget when it holds the onlyreadyenrolled match - (routing)
selectdrops, for a physical request, a worker that is notrunning, as for a virtual one - (routing)
selectpicks, for a physical request, a worker with areadyenrolled match whosequeueDepthandphysicalQueueDepthare both above zero; for a virtual request it still drops a worker whosequeueDepthis above zero - (routing) in
warm-then-free, exactlyidle-queue,free-slot,ram-budgetandfree-capacityhaveappliesToPhysical: false, andrunStagesnever calls a skipped stage for a physical request - (routing) no policy lists a
physical-interimstage - (routing) a virtual request's pick, reason and stage are the same with and without a
readyphysical device of its model in the views - (registry) a starting worker's routing view carries the physical devices last read from it, and its
views()entry lists no devices - (registry) a removed worker's last physical devices are gone from
routingViews - (core,
readPhysicalPresence) a device's latest read is{ present: true }after a present read, and{ present: false, reason }with the read's reason after a not-present one; it has none before its first read; a read that rejects leaves it unchanged; a grant's presence read changes it the same way - (watcher, fake clock) a device the fake driver reports present again has a latest read of present within one
health.probeIntervalMs - (dispatcher)
status.getandlist.getcarry the latest presence read aspresenceon a physical device; a virtual device, and a physical device not yet read, carry none - (contract)
statusDeviceSchemaanddeviceRecordSchemaacceptpresencepresent, and not present with each oflocked,untrustedandnot-present, and refuse any other reason; the worker view built fromlist.getkeeps it - (view-state) two views that differ only in one physical device's
presence, from not present to present or from one reason to another, have different load keys - (coordinator) a physical request whose only enrolled match is on a restarting worker waits, and is not failed
UNKNOWN_MODEL - (coordinator)
request.dispatchedfor a physical request carriesphysical: true; a virtual request's carries nophysical - (coordinator) a worker that answers a physical request
NO_CAPACITYbefore progress is not sent it again by the refresh the refusal asks for, nor by any later refresh with an unchanged view; the request waits meanwhile - (coordinator) after that refusal, a refresh in which the device's
presenceturns not present sends the request nowhere, and the next refresh in which it turns present sends it to that worker again - (coordinator) a virtual request refused
NO_CAPACITYis still kept out until its worker's view changes, as today - (service with the coordinator, fake clock) a waiting physical request its worker refused is sent to that worker again on the first periodic refresh after the worker's view reports the device present, with no worker event, so within one refresh interval
- (coordinator) a
noWaitphysical request refusedNO_CAPACITYby its only candidate failsNO_CAPACITYand is sent to that worker once - (coordinator) a worker that answers a physical request
UNKNOWN_MODELbefore progress is left out, and the request failsUNKNOWN_MODELwhen no other worker has an enrolled match - (coordinator) a physical request whose only enrolled match is on a disconnected worker fails
NO_CAPACITYno-worker; afterpruneExpiredforgets that worker, the same request failsUNKNOWN_MODEL - (coordinator) in one dispatch pass, a waiting virtual request and a waiting physical request are both sent to worker 1, the only worker that can take either
- (coordinator) in one dispatch pass, two waiting physical requests for different models, whose
readymatches are both on worker 1, are both sent to it - (coordinator) in one dispatch pass, of two waiting physical requests with the same platform, model, class and OS, only the older is sent to worker 1, though it has two
readymatches; the newer is sent in the next pass
Done when
- On a gateway with two workers,
simlock lease --platform ios --physicalis granted the enrolled iPhone on the worker that has it. - On a gateway,
--physical --class tabletwith only iPhones enrolled fails at once withUNKNOWN_MODEL.--physical --no-waitwith every enrolled match leased fails withNO_CAPACITY. - On a gateway, a waiting
--physicalrequest fails withUNKNOWN_MODELoncesimlock device remove <id>, run on the worker, removes its last enrolled match. - On a gateway, a lease request without
--physicalis granted a simulator although a matching physical device is enrolled and ready. - Through a gateway,
simlock adb --lease <id>on a physical Android lease, andPOST /v1/leases/{id}/execwithdevicectlon a physical iOS lease, run on the worker aimed at the leased device.devicectlon a virtual lease is refused. request.dispatchedfor a physical request showsphysical: true, and bothEVENTS.mdfiles list it.- On a worker,
simlock status --jsonshowspresenceon each physical device read at least once, and the gateway's view of that worker carries it (dispatcher and contract tests above). - After a device is plugged back in, a physical request its worker refused
NO_CAPACITYis sent to it again on the first view refresh after the watcher reads it present: within onehealth.probeIntervalMsplus one refresh interval (the watcher, view-state, coordinator and service tests above, with a fake clock). - In one dispatch pass, a gateway sends a virtual and a physical request to the same worker, and two physical requests for different models to the same worker (coordinator tests above).
- No line needs real hardware: the gateway forwards tools verbatim, and #440 and #441 prove the tools on real devices.
Out of scope
- A
simlock devicectlCLI command. A remote iOS agent usesPOST /v1/leases/{id}/execor the client'sexec. - Enrolling and removing devices through a gateway. On a gateway,
device addanddevice removeanswerUNSUPPORTED_IN_GATEWAY_MODE; they run on the worker the device is plugged into (#437). - The physical handlers and their refusal lists (#440, #441).
- Uploading files to a worker, and isolation between lease holders (feature non-goals).
- Any change to how virtual requests route.
Depends on
- #435
- #441
Approval
- Approved for delivery
Written by an agent.
- Dominant language
- TypeScript
- Stars
- 19
- Forks
- 1
- Avg merge
- 7h 49m
- Merged PRs (30d)
- 228
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from callstackincubator/simlock
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
callstackincubator/simlock#639 ·
Maintainers usually reply within 1 day
-
Lint does not enforce index.ts boundaries for cleanup/, routing/, warm-pool/ and contract/ helpersPossibly taken @V3RON claimed this today. Openbug:ready
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
callstackincubator/simlock#445 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
daemon-lifecycle e2e: the it.fails status test passes for the wrong reasonPossibly taken @V3RON claimed this 7 days ago. Openbug:triage
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
callstackincubator/simlock#422 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
task:ready
Difficulty 5/5 Over a week Newbie friendliness 8/100
callstackincubator/simlock#637 ·
Maintainers usually reply within 1 day
-
task:ready
Difficulty 5/5 Over a week Newbie friendliness 12/100
callstackincubator/simlock#636 ·
Maintainers usually reply within 1 day
All issues in callstackincubator/simlock
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
melgarafael/DeskcommCRM#3000 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
aws/agentcore-cli#2606 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 7 days
-
ai-driven-qa bug Cosmetic
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
ai-driven-qa bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
linagora/twake-mail-admin#301 ·
Maintainers usually reply within 1 day