@tapflowio/ios-agent 0.27.0 → 0.27.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.
@@ -1 +1 @@
1
- {"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"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"}
package/dist/IOSAgent.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { randomUUID } from 'crypto';
6
- import { spawnSync } from 'child_process';
6
+ 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');
@@ -39,7 +39,7 @@ export const IOS_BUTTON_ALIASES = {
39
39
  volume_up: 'volume-up',
40
40
  volume_down: 'volume-down',
41
41
  };
42
- import { createResourceSampler, registerStreamWs, disableNagle, createKeyframeAwareSender, pickMaxSize, createRateLimitedDropWarn, createThroughputSampler, createSleepBlocker, getMachineId, isLocalhostWss, DEFAULT_BACKPRESSURE_BYTES, writeEnvelopeHeader, rewriteLowLatencySpsInFrame, CODEC_JPEG, CODEC_H264, CODEC_AUDIO, sendAudioYieldingToVideo, } from '@tapflowio/agent-core/utils';
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';
43
43
  import { SimctlWrapper, isDeviceMissingError, ClipboardTooLargeError, firstLine, launchFailureReason } from './SimctlWrapper.js';
44
44
  import { SimulatorNetwork } from './SimulatorNetwork.js';
45
45
  import { LeanStore } from './LeanStore.js';
@@ -58,8 +58,8 @@ import { KEY_CODE_MAP, MODIFIER_BITS } from './KeyCodeMap.js';
58
58
  // processes (launched apps, WebKit WebContent). Short enough that a tab's audio starts promptly,
59
59
  // long enough to keep `ps` overhead negligible.
60
60
  const AUDIO_POLL_MS = 1500;
61
- // 아카이브 추출(tar/unzip) 시 stdout 상한. 기본 1MB 로는 파일 많은 큰 .app 에서 넘칠 수 있어 넉넉히 잡는다.
62
- const EXTRACT_MAXBUFFER = 256 * 1024 * 1024; // 256 MB
61
+ /** Bound on `simctl list` during a reconnect attempt only; see `connectOnce`. */
62
+ const RECONNECT_LIST_TIMEOUT_MS = 10_000;
63
63
  /** Lean mode needs an iOS runtime of 18.5 or later, where launchd honours the host-side overrides
64
64
  * across reboots. tvOS, watchOS and visionOS devices come through the same list and are left alone. */
65
65
  export function leanSupported(osVersion) {
@@ -146,6 +146,10 @@ export class IOSAgent {
146
146
  // Holds a macOS power assertion while connected so the host doesn't idle-throttle the
147
147
  // simulator capture/encode when the Mac is unattended. No-op off macOS.
148
148
  sleepBlocker;
149
+ /** What the next "relay disconnected" line reports: when the relay last pinged, and the socket's last error. */
150
+ lastPingAt = null;
151
+ lastSocketError;
152
+ releaseStallWatch = null;
149
153
  relayUrl = null;
150
154
  /** The last device-type lookup that answered. A udid's type never changes, so on a reconnect whose
151
155
  * lookup fails this is still right — and the relay overwrites device fields on every register, so
@@ -188,7 +192,9 @@ export class IOSAgent {
188
192
  this.fps = options.fps ?? 30;
189
193
  this.intervalMs = options.intervalMs;
190
194
  this.reconnectDelays = options.reconnectDelays ?? [1000, 2000, 4000, 8000, 16000, 30000];
191
- this.chromeLoader = new DeviceChromeLoader();
195
+ this.chromeLoader = options.chromeLoader ?? new DeviceChromeLoader(process.env.VITEST
196
+ ? { run: () => Promise.reject(new Error('no chrome under vitest')) }
197
+ : {});
192
198
  this.deviceFilter = options.deviceFilter;
193
199
  this.token = options.token;
194
200
  this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? 10_000;
@@ -200,6 +206,15 @@ export class IOSAgent {
200
206
  return first?.sessionId ?? null;
201
207
  }
202
208
  async connect(relayUrl) {
209
+ return this.connectOnce(relayUrl);
210
+ }
211
+ /**
212
+ * `listTimeoutMs` is set only by the reconnect loop. `simctl list` is left unbounded elsewhere on purpose
213
+ * (see `SimctlWrapper.listDevices`), but here a hang has a worse outcome than a failure: the attempt never
214
+ * settles, `_scheduleReconnect` is never reached, and the agent stays offline with nothing in its output
215
+ * even after CoreSimulator recovers. A bounded attempt fails and the loop tries again.
216
+ */
217
+ async connectOnce(relayUrl, listTimeoutMs) {
203
218
  this._stopping = false;
204
219
  // Paired with the `dispose()` in `disconnect()`. This method is public and reuses the network, so
205
220
  // without it a reconnect leaves the liveness watcher off for good — see `SimulatorNetwork.resume`.
@@ -209,7 +224,7 @@ export class IOSAgent {
209
224
  this._reconnectTimer = null;
210
225
  }
211
226
  this.relayUrl = relayUrl;
212
- const allDevices = await this.simctl.listDevices();
227
+ const allDevices = await this.simctl.listDevices(listTimeoutMs);
213
228
  this.revertLeftoverLean(allDevices);
214
229
  const devices = this.deviceFilter
215
230
  ? allDevices.filter((d) => d.name === this.deviceFilter || d.id === this.deviceFilter)
@@ -285,7 +300,18 @@ export class IOSAgent {
285
300
  });
286
301
  this.reportResources();
287
302
  this.resourcesTimer = setInterval(() => this.reportResources(), 5000);
288
- ws.on('close', () => this._scheduleReconnect());
303
+ this.lastPingAt = null;
304
+ this.lastSocketError = undefined;
305
+ ws.on('ping', () => { this.lastPingAt = Date.now(); });
306
+ // After registration the handshake's `once('error', reject)` lands on a settled promise, so the
307
+ // code (ETIMEDOUT, ECONNRESET…) was dropped. It is the network half of the disconnect line.
308
+ ws.on('error', (e) => { this.lastSocketError = e.code ?? e.message; });
309
+ // `now` is taken here, at the close, not when the warning prints: the cleanup in between can run
310
+ // work that holds the event loop and would inflate the ping age.
311
+ ws.on('close', (code, reason) => this._scheduleReconnect({
312
+ code, reason: reason.toString(), errorCode: this.lastSocketError, lastPingAt: this.lastPingAt, now: Date.now(),
313
+ }));
314
+ this.releaseStallWatch ??= acquireLoopStallWatch();
289
315
  resolve();
290
316
  }
291
317
  else {
@@ -358,12 +384,15 @@ export class IOSAgent {
358
384
  // other things that tick. It is `unref`'d, which is why nobody noticed it had no caller.
359
385
  this.network.dispose();
360
386
  this.sleepBlocker.release();
387
+ this.releaseStallWatch?.();
388
+ this.releaseStallWatch = null;
361
389
  this.ws?.close();
362
390
  this.ws = null;
363
391
  this.relayUrl = null;
364
392
  this.simctl.stopKeyboardDaemon();
365
393
  }
366
- _scheduleReconnect() {
394
+ /** `loss` is absent when a reconnect attempt failed: there is no close to describe, only a retry. */
395
+ _scheduleReconnect(loss) {
367
396
  if (this._stopping)
368
397
  return;
369
398
  if (this.resourcesTimer) {
@@ -385,12 +414,14 @@ export class IOSAgent {
385
414
  const delays = this.reconnectDelays;
386
415
  const delay = delays[Math.min(this._reconnectAttempt, delays.length - 1)];
387
416
  this._reconnectAttempt++;
388
- logger.warn(`relay disconnected — reconnecting in ${delay / 1000}s (attempt ${this._reconnectAttempt})`);
417
+ logger.warn(loss
418
+ ? `relay disconnected (${describeRelayLoss(loss)}) — reconnecting in ${delay / 1000}s (attempt ${this._reconnectAttempt})`
419
+ : `reconnecting in ${delay / 1000}s (attempt ${this._reconnectAttempt})`);
389
420
  this._reconnectTimer = setTimeout(() => {
390
421
  this._reconnectTimer = null;
391
422
  if (this._stopping || !this.relayUrl)
392
423
  return;
393
- this.connect(this.relayUrl).then(() => {
424
+ this.connectOnce(this.relayUrl, RECONNECT_LIST_TIMEOUT_MS).then(() => {
394
425
  this._reconnectAttempt = 0;
395
426
  logger.info('reconnected to relay');
396
427
  }).catch((e) => {
@@ -426,7 +457,7 @@ export class IOSAgent {
426
457
  void this.network.forget(state.deviceId).catch((e) => {
427
458
  logger.warn('could not clear the network rule for a retired device:', e.message);
428
459
  });
429
- void state.streamReader?.cancel();
460
+ state.streamReader?.cancel().catch(() => { });
430
461
  state.streamReader = null;
431
462
  state.captureStreamer = null; // reader.cancel() kills the helper proc; drop the ref so a stale requestKeyframe() no-ops
432
463
  if (state.audioPoll) {
@@ -443,12 +474,15 @@ export class IOSAgent {
443
474
  state.streamWs?.close();
444
475
  state.streamWs = null;
445
476
  }
446
- sendChromeData(state, device) {
477
+ /** Resolves `false` when a newer boot or a relay loss retired this one during the chrome load, in which case
478
+ * nothing was stored and the caller abandons the boot. A closed socket alone continues as it always did —
479
+ * the relay loss that follows bumps the seq. */
480
+ async sendChromeData(state, device, seq) {
447
481
  // `readyState`, not presence: this runs mid-boot, and a socket that closed since the entry guard
448
482
  // takes the payload into a buffer nobody flushes while `device:ready` is dropped by `sendMsg`'s own
449
483
  // check — leaving the caller with neither the data nor an answer.
450
484
  if (this.ws?.readyState !== WebSocket.OPEN)
451
- return;
485
+ return true;
452
486
  // Stop the outgoing helper before dropping the reference. This used to leak a child process;
453
487
  // now that a helper revives itself on death, an orphan would also keep respawning with
454
488
  // nobody left holding a reference to stop it.
@@ -463,16 +497,28 @@ export class IOSAgent {
463
497
  osVersion: device.osVersion ?? '',
464
498
  },
465
499
  });
466
- state.loadedChrome = this.chromeLoader.load(device.typeId ?? device.name);
467
- if (!state.loadedChrome)
468
- return;
469
- this.sendOn(this.ws, {
500
+ // Awaited where it used to block: a cold load is seconds of swift renders. A newer boot or a shutdown can
501
+ // land meanwhile, so the result is stored only after the seq is checked — otherwise a slow stale load
502
+ // would overwrite the chrome a faster newer boot already set, and the button path reads it.
503
+ const chrome = await this.chromeLoader.load(device.typeId ?? device.name);
504
+ if (seq !== state.bootSeq)
505
+ return false;
506
+ const ws = this.ws;
507
+ if (ws?.readyState !== WebSocket.OPEN)
508
+ return true;
509
+ state.loadedChrome = chrome;
510
+ if (!chrome)
511
+ return true;
512
+ this.sendOn(ws, {
470
513
  type: 'session:chrome',
471
514
  sessionId: state.sessionId,
472
- payload: state.loadedChrome,
515
+ payload: chrome,
473
516
  });
517
+ return true;
474
518
  }
475
- startBinaryStream(state, streamWs) {
519
+ /** `failures` counts the frameless runs before this one; only the restart in `pump` passes it, so a new
520
+ * boot starts from zero without any state to reset. */
521
+ startBinaryStream(state, streamWs, failures = 0) {
476
522
  // H.264 is the default; opt out per-agent with TAPFLOW_IOS_CODEC=jpeg. It also needs
477
523
  // a browser that reported it can decode it (device:boot acceptH264) — otherwise JPEG.
478
524
  // Only on the ScreenCaptureStreamer path — the MjpegStreamer fallback is always JPEG, and now
@@ -520,11 +566,13 @@ export class IOSAgent {
520
566
  state.captureStreamer?.requestKeyframe();
521
567
  } };
522
568
  const pump = async () => {
569
+ let delivered = false;
523
570
  try {
524
571
  while (true) {
525
572
  const { value, done } = await reader.read();
526
573
  if (done)
527
574
  break;
575
+ delivered = true;
528
576
  // Declare reorder=0 on the keyframe SPS so every decoder (WebCodecs, MSE,
529
577
  // WASM) emits frames immediately instead of buffering the level's max DPB
530
578
  // (~8 frames ≈ 250ms). Keyframe-only (SPS lives there); no-op otherwise.
@@ -543,7 +591,26 @@ export class IOSAgent {
543
591
  if (metricsTimer)
544
592
  clearInterval(metricsTimer);
545
593
  if (state.streamReader === reader && streamWs.readyState === WebSocket.OPEN) {
546
- this.startBinaryStream(state, streamWs);
594
+ const next = streamRestartPlan(failures, delivered);
595
+ if (next.failures === 2) {
596
+ logger.warn(`screen stream for ${state.deviceId} keeps ending before its first frame — backing off restarts`);
597
+ }
598
+ // Re-checked after the delay: a new boot, a shutdown, a relay loss or disconnect() all swap or clear
599
+ // the reader, or close the socket, in the meantime.
600
+ const restart = () => {
601
+ if (this._stopping || state.streamReader !== reader || streamWs.readyState !== WebSocket.OPEN)
602
+ return;
603
+ try {
604
+ this.startBinaryStream(state, streamWs, next.failures);
605
+ }
606
+ catch (e) {
607
+ logger.error(`screen stream for ${state.deviceId} could not restart: ${e instanceof Error ? e.message : String(e)}`);
608
+ }
609
+ };
610
+ if (next.delayMs === 0)
611
+ restart();
612
+ else
613
+ setTimeout(restart, next.delayMs).unref();
547
614
  }
548
615
  };
549
616
  void pump();
@@ -631,7 +698,7 @@ export class IOSAgent {
631
698
  // superseded was gone as well. The caller then waited out the very deadline this change exists to
632
699
  // end. `finally` is what stops `bootAbandon` outliving the boot that would have read it.
633
700
  try {
634
- void state.streamReader?.cancel();
701
+ state.streamReader?.cancel().catch(() => { });
635
702
  state.streamReader = null;
636
703
  state.captureStreamer = null; // reader.cancel() kills the helper proc; drop the ref so a stale requestKeyframe() no-ops
637
704
  state.touchHelper?.stop();
@@ -750,7 +817,10 @@ export class IOSAgent {
750
817
  this.abandonBoot(state, seq, sessionId, requestId);
751
818
  return;
752
819
  }
753
- this.sendChromeData(state, bootedDevice);
820
+ if (!(await this.sendChromeData(state, bootedDevice, seq))) {
821
+ this.abandonBoot(state, seq, sessionId, requestId);
822
+ return;
823
+ }
754
824
  const streamWs = await this.openStreamWs(state);
755
825
  if (seq !== state.bootSeq) {
756
826
  streamWs.close();
@@ -890,7 +960,7 @@ export class IOSAgent {
890
960
  void this.network.forget(deviceId).catch((e) => {
891
961
  logger.warn('could not clear the network rule for a shut-down device:', e.message);
892
962
  });
893
- void state.streamReader?.cancel();
963
+ state.streamReader?.cancel().catch(() => { });
894
964
  state.streamReader = null;
895
965
  state.captureStreamer = null; // reader.cancel() kills the helper proc; drop the ref so a stale requestKeyframe() no-ops
896
966
  state.touchHelper?.stop();
@@ -1779,7 +1849,8 @@ export class IOSAgent {
1779
1849
  return await this.installFrom(udid, source, tmpDir);
1780
1850
  }
1781
1851
  finally {
1782
- fs.rmSync(tmpDir, { recursive: true, force: true });
1852
+ // Async for the same reason as the extraction: a large .app is a lot of files to unlink.
1853
+ await fs.promises.rm(tmpDir, { recursive: true, force: true });
1783
1854
  }
1784
1855
  }
1785
1856
  /** Extraction + `simctl install`, given a file this machine can actually open. */
@@ -1794,11 +1865,12 @@ export class IOSAgent {
1794
1865
  // `tmpDir`, and extracting beside it would put the two in one listing.
1795
1866
  const outDir = path.join(tmpDir, 'x');
1796
1867
  fs.mkdirSync(outDir, { recursive: true });
1797
- // tar 는 기본 무음, unzip 은 -q 로 무음화해 큰 .app 에서 verbose stdout 이 기본
1798
- // maxBuffer(1MB)를 넘겨 추출이 죽는 것을 막는다.
1868
+ // **Not `spawnSync`.** It held the event loop for as long as the extraction ran, which on a large build
1869
+ // or a busy Mac was long enough for the relay's heartbeat to end this agent's connection — and every
1870
+ // other session on the Mac froze with it.
1799
1871
  const result = isTar
1800
- ? spawnSync('tar', ['-xzf', filePath, '-C', outDir], { maxBuffer: EXTRACT_MAXBUFFER })
1801
- : spawnSync('unzip', ['-q', '-o', filePath, '-d', outDir], { maxBuffer: EXTRACT_MAXBUFFER });
1872
+ ? await runExtractor('tar', ['-xzf', filePath, '-C', outDir])
1873
+ : await runExtractor('unzip', ['-q', '-o', filePath, '-d', outDir]);
1802
1874
  // 실행 자체 실패(tar/unzip 부재=ENOENT 등)는 아카이브 무효와 구분한다.
1803
1875
  if (result.error) {
1804
1876
  const code = result.error.code ?? result.error.message;
@@ -1809,7 +1881,7 @@ export class IOSAgent {
1809
1881
  // evidence away, so a build that was fine read as corrupt — `unzip` had said
1810
1882
  // `cannot find or open`, which names a missing file rather than a bad archive, and nobody
1811
1883
  // could see it. Trimmed because the whole thing reaches a browser toast.
1812
- const detail = (result.stderr?.toString() ?? '').trim().split('\n')[0]?.slice(0, 300);
1884
+ const detail = result.stderr.trim().split('\n')[0]?.slice(0, 300);
1813
1885
  throw new ValidationError((isTar
1814
1886
  ? 'tar.gz 압축 해제 실패 — 시뮬레이터용 .tar.gz(경로 탈출/심볼릭 링크 없는)인지 확인하세요.'
1815
1887
  : 'zip 압축 해제 실패 — 시뮬레이터용 .app.zip 파일인지 확인하세요.')
@@ -2157,4 +2229,34 @@ export class IOSAgent {
2157
2229
  return this.simctl.openUrl(await this.soleLiveDeviceId(), url);
2158
2230
  }
2159
2231
  }
2232
+ /**
2233
+ * Runs an archive tool without blocking, in the shape `spawnSync` returned: a spawn failure (the tool is
2234
+ * missing) in `error`, otherwise the exit `status` and what it wrote to stderr. stdout is discarded — the
2235
+ * tools run quiet, and nothing reads it.
2236
+ */
2237
+ function runExtractor(cmd, args) {
2238
+ return new Promise((resolve) => {
2239
+ const child = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] });
2240
+ let stderr = '';
2241
+ // Decoded by the stream, not per chunk: a chunk boundary inside a multibyte character (a Korean file
2242
+ // name in unzip's message) would otherwise turn into replacement characters.
2243
+ child.stderr?.setEncoding('utf8');
2244
+ child.stderr?.on('data', (chunk) => { if (stderr.length < 64 * 1024)
2245
+ stderr += chunk; });
2246
+ child.once('error', (error) => resolve({ error, status: null, stderr }));
2247
+ child.once('close', (status) => resolve({ status, stderr }));
2248
+ });
2249
+ }
2250
+ /**
2251
+ * When to restart a screen stream that just ended. Only a run that delivered a frame resets the count — not
2252
+ * how long it lasted: the helper retries `device.io` and its ports for up to ~10s before it gives up, so a
2253
+ * duration rule would call every first-boot failure healthy. A simulator shut down from outside makes the
2254
+ * helper exit at once, which without this was a restart loop at full speed.
2255
+ */
2256
+ export function streamRestartPlan(previousFailures, deliveredFrame) {
2257
+ if (deliveredFrame)
2258
+ return { failures: 0, delayMs: 0 };
2259
+ const failures = previousFailures + 1;
2260
+ return { failures, delayMs: failures <= 1 ? 0 : Math.min(250 * 2 ** (failures - 2), 5000) };
2261
+ }
2160
2262
  //# sourceMappingURL=IOSAgent.js.map