@agimon-ai/po-ios 0.0.1-alpha.0 → 0.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/extensions/server.cjs +179 -109
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +179 -109
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/index.d.mts +10 -6
- package/dist/index.d.mts.map +1 -1
- package/native/sim-helper/PRIVATE_APIS.md +85 -16
- package/native/sim-helper/Sources/PoIosCore/Compaction.swift +8 -0
- package/native/sim-helper/Sources/PoIosCore/Protocol.swift +24 -0
- package/native/sim-helper/Sources/PoIosSimHelper/Backends.swift +287 -24
- package/native/sim-helper/Sources/PoIosSimHelper/PrivateApis.swift +135 -7
- package/native/sim-helper/Sources/PoIosSimHelper/main.swift +3 -1
- package/native/sim-helper/Tests/PoIosCoreTests/CompactionTests.swift +2 -1
- package/native/sim-helper/Tests/PoIosCoreTests/ProtocolTests.swift +17 -0
- package/package.json +6 -6
- package/src/prompts/codemode-how-to-ios/SKILL.md +5 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This package is a composable [Po](https://www.npmjs.com/package/@agimon-ai/po) s
|
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
|
-
The
|
|
14
|
+
The package is opt-in and intentionally absent from init templates. Define an `ios` layer with `packages: ['@agimon-ai/po-ios']` in your modes file, then select that layer in a major mode. The package declares poServer metadata for native server discovery.
|
|
15
15
|
|
|
16
16
|
## Use
|
|
17
17
|
|
|
@@ -41,9 +41,11 @@ ios:
|
|
|
41
41
|
|
|
42
42
|
Release uninstalls the apps a session installed and keeps the device warm. Devices of a dead server are erased before reuse.
|
|
43
43
|
|
|
44
|
-
Accessibility and touch (`observe`, `tap`, `type`, `swipe`, `pressButton`, `waitFor`, `settle`) go through a resident Swift helper in `native/sim-helper`. The server builds it on first use with the person's Xcode (`xcrun swift build`) into `<po home>/ios/native/<hash>/`; set `ios.helperPath` to use a prebuilt one instead.
|
|
44
|
+
Accessibility and touch (`observe`, `tap`, `type`, `swipe`, `pressButton`, `waitFor`, `settle`) go through a resident Swift helper in `native/sim-helper`. The server builds it on first use with the person's Xcode (`xcrun swift build`) into `<po home>/ios/native/<hash>/`; set `ios.helperPath` to use a prebuilt one instead. On Xcode 27.1 (27A9275) and iOS 26.5, direct native-protocol trials verified observation, taps, timed swipes, US-layout ASCII typing (including shifted punctuation and return), home and lock. Arbitrary Unicode text and Siri/volume buttons explicitly return `IOS_HELPER_UNAVAILABLE`, with no clipboard or AX-value fallback. Unsupported text is rejected before any keys are emitted. Missing or changed private API shapes also fail explicitly. See `native/sim-helper/PRIVATE_APIS.md` for evidence and limits.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
This is a documented partial implementation, not completed acceptance of the full iOS plan. Rotation, two-device isolation and built-layer smoke remain unverified. Input acknowledgment does not mean the UI transition has finished; a lock transition took about three seconds to appear in AX observations.
|
|
47
|
+
|
|
48
|
+
Physical devices: `ios.devices()` lists what CoreDevice sees (`xcrun devicectl list devices`), and `ios.acquireDevice({ udid })` leases a paired, reachable phone after the person approves it. An idle disconnected tunnel is checked with a bounded device-info request rather than rejected solely by its listed state. A phone lease shares the `ios_lease` table (`kind = 'device'`) so two sessions never hold one phone, but it has no `ios_device` row: the pool never boots, erases or counts it against `maxBooted`. On a phone only `install` (a device-signed `.app`), `uninstall`, `launch` and `release` work; release only drops the lease. Phone console capture and the XCUITest runner are not implemented. Other phone calls explicitly return `IOS_UNSUPPORTED_HOST`. Live signed-app and runner acceptance is still pending.
|
|
47
49
|
|
|
48
50
|
## Help
|
|
49
51
|
|
|
@@ -53,14 +53,12 @@ var IosError = class extends Error {
|
|
|
53
53
|
};
|
|
54
54
|
//#endregion
|
|
55
55
|
//#region src/services/devicectl/index.ts
|
|
56
|
+
const CONNECTION_TIMEOUT_MS = 2e4;
|
|
56
57
|
function record(value) {
|
|
57
58
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
58
59
|
}
|
|
59
60
|
const text$1 = (value) => typeof value === "string" ? value : "";
|
|
60
|
-
/**
|
|
61
|
-
* Reads `devicectl list devices --json-output`. The shape is hand-written from Apple's documented output and is
|
|
62
|
-
* not yet checked against a real run; unknown fields read as empty rather than failing the whole list.
|
|
63
|
-
*/
|
|
61
|
+
/** Reads physical devices from Xcode 27.1 output, with legacy-field fallback for older Xcode versions. */
|
|
64
62
|
function parsePhysicalDevices(output) {
|
|
65
63
|
let parsed;
|
|
66
64
|
try {
|
|
@@ -69,21 +67,30 @@ function parsePhysicalDevices(output) {
|
|
|
69
67
|
throw new IosError("IOS_COMMAND_FAILED", "devicectl returned unreadable device JSON.");
|
|
70
68
|
}
|
|
71
69
|
const devices = record(record(parsed).result).devices;
|
|
72
|
-
return (Array.isArray(devices) ? devices : []).
|
|
70
|
+
return (Array.isArray(devices) ? devices : []).flatMap((entry) => {
|
|
73
71
|
const device = record(entry);
|
|
74
|
-
const properties = record(device.
|
|
75
|
-
const hardware = record(
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const properties = record(device.properties);
|
|
73
|
+
const hardware = record(properties.hardware);
|
|
74
|
+
const state = record(properties.state);
|
|
75
|
+
const software = record(properties.software);
|
|
76
|
+
const connection = record(properties.connection);
|
|
77
|
+
const legacyHardware = record(device.hardwareProperties);
|
|
78
|
+
const legacyProperties = record(device.deviceProperties);
|
|
79
|
+
const legacyConnection = record(device.connectionProperties);
|
|
80
|
+
const realities = [hardware.reality, legacyHardware.reality].filter((value) => value !== void 0);
|
|
81
|
+
if (realities.length === 0 || realities.some((value) => value !== "physical") || state.visibilityClass === "simulators" || device.visibilityClass === "simulators") return [];
|
|
82
|
+
const udid = text$1(hardware.udid ?? legacyHardware.udid);
|
|
83
|
+
if (udid === "") return [];
|
|
84
|
+
return [{
|
|
85
|
+
udid,
|
|
79
86
|
identifier: text$1(device.identifier),
|
|
80
|
-
name: text$1(
|
|
81
|
-
model: text$1(hardware.marketingName),
|
|
82
|
-
osVersion: text$1(
|
|
83
|
-
platform: text$1(hardware.platform),
|
|
84
|
-
paired: connection.pairingState === "paired",
|
|
85
|
-
connected: connection.tunnelState === "connected"
|
|
86
|
-
};
|
|
87
|
+
name: text$1(state.name ?? legacyProperties.name),
|
|
88
|
+
model: text$1(hardware.marketingName ?? legacyHardware.marketingName),
|
|
89
|
+
osVersion: text$1(record(software.osVersionNumber).stringValue ?? legacyProperties.osVersionNumber),
|
|
90
|
+
platform: text$1(hardware.platform ?? legacyHardware.platform),
|
|
91
|
+
paired: (connection.pairingState ?? legacyConnection.pairingState) === "paired",
|
|
92
|
+
connected: (connection.state ?? legacyConnection.tunnelState) === "connected"
|
|
93
|
+
}];
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
const firstLine$1 = (output) => output.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "";
|
|
@@ -116,6 +123,17 @@ function createDeviceCtl(options) {
|
|
|
116
123
|
await options.remove(file);
|
|
117
124
|
}
|
|
118
125
|
},
|
|
126
|
+
async ensureReachable(identifier, signal) {
|
|
127
|
+
await step([
|
|
128
|
+
"device",
|
|
129
|
+
"info",
|
|
130
|
+
"details",
|
|
131
|
+
"--device",
|
|
132
|
+
identifier,
|
|
133
|
+
"--timeout",
|
|
134
|
+
String(CONNECTION_TIMEOUT_MS / 1e3)
|
|
135
|
+
], signal, 25e3);
|
|
136
|
+
},
|
|
119
137
|
async install(identifier, appPath, signal) {
|
|
120
138
|
await step([
|
|
121
139
|
"device",
|
|
@@ -144,6 +162,7 @@ function createDeviceCtl(options) {
|
|
|
144
162
|
"--device",
|
|
145
163
|
identifier,
|
|
146
164
|
bundleId,
|
|
165
|
+
"--",
|
|
147
166
|
...args
|
|
148
167
|
], signal);
|
|
149
168
|
}
|
|
@@ -484,7 +503,7 @@ function compareVersions(left, right) {
|
|
|
484
503
|
}
|
|
485
504
|
/**
|
|
486
505
|
* The runtime and device type new pool devices use. `runtime` may be `latest`, an identifier, a version or a
|
|
487
|
-
* name; `deviceType` an identifier or a name. Unset means the newest available iOS runtime and its
|
|
506
|
+
* name; `deviceType` an identifier or a name. Unset means the newest available iOS runtime and its first listed iPhone.
|
|
488
507
|
*/
|
|
489
508
|
function resolveDeviceSpec(runtimes, wanted) {
|
|
490
509
|
const ios = runtimes.filter((runtime) => runtime.isAvailable && (runtime.platform === "iOS" || runtime.name.startsWith("iOS"))).sort((left, right) => compareVersions(right.version, left.version));
|
|
@@ -499,7 +518,7 @@ function resolveDeviceSpec(runtimes, wanted) {
|
|
|
499
518
|
throw new IosError("IOS_NO_RUNTIME", `No available iOS simulator runtime matches "${query ?? "latest"}" (available: ${available}). Install one in Xcode settings, Components.`);
|
|
500
519
|
}
|
|
501
520
|
const types = runtime.supportedDeviceTypes;
|
|
502
|
-
const deviceType = wanted.deviceType === void 0 ? types.
|
|
521
|
+
const deviceType = wanted.deviceType === void 0 ? types.find((type) => type.productFamily === "iPhone") : types.find((type) => type.identifier === wanted.deviceType || type.name === wanted.deviceType);
|
|
503
522
|
if (deviceType === void 0) throw new IosError("IOS_NO_RUNTIME", `${runtime.name} supports no device type matching "${wanted.deviceType ?? "iPhone"}".`);
|
|
504
523
|
return {
|
|
505
524
|
runtime: runtime.identifier,
|
|
@@ -701,6 +720,7 @@ function createDevicePool(options) {
|
|
|
701
720
|
const supported = probe.platform === "darwin";
|
|
702
721
|
const wakers = /* @__PURE__ */ new Set();
|
|
703
722
|
const orphans = /* @__PURE__ */ new Map();
|
|
723
|
+
const queues = /* @__PURE__ */ new Map();
|
|
704
724
|
let spec;
|
|
705
725
|
let timer;
|
|
706
726
|
let ticking;
|
|
@@ -922,18 +942,31 @@ function createDevicePool(options) {
|
|
|
922
942
|
if (leases.length > 1) throw new IosError("IOS_INVALID_ARGUMENT", `This session holds ${String(leases.length)} leases; pass leaseId (${leases.map((lease) => lease.lease_id).join(", ")}).`);
|
|
923
943
|
return leases[0];
|
|
924
944
|
};
|
|
925
|
-
|
|
945
|
+
/** Calls and release share one queue, including session teardown outside the namespace. */
|
|
946
|
+
const queueLease = async (owner, leaseId, work) => {
|
|
926
947
|
const lease = await resolveLease(owner, leaseId);
|
|
948
|
+
const next = (queues.get(lease.lease_id) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
949
|
+
if (stopped) throw new IosError("IOS_LEASE_LOST", "The iOS pool is stopping.");
|
|
950
|
+
return work(await resolveLease(owner, lease.lease_id));
|
|
951
|
+
});
|
|
952
|
+
queues.set(lease.lease_id, next);
|
|
953
|
+
try {
|
|
954
|
+
return await next;
|
|
955
|
+
} finally {
|
|
956
|
+
if (queues.get(lease.lease_id) === next) queues.delete(lease.lease_id);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
const release = (owner, leaseId, reset = "apps") => queueLease(owner, leaseId, async (lease) => {
|
|
927
960
|
const result = await view(lease);
|
|
961
|
+
if (lease.kind === "simulator") await options.helper?.forget(lease.udid);
|
|
928
962
|
const udid = await store.release(lease.lease_id, reset === "erase", now());
|
|
929
963
|
if (udid !== void 0 && lease.kind === "simulator") {
|
|
930
|
-
options.helper?.forget(udid);
|
|
931
964
|
const device = await store.claimDirty(serverId, udid);
|
|
932
965
|
if (device !== void 0) await clean(device);
|
|
933
966
|
}
|
|
934
967
|
wake();
|
|
935
968
|
return result;
|
|
936
|
-
};
|
|
969
|
+
});
|
|
937
970
|
const reconcile = async () => {
|
|
938
971
|
const rows = await store.listDevices();
|
|
939
972
|
const listed = await simctl.listDevices();
|
|
@@ -1090,11 +1123,16 @@ function createDevicePool(options) {
|
|
|
1090
1123
|
};
|
|
1091
1124
|
});
|
|
1092
1125
|
},
|
|
1093
|
-
async acquireDevice(owner, udid, label) {
|
|
1126
|
+
async acquireDevice(owner, udid, label, signal) {
|
|
1094
1127
|
requireSupported();
|
|
1095
|
-
|
|
1128
|
+
if (stopped) throw new IosError("IOS_POOL_BUSY", "The iOS pool is stopping.");
|
|
1129
|
+
signal?.throwIfAborted();
|
|
1130
|
+
const devicectl = requireDevicectl();
|
|
1131
|
+
const device = (await devicectl.listDevices(signal)).find((candidate) => candidate.udid === udid);
|
|
1096
1132
|
if (device === void 0) throw new IosError("IOS_INVALID_ARGUMENT", `No physical device ${udid} is visible. Call ios.devices() to list them.`);
|
|
1097
|
-
if (!device.paired
|
|
1133
|
+
if (!device.paired) throw new IosError("IOS_INVALID_ARGUMENT", `${device.name || udid} is not ready: pair it with this Mac, unlock it, and enable Developer Mode.`);
|
|
1134
|
+
if (!device.connected) await devicectl.ensureReachable(udid, signal);
|
|
1135
|
+
signal?.throwIfAborted();
|
|
1098
1136
|
const lease = await store.leasePhone(udid, {
|
|
1099
1137
|
leaseId: crypto.randomUUID(),
|
|
1100
1138
|
owner,
|
|
@@ -1102,6 +1140,10 @@ function createDevicePool(options) {
|
|
|
1102
1140
|
label: label ?? null,
|
|
1103
1141
|
now: now()
|
|
1104
1142
|
});
|
|
1143
|
+
if (signal?.aborted) {
|
|
1144
|
+
if (lease !== void 0) await store.release(lease.lease_id, false, now());
|
|
1145
|
+
signal.throwIfAborted();
|
|
1146
|
+
}
|
|
1105
1147
|
if (lease !== void 0) return view(lease);
|
|
1106
1148
|
const held = (await store.allLeases()).find((candidate) => candidate.udid === udid);
|
|
1107
1149
|
if (held !== void 0 && held.owner_root === owner) return view(held);
|
|
@@ -1112,6 +1154,10 @@ function createDevicePool(options) {
|
|
|
1112
1154
|
await store.touchLease(lease.lease_id, now());
|
|
1113
1155
|
return view(lease);
|
|
1114
1156
|
},
|
|
1157
|
+
withLease: (owner, leaseId, work) => queueLease(owner, leaseId, async (lease) => {
|
|
1158
|
+
await store.touchLease(lease.lease_id, now());
|
|
1159
|
+
return work(await view(lease));
|
|
1160
|
+
}),
|
|
1115
1161
|
async leases(owner) {
|
|
1116
1162
|
return Promise.all((await store.leasesOf(owner)).map(view));
|
|
1117
1163
|
},
|
|
@@ -1126,7 +1172,8 @@ function createDevicePool(options) {
|
|
|
1126
1172
|
if (stopped) return;
|
|
1127
1173
|
stopped = true;
|
|
1128
1174
|
if (timer !== void 0) clearInterval(timer);
|
|
1129
|
-
|
|
1175
|
+
await Promise.allSettled(queues.values());
|
|
1176
|
+
await options.helper?.dispose();
|
|
1130
1177
|
wake();
|
|
1131
1178
|
await ticking;
|
|
1132
1179
|
if (!supported) return;
|
|
@@ -1261,9 +1308,18 @@ function spawnLineProcess(binary, env) {
|
|
|
1261
1308
|
const exitListeners = [];
|
|
1262
1309
|
let buffer = "";
|
|
1263
1310
|
let exited = false;
|
|
1311
|
+
let resolveExit;
|
|
1312
|
+
const closed = new Promise((resolve) => {
|
|
1313
|
+
resolveExit = resolve;
|
|
1314
|
+
});
|
|
1315
|
+
const kill = () => {
|
|
1316
|
+
if (!exited) child.kill("SIGKILL");
|
|
1317
|
+
return closed;
|
|
1318
|
+
};
|
|
1264
1319
|
const exit = (code) => {
|
|
1265
1320
|
if (exited) return;
|
|
1266
1321
|
exited = true;
|
|
1322
|
+
resolveExit();
|
|
1267
1323
|
for (const listener of exitListeners) listener(code);
|
|
1268
1324
|
};
|
|
1269
1325
|
child.stdout.setEncoding("utf8");
|
|
@@ -1272,9 +1328,13 @@ function spawnLineProcess(binary, env) {
|
|
|
1272
1328
|
for (let index = buffer.indexOf("\n"); index >= 0; index = buffer.indexOf("\n")) {
|
|
1273
1329
|
const line = buffer.slice(0, index);
|
|
1274
1330
|
buffer = buffer.slice(index + 1);
|
|
1331
|
+
if (line.length > 8388608) {
|
|
1332
|
+
kill();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1275
1335
|
if (line.trim() !== "") for (const listener of lineListeners) listener(line);
|
|
1276
1336
|
}
|
|
1277
|
-
if (buffer.length > 8388608)
|
|
1337
|
+
if (buffer.length > 8388608) kill();
|
|
1278
1338
|
});
|
|
1279
1339
|
child.stderr.resume();
|
|
1280
1340
|
child.stdin.on("error", () => void 0);
|
|
@@ -1286,9 +1346,7 @@ function spawnLineProcess(binary, env) {
|
|
|
1286
1346
|
},
|
|
1287
1347
|
onLine: (listener) => lineListeners.push(listener),
|
|
1288
1348
|
onExit: (listener) => exitListeners.push(listener),
|
|
1289
|
-
kill
|
|
1290
|
-
child.kill();
|
|
1291
|
-
}
|
|
1349
|
+
kill
|
|
1292
1350
|
};
|
|
1293
1351
|
}
|
|
1294
1352
|
//#endregion
|
|
@@ -1300,8 +1358,8 @@ function replyError(error) {
|
|
|
1300
1358
|
return require_contracts.IOS_ERROR_CODES.includes(code) ? new IosError(code, message) : unavailable(`${code ? `${code}: ` : ""}${message}`);
|
|
1301
1359
|
}
|
|
1302
1360
|
/**
|
|
1303
|
-
* Client for the resident Swift helper.
|
|
1304
|
-
*
|
|
1361
|
+
* Client for the resident Swift helper. Requests dispatch in order, matched by id. A timeout stops the process;
|
|
1362
|
+
* restart waits for its observed exit, at most three starts in five minutes. A protocol mismatch rebuilds once.
|
|
1305
1363
|
*/
|
|
1306
1364
|
function createSimHelper(options) {
|
|
1307
1365
|
const now = options.now ?? Date.now;
|
|
@@ -1310,90 +1368,113 @@ function createSimHelper(options) {
|
|
|
1310
1368
|
const starts = [];
|
|
1311
1369
|
let current;
|
|
1312
1370
|
let starting;
|
|
1371
|
+
let stopping = Promise.resolve();
|
|
1372
|
+
let dispatch = Promise.resolve();
|
|
1313
1373
|
let nextId = 1;
|
|
1314
1374
|
let disposed = false;
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
entry.reject(error);
|
|
1375
|
+
const take = (id) => {
|
|
1376
|
+
const entry = pending.get(id);
|
|
1377
|
+
if (entry !== void 0) {
|
|
1319
1378
|
pending.delete(id);
|
|
1379
|
+
entry.cleanup();
|
|
1320
1380
|
}
|
|
1381
|
+
return entry;
|
|
1382
|
+
};
|
|
1383
|
+
const failAll = (error, target) => {
|
|
1384
|
+
for (const [id, entry] of pending) if (target === void 0 || entry.target === target) take(id)?.reject(error);
|
|
1321
1385
|
};
|
|
1322
|
-
const stop = (target) => {
|
|
1323
|
-
if (current === target)
|
|
1324
|
-
|
|
1325
|
-
|
|
1386
|
+
const stop = (target, error = unavailable("The iOS helper stopped; it restarts on the next call.")) => {
|
|
1387
|
+
if (current === target) {
|
|
1388
|
+
current = void 0;
|
|
1389
|
+
dispatch = Promise.resolve();
|
|
1390
|
+
}
|
|
1391
|
+
stopping = Promise.all([stopping, target.kill()]).then(() => void 0);
|
|
1392
|
+
failAll(error, target);
|
|
1393
|
+
return stopping;
|
|
1326
1394
|
};
|
|
1327
1395
|
const send = (target, op, udid, params, signal) => new Promise((resolve, reject) => {
|
|
1396
|
+
if (disposed || current !== target) {
|
|
1397
|
+
reject(unavailable("The iOS helper is not running."));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1328
1400
|
if (signal?.aborted) {
|
|
1329
1401
|
reject(signal.reason);
|
|
1330
1402
|
return;
|
|
1331
1403
|
}
|
|
1332
1404
|
const id = nextId++;
|
|
1405
|
+
const aborted = () => take(id)?.reject(signal?.reason);
|
|
1333
1406
|
const timer = setTimeout(() => {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
stop(target);
|
|
1407
|
+
if (current === target) starting = void 0;
|
|
1408
|
+
stop(target, unavailable(`The iOS helper did not answer ${op} within ${String(timeoutMs)} ms. It restarts on the next call.`));
|
|
1337
1409
|
}, timeoutMs);
|
|
1338
1410
|
pending.set(id, {
|
|
1411
|
+
target,
|
|
1339
1412
|
resolve,
|
|
1340
1413
|
reject,
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (entry === void 0) return;
|
|
1346
|
-
clearTimeout(entry.timer);
|
|
1347
|
-
pending.delete(id);
|
|
1348
|
-
reject(signal.reason);
|
|
1349
|
-
}, { once: true });
|
|
1350
|
-
options.developerDir().then((developerDir) => target.write(JSON.stringify({
|
|
1351
|
-
v: 1,
|
|
1352
|
-
id,
|
|
1353
|
-
op,
|
|
1354
|
-
...udid === void 0 ? {} : { udid },
|
|
1355
|
-
deviceSet: options.deviceSet,
|
|
1356
|
-
developerDir,
|
|
1357
|
-
params
|
|
1358
|
-
}))).catch((error) => {
|
|
1359
|
-
const entry = pending.get(id);
|
|
1360
|
-
if (entry === void 0) return;
|
|
1361
|
-
clearTimeout(entry.timer);
|
|
1362
|
-
pending.delete(id);
|
|
1363
|
-
reject(error);
|
|
1414
|
+
cleanup: () => {
|
|
1415
|
+
clearTimeout(timer);
|
|
1416
|
+
signal?.removeEventListener("abort", aborted);
|
|
1417
|
+
}
|
|
1364
1418
|
});
|
|
1419
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
1420
|
+
dispatch = dispatch.then(async () => {
|
|
1421
|
+
if (!pending.has(id)) return;
|
|
1422
|
+
const developerDir = await options.developerDir();
|
|
1423
|
+
if (!pending.has(id)) return;
|
|
1424
|
+
target.write(JSON.stringify({
|
|
1425
|
+
v: 1,
|
|
1426
|
+
id,
|
|
1427
|
+
op,
|
|
1428
|
+
...udid === void 0 ? {} : { udid },
|
|
1429
|
+
deviceSet: options.deviceSet,
|
|
1430
|
+
developerDir,
|
|
1431
|
+
params
|
|
1432
|
+
}));
|
|
1433
|
+
}).catch((error) => take(id)?.reject(error));
|
|
1365
1434
|
});
|
|
1366
1435
|
const launch = async (rebuild) => {
|
|
1367
1436
|
const recent = starts.filter((started) => now() - started < require_contracts.HELPER_RESTART_WINDOW_MS);
|
|
1368
1437
|
starts.splice(0, starts.length, ...recent);
|
|
1369
1438
|
if (starts.length >= 3) throw unavailable("The iOS helper keeps failing; it restarted three times in five minutes. Try again later.");
|
|
1370
1439
|
starts.push(now());
|
|
1371
|
-
const
|
|
1440
|
+
const binary = await options.binary(rebuild);
|
|
1441
|
+
if (disposed) throw unavailable("The iOS helper is shut down.");
|
|
1442
|
+
const spawned = options.spawn(binary);
|
|
1372
1443
|
current = spawned;
|
|
1373
1444
|
spawned.onLine((line) => {
|
|
1374
1445
|
let reply;
|
|
1375
1446
|
try {
|
|
1376
|
-
|
|
1447
|
+
const parsed = JSON.parse(line);
|
|
1448
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
1449
|
+
reply = parsed;
|
|
1377
1450
|
} catch {
|
|
1378
1451
|
return;
|
|
1379
1452
|
}
|
|
1380
|
-
const
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
else entry.reject(replyError(reply.error));
|
|
1453
|
+
const id = typeof reply.id === "number" ? reply.id : void 0;
|
|
1454
|
+
if (id === void 0 || pending.get(id)?.target !== spawned) return;
|
|
1455
|
+
const entry = take(id);
|
|
1456
|
+
if (reply.ok === true) entry?.resolve(reply.result);
|
|
1457
|
+
else entry?.reject(replyError(reply.error));
|
|
1386
1458
|
});
|
|
1387
1459
|
spawned.onExit((code) => {
|
|
1388
1460
|
if (current === spawned) {
|
|
1389
1461
|
current = void 0;
|
|
1390
1462
|
starting = void 0;
|
|
1463
|
+
dispatch = Promise.resolve();
|
|
1391
1464
|
}
|
|
1392
|
-
failAll(unavailable(`The iOS helper exited (code ${String(code)}); it restarts on the next call.`));
|
|
1465
|
+
failAll(unavailable(`The iOS helper exited (code ${String(code)}); it restarts on the next call.`), spawned);
|
|
1393
1466
|
});
|
|
1394
|
-
|
|
1467
|
+
let probe;
|
|
1468
|
+
try {
|
|
1469
|
+
const result = await send(spawned, "probe", void 0, {});
|
|
1470
|
+
if (result === null || typeof result !== "object" || !("protocolVersion" in result) || !Number.isInteger(result.protocolVersion) || !("missing" in result) || !Array.isArray(result.missing) || !result.missing.every((capability) => typeof capability === "string")) throw unavailable("The iOS helper returned an invalid probe.");
|
|
1471
|
+
probe = result;
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
await stop(spawned);
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1395
1476
|
if (probe.protocolVersion !== 1) {
|
|
1396
|
-
stop(spawned);
|
|
1477
|
+
await stop(spawned);
|
|
1397
1478
|
if (rebuild) throw unavailable(`The iOS helper speaks protocol ${String(probe.protocolVersion)}, not ${String(1)}, even after a rebuild.`);
|
|
1398
1479
|
return launch(true);
|
|
1399
1480
|
}
|
|
@@ -1402,12 +1483,16 @@ function createSimHelper(options) {
|
|
|
1402
1483
|
probe
|
|
1403
1484
|
};
|
|
1404
1485
|
};
|
|
1405
|
-
const ready = () => {
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1486
|
+
const ready = async () => {
|
|
1487
|
+
await stopping;
|
|
1488
|
+
if (disposed) throw unavailable("The iOS helper is shut down.");
|
|
1489
|
+
if (starting === void 0) {
|
|
1490
|
+
const attempt = launch(false).catch((error) => {
|
|
1491
|
+
if (starting === attempt) starting = void 0;
|
|
1492
|
+
throw error;
|
|
1493
|
+
});
|
|
1494
|
+
starting = attempt;
|
|
1495
|
+
}
|
|
1411
1496
|
return starting;
|
|
1412
1497
|
};
|
|
1413
1498
|
return {
|
|
@@ -1422,22 +1507,15 @@ function createSimHelper(options) {
|
|
|
1422
1507
|
const { child: target } = await ready();
|
|
1423
1508
|
await send(target, "act", udid, action, signal);
|
|
1424
1509
|
},
|
|
1425
|
-
forget(udid) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1510
|
+
async forget(udid) {
|
|
1511
|
+
await stopping;
|
|
1512
|
+
if (starting !== void 0) await starting;
|
|
1513
|
+
if (current !== void 0) await send(current, "forget", udid, {});
|
|
1428
1514
|
},
|
|
1429
|
-
dispose() {
|
|
1515
|
+
async dispose() {
|
|
1430
1516
|
disposed = true;
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (target === void 0) return;
|
|
1434
|
-
target.write(JSON.stringify({
|
|
1435
|
-
v: 1,
|
|
1436
|
-
id: 0,
|
|
1437
|
-
op: "shutdown",
|
|
1438
|
-
params: {}
|
|
1439
|
-
}));
|
|
1440
|
-
stop(target);
|
|
1517
|
+
if (current !== void 0) await stop(current, unavailable("The iOS helper is shut down."));
|
|
1518
|
+
else await stopping;
|
|
1441
1519
|
}
|
|
1442
1520
|
};
|
|
1443
1521
|
}
|
|
@@ -1970,7 +2048,7 @@ declare const ios: {
|
|
|
1970
2048
|
|
|
1971
2049
|
/** Physical devices this Mac sees over CoreDevice, and whether a session holds them. */
|
|
1972
2050
|
devices(): Promise<{ udid: string; name: string; model: string; osVersion: string; paired: boolean; connected: boolean; leased: boolean; leaseId?: string }[]>;
|
|
1973
|
-
/** Leases a
|
|
2051
|
+
/** Leases a paired, reachable physical device by UDID, reconnecting an idle tunnel. Asks the person first. */
|
|
1974
2052
|
acquireDevice(options: { udid: string; label?: string }): Promise<IosLease>;
|
|
1975
2053
|
};`;
|
|
1976
2054
|
/** A physical device is the person's own hardware, so leasing one always asks them first. */
|
|
@@ -2003,8 +2081,6 @@ function base64(bytes) {
|
|
|
2003
2081
|
}
|
|
2004
2082
|
/** The `ios` code-mode namespace: lease a pool simulator, then install, launch, look and read logs on it. */
|
|
2005
2083
|
function createIosNamespace(options) {
|
|
2006
|
-
/** Calls on one lease run one at a time, so two scripts never interleave on one device. */
|
|
2007
|
-
const queues = /* @__PURE__ */ new Map();
|
|
2008
2084
|
return {
|
|
2009
2085
|
name: "ios",
|
|
2010
2086
|
summary: "Leased iOS simulators from a machine-wide pool shared by every Po session: ios.acquire() a device, install and launch an app, observe the accessibility tree, tap, type and swipe, take screenshots and read logs, then ios.release().",
|
|
@@ -2025,17 +2101,11 @@ function createIosNamespace(options) {
|
|
|
2025
2101
|
};
|
|
2026
2102
|
const onLease = async (leaseId, work) => {
|
|
2027
2103
|
const live = pool();
|
|
2028
|
-
|
|
2029
|
-
const next = (queues.get(lease.leaseId) ?? Promise.resolve()).catch(() => void 0).then(() => {
|
|
2104
|
+
return live.withLease(options.owner, leaseId, (lease) => {
|
|
2030
2105
|
run.signal.throwIfAborted();
|
|
2106
|
+
if (disposed) throw new Error("iOS namespace run is disposed.");
|
|
2031
2107
|
return work(lease, live);
|
|
2032
2108
|
});
|
|
2033
|
-
queues.set(lease.leaseId, next);
|
|
2034
|
-
try {
|
|
2035
|
-
return await next;
|
|
2036
|
-
} finally {
|
|
2037
|
-
if (queues.get(lease.leaseId) === next) queues.delete(lease.leaseId);
|
|
2038
|
-
}
|
|
2039
2109
|
};
|
|
2040
2110
|
/** As onLease, refusing physical devices for calls only simulators support so far. */
|
|
2041
2111
|
const onSimulator = (leaseId, call, work) => onLease(leaseId, (lease, live) => {
|
|
@@ -2308,7 +2378,7 @@ function createIosNamespace(options) {
|
|
|
2308
2378
|
members.devices = async () => pool().devices(options.owner);
|
|
2309
2379
|
members.acquireDevice = async (input) => {
|
|
2310
2380
|
const params = parse(AcquireDeviceSchema, input, "acquireDevice");
|
|
2311
|
-
return pool().acquireDevice(options.owner, params.udid, params.label);
|
|
2381
|
+
return pool().acquireDevice(options.owner, params.udid, params.label, run.signal);
|
|
2312
2382
|
};
|
|
2313
2383
|
return members;
|
|
2314
2384
|
}
|