mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
6e9e45c9f0
This pull request lets Argent drive a simulator or emulator another process is already running, attaching to its simulator-server rather than spawning a second one. A provider writes a JSON descriptor to `~/.argent/providers/` listing the devices it offers and the mechanisms Argent may use on each. They appear in `list-devices` with an `ext:` id and work with the existing tools. The file is re-read on every call, so a withdrawal or a narrowed grant applies immediately. `ios.additionalDeviceSets` (#600) already makes such a simulator reachable by UDID. What changes is ownership. `boot-device` and `stop-simulator-server` refuse these devices, anything the provider did not grant is refused with a message naming it and Argent uses only the endpoints its own simulator-server build serves. A grant binds to the device rather than to one of its names, so the real udid or serial is gated exactly like the `ext:` id. Android emulators are covered too and a device visible to both a provider and `adb`/`simctl` is listed once. The contract ships as `schemas/device-provider-v1.json`, validated by `argent providers check`, and is documented in `docs/reference/device-providers.mdx`. Includes unit tests and an e2e phase where the harness acts as its own provider. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for discovering and connecting to externally managed devices. * Added `argent providers` commands for listing, validating, publishing, withdrawing, and pruning providers. * Added provider-aware simulator, debugger, profiler, native tools, and device listings. * Added capability controls, endpoint validation, revocation handling, and provider-specific diagnostics. * Added automatic CLI discovery for provider integrations. * **Bug Fixes** * Improved service recovery and paused-runtime error reporting. * Improved simulator keyboard and paste command reliability. * **Documentation** * Documented provider descriptors, capabilities, lifecycle rules, and CLI usage. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
167 lines
6.2 KiB
TypeScript
167 lines
6.2 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
buildDyldInsertLibraries,
|
|
withdrawDyldInsertLibraries,
|
|
} from "../src/blueprints/native-devtools";
|
|
|
|
describe("buildDyldInsertLibraries", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function makeTempFile(name: string): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "native-devtools-env-"));
|
|
tempDirs.push(dir);
|
|
const filePath = path.join(dir, name);
|
|
fs.writeFileSync(filePath, "");
|
|
return filePath;
|
|
}
|
|
|
|
it("strips stale bootstrap entries and non-existent paths, keeps files present on disk", () => {
|
|
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const unrelated = makeTempFile("libOtherInspector.dylib");
|
|
const staleBootstrap = "/tmp/old/location/libArgentInjectionBootstrap.dylib";
|
|
const truncatedEntry = "/Users/filip/.nvm/versi";
|
|
|
|
const result = buildDyldInsertLibraries(
|
|
[staleBootstrap, truncatedEntry, unrelated].join(":"),
|
|
currentBootstrap
|
|
);
|
|
|
|
// staleBootstrap stripped (argent basename); truncatedEntry stripped (not on disk)
|
|
expect(result).toBe([unrelated, currentBootstrap].join(":"));
|
|
});
|
|
|
|
it("preserves valid non-bootstrap loader entries while deduplicating the active bootstrap", () => {
|
|
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const unrelated = makeTempFile("libCustomTracing.dylib");
|
|
|
|
const result = buildDyldInsertLibraries(
|
|
[currentBootstrap, "@loader_path/Other.dylib", unrelated].join(":"),
|
|
currentBootstrap
|
|
);
|
|
|
|
expect(result).toBe(["@loader_path/Other.dylib", unrelated, currentBootstrap].join(":"));
|
|
});
|
|
|
|
it("strips stale legacy libInjectionBootstrap.dylib paths when merging with the renamed bootstrap", () => {
|
|
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const legacyBootstrap = "/tmp/old/libInjectionBootstrap.dylib";
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
|
|
const result = buildDyldInsertLibraries(
|
|
[legacyBootstrap, thirdParty].join(":"),
|
|
currentBootstrap
|
|
);
|
|
|
|
expect(result).toBe([thirdParty, currentBootstrap].join(":"));
|
|
});
|
|
|
|
it("is idempotent when called with its own output", () => {
|
|
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
|
|
const first = buildDyldInsertLibraries(thirdParty, currentBootstrap);
|
|
const second = buildDyldInsertLibraries(first, currentBootstrap);
|
|
|
|
expect(second).toBe(first);
|
|
});
|
|
|
|
it("cleans up truncated entries from simctl getenv 127-byte corruption", () => {
|
|
const currentBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
// Simulate the truncated entries that accumulate from the simctl getenv bug
|
|
const truncated1 = "/Users/mdk/.nvm/versions/node/v24.5.0/lib/nod";
|
|
const truncated2 =
|
|
"/Users/mdk/.nvm/versions/node/v24.5.0/lib/node_modules/@swmansion/argent/dylibs/l";
|
|
|
|
const result = buildDyldInsertLibraries(
|
|
[truncated1, truncated2, currentBootstrap, thirdParty].join(":"),
|
|
currentBootstrap
|
|
);
|
|
|
|
// Both truncated entries stripped (not on disk), bootstrap deduped, thirdParty preserved
|
|
expect(result).toBe([thirdParty, currentBootstrap].join(":"));
|
|
});
|
|
});
|
|
|
|
describe("withdrawDyldInsertLibraries", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function makeTempFile(name: string): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "native-devtools-env-"));
|
|
tempDirs.push(dir);
|
|
const filePath = path.join(dir, name);
|
|
fs.writeFileSync(filePath, "");
|
|
return filePath;
|
|
}
|
|
|
|
/**
|
|
* The case this exists for; we armed a simulator a device provider went on to
|
|
* claim. Its bootstrap has to survive, because it is the injection the
|
|
* provider's app is about to load.
|
|
*/
|
|
it("removes our bootstrap and leaves a provider's in place", () => {
|
|
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const providerBootstrap = makeTempFile("libInjectionBootstrap.dylib");
|
|
|
|
expect(
|
|
withdrawDyldInsertLibraries([providerBootstrap, ourBootstrap].join(":"), ourBootstrap)
|
|
).toBe(providerBootstrap);
|
|
});
|
|
|
|
/**
|
|
* `null`, not `""`; dyld reads an empty entry as a malformed list, so the
|
|
* variable has to be unset rather than blanked.
|
|
*/
|
|
it("asks for the variable to be unset when only our entries were in it", () => {
|
|
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const staleBootstrap = "/tmp/old/location/libArgentInjectionBootstrap.dylib";
|
|
|
|
expect(
|
|
withdrawDyldInsertLibraries([staleBootstrap, ourBootstrap].join(":"), ourBootstrap)
|
|
).toBe(null);
|
|
expect(withdrawDyldInsertLibraries("", ourBootstrap)).toBe(null);
|
|
});
|
|
|
|
it("keeps third-party and loader-path entries we never owned", () => {
|
|
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
|
|
expect(
|
|
withdrawDyldInsertLibraries(
|
|
[thirdParty, "@loader_path/Other.dylib", ourBootstrap].join(":"),
|
|
ourBootstrap
|
|
)
|
|
).toBe([thirdParty, "@loader_path/Other.dylib"].join(":"));
|
|
});
|
|
|
|
it("is a no-op on an environment that never carried us", () => {
|
|
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
|
|
expect(withdrawDyldInsertLibraries(thirdParty, ourBootstrap)).toBe(thirdParty);
|
|
});
|
|
|
|
it("undoes exactly what arming did", () => {
|
|
const ourBootstrap = makeTempFile("libArgentInjectionBootstrap.dylib");
|
|
const thirdParty = makeTempFile("libSimCamLoader.dylib");
|
|
|
|
const armed = buildDyldInsertLibraries(thirdParty, ourBootstrap);
|
|
expect(withdrawDyldInsertLibraries(armed, ourBootstrap)).toBe(thirdParty);
|
|
});
|
|
});
|