appium-mac2-driver 3.4.0 → 3.4.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/CHANGELOG.md +6 -0
- package/build/lib/commands/native-record-screen.d.ts +29 -3
- package/build/lib/commands/native-record-screen.d.ts.map +1 -1
- package/build/lib/commands/native-record-screen.js +136 -36
- package/build/lib/commands/native-record-screen.js.map +1 -1
- package/lib/commands/native-record-screen.ts +146 -46
- package/npm-shrinkwrap.json +21 -8
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [3.4.1](https://github.com/appium/appium-mac2-driver/compare/v3.4.0...v3.4.1) (2026-05-13)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* Unhandled error for native video recording ([#383](https://github.com/appium/appium-mac2-driver/issues/383)) ([22a8bff](https://github.com/appium/appium-mac2-driver/commit/22a8bff4efc731b81737fa6d5c36de63582db4f3))
|
|
6
|
+
|
|
1
7
|
## [3.4.0](https://github.com/appium/appium-mac2-driver/compare/v3.3.4...v3.4.0) (2026-05-09)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -13,15 +13,41 @@ interface DisplayInfo {
|
|
|
13
13
|
isMain: boolean;
|
|
14
14
|
}
|
|
15
15
|
export declare class NativeVideoChunksBroadcaster {
|
|
16
|
-
private _ee;
|
|
17
|
-
private _log;
|
|
18
|
-
private _publishers;
|
|
16
|
+
private readonly _ee;
|
|
17
|
+
private readonly _log;
|
|
18
|
+
private readonly _publishers;
|
|
19
|
+
private readonly _wakeups;
|
|
20
|
+
private readonly _terminationRequests;
|
|
19
21
|
private _terminated;
|
|
20
22
|
constructor(ee: EventEmitter, log: AppiumLogger);
|
|
21
23
|
get hasPublishers(): boolean;
|
|
22
24
|
schedule(uuid: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Signals the publisher for `uuid` that its underlying recording has been
|
|
27
|
+
* stopped. The publisher wakes from its current polling sleep, performs a
|
|
28
|
+
* final stat + read + emit pass to flush any remaining bytes of the
|
|
29
|
+
* recording file to BiDi consumers, and exits.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent and safe to call for an unknown uuid (no-op in that case).
|
|
32
|
+
*
|
|
33
|
+
* Callers SHOULD invoke this immediately after the recording has been
|
|
34
|
+
* stopped on the underlying XCTest side; `waitFor(uuid)` is then
|
|
35
|
+
* contractually guaranteed to resolve as soon as the publisher has
|
|
36
|
+
* finished flushing, without having to wait for the next polling tick.
|
|
37
|
+
*/
|
|
38
|
+
notifyStopped(uuid: string): void;
|
|
23
39
|
waitFor(uuid: string): Promise<void>;
|
|
24
40
|
shutdown(timeoutMs: number): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Background monitor for a native screen recording attachment.
|
|
43
|
+
*
|
|
44
|
+
* This is scheduled as a fire-and-forget task by {@link schedule} and the
|
|
45
|
+
* resulting promise may sit in `_publishers` for a long time before any
|
|
46
|
+
* consumer awaits it. To make sure a failure here can never surface as an
|
|
47
|
+
* unhandled promise rejection (which terminates the Appium server process
|
|
48
|
+
* on Node.js 24+), this method MUST never reject: every error path logs a
|
|
49
|
+
* warning and returns normally.
|
|
50
|
+
*/
|
|
25
51
|
private _createPublisher;
|
|
26
52
|
private _wait;
|
|
27
53
|
private _cleanup;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-record-screen.d.ts","sourceRoot":"","sources":["../../../lib/commands/native-record-screen.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native-record-screen.d.ts","sourceRoot":"","sources":["../../../lib/commands/native-record-screen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,WAAW,CAAC;AAE1C,OAAO,KAAK,EAAC,YAAY,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AAC9D,OAAO,KAAK,YAAY,MAAM,aAAa,CAAC;AAc5C,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAIzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IAKjE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAc;IACnD,OAAO,CAAC,WAAW,CAAU;gBAEjB,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY;IAS/C,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM5B;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ3B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBhD;;;;;;;;;OASG;YACW,gBAAgB;YA+GhB,KAAK;YAiBL,QAAQ;CA2BvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,UAAU,EAChB,GAAG,CAAC,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC,CAQ1B;AAED;;;;GAIG;AACH,wBAAsB,iCAAiC,CACrD,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAEjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACxC,aAAa,CAAC,EAAE,MAAM,EACtB,UAAU,CAAC,EAAE,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAC9C,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAE5F"}
|
|
@@ -8,7 +8,6 @@ exports.macosStartNativeScreenRecording = macosStartNativeScreenRecording;
|
|
|
8
8
|
exports.macosGetNativeScreenRecordingInfo = macosGetNativeScreenRecordingInfo;
|
|
9
9
|
exports.macosStopNativeScreenRecording = macosStopNativeScreenRecording;
|
|
10
10
|
exports.macosListDisplays = macosListDisplays;
|
|
11
|
-
const promises_1 = require("node:timers/promises");
|
|
12
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
12
|
const support_1 = require("appium/support");
|
|
14
13
|
const helpers_1 = require("./helpers");
|
|
@@ -21,15 +20,27 @@ const RECORDING_STARTUP_TIMEOUT_MS = 5000;
|
|
|
21
20
|
const BUFFER_SIZE = 0xffff;
|
|
22
21
|
const MONITORING_INTERVAL_DURATION_MS = 1000;
|
|
23
22
|
const MAX_MONITORING_DURATION_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
23
|
+
const STOP_WAIT_TIMEOUT_MS = 5000;
|
|
24
24
|
class NativeVideoChunksBroadcaster {
|
|
25
25
|
_ee;
|
|
26
26
|
_log;
|
|
27
27
|
_publishers;
|
|
28
|
+
// The currently scheduled polling sleep of each publisher, indexed by uuid.
|
|
29
|
+
// Cancelling it wakes the publisher up before the next polling tick so it
|
|
30
|
+
// can perform a final stat + read + emit pass and exit.
|
|
31
|
+
_wakeups;
|
|
32
|
+
// Set of uuids whose underlying recording has been explicitly stopped via
|
|
33
|
+
// `notifyStopped`. The publisher uses this as the primary completion
|
|
34
|
+
// signal; the `lsof` check is only consulted as a fallback for the case
|
|
35
|
+
// where the host XCTest process disappears without anyone telling us.
|
|
36
|
+
_terminationRequests;
|
|
28
37
|
_terminated;
|
|
29
38
|
constructor(ee, log) {
|
|
30
39
|
this._ee = ee;
|
|
31
40
|
this._log = log;
|
|
32
41
|
this._publishers = new Map();
|
|
42
|
+
this._wakeups = new Map();
|
|
43
|
+
this._terminationRequests = new Set();
|
|
33
44
|
this._terminated = false;
|
|
34
45
|
}
|
|
35
46
|
get hasPublishers() {
|
|
@@ -40,6 +51,26 @@ class NativeVideoChunksBroadcaster {
|
|
|
40
51
|
this._publishers.set(uuid, this._createPublisher(uuid));
|
|
41
52
|
}
|
|
42
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Signals the publisher for `uuid` that its underlying recording has been
|
|
56
|
+
* stopped. The publisher wakes from its current polling sleep, performs a
|
|
57
|
+
* final stat + read + emit pass to flush any remaining bytes of the
|
|
58
|
+
* recording file to BiDi consumers, and exits.
|
|
59
|
+
*
|
|
60
|
+
* Idempotent and safe to call for an unknown uuid (no-op in that case).
|
|
61
|
+
*
|
|
62
|
+
* Callers SHOULD invoke this immediately after the recording has been
|
|
63
|
+
* stopped on the underlying XCTest side; `waitFor(uuid)` is then
|
|
64
|
+
* contractually guaranteed to resolve as soon as the publisher has
|
|
65
|
+
* finished flushing, without having to wait for the next polling tick.
|
|
66
|
+
*/
|
|
67
|
+
notifyStopped(uuid) {
|
|
68
|
+
if (!this._publishers.has(uuid)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this._terminationRequests.add(uuid);
|
|
72
|
+
this._wakeups.get(uuid)?.cancel();
|
|
73
|
+
}
|
|
43
74
|
async waitFor(uuid) {
|
|
44
75
|
const publisher = this._publishers.get(uuid);
|
|
45
76
|
if (publisher) {
|
|
@@ -47,6 +78,11 @@ class NativeVideoChunksBroadcaster {
|
|
|
47
78
|
}
|
|
48
79
|
}
|
|
49
80
|
async shutdown(timeoutMs) {
|
|
81
|
+
// Wake up every active publisher so they don't have to wait for their
|
|
82
|
+
// next polling tick before flushing and exiting.
|
|
83
|
+
for (const uuid of this._publishers.keys()) {
|
|
84
|
+
this.notifyStopped(uuid);
|
|
85
|
+
}
|
|
50
86
|
try {
|
|
51
87
|
await this._wait(timeoutMs);
|
|
52
88
|
}
|
|
@@ -54,11 +90,22 @@ class NativeVideoChunksBroadcaster {
|
|
|
54
90
|
this._log.warn(e instanceof Error ? e.message : String(e));
|
|
55
91
|
}
|
|
56
92
|
await this._cleanup();
|
|
57
|
-
this._publishers
|
|
93
|
+
this._publishers.clear();
|
|
94
|
+
this._wakeups.clear();
|
|
95
|
+
this._terminationRequests.clear();
|
|
58
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Background monitor for a native screen recording attachment.
|
|
99
|
+
*
|
|
100
|
+
* This is scheduled as a fire-and-forget task by {@link schedule} and the
|
|
101
|
+
* resulting promise may sit in `_publishers` for a long time before any
|
|
102
|
+
* consumer awaits it. To make sure a failure here can never surface as an
|
|
103
|
+
* unhandled promise rejection (which terminates the Appium server process
|
|
104
|
+
* on Node.js 24+), this method MUST never reject: every error path logs a
|
|
105
|
+
* warning and returns normally.
|
|
106
|
+
*/
|
|
59
107
|
async _createPublisher(uuid) {
|
|
60
108
|
let fullPath = '';
|
|
61
|
-
let bytesRead = 0n;
|
|
62
109
|
try {
|
|
63
110
|
await (0, asyncbox_1.waitForCondition)(async () => {
|
|
64
111
|
const paths = await listAttachments();
|
|
@@ -74,33 +121,81 @@ class NativeVideoChunksBroadcaster {
|
|
|
74
121
|
});
|
|
75
122
|
}
|
|
76
123
|
catch {
|
|
77
|
-
|
|
124
|
+
this._log.warn(`The video recording identified by ${uuid} did not ` +
|
|
78
125
|
`start within ${RECORDING_STARTUP_TIMEOUT_MS}ms timeout`);
|
|
126
|
+
return;
|
|
79
127
|
}
|
|
128
|
+
let bytesRead = 0n;
|
|
80
129
|
const startedMs = performance.now();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
130
|
+
try {
|
|
131
|
+
while (!this._terminated && performance.now() - startedMs < MAX_MONITORING_DURATION_MS) {
|
|
132
|
+
// Primary completion signal: `notifyStopped(uuid)` was called.
|
|
133
|
+
// Secondary fallback: lsof reports no `testman` process holding
|
|
134
|
+
// the recording file open (e.g. the host process exited without
|
|
135
|
+
// anyone telling us — see appium/appium#22269).
|
|
136
|
+
const stopRequested = this._terminationRequests.has(uuid);
|
|
137
|
+
const isCompleted = stopRequested || !(await isFileUsed(fullPath, 'testman'));
|
|
138
|
+
let size;
|
|
86
139
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
140
|
+
({ size } = await support_1.fs.stat(fullPath, { bigint: true }));
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// The recording file may have been removed by the OS once the
|
|
144
|
+
// host XCTest process owning the attachment has exited. In that
|
|
145
|
+
// case there is nothing more to read and we should stop monitoring.
|
|
146
|
+
if (e?.code === 'ENOENT') {
|
|
147
|
+
this._log.debug(`The native video recording file for ${uuid} is no longer available. ` +
|
|
148
|
+
`Assuming the recording has been terminated`);
|
|
149
|
+
return;
|
|
93
150
|
}
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
if (bytesRead < size) {
|
|
154
|
+
const handle = await support_1.fs.open(fullPath, 'r');
|
|
155
|
+
try {
|
|
156
|
+
while (bytesRead < size) {
|
|
157
|
+
const bufferSize = Number(size - bytesRead > BUFFER_SIZE ? BUFFER_SIZE : size - bytesRead);
|
|
158
|
+
const buf = Buffer.alloc(bufferSize);
|
|
159
|
+
await support_1.fs.read(handle, buf, 0, bufferSize, bytesRead);
|
|
160
|
+
this._ee.emit(constants_1.BIDI_EVENT_NAME, (0, models_1.toNativeVideoChunkAddedEvent)(uuid, buf));
|
|
161
|
+
bytesRead += BigInt(bufferSize);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await support_1.fs.close(handle);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (isCompleted) {
|
|
169
|
+
this._log.debug(`The native video recording identified by ${uuid} has been detected as completed`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Cancellable sleep until the next polling tick. `notifyStopped`
|
|
173
|
+
// cancels this wakeup so the loop re-runs immediately and detects
|
|
174
|
+
// termination on the very next iteration. `cancelError: null` makes
|
|
175
|
+
// the cancel resolve the promise instead of rejecting it.
|
|
176
|
+
const wakeup = (0, asyncbox_1.sleep)({ ms: MONITORING_INTERVAL_DURATION_MS, cancelError: null });
|
|
177
|
+
this._wakeups.set(uuid, wakeup);
|
|
178
|
+
try {
|
|
179
|
+
// Guard against a notifyStopped() that landed between the
|
|
180
|
+
// iteration's stop-check above and the wakeup being stored:
|
|
181
|
+
// the cancel above would have been a no-op, so cancel here.
|
|
182
|
+
if (this._terminationRequests.has(uuid)) {
|
|
183
|
+
wakeup.cancel();
|
|
184
|
+
}
|
|
185
|
+
await wakeup;
|
|
94
186
|
}
|
|
95
187
|
finally {
|
|
96
|
-
|
|
188
|
+
this._wakeups.delete(uuid);
|
|
97
189
|
}
|
|
98
190
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
this._log.warn(`Native video chunks publisher for ${uuid} stopped unexpectedly: ` +
|
|
194
|
+
(e instanceof Error ? e.message : String(e)));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
this._terminationRequests.delete(uuid);
|
|
104
199
|
}
|
|
105
200
|
this._log.warn(`Stopped monitoring of the native video recording identified by ${uuid} ` +
|
|
106
201
|
`because of the timeout`);
|
|
@@ -112,18 +207,13 @@ class NativeVideoChunksBroadcaster {
|
|
|
112
207
|
const timer = setTimeout(() => {
|
|
113
208
|
this._terminated = true;
|
|
114
209
|
}, timeoutMs);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
catch (e) {
|
|
121
|
-
publishingErrors.push(e instanceof Error ? e.message : String(e));
|
|
122
|
-
}
|
|
210
|
+
try {
|
|
211
|
+
// Publishers are guaranteed by `_createPublisher` to never reject,
|
|
212
|
+
// so we can simply await all of them.
|
|
213
|
+
await Promise.all(this._publishers.values());
|
|
123
214
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
throw new Error(publishingErrors.join('\n'));
|
|
215
|
+
finally {
|
|
216
|
+
clearTimeout(timer);
|
|
127
217
|
}
|
|
128
218
|
}
|
|
129
219
|
async _cleanup() {
|
|
@@ -216,13 +306,18 @@ async function macosStopNativeScreenRecording(remotePath, user, pass, method, he
|
|
|
216
306
|
throw new Error('There is no active screen recording, thus nothing to stop. Did you start it before?');
|
|
217
307
|
}
|
|
218
308
|
const { uuid } = response;
|
|
309
|
+
// Wake the publisher up so it stops polling and flushes any remaining
|
|
310
|
+
// bytes immediately. Without this, `waitFor` below could still block
|
|
311
|
+
// for up to `MONITORING_INTERVAL_DURATION_MS` while the publisher sits
|
|
312
|
+
// in its polling sleep.
|
|
313
|
+
this._videoChunksBroadcaster.notifyStopped(uuid);
|
|
219
314
|
try {
|
|
220
|
-
await (0, asyncbox_1.withTimeout)(this._videoChunksBroadcaster.waitFor(uuid),
|
|
315
|
+
await (0, asyncbox_1.withTimeout)(this._videoChunksBroadcaster.waitFor(uuid), STOP_WAIT_TIMEOUT_MS);
|
|
221
316
|
}
|
|
222
317
|
catch (e) {
|
|
223
318
|
if (e instanceof asyncbox_1.TimeoutError) {
|
|
224
319
|
this.log.debug(`The BiDi chunks broadcaster for the native screen recording identified ` +
|
|
225
|
-
`by ${uuid} cannot complete within
|
|
320
|
+
`by ${uuid} cannot complete within ${STOP_WAIT_TIMEOUT_MS}ms timeout`);
|
|
226
321
|
}
|
|
227
322
|
else {
|
|
228
323
|
this.log.debug(e instanceof Error ? e.stack : String(e));
|
|
@@ -268,7 +363,12 @@ async function listAttachments() {
|
|
|
268
363
|
});
|
|
269
364
|
}
|
|
270
365
|
async function isFileUsed(fpath, userProcessName) {
|
|
271
|
-
|
|
272
|
-
|
|
366
|
+
try {
|
|
367
|
+
const { stdout } = await (0, teen_process_1.exec)('lsof', [fpath]);
|
|
368
|
+
return stdout.includes(userProcessName);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
273
373
|
}
|
|
274
374
|
//# sourceMappingURL=native-record-screen.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-record-screen.js","sourceRoot":"","sources":["../../../lib/commands/native-record-screen.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"native-record-screen.js","sourceRoot":"","sources":["../../../lib/commands/native-record-screen.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {setTimeout as delay} from 'node:timers/promises';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import {fs, util} from 'appium/support';
|
|
4
3
|
import type {Mac2Driver} from '../driver';
|
|
5
4
|
import {uploadRecordedMedia} from './helpers';
|
|
6
5
|
import type {AppiumLogger, StringRecord} from '@appium/types';
|
|
7
6
|
import type EventEmitter from 'node:events';
|
|
8
|
-
import {
|
|
7
|
+
import type {CancellablePromise} from 'asyncbox';
|
|
8
|
+
import {TimeoutError, sleep, waitForCondition, withTimeout} from 'asyncbox';
|
|
9
9
|
import {exec} from 'teen_process';
|
|
10
10
|
import {BIDI_EVENT_NAME} from './bidi/constants';
|
|
11
11
|
import {toNativeVideoChunkAddedEvent} from './bidi/models';
|
|
@@ -15,6 +15,7 @@ const RECORDING_STARTUP_TIMEOUT_MS = 5000;
|
|
|
15
15
|
const BUFFER_SIZE = 0xffff;
|
|
16
16
|
const MONITORING_INTERVAL_DURATION_MS = 1000;
|
|
17
17
|
const MAX_MONITORING_DURATION_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
18
|
+
const STOP_WAIT_TIMEOUT_MS = 5000;
|
|
18
19
|
|
|
19
20
|
interface ActiveVideoInfo {
|
|
20
21
|
fps: number;
|
|
@@ -30,15 +31,26 @@ interface DisplayInfo {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class NativeVideoChunksBroadcaster {
|
|
33
|
-
private _ee: EventEmitter;
|
|
34
|
-
private _log: AppiumLogger;
|
|
35
|
-
private _publishers: Map<string, Promise<void>>;
|
|
34
|
+
private readonly _ee: EventEmitter;
|
|
35
|
+
private readonly _log: AppiumLogger;
|
|
36
|
+
private readonly _publishers: Map<string, Promise<void>>;
|
|
37
|
+
// The currently scheduled polling sleep of each publisher, indexed by uuid.
|
|
38
|
+
// Cancelling it wakes the publisher up before the next polling tick so it
|
|
39
|
+
// can perform a final stat + read + emit pass and exit.
|
|
40
|
+
private readonly _wakeups: Map<string, CancellablePromise<void>>;
|
|
41
|
+
// Set of uuids whose underlying recording has been explicitly stopped via
|
|
42
|
+
// `notifyStopped`. The publisher uses this as the primary completion
|
|
43
|
+
// signal; the `lsof` check is only consulted as a fallback for the case
|
|
44
|
+
// where the host XCTest process disappears without anyone telling us.
|
|
45
|
+
private readonly _terminationRequests: Set<string>;
|
|
36
46
|
private _terminated: boolean;
|
|
37
47
|
|
|
38
48
|
constructor(ee: EventEmitter, log: AppiumLogger) {
|
|
39
49
|
this._ee = ee;
|
|
40
50
|
this._log = log;
|
|
41
51
|
this._publishers = new Map();
|
|
52
|
+
this._wakeups = new Map();
|
|
53
|
+
this._terminationRequests = new Set();
|
|
42
54
|
this._terminated = false;
|
|
43
55
|
}
|
|
44
56
|
|
|
@@ -52,6 +64,27 @@ export class NativeVideoChunksBroadcaster {
|
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Signals the publisher for `uuid` that its underlying recording has been
|
|
69
|
+
* stopped. The publisher wakes from its current polling sleep, performs a
|
|
70
|
+
* final stat + read + emit pass to flush any remaining bytes of the
|
|
71
|
+
* recording file to BiDi consumers, and exits.
|
|
72
|
+
*
|
|
73
|
+
* Idempotent and safe to call for an unknown uuid (no-op in that case).
|
|
74
|
+
*
|
|
75
|
+
* Callers SHOULD invoke this immediately after the recording has been
|
|
76
|
+
* stopped on the underlying XCTest side; `waitFor(uuid)` is then
|
|
77
|
+
* contractually guaranteed to resolve as soon as the publisher has
|
|
78
|
+
* finished flushing, without having to wait for the next polling tick.
|
|
79
|
+
*/
|
|
80
|
+
notifyStopped(uuid: string): void {
|
|
81
|
+
if (!this._publishers.has(uuid)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this._terminationRequests.add(uuid);
|
|
85
|
+
this._wakeups.get(uuid)?.cancel();
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
async waitFor(uuid: string): Promise<void> {
|
|
56
89
|
const publisher = this._publishers.get(uuid);
|
|
57
90
|
if (publisher) {
|
|
@@ -60,6 +93,11 @@ export class NativeVideoChunksBroadcaster {
|
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
async shutdown(timeoutMs: number): Promise<void> {
|
|
96
|
+
// Wake up every active publisher so they don't have to wait for their
|
|
97
|
+
// next polling tick before flushing and exiting.
|
|
98
|
+
for (const uuid of this._publishers.keys()) {
|
|
99
|
+
this.notifyStopped(uuid);
|
|
100
|
+
}
|
|
63
101
|
try {
|
|
64
102
|
await this._wait(timeoutMs);
|
|
65
103
|
} catch (e) {
|
|
@@ -68,12 +106,23 @@ export class NativeVideoChunksBroadcaster {
|
|
|
68
106
|
|
|
69
107
|
await this._cleanup();
|
|
70
108
|
|
|
71
|
-
this._publishers
|
|
109
|
+
this._publishers.clear();
|
|
110
|
+
this._wakeups.clear();
|
|
111
|
+
this._terminationRequests.clear();
|
|
72
112
|
}
|
|
73
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Background monitor for a native screen recording attachment.
|
|
116
|
+
*
|
|
117
|
+
* This is scheduled as a fire-and-forget task by {@link schedule} and the
|
|
118
|
+
* resulting promise may sit in `_publishers` for a long time before any
|
|
119
|
+
* consumer awaits it. To make sure a failure here can never surface as an
|
|
120
|
+
* unhandled promise rejection (which terminates the Appium server process
|
|
121
|
+
* on Node.js 24+), this method MUST never reject: every error path logs a
|
|
122
|
+
* warning and returns normally.
|
|
123
|
+
*/
|
|
74
124
|
private async _createPublisher(uuid: string): Promise<void> {
|
|
75
125
|
let fullPath = '';
|
|
76
|
-
let bytesRead = 0n;
|
|
77
126
|
try {
|
|
78
127
|
await waitForCondition(
|
|
79
128
|
async () => {
|
|
@@ -91,42 +140,90 @@ export class NativeVideoChunksBroadcaster {
|
|
|
91
140
|
},
|
|
92
141
|
);
|
|
93
142
|
} catch {
|
|
94
|
-
|
|
143
|
+
this._log.warn(
|
|
95
144
|
`The video recording identified by ${uuid} did not ` +
|
|
96
145
|
`start within ${RECORDING_STARTUP_TIMEOUT_MS}ms timeout`,
|
|
97
146
|
);
|
|
147
|
+
return;
|
|
98
148
|
}
|
|
99
149
|
|
|
150
|
+
let bytesRead = 0n;
|
|
100
151
|
const startedMs = performance.now();
|
|
101
|
-
|
|
102
|
-
|
|
152
|
+
try {
|
|
153
|
+
while (!this._terminated && performance.now() - startedMs < MAX_MONITORING_DURATION_MS) {
|
|
154
|
+
// Primary completion signal: `notifyStopped(uuid)` was called.
|
|
155
|
+
// Secondary fallback: lsof reports no `testman` process holding
|
|
156
|
+
// the recording file open (e.g. the host process exited without
|
|
157
|
+
// anyone telling us — see appium/appium#22269).
|
|
158
|
+
const stopRequested = this._terminationRequests.has(uuid);
|
|
159
|
+
const isCompleted = stopRequested || !(await isFileUsed(fullPath, 'testman'));
|
|
103
160
|
|
|
104
|
-
|
|
105
|
-
if (bytesRead < size) {
|
|
106
|
-
const handle = await fs.open(fullPath, 'r');
|
|
161
|
+
let size: bigint;
|
|
107
162
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
163
|
+
({size} = await fs.stat(fullPath, {bigint: true}));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// The recording file may have been removed by the OS once the
|
|
166
|
+
// host XCTest process owning the attachment has exited. In that
|
|
167
|
+
// case there is nothing more to read and we should stop monitoring.
|
|
168
|
+
if ((e as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
169
|
+
this._log.debug(
|
|
170
|
+
`The native video recording file for ${uuid} is no longer available. ` +
|
|
171
|
+
`Assuming the recording has been terminated`,
|
|
111
172
|
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
if (bytesRead < size) {
|
|
178
|
+
const handle = await fs.open(fullPath, 'r');
|
|
179
|
+
try {
|
|
180
|
+
while (bytesRead < size) {
|
|
181
|
+
const bufferSize = Number(
|
|
182
|
+
size - bytesRead > BUFFER_SIZE ? BUFFER_SIZE : size - bytesRead,
|
|
183
|
+
);
|
|
184
|
+
const buf = Buffer.alloc(bufferSize);
|
|
185
|
+
await fs.read(handle, buf as any, 0, bufferSize, bytesRead as any);
|
|
186
|
+
this._ee.emit(BIDI_EVENT_NAME, toNativeVideoChunkAddedEvent(uuid, buf));
|
|
187
|
+
bytesRead += BigInt(bufferSize);
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
await fs.close(handle);
|
|
116
191
|
}
|
|
117
|
-
} finally {
|
|
118
|
-
await fs.close(handle);
|
|
119
192
|
}
|
|
120
|
-
}
|
|
121
193
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
194
|
+
if (isCompleted) {
|
|
195
|
+
this._log.debug(
|
|
196
|
+
`The native video recording identified by ${uuid} has been detected as completed`,
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
128
200
|
|
|
129
|
-
|
|
201
|
+
// Cancellable sleep until the next polling tick. `notifyStopped`
|
|
202
|
+
// cancels this wakeup so the loop re-runs immediately and detects
|
|
203
|
+
// termination on the very next iteration. `cancelError: null` makes
|
|
204
|
+
// the cancel resolve the promise instead of rejecting it.
|
|
205
|
+
const wakeup = sleep({ms: MONITORING_INTERVAL_DURATION_MS, cancelError: null});
|
|
206
|
+
this._wakeups.set(uuid, wakeup);
|
|
207
|
+
try {
|
|
208
|
+
// Guard against a notifyStopped() that landed between the
|
|
209
|
+
// iteration's stop-check above and the wakeup being stored:
|
|
210
|
+
// the cancel above would have been a no-op, so cancel here.
|
|
211
|
+
if (this._terminationRequests.has(uuid)) {
|
|
212
|
+
wakeup.cancel();
|
|
213
|
+
}
|
|
214
|
+
await wakeup;
|
|
215
|
+
} finally {
|
|
216
|
+
this._wakeups.delete(uuid);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
this._log.warn(
|
|
221
|
+
`Native video chunks publisher for ${uuid} stopped unexpectedly: ` +
|
|
222
|
+
(e instanceof Error ? e.message : String(e)),
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
} finally {
|
|
226
|
+
this._terminationRequests.delete(uuid);
|
|
130
227
|
}
|
|
131
228
|
|
|
132
229
|
this._log.warn(
|
|
@@ -143,18 +240,12 @@ export class NativeVideoChunksBroadcaster {
|
|
|
143
240
|
const timer = setTimeout(() => {
|
|
144
241
|
this._terminated = true;
|
|
145
242
|
}, timeoutMs);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
clearTimeout(timer);
|
|
155
|
-
|
|
156
|
-
if (publishingErrors.length > 0) {
|
|
157
|
-
throw new Error(publishingErrors.join('\n'));
|
|
243
|
+
try {
|
|
244
|
+
// Publishers are guaranteed by `_createPublisher` to never reject,
|
|
245
|
+
// so we can simply await all of them.
|
|
246
|
+
await Promise.all(this._publishers.values());
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timer);
|
|
158
249
|
}
|
|
159
250
|
}
|
|
160
251
|
|
|
@@ -278,13 +369,18 @@ export async function macosStopNativeScreenRecording(
|
|
|
278
369
|
}
|
|
279
370
|
|
|
280
371
|
const {uuid} = response;
|
|
372
|
+
// Wake the publisher up so it stops polling and flushes any remaining
|
|
373
|
+
// bytes immediately. Without this, `waitFor` below could still block
|
|
374
|
+
// for up to `MONITORING_INTERVAL_DURATION_MS` while the publisher sits
|
|
375
|
+
// in its polling sleep.
|
|
376
|
+
this._videoChunksBroadcaster.notifyStopped(uuid);
|
|
281
377
|
try {
|
|
282
|
-
await withTimeout(this._videoChunksBroadcaster.waitFor(uuid),
|
|
378
|
+
await withTimeout(this._videoChunksBroadcaster.waitFor(uuid), STOP_WAIT_TIMEOUT_MS);
|
|
283
379
|
} catch (e) {
|
|
284
380
|
if (e instanceof TimeoutError) {
|
|
285
381
|
this.log.debug(
|
|
286
382
|
`The BiDi chunks broadcaster for the native screen recording identified ` +
|
|
287
|
-
`by ${uuid} cannot complete within
|
|
383
|
+
`by ${uuid} cannot complete within ${STOP_WAIT_TIMEOUT_MS}ms timeout`,
|
|
288
384
|
);
|
|
289
385
|
} else {
|
|
290
386
|
this.log.debug(e instanceof Error ? e.stack : String(e));
|
|
@@ -341,6 +437,10 @@ async function listAttachments(): Promise<string[]> {
|
|
|
341
437
|
}
|
|
342
438
|
|
|
343
439
|
async function isFileUsed(fpath: string, userProcessName: string): Promise<boolean> {
|
|
344
|
-
|
|
345
|
-
|
|
440
|
+
try {
|
|
441
|
+
const {stdout} = await exec('lsof', [fpath]);
|
|
442
|
+
return stdout.includes(userProcessName);
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
346
446
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-mac2-driver",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-mac2-driver",
|
|
9
|
-
"version": "3.4.
|
|
9
|
+
"version": "3.4.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@appium/strongbox": "^1.0.0-rc.1",
|
|
13
13
|
"@colors/colors": "^1.6.0",
|
|
14
14
|
"appium-xcode": "^6.0.0",
|
|
15
|
-
"asyncbox": "^6.0
|
|
16
|
-
"axios": "^1.
|
|
17
|
-
"portscanner": "2.2.0",
|
|
15
|
+
"asyncbox": "^6.3.0",
|
|
16
|
+
"axios": "^1.16.0",
|
|
17
|
+
"portscanner": "^2.2.0",
|
|
18
18
|
"teen_process": "^4.0.4"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -138,6 +138,19 @@
|
|
|
138
138
|
"sharp": "0.34.5"
|
|
139
139
|
}
|
|
140
140
|
},
|
|
141
|
+
"node_modules/@appium/support/node_modules/asyncbox": {
|
|
142
|
+
"version": "6.2.0",
|
|
143
|
+
"resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.2.0.tgz",
|
|
144
|
+
"integrity": "sha512-z1XpHkoT3y+1aXfazEY5d7HN2eOi50fLq7ZTxG0H4WegLxrtEAI5Vsc6OR9dOwoC3SJQLXyV0ZVnPEh6GIgMKQ==",
|
|
145
|
+
"license": "Apache-2.0",
|
|
146
|
+
"dependencies": {
|
|
147
|
+
"p-limit": "^7.2.0"
|
|
148
|
+
},
|
|
149
|
+
"engines": {
|
|
150
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
|
151
|
+
"npm": ">=10"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
141
154
|
"node_modules/@appium/tsconfig": {
|
|
142
155
|
"version": "1.1.2",
|
|
143
156
|
"resolved": "https://registry.npmjs.org/@appium/tsconfig/-/tsconfig-1.1.2.tgz",
|
|
@@ -556,9 +569,9 @@
|
|
|
556
569
|
"license": "MIT"
|
|
557
570
|
},
|
|
558
571
|
"node_modules/asyncbox": {
|
|
559
|
-
"version": "6.
|
|
560
|
-
"resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.
|
|
561
|
-
"integrity": "sha512-
|
|
572
|
+
"version": "6.3.0",
|
|
573
|
+
"resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.3.0.tgz",
|
|
574
|
+
"integrity": "sha512-7IFpnQDltd5rYQjhIJIpyismJtdWmw/pOABZKJfv2WVo0a6iYh2ZzUuCJJclae5mBtK0H/EychxXg91GB7rGdQ==",
|
|
562
575
|
"license": "Apache-2.0",
|
|
563
576
|
"dependencies": {
|
|
564
577
|
"p-limit": "^7.2.0"
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"mac",
|
|
7
7
|
"XCTest"
|
|
8
8
|
],
|
|
9
|
-
"version": "3.4.
|
|
9
|
+
"version": "3.4.1",
|
|
10
10
|
"author": "Appium Contributors",
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"repository": {
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"@appium/strongbox": "^1.0.0-rc.1",
|
|
50
50
|
"@colors/colors": "^1.6.0",
|
|
51
51
|
"appium-xcode": "^6.0.0",
|
|
52
|
-
"asyncbox": "^6.0
|
|
53
|
-
"axios": "^1.
|
|
54
|
-
"portscanner": "2.2.0",
|
|
52
|
+
"asyncbox": "^6.3.0",
|
|
53
|
+
"axios": "^1.16.0",
|
|
54
|
+
"portscanner": "^2.2.0",
|
|
55
55
|
"teen_process": "^4.0.4"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|