@swmansion/argent 0.31.2-next.3 → 0.31.2-next.5

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/dist/cli-cmds.mjs CHANGED
@@ -35797,7 +35797,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
35797
35797
  var SESSION_ID2 = randomUUID5();
35798
35798
  function readCliVersion() {
35799
35799
  if (true) {
35800
- return "0.31.2-next.3";
35800
+ return "0.31.2-next.5";
35801
35801
  }
35802
35802
  return "0.0.0";
35803
35803
  }
@@ -16922,7 +16922,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16922
16922
  var SESSION_ID = randomUUID4();
16923
16923
  function readCliVersion() {
16924
16924
  if (true) {
16925
- return "0.31.2-next.3";
16925
+ return "0.31.2-next.5";
16926
16926
  }
16927
16927
  return "0.0.0";
16928
16928
  }
@@ -11,6 +11,9 @@ NS_ASSUME_NONNULL_BEGIN
11
11
  /// Returns nil when the block completed, otherwise "Name: reason".
12
12
  + (NSString *_Nullable)runCatching:(NS_NOESCAPE dispatch_block_t)block;
13
13
 
14
+ /// Returns nil when the block completed, otherwise the exception it raised.
15
+ + (NSException *_Nullable)exceptionFrom:(NS_NOESCAPE dispatch_block_t)block;
16
+
14
17
  @end
15
18
 
16
19
  NS_ASSUME_NONNULL_END
@@ -3,13 +3,21 @@
3
3
  @implementation ArgentExceptionGuard
4
4
 
5
5
  + (NSString *_Nullable)runCatching:(NS_NOESCAPE dispatch_block_t)block {
6
+ NSException *exception = [self exceptionFrom:block];
7
+ if (exception == nil) {
8
+ return nil;
9
+ }
10
+ NSString *name = exception.name ?: @"NSException";
11
+ NSString *reason = exception.reason ?: @"unhandled Objective-C exception";
12
+ return [NSString stringWithFormat:@"%@: %@", name, reason];
13
+ }
14
+
15
+ + (NSException *_Nullable)exceptionFrom:(NS_NOESCAPE dispatch_block_t)block {
6
16
  @try {
7
17
  block();
8
18
  return nil;
9
19
  } @catch (NSException *exception) {
10
- NSString *name = exception.name ?: @"NSException";
11
- NSString *reason = exception.reason ?: @"unhandled Objective-C exception";
12
- return [NSString stringWithFormat:@"%@: %@", name, reason];
20
+ return exception;
13
21
  }
14
22
  }
15
23
 
