Repository navigation
Conversation
swift-corelibs-foundation's FileManager.replaceItemAt fails with "file doesn't exist" whether or not the target exists, so every cache write threw there. describe-ui only warns when the write fails, but every @n / #N tap then reads a missing cache. Keep replaceItemAt on Apple platforms and use rename(2) elsewhere — the same atomic swap. The new test covers the overwrite path: every ui after the first replaces an existing cache file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
swift-corelibs-foundation surfaces a missing executable as NSCocoaErrorDomain 260 (NSFileReadNoSuchFileError), not 4 or ENOENT, so Linux users got a generic transport error instead of the adb install hint. AdbRunnerTests.testRunMapsMissingBinaryToAdbMissing already pins this; it fails on Linux without the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
No behaviour change on Apple platforms; this is what lets the two targets compile with swift-corelibs-foundation on Linux. - import Darwin / os only where they exist, Glibc otherwise - import FoundationNetworking where URLSession / HTTPURLResponse are used — they live outside Foundation in swift-corelibs-foundation - LinuxCompat.swift (internal, compiled out on Apple platforms): a Darwin namespace forwarding to Glibc, so the daemon's qualified Darwin.close(fd)-style calls compile unchanged, and an NSLock-backed OSAllocatedUnfairLock for ProcessControl - tests: same conditional imports; the AdbStreamingProcess test is compiled only where os exists (the Linux build leaves AdbStreamingProcess out) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The daemon command only needs the executable for three hooks: its root parser, the iOS stale-HID cleanup, and the per-platform liveness probe. Move the command to SimUseCore and have the entry point inject those through Daemon.installPlatformHooks, called by `daemon start` exactly where the hooks were wired before. Behaviour is unchanged; a second executable can now register the same command instead of copying it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The top-level and iOS describe-ui already answer to `ui`; the Android one is registered directly at the top level of the Linux build, where `sim-use ui` depends on this alias. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The Android backend drives devices through adb and the bridge APK's HTTP server and needs none of the Apple frameworks, so Package.swift gains an #if os(Linux) branch declaring SimUseCore, AndroidBackend and a new SimUseLinux executable; the macOS manifest in #else is unchanged. - AndroidBackend leaves out its three video-capture files (host-side muxing/encoding needs AVFoundation via SimUseVideo), and AndroidCommand registers record-video / stream-video only when SimUseVideo is importable, keeping the macOS subcommand order. - SimUseLinux registers the Android implementations of the top-level verbs under the same names (ui, tap, type, swipe, screenshot, ...), the android namespace, and the shared daemon command with Android-only hooks. - SimUseCoreTests and AndroidBackendTests build on Linux; the record argument tests are left out with the command they test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
adb forward opens its listener on the machine that runs the adb server, which is only this machine when the server is local. When ADB_SERVER_SOCKET points at a remote server (tcp:<host>:<port> — e.g. WSL using the Windows host's adb so USB devices stay visible), the bridge is now reached on that host instead of 127.0.0.1. SIM_USE_BRIDGE_HOST overrides the host. IPv6 hosts come back bracketed with any zone id escaped, so the result always forms a URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
docs/linux.md covers what the Linux build offers, building and installing it (scripts/install-linux.sh), and reaching devices from WSL through the Windows host's adb server, including the exposure that `adb -a` creates. README, AGENTS.md, the skill and the changelog point at it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
Runs swift build / swift test in the swift:6.3 image on ubuntu — SimUseCore, AndroidBackend, SimUseLinux and their two test targets — so a change that breaks the Linux build is caught on the PR. It needs no XCFrameworks and runs alongside the macOS job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
|
Thank you for your contribution! That is actually what on our plan list and the implementation looks good for the first glance. I will try to take a deeper look soon and see if we need more modification before we can determine the next action. I will reach you back soon. |
onevtail
left a comment
There was a problem hiding this comment.
The Linux cache fix is supported by the reported reproduction and concurrent read/write checks. Review of head 27b558c09c785b6940c592d3469ad1f23d2ddc76 identified two blocking issues in remote adb connection scoping:
-
[P1] A warm daemon keeps targeting the previous adb server. BridgeClient.swift derives its host from the daemon's environment, while daemon lookup uses only the device serial and forwarded requests carry no connection configuration. Starting with server A and then switching the client to B with the same serial returned
ok:truefor bothbutton homecommands, but both button requests reached A. Please include the connection configuration in daemon identity or validate it before reuse; recalculating the host inside the existing daemon would still read its unchanged environment. -
[P1] Restarting the daemon can send the previous session's credentials to the new host. Session restoration restores the port and token by serial, allowing the URL construction to combine B's host with A's cached port and token. With A using port 18080 and B expected to use 18081, the cold-start probe reached an unrelated mock listener at B:18080 carrying A's synthetic token. Reconnecting only after a connection failure does not cover an old port that still responds. Please persist and validate the session's connection identity, and recreate forwarding and credentials when it changes or is absent from a legacy cache.
Please add regression coverage for switching servers with a warm daemon, switching with a persisted session, and an unrelated listener occupying the cached port. These reproductions used simulated adb/HTTP services and synthetic credentials; real Android/WSL end-to-end behavior remains unverified.
onevtail - an assistant to @onevcat
The bridge host follows ADB_SERVER_SOCKET, but the warm daemon and bridge.json were keyed by serial alone, so a client switched to another adb server could still be served through the old one, or send the old session's token to whatever listened at its cached port. - A daemon now reports the connection it started under (adb server and bridge host) in _ping, and the reuse gate restarts it when the client's connection differs. - bridge.json records that connection. A session from another connection, or a cache that predates the field, is ignored, and a matching one is used only while adb forward --list still shows its port for the serial; otherwise the forward and token are recreated. The new tests cover a warm daemon after switching servers, a persisted session after switching, a legacy cache, and an unrelated listener on the cached port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The connection scoping missed two cases: - The daemon connection identity was only reported for serials that PlatformRouter.looksLikeAndroid accepts. Wireless-debugging (mDNS) serials exceed its 32-character cap but still reach a per-device daemon, so a daemon started against another adb server kept serving them. The identity is now withheld only for iOS simulator and device UDIDs. - A failed adb forward --list was treated as the cached forward being gone, so each failure opened another forward and left the old one registered on the adb server. The failure now surfaces and the cached session is kept for the next call to confirm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
Dropping a stale bridge forward ran `adb forward --remove tcp:<port>` without a serial. With more than one device on the adb server, adb rejects that with "more than one device/emulator", and the error was swallowed, so every reconnect left the old forward registered. The removal now names the device. `android init` always printed `localhost` as the HTTP endpoint, which is wrong when the bridge is reached through a remote adb server. It now prints the resolved bridge host and adds `bridgeHost` to the `--json` result. Both were found testing against a real device through a remote adb server with two devices attached. The new tests cover forward removal naming the serial and the init output naming the host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
|
Thanks for the careful repro. I could reproduce both issues. The fixes are in three commits on top of 27b558c: e3c7365, scope cached connections to the adb server
8175e51, two gaps in the first fix
4583430, found while testing on a real device
Tests Verification
|
|
hi, |
|
Yes! The failing test is unrelated to this PR. I’ll try to find some time to go through this in detail and see how it all works. Thank you for the great work—I really appreciate it! |
onevcat
left a comment
There was a problem hiding this comment.
Thanks for the fixes — both P1s from the previous round are resolved. I verified this round end to end:
- macOS:
make build/make testgreen (1447 tests); live checks on an iOS 26.5 simulator and an Android emulator are unchanged frommain. - Linux (
swift:6.3, cleangit archive):swift build/swift testgreen (276 XCTest + 140 swift-testing). - Remote adb (
ADB_SERVER_SOCKET=tcp:<host>:5037from a container):android init/ui/tap/screenshotwork; switchingANDROID_ADB_SERVER_PORTorSIM_USE_BRIDGE_HOSTrestarts the warm daemon and recreates the forward and session, as designed. - End to end on Linux: the Linux binary, its daemon, and a local adb server in a container,
adb connectto an emulator, driven by the existing Android E2E suites throughSIM_USE_TEST_BINARY. 17 of 20 tests pass. All 3 failures come fromlong-press/app-statebeing absent on Linux.
Remaining items before we can claim Linux support:
1. app-state (and long-press) on Linux. The bundled skill tells agents to run sim-use app-state --reset after an intentional relaunch (skills/sim-use/SKILL.md L157), so the skill's loop breaks on the Linux build. You offered to move the Android halves of long-press / app-state into AndroidBackend as a follow-up; I would like that to land in this PR, or before we announce Linux support.
2. ANDROID_ADB_SERVER_ADDRESS selects a remote server but not the bridge host. BridgeConnection.init includes ANDROID_ADB_SERVER_ADDRESS / ANDROID_ADB_SERVER_PORT in adbServer, and docs/linux.md lists them as scoping inputs, but BridgeClient.resolveBridgeHost only reads SIM_USE_BRIDGE_HOST and ADB_SERVER_SOCKET. Reproduced: with only ANDROID_ADB_SERVER_ADDRESS=<remote> set, adb devices works, adb forward opens on the remote host, but requests go to 127.0.0.1:<port> (Failed to connect to 127.0.0.1 port …), and every attempt leaves another forward on the remote server. This is the same class as the earlier P2: a local listener on that port would receive the token. Please derive the host from ANDROID_ADB_SERVER_ADDRESS when ADB_SERVER_SOCKET is unset (adb's own precedence) and add cases to BridgeHostTests.
3. Unsupported verbs give misleading errors on Linux. sim-use long-press -x 1 -y 1 --device … prints Unknown option '-x', and sim-use app-state --device … prints Unknown option '--device'. Agents read these errors literally. Please add a redirect table to SimUseLinux, similar to iOSOnlyVerbRedirects in Sources/SimUse/main.swift, that names the verb and says it is not available in the Linux build (ios, ios-device, record-video, stream-video, viewer, init, plus long-press / app-state until item 1 lands).
4. Forwards leak when the connection changes (minor). When a cached session is rejected for a different identity, its forward stays on the adb server. I switched between the default environment and ANDROID_ADB_SERVER_PORT=5037 4 times and got 4 extra forwards. The two configurations are the same server, but adb=default and adb=tcp:localhost:5037 compare as different identities, so each switch also restarts the daemon without need. Normalizing the identity (e.g. resolve default to tcp:localhost:5037) fixes the common case; the cross-server leak is acceptable if documented.
5. Containers without an init process (docs). In a container whose PID 1 does not reap children (e.g. docker run … sleep infinity), an exited daemon stays a zombie, kill(pid, 0) still succeeds, and daemon stop reports stopped=false. With docker run --init everything is correct. Since containers are the main use case, please add a note to docs/linux.md.
Nits
Sources/SimUseCore/Daemon/DaemonClient.swift: the newshouldRestartForConnection/connectionIdentityProvider/connectionIdentity(for:)are inserted between the doc comment ofshouldRestartForVersionand its declaration, so the "Pure comparator…" comment now documentsshouldRestartForConnectionandshouldRestartForVersionhas none.resolveBridgeHostlives onBridgeClientbut onlyBridgeConnectioncalls it; moving it intoBridgeConnectionkeeps the dependency in one direction.- The branch has a small conflict with
maininskills/sim-use/SKILL.md(from #145).
The red macOS check is the known ProcessControlTests timing flake and is unrelated.
Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com> # Conflicts: # skills/sim-use/SKILL.md
With only ANDROID_ADB_SERVER_ADDRESS set, adb talks to the remote server and `adb forward` listens there, but the bridge host stayed on 127.0.0.1: requests failed, and each attempt left another forward on the remote server. The bridge host now follows adb's own precedence: SIM_USE_BRIDGE_HOST, then ADB_SERVER_SOCKET, then ANDROID_ADB_SERVER_ADDRESS. The resolver moves from BridgeClient into BridgeConnection, its only caller. The connection identity compared the adb server spec as written, so the default server and ANDROID_ADB_SERVER_PORT=5037 looked like two servers. Switching between them restarted the daemon and orphaned the cached forward. Equivalent specs now normalise to one form (tcp:localhost:5037; localhost aliases, lowercase hosts, bracketed IPv6), and anything unparseable is kept verbatim. Sessions also record their adb server. When a cached session is rejected because only the bridge host changed, its forward is on the current server and is removed before a new one is opened. A forward on a different server is left alone, since this process cannot address that server; docs/linux.md says how to clear it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
In a container whose PID 1 does not reap children (for example `docker run ... sleep infinity`), an exited daemon stays a zombie, `kill(pid, 0)` still succeeds, and `daemon stop` reports stopped=false. Containers are the main Linux use case, so docs/linux.md now says to run them with `--init`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The connection-identity helpers were inserted between shouldRestartForVersion's doc comment and its declaration, so the comment documented shouldRestartForConnection instead. It is back on its function, and connectionIdentity(for:) gets its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
The bundled skill tells agents to run `sim-use app-state --reset` after an intentional relaunch, so the Linux build broke the skill's loop. Both verbs kept their Android paths inside the macOS forwarders, which import the iOS modules. The Android halves now live in AndroidBackend as AndroidLongPressCommand and AndroidAppStateCommand, and the macOS forwarders call their static entry points for Android serials. The app-state result types, mapping and text output move to SimUseCore (AppStateReport), and the help strings of both verbs to LongPressHelp / AppStateHelp, so the two surfaces share one implementation, one JSON shape and one help text. macOS commands, flags and output are unchanged. The Linux root registers both at the top level. Like the other Linux verbs they take AndroidDeviceOptions, so a missing --device asks for an adb serial instead of failing in simctl, and iOS identifiers are rejected before they reach adb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
On Linux, `sim-use long-press -x 1 -y 1` printed "Unknown option '-x'" and `sim-use app-state --device ...` printed "Unknown option '--device'", because a verb the root does not know is parsed as its argument. Agents read those errors literally. The Linux entry point now checks the first argument against a table of verbs this build does not ship (ios, ios-device, record-video, stream-video, viewer, init), in the style of iOSOnlyVerbRedirects on macOS, and prints an error that names the verb and says it is not available in the Linux build, with a hint where one helps, exiting 64. long-press and app-state are not in the table, since the previous commit brings them over. The table and message live in SimUseCore so a Linux test target covers them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
When a request fails, BridgeClient drops its forward and retries once through a new one. If the retry also fails (nothing answers at the bridge host, e.g. a wrong SIM_USE_BRIDGE_HOST, a firewall, or a device that was never initialised), the error was thrown with the retry's forward still registered. No token had been fetched, so no session was saved for it and nothing ever removed it: every failing attempt left one more forward on the adb server, which is how the reviewer's ANDROID_ADB_SERVER_ADDRESS repro accumulated forwards. The final failure now computes the error first (its probe uses adb shell, not the forward), then invalidates, removing that forward. A test makes the bridge unreachable and checks that every forward the client opened is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Yang <2832943+iopass4@users.noreply.github.com>
|
Thanks for the thorough round. All items are addressed in new commits on top of 4583430 (plus a merge of main for the SKILL.md conflict):
Nits: the doc comment is back on Verification:
|
onevclaw
left a comment
There was a problem hiding this comment.
Reviewed head 34ea016052079fae05b9fae94f001f9c35926364. The Linux build and connection-scoping changes have substantial test coverage, but one blocking isolation defect remains.
[P1] A timed-out identity probe allows requests to execute against the wrong adb environment. In DaemonClient.swift, _ping times out after two seconds and the failure path still permits the business request to reach the existing daemon. Because the daemon processes connections serially, a busy daemon can trigger this without being unhealthy.
This was reproduced on Linux and macOS with a fake adb:
- Environment A starts a daemon and runs
app-state, with its adbpsdelayed for four seconds. - Environment B requests
app-statefor the same serial while A is busy. - B's identity probe times out, but its business request subsequently executes in A's daemon and returns
ok: truewithcom.example.a. All recorded adb calls use A's environment.
Please prevent business requests from executing when connection identity has not been confirmed. Carrying the expected identity in each request and validating it in the daemon before execution would also address the race between probing and execution. Please add a regression test through the actual invoke path with a busy A daemon and a B request sharing the same serial; B's operation must never execute in A.
Separately, please narrow the absolute credential-safety claim in CHANGELOG.md. Forward ownership is checked when restoring the disk cache, but an already-running client can continue using a port after its forward changes. This is a pre-existing weakness rather than a regression introduced here, so the underlying behavior can be tracked separately.
Validation included a passing Linux build and test suite and successful macOS builds. The full macOS test run had one timing-test failure that passed in isolation; Android device/E2E validation remains incomplete.
onevclaw - an assistant to @onevcat
|
I reproduced the P1 through the real I implemented and verified the minimal fail-closed fix locally:
Verification: the focused invoke-path suite passes (6/6), I could not push the validated commit to this cross-repository branch: the required
|
|
Thanks for picking this up and keeping the commits as they are. I compared 36013d5 with the fix I had locally and it's the same approach, so I'm not pushing anything more to #141. Please merge with a merge commit as noted, so the original authorship stays. I'm fine with closing #141 once this lands. |
|
@iopass4 Thanks for the feedback! Absolutely—the new PR will include your contribution. I still have a few final tests to run in a real Linux/Android environment. Once those are done, we should be ready to merge it! It’s great to have you involved. Thanks again for your contribution! |
Summary
Builds the Android half of sim-use on Linux. The Android backend drives devices through
adband the bridge APK's HTTP server and needsnone of the Apple frameworks, so this adds an
#if os(Linux)target graph —SimUseCore,AndroidBackend, and a newSimUseLinuxexecutable — next to the unchanged macOS manifest. The main use case is agents running in Linux containers, CI, and WSL.
The macOS build is unchanged: the macOS manifest sits verbatim in the
#elsebranch, and every source change is either compiled outon Apple platforms or behaviour-preserving (details below, verified with
make testand on a simulator).Commits (review one at a time)
replaceItemAtfails whether or not the target exists, so
ui→tap @Nbroke on Linux. Apple platforms keepreplaceItemAt. Adds an overwrite test.adbMissingon Linux — corelibs reports it as Cocoa 260, not 4 / ENOENT. Already covered bytestRunMapsMissingBinaryToAdbMissing.Darwin/Glibc/os,FoundationNetworking; aninter nalLinuxCompat.swift(compiled out on Apple platforms) provides aDarwinnamespace forwarding to Glibc, so the daemon's qualifiedDarwin.close(fd)calls compile unchanged, plus anNSLock-backedOSAllocatedUnfairLock.e-HID cleanup, liveness probe); they are now injected via
Daemon.installPlatformHooks, called at the same point indaemon start. Avoids a second copy for the Linux executable.
uion android describe-ui — matches the top-level / iOS verb; the Linux root depends on it forsim-use ui.SimUseLinux, andAndroidCommandregistering record/stream only when
SimUseVideois importable (macOS subcommand order unchanged).adb forwardlistens on the adb server's host; whenADB_SERVER_SOCKET=tcp: <host>:<port>is remote (e.g. WSL → Windows adb), the bridge is reached there.SIM_USE_BRIDGE_HOSToverrides. Pure function + tests (IPv6/zone ids). Also applies on macOS; the default (no env) is unchanged.
docs/linux.md(scope, install, WSL setup incl. the security implications ofadb -a), README, AGENTS.md, skill, CHANGELOG.
swift:6.3ubuntu job runningswift build/swift testfor the Linux graph.Scope on Linux
Available: the Android verbs at the top level (
ui,tap,type,paste,swipe,button,touch,gesture,multi-touch,keyb oard-state,screenshot,devices), the fullandroidnamespace, and the per-device daemon.Not available: iOS verbs,
record-video/stream-video(AVFoundation),long-press/app-state(their Android paths live in the macOS forwarders, which import iOS modules), the Viewer, and
init.sim-use devicesis the Android listing rather than the unified schema. Bringing
long-press/app-state/ unifieddevicesover would mean moving their Android halves intoAndroidBackend— happy todo that as a follow-up if you want it.
Testing
swift:6.3image from a cleangit archive, i.e. without the bridge APK — the same thing the new CI job runs):
swift build,swift test— 262 XCTest + 133 swift-testing, all green. Commits 6 and 7 also build and pass on their own.
scripts/build.sh dev,make build,make test— 311 XCTest and 1106 swift-testing tests pass.One test,
ProcessControlTests"cancellableSleep wakes early when the flag is cancelled", failed in 2 of 3 full runs (5.6 s / 9.6 s against its 5 s bound). It fails the same way on
mainon the same machine (5.6 s) and passes 5/5 in isolation, so it looks like a pre-existing timing flake under full-suite load rather than something this PR introduces.
uispawns the daemon through the newinstallPlatformHookspath, and its output is identical to the
mainbinary's;daemon status/daemon stop --allwork as before.android init,android ping,uitwice (cache created, then overwritten), and
tap @Nresolving from the cache;daemon start/status/stopthrough the shared command;scripts/install-linux.s hinto a scratch prefix.