@tapflowio/ios-agent 0.28.1 → 0.29.0
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/NOTICE +1 -1
- package/dist/IOSAgent.d.ts +24 -3
- package/dist/IOSAgent.d.ts.map +1 -1
- package/dist/IOSAgent.js +117 -29
- package/dist/IOSAgent.js.map +1 -1
- package/dist/LeanStore.d.ts +24 -3
- package/dist/LeanStore.d.ts.map +1 -1
- package/dist/LeanStore.js +61 -2
- package/dist/LeanStore.js.map +1 -1
- package/dist/SimctlWrapper.d.ts +14 -1
- package/dist/SimctlWrapper.d.ts.map +1 -1
- package/dist/SimctlWrapper.js +46 -3
- package/dist/SimctlWrapper.js.map +1 -1
- package/dist/TouchHelper.d.ts +11 -1
- package/dist/TouchHelper.d.ts.map +1 -1
- package/dist/TouchHelper.js +53 -2
- package/dist/TouchHelper.js.map +1 -1
- package/dist/leanLabels.d.ts +5 -0
- package/dist/leanLabels.d.ts.map +1 -1
- package/dist/leanLabels.js +5 -1
- package/dist/leanLabels.js.map +1 -1
- package/dist/serviceTiers.d.ts +25 -0
- package/dist/serviceTiers.d.ts.map +1 -0
- package/dist/serviceTiers.js +251 -0
- package/dist/serviceTiers.js.map +1 -0
- package/dist/xcuiTree.d.ts.map +1 -1
- package/dist/xcuiTree.js +23 -6
- package/dist/xcuiTree.js.map +1 -1
- package/package.json +5 -5
package/NOTICE
CHANGED
|
@@ -9,4 +9,4 @@ licensed under the Apache License, Version 2.0 (see LICENSE-APACHE-2.0).
|
|
|
9
9
|
It includes a list of launchd labels selected from simslim
|
|
10
10
|
(https://github.com/MobAI-App/simslim),
|
|
11
11
|
licensed under the MIT License. Copyright (c) 2026 Interlap.
|
|
12
|
-
The full notice is in dist/leanLabels.js.
|
|
12
|
+
The full notice is in dist/leanLabels.js and dist/serviceTiers.js.
|
package/dist/IOSAgent.d.ts
CHANGED
|
@@ -157,14 +157,22 @@ export declare class IOSAgent implements DeviceAgent, NetworkControlCapability {
|
|
|
157
157
|
private abandonBoot;
|
|
158
158
|
private handleDeviceBoot;
|
|
159
159
|
private bootWithZombieRecovery;
|
|
160
|
+
/** The tier a boot gets when it does not ask for one: Lean mode's setting. A person's boot is always this. */
|
|
161
|
+
private defaultTier;
|
|
162
|
+
/** What the device boots with now, for the relay: the marker's tier, or `unsupported` on a runtime that cannot take one. */
|
|
163
|
+
private reportedTier;
|
|
160
164
|
/**
|
|
161
165
|
* Called only when the device is known to be shut down: `launchd_sim` reads the store when it
|
|
162
166
|
* starts, and a write while it runs is not known to survive. So a device that was already up when
|
|
163
|
-
* asked for is booted as it is, and
|
|
167
|
+
* asked for is booted as it is, and takes the default tier from its next boot through tapflow —
|
|
168
|
+
* unless a run asked for a tier, which restarts it (`retier` in `handleDeviceBoot`).
|
|
164
169
|
*
|
|
165
|
-
* **
|
|
170
|
+
* **Every boot sets an exact tier**, the asked-for one or the default. Adding to whatever the last boot left
|
|
171
|
+
* is how a device a run left slim reached a person still slim (L2 review R4-S1).
|
|
172
|
+
*
|
|
173
|
+
* **A failure here never stops the boot.** A tier is an optimisation and booting is the job.
|
|
166
174
|
*/
|
|
167
|
-
private
|
|
175
|
+
private prepareTier;
|
|
168
176
|
/** Put back what Lean mode disabled on a device that is now shut down. A no-op for devices it never touched. */
|
|
169
177
|
private revertLean;
|
|
170
178
|
/**
|
|
@@ -175,6 +183,19 @@ export declare class IOSAgent implements DeviceAgent, NetworkControlCapability {
|
|
|
175
183
|
*/
|
|
176
184
|
private revertLeftoverLean;
|
|
177
185
|
private handleDeviceShutdown;
|
|
186
|
+
/**
|
|
187
|
+
* What `flow run` asks reset before a run, carried out on a booted device. Only the agent's report says it happened.
|
|
188
|
+
* It waits for the system services first: `Booted` shows before tccd and securityd serve, and not only on a boot this
|
|
189
|
+
* agent started — a device someone else just booted reads `booted` too. On a settled device the wait returns at once.
|
|
190
|
+
* The wait does not watch `isStale`; a shutdown meanwhile holds this boot's abandonment until its deadline.
|
|
191
|
+
*/
|
|
192
|
+
private resetForRun;
|
|
193
|
+
/**
|
|
194
|
+
* The pasteboard step of a run reset. The queue can be held by a clipboard operation already running, so the wait for
|
|
195
|
+
* it is bounded too; a clear that reaches the front after the step gave up does nothing, because by then the run may
|
|
196
|
+
* have put something of its own there.
|
|
197
|
+
*/
|
|
198
|
+
private clearPasteboardQueued;
|
|
178
199
|
private ensureTouchHelper;
|
|
179
200
|
private readonly clipboardQueue;
|
|
180
201
|
private readonly parkedSentinels;
|
package/dist/IOSAgent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA4C,MAAM,EAAE,WAAW,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"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"}
|
package/dist/IOSAgent.js
CHANGED
|
@@ -7,6 +7,8 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import { WebSocket } from 'ws';
|
|
8
8
|
import { createLogger, PlatformError, ValidationError, bootAbandonMessage, BOOT_NO_SESSION_STATE, SHUTDOWN_NO_SESSION_STATE, downloadBuild } from '@tapflowio/agent-core';
|
|
9
9
|
const logger = createLogger('ios-agent');
|
|
10
|
+
/** The run reset's pasteboard step, the wait for the clipboard queue included; one step's bound like the others. */
|
|
11
|
+
const RUN_RESET_PASTEBOARD_TIMEOUT_MS = 10_000;
|
|
10
12
|
// Typed so a typo cannot ship silently — the viewer gates the whole clipboard bridge on this.
|
|
11
13
|
// `full-reset` is honoured in `handleDeviceBoot`, which shuts a running device down before
|
|
12
14
|
// `simctl erase` (which refuses anything but a shut-down device) and boots it again.
|
|
@@ -39,11 +41,11 @@ export const IOS_BUTTON_ALIASES = {
|
|
|
39
41
|
volume_up: 'volume-up',
|
|
40
42
|
volume_down: 'volume-down',
|
|
41
43
|
};
|
|
42
|
-
import { createResourceSampler, registerStreamWs, disableNagle, createKeyframeAwareSender, pickMaxSize, createRateLimitedDropWarn, createThroughputSampler, createSleepBlocker, acquireLoopStallWatch, describeRelayLoss, getMachineId, isLocalhostWss, DEFAULT_BACKPRESSURE_BYTES, writeEnvelopeHeader, rewriteLowLatencySpsInFrame, CODEC_JPEG, CODEC_H264, CODEC_AUDIO, sendAudioYieldingToVideo, } from '@tapflowio/agent-core/utils';
|
|
44
|
+
import { createResourceSampler, registerStreamWs, disableNagle, createKeyframeAwareSender, pickMaxSize, createRateLimitedDropWarn, createThroughputSampler, createSleepBlocker, acquireLoopStallWatch, describeRelayLoss, getMachineId, sameServiceTier, resetApps, runResetSteps, RUN_RESET_BUDGET_MS, isLocalhostWss, DEFAULT_BACKPRESSURE_BYTES, writeEnvelopeHeader, rewriteLowLatencySpsInFrame, CODEC_JPEG, CODEC_H264, CODEC_AUDIO, sendAudioYieldingToVideo, } from '@tapflowio/agent-core/utils';
|
|
43
45
|
import { SimctlWrapper, isDeviceMissingError, ClipboardTooLargeError, firstLine, launchFailureReason } from './SimctlWrapper.js';
|
|
44
46
|
import { SimulatorNetwork } from './SimulatorNetwork.js';
|
|
45
47
|
import { LeanStore } from './LeanStore.js';
|
|
46
|
-
import {
|
|
48
|
+
import { MANAGED_LABELS, labelsFor } from './serviceTiers.js';
|
|
47
49
|
import { MAX_CLIPBOARD_BYTES, clipboardByteLength, CLIPBOARD_SENTINEL_PREFIX as SENTINEL_PREFIX, isClipboardSentinel as isSentinel, CLIPBOARD_COPY_DEADLINE_MS, CLIPBOARD_WRITE_DEADLINE_MS, CLIPBOARD_RESTORE_DEADLINE_MS, CLIPBOARD_POLL_MS, createKeyedSerialQueue, } from '@tapflowio/agent-core';
|
|
48
50
|
import { ScreenCaptureStreamer } from './ScreenCaptureStreamer.js';
|
|
49
51
|
import { AudioCaptureStreamer, readSimVolume, applyGain } from './AudioCaptureStreamer.js';
|
|
@@ -182,7 +184,7 @@ export class IOSAgent {
|
|
|
182
184
|
// reason, as `sleepBlocker` below.
|
|
183
185
|
this.lean = options.lean ?? false;
|
|
184
186
|
// Never the host's real store under vitest, for the reason given for `network` just below.
|
|
185
|
-
this.leanStore = options.leanStore ?? new LeanStore(process.env.VITEST ? path.join(tmpdir(), 'tapflow-no-lean-store') : undefined);
|
|
187
|
+
this.leanStore = options.leanStore ?? new LeanStore(process.env.VITEST ? path.join(tmpdir(), 'tapflow-no-lean-store') : undefined, MANAGED_LABELS);
|
|
186
188
|
this.network = options.network ?? new SimulatorNetwork(this.simctl, process.env.VITEST ? { filterHostBinary: path.join(tmpdir(), 'tapflow-no-filter-host') } : {});
|
|
187
189
|
// **Wired here rather than in the constructor call above, and the difference is coverage.** An
|
|
188
190
|
// injected `SimulatorNetwork` — which is what every test uses — would otherwise never carry the
|
|
@@ -264,6 +266,7 @@ export class IOSAgent {
|
|
|
264
266
|
status: d.status,
|
|
265
267
|
osVersion: d.osVersion,
|
|
266
268
|
formFactor: d.typeId ? formFactors.get(d.typeId) : undefined,
|
|
269
|
+
serviceTier: this.reportedTier(d),
|
|
267
270
|
})),
|
|
268
271
|
});
|
|
269
272
|
});
|
|
@@ -446,6 +449,7 @@ export class IOSAgent {
|
|
|
446
449
|
slotsAvailable: Math.max(0, slotsTotal - bootedCount),
|
|
447
450
|
slotsTotal,
|
|
448
451
|
reportedAt: Date.now(),
|
|
452
|
+
memoryPressure: this.resources.getMemoryPressure(),
|
|
449
453
|
},
|
|
450
454
|
});
|
|
451
455
|
}
|
|
@@ -674,7 +678,7 @@ export class IOSAgent {
|
|
|
674
678
|
// `requestId` is a parameter, never a field on `state`: `bootSeq` exists because two boots overlap,
|
|
675
679
|
// and a correlator hoisted onto shared state would answer the first request with the second's id.
|
|
676
680
|
// Optional because the relay's idle timer boots nothing — but every *browser* boot carries one.
|
|
677
|
-
async handleDeviceBoot(sessionId, deviceId, fullErase = false, acceptH264 = false, tier, requestId, automation = false) {
|
|
681
|
+
async handleDeviceBoot(sessionId, deviceId, fullErase = false, acceptH264 = false, tier, requestId, automation = false, serviceTier, runReset) {
|
|
678
682
|
const state = this.deviceStates.get(sessionId);
|
|
679
683
|
// Split, because the two halves are not the same kind of nothing. No open control channel means the
|
|
680
684
|
// answer itself has nowhere to go, so this is the one abandonment that stays silent — and the caller
|
|
@@ -721,6 +725,13 @@ export class IOSAgent {
|
|
|
721
725
|
const target = devices.find((d) => d.id === deviceId);
|
|
722
726
|
if (!target)
|
|
723
727
|
throw new PlatformError(`Device not found: ${deviceId}`);
|
|
728
|
+
// A running device on another tier than the run asked for is restarted onto it: the services have to be off
|
|
729
|
+
// from launch, and replaying on whatever the last boot left is what the tier exists to stop.
|
|
730
|
+
const retier = !fullErase && target.status === 'booted' && serviceTier !== undefined && leanSupported(target.osVersion)
|
|
731
|
+
&& !sameServiceTier(this.leanStore.currentTier(deviceId), serviceTier);
|
|
732
|
+
// Read before anything below changes it: a Full reset shuts the device down itself, and `unknown`
|
|
733
|
+
// (booting, shutting down) is a device paying a boot's cost now.
|
|
734
|
+
const cold = fullErase || retier || target.status !== 'booted';
|
|
724
735
|
if (fullErase) {
|
|
725
736
|
// `simctl erase` only accepts a shut-down device, and a device is often already running —
|
|
726
737
|
// it survives agent restarts and sessions that ended without a clean shutdown. Without this
|
|
@@ -747,7 +758,6 @@ export class IOSAgent {
|
|
|
747
758
|
this.abandonBoot(state, seq, sessionId, requestId);
|
|
748
759
|
return;
|
|
749
760
|
}
|
|
750
|
-
this.prepareLean(target);
|
|
751
761
|
try {
|
|
752
762
|
await this.simctl.erase(deviceId);
|
|
753
763
|
}
|
|
@@ -770,6 +780,9 @@ export class IOSAgent {
|
|
|
770
780
|
}
|
|
771
781
|
throw err;
|
|
772
782
|
}
|
|
783
|
+
// After the erase, not before: a failed erase boots the device back as it was found, and the tier is part of
|
|
784
|
+
// that. The overrides live on the host, so the erase does not touch them either way.
|
|
785
|
+
this.prepareTier(target, serviceTier);
|
|
773
786
|
await this.simctl.boot(deviceId);
|
|
774
787
|
}
|
|
775
788
|
else {
|
|
@@ -780,10 +793,22 @@ export class IOSAgent {
|
|
|
780
793
|
// deadline to discover. `SimctlWrapper.boot` swallows `Unable to boot device in current
|
|
781
794
|
// state: Booted`, so re-issuing it for a device that really is up costs one no-op
|
|
782
795
|
// subprocess and makes "the wait only ever runs after a boot was accepted" true.
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
796
|
+
if (retier) {
|
|
797
|
+
logger.info(`Service tier: restarting ${target.name} onto ${serviceTier.name}`);
|
|
798
|
+
// As for a Full reset: a runner starting for this device would boot it back up mid-change.
|
|
799
|
+
this.uiTreeReader.stopIfDevice(deviceId);
|
|
800
|
+
await this.simctl.shutdown(deviceId);
|
|
801
|
+
if (seq !== state.bootSeq) {
|
|
802
|
+
this.abandonBoot(state, seq, sessionId, requestId);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
this.prepareTier(target, serviceTier);
|
|
806
|
+
}
|
|
807
|
+
else if (target.status === 'shutdown') {
|
|
808
|
+
this.prepareTier(target, serviceTier);
|
|
809
|
+
}
|
|
810
|
+
else if (leanSupported(target.osVersion) && !sameServiceTier(this.leanStore.currentTier(deviceId), this.defaultTier())) {
|
|
811
|
+
logger.info(`Service tier: ${target.name} is already running, so ${this.defaultTier().name} applies from its next boot`);
|
|
787
812
|
}
|
|
788
813
|
await this.bootWithZombieRecovery(deviceId);
|
|
789
814
|
}
|
|
@@ -802,6 +827,13 @@ export class IOSAgent {
|
|
|
802
827
|
// twice a second against a device that is now deliberately off — for the rest of the deadline,
|
|
803
828
|
// with nothing left that wants the answer.
|
|
804
829
|
const bootedDevice = await this.simctl.waitUntilBooted(deviceId, { isStale: () => seq !== state.bootSeq });
|
|
830
|
+
// Before anything runs on the device: the network arm below starts nothing, and the run installs its app after the
|
|
831
|
+
// ready. Clearing an app now and installing it after leaves a clean container.
|
|
832
|
+
const runResetReport = runReset ? await this.resetForRun(deviceId, runReset, () => seq !== state.bootSeq) : undefined;
|
|
833
|
+
if (seq !== state.bootSeq) {
|
|
834
|
+
this.abandonBoot(state, seq, sessionId, requestId);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
805
837
|
// Arm the network injection while nothing is running on the device yet — dyld reads the
|
|
806
838
|
// environment at process start, so anything launched before this is unhooked for its whole life.
|
|
807
839
|
// Best-effort: the network toggle is opt-in and a device that cannot be armed says so through
|
|
@@ -847,7 +879,7 @@ export class IOSAgent {
|
|
|
847
879
|
// every path precisely so that case reaches here. Requiring an off→on transition would leave
|
|
848
880
|
// that device unowned and put the resolvers back to refusing between two live simulators.
|
|
849
881
|
this.ownedDevices.add(deviceId);
|
|
850
|
-
this.sendMsg({ type: 'device:ready', sessionId, requestId, payload: { deviceId } });
|
|
882
|
+
this.sendMsg({ type: 'device:ready', sessionId, requestId, payload: { deviceId, cold, serviceTier: this.reportedTier(target), ...(runResetReport ? { runReset: runResetReport } : {}) } });
|
|
851
883
|
// The unsolicited report the protocol names on `device:ready`. It follows the ready rather than
|
|
852
884
|
// riding inside it: a tester whose device just came up can arm the toggle in the same breath, and
|
|
853
885
|
// a boot that armed nothing still has to say so rather than leaving the control blank.
|
|
@@ -897,27 +929,37 @@ export class IOSAgent {
|
|
|
897
929
|
}
|
|
898
930
|
}
|
|
899
931
|
// ── Lean mode ──────────────────────────────────────────────────────────────
|
|
932
|
+
/** The tier a boot gets when it does not ask for one: Lean mode's setting. A person's boot is always this. */
|
|
933
|
+
defaultTier() {
|
|
934
|
+
return { name: this.lean ? 'lean' : 'stock', keep: [] };
|
|
935
|
+
}
|
|
936
|
+
/** What the device boots with now, for the relay: the marker's tier, or `unsupported` on a runtime that cannot take one. */
|
|
937
|
+
reportedTier(device) {
|
|
938
|
+
return leanSupported(device.osVersion) ? this.leanStore.currentTier(device.id) : 'unsupported';
|
|
939
|
+
}
|
|
900
940
|
/**
|
|
901
941
|
* Called only when the device is known to be shut down: `launchd_sim` reads the store when it
|
|
902
942
|
* starts, and a write while it runs is not known to survive. So a device that was already up when
|
|
903
|
-
* asked for is booted as it is, and
|
|
943
|
+
* asked for is booted as it is, and takes the default tier from its next boot through tapflow —
|
|
944
|
+
* unless a run asked for a tier, which restarts it (`retier` in `handleDeviceBoot`).
|
|
904
945
|
*
|
|
905
|
-
* **
|
|
946
|
+
* **Every boot sets an exact tier**, the asked-for one or the default. Adding to whatever the last boot left
|
|
947
|
+
* is how a device a run left slim reached a person still slim (L2 review R4-S1).
|
|
948
|
+
*
|
|
949
|
+
* **A failure here never stops the boot.** A tier is an optimisation and booting is the job.
|
|
906
950
|
*/
|
|
907
|
-
|
|
951
|
+
prepareTier(device, requested) {
|
|
952
|
+
const tier = requested ?? this.defaultTier();
|
|
908
953
|
try {
|
|
909
|
-
if (!
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
logger.info(`Lean mode: not applied to ${device.name} (${device.osVersion ?? 'unknown runtime'}) — iOS 18.5 or later only`);
|
|
914
|
-
}
|
|
915
|
-
else {
|
|
916
|
-
this.leanStore.apply(device.id, LEAN_LABELS);
|
|
954
|
+
if (!leanSupported(device.osVersion)) {
|
|
955
|
+
if (tier.name !== 'stock')
|
|
956
|
+
logger.info(`Service tier: ${tier.name} not applied to ${device.name} (${device.osVersion ?? 'unknown runtime'}) — iOS 18.5 or later only`);
|
|
957
|
+
return;
|
|
917
958
|
}
|
|
959
|
+
this.leanStore.setTier(device.id, tier, labelsFor(tier));
|
|
918
960
|
}
|
|
919
961
|
catch (e) {
|
|
920
|
-
logger.warn(`
|
|
962
|
+
logger.warn(`Service tier: could not set ${device.name} to ${tier.name}, booting it as it is:`, e.message);
|
|
921
963
|
}
|
|
922
964
|
}
|
|
923
965
|
/** Put back what Lean mode disabled on a device that is now shut down. A no-op for devices it never touched. */
|
|
@@ -1014,6 +1056,49 @@ export class IOSAgent {
|
|
|
1014
1056
|
});
|
|
1015
1057
|
}
|
|
1016
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* What `flow run` asks reset before a run, carried out on a booted device. Only the agent's report says it happened.
|
|
1061
|
+
* It waits for the system services first: `Booted` shows before tccd and securityd serve, and not only on a boot this
|
|
1062
|
+
* agent started — a device someone else just booted reads `booted` too. On a settled device the wait returns at once.
|
|
1063
|
+
* The wait does not watch `isStale`; a shutdown meanwhile holds this boot's abandonment until its deadline.
|
|
1064
|
+
*/
|
|
1065
|
+
async resetForRun(deviceId, request, isStale) {
|
|
1066
|
+
// One budget for the wait and the steps: a device whose services never come up spends it waiting, and the steps
|
|
1067
|
+
// that would have failed against it are reported failed instead of each running out its own timeout.
|
|
1068
|
+
const deadline = Date.now() + RUN_RESET_BUDGET_MS;
|
|
1069
|
+
const stop = () => isStale() || Date.now() >= deadline;
|
|
1070
|
+
await this.simctl.waitForBootStatus(deviceId).catch((e) => logger.warn(`run reset: boot status unknown: ${firstLine(e)}`));
|
|
1071
|
+
const steps = [];
|
|
1072
|
+
if (request.uninstall.length > 0) {
|
|
1073
|
+
steps.push({ item: 'app-uninstall', run: () => resetApps(request.uninstall, (id) => this.simctl.uninstallAppForReset(deviceId, id), stop) });
|
|
1074
|
+
}
|
|
1075
|
+
if (request.clear.length > 0) {
|
|
1076
|
+
steps.push({ item: 'app-files', run: () => resetApps(request.clear, (id) => this.simctl.clearAppDataIfInstalled(deviceId, id), stop) });
|
|
1077
|
+
}
|
|
1078
|
+
steps.push({ item: 'keychain', run: () => this.simctl.resetKeychain(deviceId) }, { item: 'privacy-services', run: () => this.simctl.resetPrivacy(deviceId) },
|
|
1079
|
+
// Through the clipboard queue: a clipboard read in flight parks a sentinel on this pasteboard.
|
|
1080
|
+
{ item: 'pasteboard', run: () => this.clearPasteboardQueued(deviceId, stop) }, { item: 'location', run: () => this.simctl.clearLocation(deviceId) });
|
|
1081
|
+
return runResetSteps(steps, isStale, (item, e) => logger.warn(`run reset: ${item} failed: ${firstLine(e)}`), deadline);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The pasteboard step of a run reset. The queue can be held by a clipboard operation already running, so the wait for
|
|
1085
|
+
* it is bounded too; a clear that reaches the front after the step gave up does nothing, because by then the run may
|
|
1086
|
+
* have put something of its own there.
|
|
1087
|
+
*/
|
|
1088
|
+
clearPasteboardQueued(deviceId, stop) {
|
|
1089
|
+
let abandoned = false;
|
|
1090
|
+
let timer;
|
|
1091
|
+
const queued = this.clipboardQueue(deviceId, async () => {
|
|
1092
|
+
if (abandoned || stop())
|
|
1093
|
+
throw new PlatformError('the clipboard was busy for the whole reset');
|
|
1094
|
+
await this.simctl.setPasteboard(deviceId, '');
|
|
1095
|
+
});
|
|
1096
|
+
const late = new Promise((_, reject) => {
|
|
1097
|
+
timer = setTimeout(() => { abandoned = true; reject(new PlatformError('pasteboard clear timed out')); }, RUN_RESET_PASTEBOARD_TIMEOUT_MS);
|
|
1098
|
+
});
|
|
1099
|
+
queued.catch(() => { });
|
|
1100
|
+
return Promise.race([queued, late]).finally(() => clearTimeout(timer));
|
|
1101
|
+
}
|
|
1017
1102
|
// Lazily set up the touch channel: a reconnect re-issues the session with touchHelper=null while the sim stays booted, so input self-heals without a fresh device:boot. Sync so a tap's start+end stay paired (async would drop touchEnd → stuck finger).
|
|
1018
1103
|
ensureTouchHelper(state) {
|
|
1019
1104
|
if (state.touchHelper)
|
|
@@ -1149,9 +1234,9 @@ export class IOSAgent {
|
|
|
1149
1234
|
switch (msg.type) {
|
|
1150
1235
|
case 'device:boot': {
|
|
1151
1236
|
// `external` and `automation` are added by the relay, never by the browser (see `DeviceBoot`).
|
|
1152
|
-
const { deviceId, resetMode, acceptH264, secureContext, external, automation } = msg.payload;
|
|
1237
|
+
const { deviceId, resetMode, acceptH264, secureContext, external, automation, serviceTier, runReset } = msg.payload;
|
|
1153
1238
|
const sessionId = msg.sessionId;
|
|
1154
|
-
this.handleDeviceBoot(sessionId, deviceId, resetMode === 'full-erase', acceptH264 === true, { secureContext: !!secureContext, external: !!external }, msg.requestId, automation === true)
|
|
1239
|
+
this.handleDeviceBoot(sessionId, deviceId, resetMode === 'full-erase', acceptH264 === true, { secureContext: !!secureContext, external: !!external }, msg.requestId, automation === true, serviceTier, runReset)
|
|
1155
1240
|
.catch((e) => logger.error('handleDeviceBoot failed:', e));
|
|
1156
1241
|
break;
|
|
1157
1242
|
}
|
|
@@ -1256,8 +1341,12 @@ export class IOSAgent {
|
|
|
1256
1341
|
// The helper's answer, not its existence: a helper whose process has died reports every
|
|
1257
1342
|
// write as dropped, and that is what the caller needs to hear (#482).
|
|
1258
1343
|
const helper = state.touchHelper;
|
|
1259
|
-
// terminal of a tap/swipe → ack the gesture
|
|
1260
|
-
|
|
1344
|
+
// terminal of a tap/swipe → ack the gesture, once the up is written (it may wait for the
|
|
1345
|
+
// touch's minimum contact). The refusal is read then too, from the helper the state holds *then*:
|
|
1346
|
+
// one replaced during the wait (a chrome reload, a reboot) is up and owns nothing, which is
|
|
1347
|
+
// `no-gesture`, where the stopped one it replaced would say the channel is gone.
|
|
1348
|
+
void (helper ? helper.touchEndAfterMinContact() : Promise.resolve(false))
|
|
1349
|
+
.then((sent) => this.ackInput(state, sent ? 'delivered' : this.refusalReason(state.touchHelper, 'continuation'), requestId));
|
|
1261
1350
|
break;
|
|
1262
1351
|
}
|
|
1263
1352
|
case 'input:pinch:start': {
|
|
@@ -2232,10 +2321,9 @@ export class IOSAgent {
|
|
|
2232
2321
|
first?.touchHelper?.touchMove(x, y);
|
|
2233
2322
|
return Promise.resolve();
|
|
2234
2323
|
}
|
|
2235
|
-
touchEnd() {
|
|
2324
|
+
async touchEnd() {
|
|
2236
2325
|
const first = this.liveDeviceState();
|
|
2237
|
-
first?.touchHelper?.
|
|
2238
|
-
return Promise.resolve();
|
|
2326
|
+
await first?.touchHelper?.touchEndAfterMinContact();
|
|
2239
2327
|
}
|
|
2240
2328
|
async openUrl(url) {
|
|
2241
2329
|
return this.simctl.openUrl(await this.soleLiveDeviceId(), url);
|