Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Gateway: remote agents lease and reach physical devices on workers

Closed
#442 3 comments 0 reactions 0 assignees View on GitHub

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
Domain
backend, devtools

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

task:draft

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 carries physical (#438).
    • FilterStage and RankStage gain a required appliesToPhysical: boolean.
    • runStages (:56) skips every stage whose appliesToPhysical is false when the request is physical. This is the one place a stage is skipped.
  • src/gateway/routing.ts: #438's interim is removed: the physical-interim rank stage and the separate walk select (:51) takes for a physical request. A physical request walks warm-then-free like 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 with physical: true that the request matches. With which "enrolled", the device may be in any state but deleted. With which "pickable", the device is ready and its presence is not a not-present read: present, or no read yet. It reads only devices from the view, never the catalog.
    • The match itself is the core's fitsPhysical that the worker's planner uses (#438): platform, model ignoring case, class (from the device entry, as #436 put it there; none means phone as requestedClass says), and OS, exact or a range (API level on Android). The gateway calls it; it never copies it.
    • matchRequest (:31) and deviceFit (:48) return undefined for a physical request. hasPlatform is unchanged.
  • 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's reason is therefore warm-hit, which matches lease.granted.source warm (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 reads queueDepth, the worker's virtual waiters, for a virtual request only. Physical waiters are counted in physicalQueueDepth and 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.ts assess (:45): a physical request walks this table, in order:
    1. no worker takes requests: NO_CAPACITY, no-worker (as today);
    2. no known worker has the platform in its catalog: NO_DRIVER (as today);
    3. no known worker has an "enrolled" match: UNKNOWN_MODEL, unresolvable-spec, with the message and details a worker gives the same request (ADR 0022 §6). Both come from #438's noPhysicalMatch(request): details is { platform, model?, class?, osVersion? }, from the request;
    4. the RUNTIME_MISSING row does not apply to a physical request;
    5. a known worker has an "enrolled" match, but none that takes requests has one: NO_CAPACITY, no-worker (as today's row 5);
    6. otherwise: route or wait.
      "Known" is as today: not incompatible. presence plays no part in the table: a device last read not present still makes the request wait, not fail.
  • src/gateway/worker-registry.ts routingViews (:162): a view that answered starting carries 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 carries devices, 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's readPhysicalPresence, 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) and deviceRecordSchema (:82) gain an optional presence: { 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 are statusDeviceSchema (src/contract/worker-view.ts:70), so a gateway reads presence from list.get with no change there, and a worker's own worker.list view gets it the same way.
    • src/daemon/dispatcher.ts #decorateDevice (:843) adds presence from the latest presence read, for status.get (:356) and list.get (:650). It reads it through a new optional presence option of Dispatcher, wired in src/daemon/main.ts beside queue: leasing (:437) and passed by buildDispatcher (src/daemon/server.ts:303).
    • src/gateway/routing/view-state.ts viewLoadKey (:9-17) needs no change: it keeps every device field but transitionAgeMs (:12), so presence is 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) carries physical: true for a physical request.
    • Refusals. A worker's NO_CAPACITY to 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 carries presence, 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.probeIntervalMs of 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 any device.* or lease.* event the worker relays (src/gateway/worker-link.ts:552). Every refresh runs a dispatch pass: WorkerRegistry#refresh notifies (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-hit above).
    • The gateway runs no timer of its own for this and reads no health.* key. health stays a worker-only key.
    • Per-pass claims. Today #dispatchPass (:779) keeps one claimedThisPass set (: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's fitsPhysical): a missing class counts as phone, 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 noWait physical request refused NO_CAPACITY gets its one more walk without that worker (:990-997), as today, and then fails.
    • Nothing else changes. routable (:1393), which carries physical, 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 and exec (:432) need no change.
  • src/bus/index.ts request.dispatched (:366): physical?: true.
  • e2e/fake-driver/: for a physical lease, its passthrough answers devicectl (iOS) and adb (Android). The argv it prints names the leased device's ID. It refuses devicectl on a virtual lease and simctl on a physical one, as ADR 0022 §5 has the real drivers do. This task adds it: #440 only renamed the fake's tool list to passthroughTools.
Contract and event changes
  • Contract shapes: statusDeviceSchema and deviceRecordSchema gain an optional presence (above). It reaches the status device entry, list.get's device records and the worker view's devices. Protocol +1 at merge. Nothing else under src/contract/ changes; the worker view already carries physical and class on each device (#436).
  • request.dispatched gains physical: true when the request asked for a physical device; absent otherwise. Additive (events rule 6). reason keeps its two values; a physical pick is warm-hit with stage warm-hit. Both EVENTS.md files 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-555 refreshes a view after every device.* and lease.* event a worker relays (changesCapacityOrLeases, :586-588). That covers device.enrolled, device.deleted, device.absence-detected, device.returned and device.reclaimed. Each refresh runs a dispatch pass. So a waiting physical request is granted when its device becomes ready, and fails when its last enrolled match is unenrolled. A ready device that reads present again emits no event (#439 wakes only the worker's own queue). Its new presence reaches 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 it NO_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 request NO_CAPACITY is kept out while its view's load key is unchanged, as for a virtual request. A worker that answers UNKNOWN_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.ts retention 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 fails NO_CAPACITY no-worker at once (row 5), so none of them is still waiting at the sweep. After the sweep, the same request fails UNKNOWN_MODEL (row 3). This is the same rule as the catalog today.
  • WorkerRegistry#incompatible: keeps the devices of an earlier session on the view. assess does not count an incompatible worker 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 presence also 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 #dispatchPass and 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, presence included. A physical grant is forwarded verbatim, as today.
  • src/gateway/dispatcher.ts:505 #deviceExec and FleetLeaseCoordinator#exec (:432): forward tool and args unchanged 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 shared LeaseRequestBook#waiting, as #438 left it.
Rules in play
  • architecture.md rule 2: devicectl and adb stay driver knowledge. The gateway forwards tool names it does not read.
  • architecture.md rule 10: one physical match (the core's, from #438) for worker and gateway; one place skips stages (runStages); one place builds the UNKNOWN_MODEL message and details (noPhysicalMatch); one place fills presence (#decorateDevice, from the latest presence read) and one place on the gateway reads it (physicalMatch).
  • architecture.md rule 16: routing stays a pure function over views.
  • safety.md rule 10: presence from 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.md rules 6, 8: the additive field and both catalogs.
  • testing.md rules 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) --physical through 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 tablet with only iPhones enrolled fails at once with UNKNOWN_MODEL, exit 12, and the worker's message
  • (e2e) --platform android --physical with no Android device enrolled on any worker fails at once with UNKNOWN_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 with UNKNOWN_MODEL, and the --os 18 message names the range form
  • (e2e) --physical --no-wait while the only enrolled match is leased fails with NO_CAPACITY, exit 11
  • (e2e) a second --physical request for the only enrolled match waits, and is granted after the first lease is released and the device reclaimed
  • (e2e) a waiting --physical request fails with UNKNOWN_MODEL once simlock device remove <id>, run on the worker, unenrolls its last enrolled match
  • (e2e) --physical whose only enrolled match is on a drained worker fails at once with NO_CAPACITY
  • (e2e) a request without --physical on 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}/exec with devicectl on a physical iOS lease streams output aimed at the leased UDID and its exit code
  • (e2e) through a gateway, devicectl on a virtual lease and simctl on a physical lease are refused with the worker's code and HTTP status, and nothing runs
  • (routing) assess routes or waits a physical request whose only enrolled match is ready, leased, reclaiming, absent or quarantined, one case per state
  • (routing) assess routes or waits a physical request whose only enrolled match is ready with presence not present
  • (routing) assess fails a physical request with UNKNOWN_MODEL when its only match is deleted, with the message and the details { platform, model, class, osVersion } that noPhysicalMatch gives for the same request, and without model, class and osVersion when the request names none
  • (routing) assess routes or waits a physical request whose worker's catalog lists no model or runtime for it but has an enrolled match (no RUNTIME_MISSING row)
  • (routing) assess answers a physical request NO_DRIVER when no known worker's catalog has the platform
  • (routing) assess fails a physical request with UNKNOWN_MODEL when only an incompatible worker lists a match, and with NO_CAPACITY no-worker when 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) select picks, for a physical request, the worker with a ready enrolled match over one with a warm simulator of the same model and more free capacity; reason and stage are warm-hit
  • (routing) select gives no pick for a physical request when no taking worker has a ready enrolled match
  • (routing) select gives no pick for a physical request whose only ready enrolled match has presence not present, one case per reason; it picks that worker when the match's presence is present, and when the match has no presence
  • (routing) select picks, for a physical request, a worker with no free running slot and at its RAM budget when it holds the only ready enrolled match
  • (routing) select drops, for a physical request, a worker that is not running, as for a virtual one
  • (routing) select picks, for a physical request, a worker with a ready enrolled match whose queueDepth and physicalQueueDepth are both above zero; for a virtual request it still drops a worker whose queueDepth is above zero
  • (routing) in warm-then-free, exactly idle-queue, free-slot, ram-budget and free-capacity have appliesToPhysical: false, and runStages never calls a skipped stage for a physical request
  • (routing) no policy lists a physical-interim stage
  • (routing) a virtual request's pick, reason and stage are the same with and without a ready physical 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.get and list.get carry the latest presence read as presence on a physical device; a virtual device, and a physical device not yet read, carry none
  • (contract) statusDeviceSchema and deviceRecordSchema accept presence present, and not present with each of locked, untrusted and not-present, and refuse any other reason; the worker view built from list.get keeps 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.dispatched for a physical request carries physical: true; a virtual request's carries no physical
  • (coordinator) a worker that answers a physical request NO_CAPACITY before 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 presence turns 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_CAPACITY is 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 noWait physical request refused NO_CAPACITY by its only candidate fails NO_CAPACITY and is sent to that worker once
  • (coordinator) a worker that answers a physical request UNKNOWN_MODEL before progress is left out, and the request fails UNKNOWN_MODEL when no other worker has an enrolled match
  • (coordinator) a physical request whose only enrolled match is on a disconnected worker fails NO_CAPACITY no-worker; after pruneExpired forgets that worker, the same request fails UNKNOWN_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 ready matches 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 ready matches; the newer is sent in the next pass

Done when

  • On a gateway with two workers, simlock lease --platform ios --physical is granted the enrolled iPhone on the worker that has it.
  • On a gateway, --physical --class tablet with only iPhones enrolled fails at once with UNKNOWN_MODEL. --physical --no-wait with every enrolled match leased fails with NO_CAPACITY.
  • On a gateway, a waiting --physical request fails with UNKNOWN_MODEL once simlock device remove <id>, run on the worker, removes its last enrolled match.
  • On a gateway, a lease request without --physical is granted a simulator although a matching physical device is enrolled and ready.
  • Through a gateway, simlock adb --lease <id> on a physical Android lease, and POST /v1/leases/{id}/exec with devicectl on a physical iOS lease, run on the worker aimed at the leased device. devicectl on a virtual lease is refused.
  • request.dispatched for a physical request shows physical: true, and both EVENTS.md files list it.
  • On a worker, simlock status --json shows presence on 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_CAPACITY is sent to it again on the first view refresh after the watcher reads it present: within one health.probeIntervalMs plus 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 devicectl CLI command. A remote iOS agent uses POST /v1/leases/{id}/exec or the client's exec.
  • Enrolling and removing devices through a gateway. On a gateway, device add and device remove answer UNSUPPORTED_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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from callstackincubator/simlock

All issues in callstackincubator/simlock

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.