@@ -21,10 +21,10 @@ extension ArgentRunnerSession {
21
21
 
22
22
  /// Taps at the wire coordinates: one tap, the native double-tap when
23
23
  /// `numberOfTaps` is 2, or a loop of single taps above that. XCUICoordinate
24
- /// has no N-tap API, and the loop is not one gesture on hardware: each tap
25
- /// is its own synthesized event behind XCTest's idle wait and an XPC round
26
- /// trip, hundreds of milliseconds apart, so the taps land separately and do
27
- /// not trigger a multi-tap recognizer. That is documented, not refused.
24
+ /// has no N-tap API, and the loop is not one gesture: each tap is its own
25
+ /// synthesized event and XPC round trip, about 300 ms apart on a
26
+ /// simulator, which a double-tap recognizer can still pair. That is
27
+ /// documented, not refused.
28
28
  func performTap(_ request: CommandRequest, on app: XCUIApplication)
29
29
  -> Envelope
30
30
  {
@@ -39,15 +39,17 @@ extension ArgentRunnerSession {
39
39
 
40
40
  let coordinate = point(app, x, y, relativeTo: app.frame.origin)
41
41
 
42
- switch taps {
43
- case 1:
44
- coordinate.tap()
45
- case 2:
46
- coordinate.doubleTap()
47
- default:
48
- // Separate taps, not one gesture (see the doc comment above).
49
- for _ in 0..<taps {
42
+ skippingQuiescence(of: app) {
43
+ switch taps {
44
+ case 1:
50
45
  coordinate.tap()
46
+ case 2:
47
+ coordinate.doubleTap()
48
+ default:
49
+ // Separate taps, not one gesture (see the doc comment above).
50
+ for _ in 0..<taps {
51
+ coordinate.tap()
52
+ }
51
53
  }
52
54
  }
53
55
 
@@ -63,7 +65,11 @@ extension ArgentRunnerSession {
63
65
  }
64
66
 
65
67
  let seconds = max(0.05, (request.durationMs ?? 800) / 1000)
66
- point(app, x, y, relativeTo: app.frame.origin).press(forDuration: seconds)
68
+ let coordinate = point(app, x, y, relativeTo: app.frame.origin)
69
+
70
+ skippingQuiescence(of: app) {
71
+ coordinate.press(forDuration: seconds)
72
+ }
67
73
 
68
74
  return .success(MessagePayload(message: "long-pressed"))
69
75
  }
@@ -112,12 +118,14 @@ extension ArgentRunnerSession {
112
118
  velocity = .default
113
119
  }
114
120
 
115
- start.press(
116
- forDuration: startHold,
117
- thenDragTo: end,
118
- withVelocity: velocity,
119
- thenHoldForDuration: endHold
120
- )
121
+ skippingQuiescence(of: app) {
122
+ start.press(
123
+ forDuration: startHold,
124
+ thenDragTo: end,
125
+ withVelocity: velocity,
126
+ thenHoldForDuration: endHold
127
+ )
128
+ }
121
129
 
122
130
  return .success(MessagePayload(message: "dragged"))
123
131
  }
@@ -0,0 +1,95 @@
1
+ import XCTest
2
+
3
+ /// XCTest waits up to 60s before and after every synthesized event for the
4
+ /// app to report idle. A screen that never does, such as one holding a paused
5
+ /// UIViewPropertyAnimator (expo-blur), stalls each gesture for two minutes,
6
+ /// past its watchdog budget. The runner skips those waits and runs XCTest's
7
+ /// idle check itself under a short cap. Every API here is XCTest SPI, probed
8
+ /// before use: when any is missing, the event runs with XCTest's own waits.
9
+ extension ArgentRunnerSession {
10
+ /// The longest a gesture waits for the app to go idle before its event.
11
+ /// Covers a navigation transition, so a gesture sent right after the one
12
+ /// that started it lands on the new screen instead of mid-transition.
13
+ static let preEventSettleTimeout: TimeInterval = 2
14
+
15
+ private static let performWithInteractionOptions = NSSelectorFromString(
16
+ "_performWithInteractionOptions:block:"
17
+ )
18
+ /// Bit 1 skips the pre-event wait, bit 2 the post-event one.
19
+ private static let skipQuiescenceOptions: UInt32 = 0b11
20
+
21
+ private static let applicationImpl = NSSelectorFromString("applicationImpl")
22
+ private static let currentProcess = NSSelectorFromString("currentProcess")
23
+ private static let initiateQuiescenceChecks = NSSelectorFromString(
24
+ "_initiateQuiescenceChecksIncludingAnimationsIdle:"
25
+ )
26
+ private static let makeQuiescenceExpectation = NSSelectorFromString(
27
+ "_makeQuiescenceExpectation"
28
+ )
29
+
30
+ /// Waits at most `preEventSettleTimeout` for `app` to go idle, then runs
31
+ /// `body`, which synthesizes events into it, without XCTest's waits.
32
+ func skippingQuiescence(of app: XCUIApplication, _ body: () -> Void) {
33
+ let selector = Self.performWithInteractionOptions
34
+
35
+ guard app.responds(to: selector),
36
+ awaitQuiescence(of: app, timeout: Self.preEventSettleTimeout)
37
+ else {
38
+ body()
39
+ return
40
+ }
41
+
42
+ typealias Perform = @convention(c) (
43
+ NSObject, Selector, UInt32, @convention(block) () -> Void
44
+ ) -> Void
45
+ let perform = unsafeBitCast(app.method(for: selector), to: Perform.self)
46
+
47
+ // XCTest restores the prior options only when the block returns, and
48
+ // every XCUIApplication for this bundle id shares them, so an
49
+ // exception must not unwind through it.
50
+ var exception: NSException?
51
+ withoutActuallyEscaping(body) { body in
52
+ perform(app, selector, Self.skipQuiescenceOptions) {
53
+ exception = ArgentExceptionGuard.exception(from: body)
54
+ }
55
+ }
56
+ exception?.raise()
57
+ }
58
+
59
+ /// XCTest's own idle check (event loop and animations), under `timeout`.
60
+ /// False when any SPI it needs is missing.
61
+ private func awaitQuiescence(of app: XCUIApplication, timeout: TimeInterval)
62
+ -> Bool
63
+ {
64
+ guard
65
+ let impl = Self.object(app, Self.applicationImpl),
66
+ let process = Self.object(impl, Self.currentProcess),
67
+ process.responds(to: Self.initiateQuiescenceChecks)
68
+ else {
69
+ return false
70
+ }
71
+
72
+ typealias Initiate = @convention(c) (NSObject, Selector, Bool) -> Void
73
+ let initiate = unsafeBitCast(
74
+ process.method(for: Self.initiateQuiescenceChecks),
75
+ to: Initiate.self
76
+ )
77
+ initiate(process, Self.initiateQuiescenceChecks, true)
78
+
79
+ guard
80
+ let expectation = Self.object(process, Self.makeQuiescenceExpectation)
81
+ as? XCTestExpectation
82
+ else {
83
+ return false
84
+ }
85
+
86
+ _ = XCTWaiter.wait(for: [expectation], timeout: timeout)
87
+ return true
88
+ }
89
+
90
+ /// The object a zero-argument getter returns, or nil when `target` lacks it.
91
+ private static func object(_ target: NSObject, _ getter: Selector) -> NSObject? {
92
+ guard target.responds(to: getter) else { return nil }
93
+ return target.perform(getter)?.takeUnretainedValue() as? NSObject
94
+ }
95
+ }
@@ -77,7 +77,7 @@ extension ArgentRunnerSession {
77
77
  app.keyboards.keys.matching(submitKey).firstMatch,
78
78
  ] where candidate.exists && candidate.isHittable {
79
79
  let label = candidate.label
80
- candidate.tap()
80
+ skippingQuiescence(of: app) { candidate.tap() }
81
81
 
82
82
  return .success(
83
83
  MessagePayload(message: "pressed keyboard \(label)")
@@ -131,7 +131,7 @@ extension ArgentRunnerSession {
131
131
  }
132
132
 
133
133
  let exceptionDescription = ArgentExceptionGuard.runCatching {
134
- app.typeText(text)
134
+ self.skippingQuiescence(of: app) { app.typeText(text) }
135
135
  }
136
136
 
137
137
  if let exceptionDescription {
@@ -70,10 +70,10 @@ extension CommandKind {
70
70
  switch self {
71
71
  // XCTest types long strings in real time, so typing needs extra room.
72
72
  case .type: return 55
73
- // Gestures must outlast XCTest's ~60s pre-event idle wait. A screen that
74
- // never reports quiescent stalls the wait until XCTest gives up and
75
- // synthesizes the event anyway, and a shorter budget would abandon a
76
- // command that still completes.
73
+ // tap, longPress and drag outlast XCTest's ~60s pre-event idle wait,
74
+ // which runs where the runner cannot skip it
75
+ // (ArgentRunnerSession+Quiescence.swift). touchPaths never waits for
76
+ // idle but shares the gesture budget.
77
77
  case .tap, .longPress, .drag, .touchPaths: return 75
78
78
  default: return 30
79
79
  }
@@ -117,7 +117,7 @@ struct CommandRequest: Codable {
117
117
  let y: Double?
118
118
  /// `tap`: number of taps. Defaults to 1; 2 is the native double-tap.
119
119
  /// Higher counts loop single taps on the device: there is no native N-tap
120
- /// API, so they land as separate taps, not one multi-tap gesture.
120
+ /// API, so they are not one multi-tap gesture.
121
121
  let numberOfTaps: Int?
122
122
  /// `drag`: absolute start and end points.
123
123
  let fromX: Double?
@@ -408,7 +408,7 @@ export interface ArgentToolArgs {
408
408
  x: number;
409
409
  /** Normalized vertical position 0.0–1.0 (top=0, bottom=1), not pixels */
410
410
  y: number;
411
- /** Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts land as separate taps. On HarmonyOS only 2 is one gesture; 3 or more are separate injections a device round trip apart, which the OS reads as single taps. Default 1. */
411
+ /** Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts loop single taps, which a double-tap recognizer may pair. On HarmonyOS only 2 is one gesture; 3 or more are separate injections a device round trip apart, which the OS reads as single taps. Default 1. */
412
412
  clickCount?: number;
413
413
  };
414
414
  keyboard: {
@@ -101080,7 +101080,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
101080
101080
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
101081
101081
  function readCliVersion() {
101082
101082
  if (true) {
101083
- return "0.31.2-next.3";
101083
+ return "0.31.2-next.5";
101084
101084
  }
101085
101085
  return "0.0.0";
101086
101086
  }
@@ -142136,7 +142136,7 @@ var zodSchema17 = external_exports.object({
142136
142136
  x: external_exports.number().min(0).max(1).describe("Normalized horizontal position 0.0\u20131.0 (left=0, right=1), not pixels"),
142137
142137
  y: external_exports.number().min(0).max(1).describe("Normalized vertical position 0.0\u20131.0 (top=0, bottom=1), not pixels"),
142138
142138
  clickCount: external_exports.number().int().min(1).max(10).optional().describe(
142139
- "Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts land as separate taps. <flag:harmonyos>On HarmonyOS only 2 is one gesture; 3 or more are separate injections a device round trip apart, which the OS reads as single taps. </flag:harmonyos>Default 1."
142139
+ "Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires; on physical iOS 2 is the native double-tap and higher counts loop single taps, which a double-tap recognizer may pair. <flag:harmonyos>On HarmonyOS only 2 is one gesture; 3 or more are separate injections a device round trip apart, which the OS reads as single taps. </flag:harmonyos>Default 1."
142140
142140
  )
142141
142141
  });
142142
142142
  function tapDescription(params, tense) {
@@ -142209,6 +142209,9 @@ Returns { tapped: true, timestampMs }. On physical iOS, reactivated: true = app
142209
142209
  On a physical iPhone use \`describe\`; \`native-describe-screen\` is simulator-only.
142210
142210
  Before tapping, determine the correct coordinates by using discovery tools. Pick by platform: iOS / Android use \`describe\`, \`native-describe-screen\`, or \`debugger-component-tree\`; Chromium uses \`describe\` (the DOM walker)<flag:harmonyos> and HarmonyOS uses \`describe\` (the \`uitest\` layout dump)</flag:harmonyos>, since the native and RN-specific discovery tools don't apply<flag:harmonyos> to either</flag:harmonyos>. More information in \`argent-device-interact\` skill`,
142211
142211
  alwaysLoad: true,
142212
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
142213
+ // whose abort replays the call: the gesture would land once per replay.
142214
+ longRunning: true,
142212
142215
  searchHint: "tap press button element device simulator emulator chromium touch down up click",
142213
142216
  zodSchema: zodSchema17,
142214
142217
  capability: capability9,
@@ -142336,6 +142339,9 @@ Physical iOS: an edge gesture (back-swipe) needs fromX 0 exactly; durationMs set
142336
142339
  HarmonyOS: durationMs sets drag speed, clamped to uitest's 200-40000 px/s, so a short slow swipe ends early and a long fast one runs late.</flag:harmonyos>
142337
142340
  Pass momentum:false for a momentum-free swipe that lands where the finger lifts (little to no fling at the 300 default), when you need a deterministic scroll distance; it needs durationMs >= 150 and is rejected below that, a shorter ease-out leaving the OS too little wall clock to read the deceleration as a stop. At 150 it lands short of the lift point instead, and 2 of 47 runs still flung backwards. A plain swipe takes any duration up to 10000ms and is delivered as close to the speed it was authored as a 16ms frame allows: below ~32ms the whole travel lands in one or two frames, which the OS flings as hard as it flings anything. Returns { swiped: true, timestampMs }. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the simulator-server / emulator backend<flag:harmonyos>, or \`hdc\` on HarmonyOS,</flag:harmonyos> is not reachable for the given device.`,
142338
142341
  alwaysLoad: true,
142342
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
142343
+ // whose abort replays the call: the gesture would land once per replay.
142344
+ longRunning: true,
142339
142345
  searchHint: "swipe scroll drag pan gesture device simulator emulator touch move",
142340
142346
  zodSchema: zodSchema18,
142341
142347
  capability: capability10,
@@ -142893,6 +142899,9 @@ Example pinch-to-zoom (with interpolate:10 for smoothness):
142893
142899
  interpolate: 10`,
142894
142900
  zodSchema: zodSchema21,
142895
142901
  capability: capability13,
142902
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
142903
+ // whose abort replays the call: the gesture would land once per replay.
142904
+ longRunning: true,
142896
142905
  // Declare the runner only for a train execute can play. A rejected train must not pay a runner cold start.
142897
142906
  services: (params) => {
142898
142907
  const device = resolveDevice(params.udid);
@@ -142980,6 +142989,9 @@ Auto-generates interpolated frames at ~60fps<flag:harmonyos> on iOS and Android;
142980
142989
  Use when you need to zoom in or out on a map, image, or zoomable view. Physical iPhones play both fingers through the runner as one event stream. Returns { pinched: true, timestampMs }. On physical iOS, reactivated: true = app was re-fronted; re-describe. Fails if the simulator-server / emulator backend<flag:harmonyos>, or \`hdc\` on HarmonyOS,</flag:harmonyos> is not reachable for the given device, or on a physical iPhone when the app is not drawn in portrait.`,
142981
142990
  zodSchema: zodSchema22,
142982
142991
  capability: capability14,
142992
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
142993
+ // whose abort replays the call: the gesture would land once per replay.
142994
+ longRunning: true,
142983
142995
  services: (params) => {
142984
142996
  const device = resolveDevice(params.udid);
142985
142997
  if (device.platform === "harmony") return {};
@@ -143115,6 +143127,9 @@ Use when you need to rotate a map, image picker, or any rotateable UI element. R
143115
143127
  Size the orbit with radius, or with radiusX and radiusY together (the pair overrides radius); one half of the pair alone, or none of the three, is rejected.`,
143116
143128
  zodSchema: zodSchema23,
143117
143129
  capability: capability15,
143130
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
143131
+ // whose abort replays the call: the gesture would land once per replay.
143132
+ longRunning: true,
143118
143133
  services: (params) => {
143119
143134
  const device = resolveDevice(params.udid);
143120
143135
  return isIosPhysicalDevice(device) ? { iosDeviceRunner: iosDeviceRunnerRef(device) } : { simulatorServer: simulatorServerRef(device) };
@@ -143238,6 +143253,9 @@ Returns { pressed: buttonName }.
143238
143253
  Fails if the device backend is not reachable: the simulator-server for iOS, <flag:harmonyos>\`hdc\` for HarmonyOS, </flag:harmonyos>or \`adb\` for Android (Android presses are injected with \`adb shell input keyevent\`).`,
143239
143254
  zodSchema: zodSchema24,
143240
143255
  capability: capability16,
143256
+ // A physical iPhone's first call waits out the runner build, past the MCP fetch timeout,
143257
+ // whose abort replays the call: the press would land once per replay.
143258
+ longRunning: true,
143241
143259
  // Declare only the service the resolved path actually consumes. The Android
143242
143260
  // path uses `adb`, so a sim-server here would spawn a service the tool never
143243
143261
  // uses (up to a 30s ready-wait) and could throw ServiceInitializationError
@@ -158850,8 +158868,8 @@ var import_util14 = require("util");
158850
158868
  var DEFAULT_EXEC_TIMEOUT_MS = 6e4;
158851
158869
  var DEFAULT_EXEC_MAX_BUFFER = 256 * 1024 * 1024;
158852
158870
  async function execFileAsyncWithTimeout(file2, args, options = {}) {
158853
- const execFileAsync37 = (0, import_util14.promisify)(import_child_process2.execFile);
158854
- const { stdout, stderr } = await execFileAsync37(file2, args, {
158871
+ const execFileAsync36 = (0, import_util14.promisify)(import_child_process2.execFile);
158872
+ const { stdout, stderr } = await execFileAsync36(file2, args, {
158855
158873
  encoding: "utf-8",
158856
158874
  ...options,
158857
158875
  // Applied after ...options so a caller cannot weaken the guards; maxBuffer
@@ -179008,18 +179026,68 @@ function probeArgentToolServer(host, port, timeoutMs = 500) {
179008
179026
 
179009
179027
  // ../tool-server/src/utils/simulator-watcher.ts
179010
179028
  var fs69 = __toESM(require("node:fs"));
179011
- var import_node_child_process45 = require("node:child_process");
179012
- var import_node_util33 = require("node:util");
179013
179029
  init_external_devices();
179014
- var execFileAsync36 = (0, import_node_util33.promisify)(import_node_child_process45.execFile);
179030
+
179031
+ // ../tool-server/src/utils/simctl-exec.ts
179032
+ var import_node_child_process45 = require("node:child_process");
179033
+ var SimctlTimeoutError = class extends Error {
179034
+ constructor(args, timeoutMs) {
179035
+ super(`xcrun ${args.join(" ")} did not answer within ${timeoutMs} ms`);
179036
+ this.name = "SimctlTimeoutError";
179037
+ }
179038
+ };
179039
+ function execSimctl(args, options = {}) {
179040
+ const timeoutMs = options.timeoutMs ?? SIMCTL_SPAWN_TIMEOUT_MS;
179041
+ return new Promise((resolve16, reject) => {
179042
+ if (options.signal?.aborted) {
179043
+ reject(new Error(`xcrun ${args.join(" ")} aborted`));
179044
+ return;
179045
+ }
179046
+ const child = (0, import_node_child_process45.spawn)("xcrun", args, { detached: true, stdio: ["ignore", "pipe", "pipe"] });
179047
+ let stdout = "";
179048
+ let stderr = "";
179049
+ let settled = false;
179050
+ child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
179051
+ child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
179052
+ const settle3 = (err) => {
179053
+ if (settled) return;
179054
+ settled = true;
179055
+ clearTimeout(timer);
179056
+ options.signal?.removeEventListener("abort", onAbort);
179057
+ if (err) reject(err);
179058
+ else resolve16(stdout);
179059
+ };
179060
+ const killGroup2 = () => {
179061
+ if (child.pid) signalGroup(child.pid, "SIGKILL");
179062
+ };
179063
+ const onAbort = () => {
179064
+ killGroup2();
179065
+ settle3(new Error(`xcrun ${args.join(" ")} aborted`));
179066
+ };
179067
+ const timer = setTimeout(() => {
179068
+ killGroup2();
179069
+ settle3(new SimctlTimeoutError(args, timeoutMs));
179070
+ }, timeoutMs);
179071
+ options.signal?.addEventListener("abort", onAbort, { once: true });
179072
+ child.on("error", (err) => settle3(err));
179073
+ child.on("close", (code, sig) => {
179074
+ if (code === 0) return settle3(null);
179075
+ settle3(
179076
+ new Error(
179077
+ `xcrun ${args.join(" ")} exited with ${sig ?? `code ${code}`}` + (stderr.trim() ? `: ${stderr.trim()}` : "")
179078
+ )
179079
+ );
179080
+ });
179081
+ });
179082
+ }
179083
+
179084
+ // ../tool-server/src/utils/simulator-watcher.ts
179015
179085
  var POLL_INTERVAL_MS2 = 1e4;
179016
- async function getBootedUdidsInSet(deviceSet) {
179017
- const { stdout } = await execFileAsync36("xcrun", [
179018
- ...simctlPrefix(deviceSet),
179019
- "list",
179020
- "devices",
179021
- "--json"
179022
- ]);
179086
+ var MAX_BACKOFF_TICKS = 29;
179087
+ async function getBootedUdidsInSet(deviceSet, signal) {
179088
+ const stdout = await execSimctl([...simctlPrefix(deviceSet), "list", "devices", "--json"], {
179089
+ signal
179090
+ });
179023
179091
  const data = JSON.parse(stdout);
179024
179092
  const udids = /* @__PURE__ */ new Set();
179025
179093
  for (const devices of Object.values(data.devices)) {
@@ -179032,13 +179100,17 @@ async function getBootedUdidsInSet(deviceSet) {
179032
179100
  }
179033
179101
  return udids;
179034
179102
  }
179035
- async function getBootedUdids() {
179103
+ async function getBootedUdids(signal) {
179036
179104
  const sets = [
179037
179105
  null,
179038
179106
  ...configuredAdditionalDeviceSets().filter((p) => fs69.existsSync(p))
179039
179107
  ];
179040
- const perSet = await Promise.all(sets.map(getBootedUdidsInSet));
179041
- return new Set(perSet.flatMap((s) => [...s]));
179108
+ const perSet = await Promise.allSettled(sets.map((set2) => getBootedUdidsInSet(set2, signal)));
179109
+ const failures = perSet.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
179110
+ if (failures.length) {
179111
+ throw failures.find((err) => err instanceof SimctlTimeoutError) ?? failures[0];
179112
+ }
179113
+ return new Set(perSet.flatMap((r) => r.status === "fulfilled" ? [...r.value] : []));
179042
179114
  }
179043
179115
  async function initUdid(registry2, udid, trackedServices) {
179044
179116
  const ndRef = nativeDevtoolsRef({ id: udid, platform: "ios", kind: "simulator" });
@@ -179051,6 +179123,9 @@ async function initUdid(registry2, udid, trackedServices) {
179051
179123
  function startSimulatorWatcher(registry2) {
179052
179124
  const trackedServices = /* @__PURE__ */ new Map();
179053
179125
  const reportedSkips = /* @__PURE__ */ new Set();
179126
+ const stopController = new AbortController();
179127
+ let consecutiveTimeouts = 0;
179128
+ let ticksToSkip = 0;
179054
179129
  function withoutForeignSimulators(booted) {
179055
179130
  const ours = /* @__PURE__ */ new Set();
179056
179131
  const claimed = /* @__PURE__ */ new Set();
@@ -179099,10 +179174,23 @@ function startSimulatorWatcher(registry2) {
179099
179174
  let booted;
179100
179175
  let claimed;
179101
179176
  try {
179102
- ({ ours: booted, claimed } = withoutForeignSimulators(await getBootedUdids()));
179103
- } catch {
179177
+ ({ ours: booted, claimed } = withoutForeignSimulators(
179178
+ await getBootedUdids(stopController.signal)
179179
+ ));
179180
+ } catch (err) {
179181
+ if (err instanceof SimctlTimeoutError) {
179182
+ consecutiveTimeouts += 1;
179183
+ ticksToSkip = Math.min(2 ** consecutiveTimeouts - 1, MAX_BACKOFF_TICKS);
179184
+ if (consecutiveTimeouts === 1) {
179185
+ process.stderr.write(
179186
+ `[simulator-watcher] ${err.message}; retrying with backoff until it does. If Xcode was just updated, open Xcode once or run \`sudo xcodebuild -runFirstLaunch\`.
179187
+ `
179188
+ );
179189
+ }
179190
+ }
179104
179191
  return;
179105
179192
  }
179193
+ consecutiveTimeouts = 0;
179106
179194
  await handBackClaimed(claimed);
179107
179195
  const newUdids = [...booted].filter((udid) => !trackedServices.has(udid));
179108
179196
  const pendingAttempts = newUdids.map(
@@ -179127,9 +179215,24 @@ function startSimulatorWatcher(registry2) {
179127
179215
  }
179128
179216
  }
179129
179217
  const ready2 = poll(true);
179130
- const interval2 = setInterval(() => poll(false).catch(() => {
179131
- }), POLL_INTERVAL_MS2);
179132
- return { stop: () => clearInterval(interval2), ready: ready2 };
179218
+ let inFlight2 = ready2.catch(() => {
179219
+ }).finally(() => inFlight2 = null);
179220
+ const interval2 = setInterval(() => {
179221
+ if (inFlight2) return;
179222
+ if (ticksToSkip > 0) {
179223
+ ticksToSkip -= 1;
179224
+ return;
179225
+ }
179226
+ inFlight2 = poll(false).catch(() => {
179227
+ }).finally(() => inFlight2 = null);
179228
+ }, POLL_INTERVAL_MS2);
179229
+ return {
179230
+ stop: () => {
179231
+ clearInterval(interval2);
179232
+ stopController.abort();
179233
+ },
179234
+ ready: ready2
179235
+ };
179133
179236
  }
179134
179237
 
179135
179238
  // ../tool-server/src/utils/preview-window.ts
@@ -179452,6 +179555,7 @@ function start() {
179452
179555
  if (exitCode > finalExitCode) finalExitCode = exitCode;
179453
179556
  if (shuttingDown) return;
179454
179557
  shuttingDown = true;
179558
+ stopWatcher();
179455
179559
  eventLog?.info({
179456
179560
  type: "tool_server.stopping",
179457
179561
  msg: "Tool server is stopping.",
@@ -179466,7 +179570,6 @@ function start() {
179466
179570
  }
179467
179571
  previewWindow.dispose();
179468
179572
  updateChecker.dispose();
179469
- stopWatcher();
179470
179573
  httpHandle.dispose();
179471
179574
  try {
179472
179575
  await registry2.dispose();
@@ -179529,6 +179632,7 @@ function start() {
179529
179632
  }
179530
179633
  });
179531
179634
  Promise.all([watcherReady, identityWarm]).then(() => {
179635
+ if (shuttingDown) return;
179532
179636
  server = httpHandle.app.listen(PORT, HOST, () => {
179533
179637
  listening = true;
179534
179638
  const addr = server.address();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.31.2-next.3",
3
+ "version": "0.31.2-next.5",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",