@expo/build-tools 1.0.194 → 1.0.196
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/steps/easFunctions.js +2 -0
- package/dist/steps/easFunctions.js.map +1 -1
- package/dist/steps/functions/internalMaestroTest.d.ts +10 -0
- package/dist/steps/functions/internalMaestroTest.js +385 -0
- package/dist/steps/functions/internalMaestroTest.js.map +1 -0
- package/dist/steps/functions/startAndroidEmulator.js +22 -142
- package/dist/steps/functions/startAndroidEmulator.js.map +1 -1
- package/dist/steps/functions/startIosSimulator.js +35 -72
- package/dist/steps/functions/startIosSimulator.js.map +1 -1
- package/dist/utils/AndroidEmulatorUtils.d.ts +63 -0
- package/dist/utils/AndroidEmulatorUtils.js +260 -0
- package/dist/utils/AndroidEmulatorUtils.js.map +1 -0
- package/dist/utils/IosSimulatorUtils.d.ts +68 -0
- package/dist/utils/IosSimulatorUtils.js +122 -0
- package/dist/utils/IosSimulatorUtils.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AndroidEmulatorUtils = void 0;
|
|
7
|
+
const assert_1 = __importDefault(require("assert"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const promises_1 = require("node:timers/promises");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
// import { PipeMode } from '@expo/logger';
|
|
13
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
14
|
+
const retry_1 = require("./retry");
|
|
15
|
+
var AndroidEmulatorUtils;
|
|
16
|
+
(function (AndroidEmulatorUtils) {
|
|
17
|
+
AndroidEmulatorUtils.defaultSystemImagePackage = `system-images;android-30;default;${process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64'}`;
|
|
18
|
+
async function getAvailableDevicesAsync({ env, }) {
|
|
19
|
+
const result = await (0, turtle_spawn_1.default)('avdmanager', ['list', 'device', '--compact', '--null'], { env });
|
|
20
|
+
return result.stdout.split('\0').filter((line) => line !== '');
|
|
21
|
+
}
|
|
22
|
+
AndroidEmulatorUtils.getAvailableDevicesAsync = getAvailableDevicesAsync;
|
|
23
|
+
async function getAttachedDevicesAsync({ env, }) {
|
|
24
|
+
const result = await (0, turtle_spawn_1.default)('adb', ['devices', '-l'], {
|
|
25
|
+
env,
|
|
26
|
+
});
|
|
27
|
+
return result.stdout
|
|
28
|
+
.replace(/\r\n/g, '\n')
|
|
29
|
+
.split('\n')
|
|
30
|
+
.filter((line) => line.startsWith('emulator'))
|
|
31
|
+
.map((line) => {
|
|
32
|
+
const [serialId, state] = line.split(/\s+/);
|
|
33
|
+
return {
|
|
34
|
+
serialId,
|
|
35
|
+
state,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
AndroidEmulatorUtils.getAttachedDevicesAsync = getAttachedDevicesAsync;
|
|
40
|
+
async function getSerialIdAsync({ deviceName, env, }) {
|
|
41
|
+
const adbDevices = await (0, turtle_spawn_1.default)('adb', ['devices'], { env });
|
|
42
|
+
for (const adbDeviceLine of adbDevices.stdout.split('\n')) {
|
|
43
|
+
if (!adbDeviceLine.startsWith('emulator')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const matches = adbDeviceLine.match(/^(\S+)/);
|
|
47
|
+
if (!matches) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const [, serialId] = matches;
|
|
51
|
+
// Previously we were using `qemu.uuid` to identify the emulator,
|
|
52
|
+
// but this does not work for newer emulators, because there is
|
|
53
|
+
// a limit on properties and custom properties get ignored.
|
|
54
|
+
// See https://stackoverflow.com/questions/2214377/how-to-get-serial-number-or-id-of-android-emulator-after-it-runs#comment98259121_42038655
|
|
55
|
+
const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
|
56
|
+
env,
|
|
57
|
+
});
|
|
58
|
+
if (adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0] === deviceName) {
|
|
59
|
+
return serialId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
AndroidEmulatorUtils.getSerialIdAsync = getSerialIdAsync;
|
|
65
|
+
async function createAsync({ deviceName, systemImagePackage, deviceIdentifier, env, }) {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
const avdManager = (0, turtle_spawn_1.default)('avdmanager', [
|
|
68
|
+
'create',
|
|
69
|
+
'avd',
|
|
70
|
+
'--name',
|
|
71
|
+
deviceName,
|
|
72
|
+
'--package',
|
|
73
|
+
systemImagePackage,
|
|
74
|
+
'--force',
|
|
75
|
+
...(deviceIdentifier ? ['--device', deviceIdentifier] : []),
|
|
76
|
+
], {
|
|
77
|
+
env,
|
|
78
|
+
stdio: 'pipe',
|
|
79
|
+
});
|
|
80
|
+
// `avdmanager create` always asks about creating a custom hardware profile.
|
|
81
|
+
// > Do you wish to create a custom hardware profile? [no]
|
|
82
|
+
// We answer "no".
|
|
83
|
+
(_a = avdManager.child.stdin) === null || _a === void 0 ? void 0 : _a.write('no');
|
|
84
|
+
(_b = avdManager.child.stdin) === null || _b === void 0 ? void 0 : _b.end();
|
|
85
|
+
await avdManager;
|
|
86
|
+
}
|
|
87
|
+
AndroidEmulatorUtils.createAsync = createAsync;
|
|
88
|
+
async function cloneAsync({ sourceDeviceName, destinationDeviceName, env, }) {
|
|
89
|
+
const cloneIniFile = `${env.HOME}/.android/avd/${destinationDeviceName}.ini`;
|
|
90
|
+
await node_fs_1.default.promises.rm(`${env.HOME}/.android/avd/${destinationDeviceName}.avd`, {
|
|
91
|
+
recursive: true,
|
|
92
|
+
force: true,
|
|
93
|
+
});
|
|
94
|
+
await node_fs_1.default.promises.rm(cloneIniFile, { force: true });
|
|
95
|
+
await node_fs_1.default.promises.cp(`${env.HOME}/.android/avd/${sourceDeviceName}.avd`, `${env.HOME}/.android/avd/${destinationDeviceName}.avd`, { recursive: true, verbatimSymlinks: true, force: true });
|
|
96
|
+
await node_fs_1.default.promises.cp(`${env.HOME}/.android/avd/${sourceDeviceName}.ini`, cloneIniFile, {
|
|
97
|
+
verbatimSymlinks: true,
|
|
98
|
+
force: true,
|
|
99
|
+
});
|
|
100
|
+
const filesToReplaceDeviceNameIn = // TODO: Test whether we need to use `spawnAsync` here.
|
|
101
|
+
(await (0, turtle_spawn_1.default)('grep', [
|
|
102
|
+
'--binary-files=without-match',
|
|
103
|
+
'--recursive',
|
|
104
|
+
'--files-with-matches',
|
|
105
|
+
`${sourceDeviceName}`,
|
|
106
|
+
`${env.HOME}/.android/avd/${destinationDeviceName}.avd`,
|
|
107
|
+
])).stdout
|
|
108
|
+
.split('\n')
|
|
109
|
+
.filter((file) => file !== '');
|
|
110
|
+
for (const file of [...filesToReplaceDeviceNameIn, cloneIniFile]) {
|
|
111
|
+
const txtFile = await node_fs_1.default.promises.readFile(file, 'utf-8');
|
|
112
|
+
const replaceRegex = new RegExp(`${sourceDeviceName}`, 'g');
|
|
113
|
+
const updatedTxtFile = txtFile.replace(replaceRegex, destinationDeviceName);
|
|
114
|
+
await node_fs_1.default.promises.writeFile(file, updatedTxtFile);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
AndroidEmulatorUtils.cloneAsync = cloneAsync;
|
|
118
|
+
async function startAsync({ deviceName, env, }) {
|
|
119
|
+
const emulatorPromise = (0, turtle_spawn_1.default)(`${process.env.ANDROID_HOME}/emulator/emulator`, [
|
|
120
|
+
'-no-window',
|
|
121
|
+
'-no-boot-anim',
|
|
122
|
+
'-writable-system',
|
|
123
|
+
'-noaudio',
|
|
124
|
+
'-memory',
|
|
125
|
+
'8192',
|
|
126
|
+
'-no-snapshot-save',
|
|
127
|
+
'-avd',
|
|
128
|
+
deviceName,
|
|
129
|
+
], {
|
|
130
|
+
detached: true,
|
|
131
|
+
stdio: 'inherit',
|
|
132
|
+
env,
|
|
133
|
+
});
|
|
134
|
+
// If emulator fails to start, throw its error.
|
|
135
|
+
if (!emulatorPromise.child.pid) {
|
|
136
|
+
await emulatorPromise;
|
|
137
|
+
}
|
|
138
|
+
emulatorPromise.child.unref();
|
|
139
|
+
const serialId = await (0, retry_1.retryAsync)(async () => {
|
|
140
|
+
const serialId = await getSerialIdAsync({ deviceName, env });
|
|
141
|
+
(0, assert_1.default)(serialId, `Failed to configure emulator (${serialId}): emulator with required ID not found.`);
|
|
142
|
+
return serialId;
|
|
143
|
+
}, {
|
|
144
|
+
retryOptions: {
|
|
145
|
+
retries: 3 * 60,
|
|
146
|
+
retryIntervalMs: 1000,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
// We don't want to await the SpawnPromise here.
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/return-await
|
|
151
|
+
return { emulatorPromise, serialId };
|
|
152
|
+
}
|
|
153
|
+
AndroidEmulatorUtils.startAsync = startAsync;
|
|
154
|
+
async function waitForReadyAsync({ serialId, env, }) {
|
|
155
|
+
await (0, retry_1.retryAsync)(async () => {
|
|
156
|
+
const { stdout } = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'getprop', 'sys.boot_completed'], { env });
|
|
157
|
+
if (!stdout.startsWith('1')) {
|
|
158
|
+
throw new Error(`Emulator (${serialId}) boot has not completed.`);
|
|
159
|
+
}
|
|
160
|
+
}, {
|
|
161
|
+
// Retry every second for 3 minutes.
|
|
162
|
+
retryOptions: {
|
|
163
|
+
retries: 3 * 60,
|
|
164
|
+
retryIntervalMs: 1000,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
AndroidEmulatorUtils.waitForReadyAsync = waitForReadyAsync;
|
|
169
|
+
async function deleteAsync({ serialId, env, }) {
|
|
170
|
+
const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
|
171
|
+
env,
|
|
172
|
+
});
|
|
173
|
+
const deviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
|
|
174
|
+
await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'kill'], { env });
|
|
175
|
+
await (0, retry_1.retryAsync)(async () => {
|
|
176
|
+
const devices = await getAttachedDevicesAsync({ env });
|
|
177
|
+
if (devices.some((device) => device.serialId === serialId)) {
|
|
178
|
+
throw new Error(`Emulator (${serialId}) is still attached.`);
|
|
179
|
+
}
|
|
180
|
+
}, {
|
|
181
|
+
retryOptions: {
|
|
182
|
+
retries: 3 * 60,
|
|
183
|
+
retryIntervalMs: 1000,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
await (0, turtle_spawn_1.default)('avdmanager', ['delete', 'avd', '-n', deviceName], { env });
|
|
187
|
+
}
|
|
188
|
+
AndroidEmulatorUtils.deleteAsync = deleteAsync;
|
|
189
|
+
async function startScreenRecordingAsync({ serialId, env, }) {
|
|
190
|
+
let isReady = false;
|
|
191
|
+
// Ensure /sdcard/ is ready to write to. (If the emulator was just booted, it might not be ready yet.)
|
|
192
|
+
for (let i = 0; i < 30; i++) {
|
|
193
|
+
try {
|
|
194
|
+
await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'touch', '/sdcard/.expo-recording-ready'], {
|
|
195
|
+
env,
|
|
196
|
+
});
|
|
197
|
+
isReady = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
await (0, promises_1.setTimeout)(1000);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!isReady) {
|
|
205
|
+
throw new Error(`Emulator (${serialId}) filesystem was not ready in time.`);
|
|
206
|
+
}
|
|
207
|
+
const screenrecordArgs = [
|
|
208
|
+
'-s',
|
|
209
|
+
serialId,
|
|
210
|
+
'shell',
|
|
211
|
+
'screenrecord',
|
|
212
|
+
'--verbose',
|
|
213
|
+
'/sdcard/expo-recording.mp4',
|
|
214
|
+
];
|
|
215
|
+
const screenrecordHelp = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'screenrecord', '--help'], {
|
|
216
|
+
env,
|
|
217
|
+
});
|
|
218
|
+
if (screenrecordHelp.stdout.includes('remove the time limit')) {
|
|
219
|
+
screenrecordArgs.push('--time-limit', '0');
|
|
220
|
+
}
|
|
221
|
+
const recordingSpawn = (0, turtle_spawn_1.default)('adb', screenrecordArgs, {
|
|
222
|
+
env,
|
|
223
|
+
stdio: 'pipe',
|
|
224
|
+
});
|
|
225
|
+
recordingSpawn.child.unref();
|
|
226
|
+
// We are returning the SpawnPromise here, so we don't await it.
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/return-await
|
|
228
|
+
return {
|
|
229
|
+
recordingSpawn,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
AndroidEmulatorUtils.startScreenRecordingAsync = startScreenRecordingAsync;
|
|
233
|
+
async function stopScreenRecordingAsync({ serialId, recordingSpawn, env, }) {
|
|
234
|
+
recordingSpawn.child.kill(1);
|
|
235
|
+
try {
|
|
236
|
+
await recordingSpawn;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// do nothing
|
|
240
|
+
}
|
|
241
|
+
let isRecordingBusy = true;
|
|
242
|
+
for (let i = 0; i < 30; i++) {
|
|
243
|
+
const lsof = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'lsof -t /sdcard/expo-recording.mp4'], { env });
|
|
244
|
+
if (lsof.stdout.trim() === '') {
|
|
245
|
+
isRecordingBusy = false;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
await (0, promises_1.setTimeout)(1000);
|
|
249
|
+
}
|
|
250
|
+
if (isRecordingBusy) {
|
|
251
|
+
throw new Error(`Recording file is busy.`);
|
|
252
|
+
}
|
|
253
|
+
const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'android-screen-recording-'));
|
|
254
|
+
const outputPath = node_path_1.default.join(outputDir, `${serialId}.mp4`);
|
|
255
|
+
await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'pull', '/sdcard/expo-recording.mp4', outputPath], { env });
|
|
256
|
+
return { outputPath };
|
|
257
|
+
}
|
|
258
|
+
AndroidEmulatorUtils.stopScreenRecordingAsync = stopScreenRecordingAsync;
|
|
259
|
+
})(AndroidEmulatorUtils || (exports.AndroidEmulatorUtils = AndroidEmulatorUtils = {}));
|
|
260
|
+
//# sourceMappingURL=AndroidEmulatorUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AndroidEmulatorUtils.js","sourceRoot":"","sources":["../../src/utils/AndroidEmulatorUtils.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from 'assert';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport { setTimeout } from 'node:timers/promises';\nimport path from 'node:path';\n\n// import { PipeMode } from '@expo/logger';\nimport spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';\nimport { z } from 'zod';\n\nimport { retryAsync } from './retry';\n\n/** Android Virtual Device is the device we run. */\nexport type AndroidVirtualDeviceName = string & z.BRAND<'AndroidVirtualDeviceName'>;\n/** Android device is configuration for the AVD -- screen size, etc. */\nexport type AndroidDeviceName = string & z.BRAND<'AndroidDeviceName'>;\nexport type AndroidDeviceSerialId = string & z.BRAND<'AndroidDeviceSerialId'>;\n\nexport namespace AndroidEmulatorUtils {\n export const defaultSystemImagePackage = `system-images;android-30;default;${\n process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64'\n }`;\n\n export async function getAvailableDevicesAsync({\n env,\n }: {\n env: NodeJS.ProcessEnv;\n }): Promise<AndroidDeviceName[]> {\n const result = await spawn('avdmanager', ['list', 'device', '--compact', '--null'], { env });\n return result.stdout.split('\\0').filter((line) => line !== '') as AndroidDeviceName[];\n }\n\n export async function getAttachedDevicesAsync({\n env,\n }: {\n env: NodeJS.ProcessEnv;\n }): Promise<{ serialId: AndroidDeviceSerialId; state: 'offline' | 'device' }[]> {\n const result = await spawn('adb', ['devices', '-l'], {\n env,\n });\n return result.stdout\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .filter((line) => line.startsWith('emulator'))\n .map((line) => {\n const [serialId, state] = line.split(/\\s+/) as [\n AndroidDeviceSerialId,\n 'offline' | 'device',\n ];\n return {\n serialId,\n state,\n };\n });\n }\n\n export async function getSerialIdAsync({\n deviceName,\n env,\n }: {\n deviceName: AndroidVirtualDeviceName;\n env: NodeJS.ProcessEnv;\n }): Promise<AndroidDeviceSerialId | null> {\n const adbDevices = await spawn('adb', ['devices'], { env });\n for (const adbDeviceLine of adbDevices.stdout.split('\\n')) {\n if (!adbDeviceLine.startsWith('emulator')) {\n continue;\n }\n\n const matches = adbDeviceLine.match(/^(\\S+)/);\n if (!matches) {\n continue;\n }\n\n const [, serialId] = matches;\n // Previously we were using `qemu.uuid` to identify the emulator,\n // but this does not work for newer emulators, because there is\n // a limit on properties and custom properties get ignored.\n // See https://stackoverflow.com/questions/2214377/how-to-get-serial-number-or-id-of-android-emulator-after-it-runs#comment98259121_42038655\n const adbEmuAvdName = await spawn('adb', ['-s', serialId, 'emu', 'avd', 'name'], {\n env,\n });\n if (adbEmuAvdName.stdout.replace(/\\r\\n/g, '\\n').split('\\n')[0] === deviceName) {\n return serialId as AndroidDeviceSerialId;\n }\n }\n\n return null;\n }\n\n export async function createAsync({\n deviceName,\n systemImagePackage,\n deviceIdentifier,\n env,\n }: {\n deviceName: AndroidVirtualDeviceName;\n systemImagePackage: string;\n deviceIdentifier: AndroidDeviceName | null;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n const avdManager = spawn(\n 'avdmanager',\n [\n 'create',\n 'avd',\n '--name',\n deviceName,\n '--package',\n systemImagePackage,\n '--force',\n ...(deviceIdentifier ? ['--device', deviceIdentifier] : []),\n ],\n {\n env,\n stdio: 'pipe',\n }\n );\n // `avdmanager create` always asks about creating a custom hardware profile.\n // > Do you wish to create a custom hardware profile? [no]\n // We answer \"no\".\n avdManager.child.stdin?.write('no');\n avdManager.child.stdin?.end();\n await avdManager;\n }\n\n export async function cloneAsync({\n sourceDeviceName,\n destinationDeviceName,\n env,\n }: {\n sourceDeviceName: AndroidVirtualDeviceName;\n destinationDeviceName: AndroidVirtualDeviceName;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n const cloneIniFile = `${env.HOME}/.android/avd/${destinationDeviceName}.ini`;\n\n await fs.promises.rm(`${env.HOME}/.android/avd/${destinationDeviceName}.avd`, {\n recursive: true,\n force: true,\n });\n await fs.promises.rm(cloneIniFile, { force: true });\n\n await fs.promises.cp(\n `${env.HOME}/.android/avd/${sourceDeviceName}.avd`,\n `${env.HOME}/.android/avd/${destinationDeviceName}.avd`,\n { recursive: true, verbatimSymlinks: true, force: true }\n );\n\n await fs.promises.cp(`${env.HOME}/.android/avd/${sourceDeviceName}.ini`, cloneIniFile, {\n verbatimSymlinks: true,\n force: true,\n });\n\n const filesToReplaceDeviceNameIn = // TODO: Test whether we need to use `spawnAsync` here.\n (\n await spawn('grep', [\n '--binary-files=without-match',\n '--recursive',\n '--files-with-matches',\n `${sourceDeviceName}`,\n `${env.HOME}/.android/avd/${destinationDeviceName}.avd`,\n ])\n ).stdout\n .split('\\n')\n .filter((file) => file !== '');\n\n for (const file of [...filesToReplaceDeviceNameIn, cloneIniFile]) {\n const txtFile = await fs.promises.readFile(file, 'utf-8');\n const replaceRegex = new RegExp(`${sourceDeviceName}`, 'g');\n const updatedTxtFile = txtFile.replace(replaceRegex, destinationDeviceName);\n await fs.promises.writeFile(file, updatedTxtFile);\n }\n }\n\n export async function startAsync({\n deviceName,\n env,\n }: {\n deviceName: AndroidVirtualDeviceName;\n env: NodeJS.ProcessEnv;\n }): Promise<{ emulatorPromise: SpawnPromise<SpawnResult>; serialId: AndroidDeviceSerialId }> {\n const emulatorPromise = spawn(\n `${process.env.ANDROID_HOME}/emulator/emulator`,\n [\n '-no-window',\n '-no-boot-anim',\n '-writable-system',\n '-noaudio',\n '-memory',\n '8192',\n '-no-snapshot-save',\n '-avd',\n deviceName,\n ],\n {\n detached: true,\n stdio: 'inherit',\n env,\n }\n );\n // If emulator fails to start, throw its error.\n if (!emulatorPromise.child.pid) {\n await emulatorPromise;\n }\n emulatorPromise.child.unref();\n\n const serialId = await retryAsync(\n async () => {\n const serialId = await getSerialIdAsync({ deviceName, env });\n assert(\n serialId,\n `Failed to configure emulator (${serialId}): emulator with required ID not found.`\n );\n return serialId;\n },\n {\n retryOptions: {\n retries: 3 * 60,\n retryIntervalMs: 1_000,\n },\n }\n );\n\n // We don't want to await the SpawnPromise here.\n // eslint-disable-next-line @typescript-eslint/return-await\n return { emulatorPromise, serialId };\n }\n\n export async function waitForReadyAsync({\n serialId,\n env,\n }: {\n serialId: AndroidDeviceSerialId;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n await retryAsync(\n async () => {\n const { stdout } = await spawn(\n 'adb',\n ['-s', serialId, 'shell', 'getprop', 'sys.boot_completed'],\n { env }\n );\n\n if (!stdout.startsWith('1')) {\n throw new Error(`Emulator (${serialId}) boot has not completed.`);\n }\n },\n {\n // Retry every second for 3 minutes.\n retryOptions: {\n retries: 3 * 60,\n retryIntervalMs: 1_000,\n },\n }\n );\n }\n\n export async function deleteAsync({\n serialId,\n env,\n }: {\n serialId: AndroidDeviceSerialId;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n const adbEmuAvdName = await spawn('adb', ['-s', serialId, 'emu', 'avd', 'name'], {\n env,\n });\n const deviceName = adbEmuAvdName.stdout.replace(/\\r\\n/g, '\\n').split('\\n')[0];\n\n await spawn('adb', ['-s', serialId, 'emu', 'kill'], { env });\n\n await retryAsync(\n async () => {\n const devices = await getAttachedDevicesAsync({ env });\n if (devices.some((device) => device.serialId === serialId)) {\n throw new Error(`Emulator (${serialId}) is still attached.`);\n }\n },\n {\n retryOptions: {\n retries: 3 * 60,\n retryIntervalMs: 1_000,\n },\n }\n );\n\n await spawn('avdmanager', ['delete', 'avd', '-n', deviceName], { env });\n }\n\n export async function startScreenRecordingAsync({\n serialId,\n env,\n }: {\n serialId: AndroidDeviceSerialId;\n env: NodeJS.ProcessEnv;\n }): Promise<{\n recordingSpawn: SpawnPromise<SpawnResult>;\n }> {\n let isReady = false;\n\n // Ensure /sdcard/ is ready to write to. (If the emulator was just booted, it might not be ready yet.)\n for (let i = 0; i < 30; i++) {\n try {\n await spawn('adb', ['-s', serialId, 'shell', 'touch', '/sdcard/.expo-recording-ready'], {\n env,\n });\n isReady = true;\n break;\n } catch {\n await setTimeout(1000);\n }\n }\n\n if (!isReady) {\n throw new Error(`Emulator (${serialId}) filesystem was not ready in time.`);\n }\n\n const screenrecordArgs = [\n '-s',\n serialId,\n 'shell',\n 'screenrecord',\n '--verbose',\n '/sdcard/expo-recording.mp4',\n ];\n\n const screenrecordHelp = await spawn(\n 'adb',\n ['-s', serialId, 'shell', 'screenrecord', '--help'],\n {\n env,\n }\n );\n\n if (screenrecordHelp.stdout.includes('remove the time limit')) {\n screenrecordArgs.push('--time-limit', '0');\n }\n\n const recordingSpawn = spawn('adb', screenrecordArgs, {\n env,\n stdio: 'pipe',\n });\n recordingSpawn.child.unref();\n\n // We are returning the SpawnPromise here, so we don't await it.\n // eslint-disable-next-line @typescript-eslint/return-await\n return {\n recordingSpawn,\n };\n }\n\n export async function stopScreenRecordingAsync({\n serialId,\n recordingSpawn,\n env,\n }: {\n serialId: AndroidDeviceSerialId;\n recordingSpawn: SpawnPromise<SpawnResult>;\n env: NodeJS.ProcessEnv;\n }): Promise<{ outputPath: string }> {\n recordingSpawn.child.kill(1);\n\n try {\n await recordingSpawn;\n } catch {\n // do nothing\n }\n\n let isRecordingBusy = true;\n for (let i = 0; i < 30; i++) {\n const lsof = await spawn(\n 'adb',\n ['-s', serialId, 'shell', 'lsof -t /sdcard/expo-recording.mp4'],\n { env }\n );\n if (lsof.stdout.trim() === '') {\n isRecordingBusy = false;\n break;\n }\n await setTimeout(1000);\n }\n\n if (isRecordingBusy) {\n throw new Error(`Recording file is busy.`);\n }\n\n const outputDir = await fs.promises.mkdtemp(\n path.join(os.tmpdir(), 'android-screen-recording-')\n );\n const outputPath = path.join(outputDir, `${serialId}.mp4`);\n\n await spawn('adb', ['-s', serialId, 'pull', '/sdcard/expo-recording.mp4', outputPath], { env });\n\n return { outputPath };\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export type IosSimulatorUuid = string & z.BRAND<'IosSimulatorUuid'>;
|
|
5
|
+
export type IosSimulatorName = string & z.BRAND<'IosSimulatorName'>;
|
|
6
|
+
export declare namespace IosSimulatorUtils {
|
|
7
|
+
type XcrunSimctlDevice = {
|
|
8
|
+
availabilityError?: string;
|
|
9
|
+
/** e.g. /Users/sjchmiela/Library/Developer/CoreSimulator/Devices/8272DEB1-42B5-4F78-AB2D-0BC5F320B822/data */
|
|
10
|
+
dataPath: string;
|
|
11
|
+
/** e.g. 18341888 */
|
|
12
|
+
dataPathSize: number;
|
|
13
|
+
/** e.g. /Users/sjchmiela/Library/Logs/CoreSimulator/8272DEB1-42B5-4F78-AB2D-0BC5F320B822 */
|
|
14
|
+
logPath: string;
|
|
15
|
+
/** e.g. 8272DEB1-42B5-4F78-AB2D-0BC5F320B822 */
|
|
16
|
+
udid: IosSimulatorUuid;
|
|
17
|
+
isAvailable: boolean;
|
|
18
|
+
/** e.g. com.apple.CoreSimulator.SimDeviceType.iPhone-13-mini */
|
|
19
|
+
deviceTypeIdentifier: string;
|
|
20
|
+
state: 'Shutdown' | 'Booted';
|
|
21
|
+
/** e.g. iPhone 15 */
|
|
22
|
+
name: IosSimulatorName;
|
|
23
|
+
/** e.g. 2024-01-22T19:28:56Z */
|
|
24
|
+
lastBootedAt?: string;
|
|
25
|
+
};
|
|
26
|
+
type SimulatorDevice = XcrunSimctlDevice & {
|
|
27
|
+
runtime: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
};
|
|
30
|
+
export function getAvailableDevicesAsync({ env, filter, }: {
|
|
31
|
+
env: NodeJS.ProcessEnv;
|
|
32
|
+
filter: 'available' | 'booted';
|
|
33
|
+
}): Promise<SimulatorDevice[]>;
|
|
34
|
+
export function getDeviceAsync({ udid, env, }: {
|
|
35
|
+
env: NodeJS.ProcessEnv;
|
|
36
|
+
udid: IosSimulatorUuid;
|
|
37
|
+
}): Promise<SimulatorDevice | null>;
|
|
38
|
+
export function cloneAsync({ sourceDeviceIdentifier, destinationDeviceName, env, }: {
|
|
39
|
+
sourceDeviceIdentifier: IosSimulatorName | IosSimulatorUuid;
|
|
40
|
+
destinationDeviceName: IosSimulatorName;
|
|
41
|
+
env: NodeJS.ProcessEnv;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
export function startAsync({ deviceIdentifier, env, }: {
|
|
44
|
+
deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
|
|
45
|
+
env: NodeJS.ProcessEnv;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
udid: IosSimulatorUuid;
|
|
48
|
+
}>;
|
|
49
|
+
export function waitForReadyAsync({ udid, env, }: {
|
|
50
|
+
udid: IosSimulatorUuid;
|
|
51
|
+
env: NodeJS.ProcessEnv;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
export function deleteAsync({ udid, env, }: {
|
|
54
|
+
udid: IosSimulatorUuid;
|
|
55
|
+
env: NodeJS.ProcessEnv;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
export function startScreenRecordingAsync({ udid, env, }: {
|
|
58
|
+
udid: IosSimulatorUuid;
|
|
59
|
+
env: NodeJS.ProcessEnv;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
recordingSpawn: SpawnPromise<SpawnResult>;
|
|
62
|
+
outputPath: string;
|
|
63
|
+
}>;
|
|
64
|
+
export function stopScreenRecordingAsync({ recordingSpawn, }: {
|
|
65
|
+
recordingSpawn: SpawnPromise<SpawnResult>;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
export {};
|
|
68
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.IosSimulatorUtils = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const promises_1 = require("node:timers/promises");
|
|
11
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
12
|
+
const retry_1 = require("./retry");
|
|
13
|
+
var IosSimulatorUtils;
|
|
14
|
+
(function (IosSimulatorUtils) {
|
|
15
|
+
async function getAvailableDevicesAsync({ env, filter, }) {
|
|
16
|
+
const result = await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'list', 'devices', '--json', '--no-escape-slashes', filter], { env });
|
|
17
|
+
const xcrunData = JSON.parse(result.stdout);
|
|
18
|
+
const allAvailableDevices = [];
|
|
19
|
+
for (const [runtime, devices] of Object.entries(xcrunData.devices)) {
|
|
20
|
+
allAvailableDevices.push(...devices.map((device) => ({
|
|
21
|
+
...device,
|
|
22
|
+
runtime,
|
|
23
|
+
displayName: `${device.name} (${device.udid}) on ${runtime}`,
|
|
24
|
+
})));
|
|
25
|
+
}
|
|
26
|
+
return allAvailableDevices;
|
|
27
|
+
}
|
|
28
|
+
IosSimulatorUtils.getAvailableDevicesAsync = getAvailableDevicesAsync;
|
|
29
|
+
async function getDeviceAsync({ udid, env, }) {
|
|
30
|
+
var _a;
|
|
31
|
+
const devices = await getAvailableDevicesAsync({ env, filter: 'available' });
|
|
32
|
+
return (_a = devices.find((device) => device.udid === udid)) !== null && _a !== void 0 ? _a : null;
|
|
33
|
+
}
|
|
34
|
+
IosSimulatorUtils.getDeviceAsync = getDeviceAsync;
|
|
35
|
+
async function cloneAsync({ sourceDeviceIdentifier, destinationDeviceName, env, }) {
|
|
36
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'clone', sourceDeviceIdentifier, destinationDeviceName], {
|
|
37
|
+
env,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
IosSimulatorUtils.cloneAsync = cloneAsync;
|
|
41
|
+
async function startAsync({ deviceIdentifier, env, }) {
|
|
42
|
+
const bootstatusResult = await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'bootstatus', deviceIdentifier, '-b'], {
|
|
43
|
+
env,
|
|
44
|
+
});
|
|
45
|
+
const udid = parseUdidFromBootstatusStdout(bootstatusResult.stdout);
|
|
46
|
+
if (!udid) {
|
|
47
|
+
throw new Error('Failed to parse UDID from bootstatus result.');
|
|
48
|
+
}
|
|
49
|
+
return { udid };
|
|
50
|
+
}
|
|
51
|
+
IosSimulatorUtils.startAsync = startAsync;
|
|
52
|
+
async function waitForReadyAsync({ udid, env, }) {
|
|
53
|
+
await (0, retry_1.retryAsync)(async () => {
|
|
54
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'screenshot', '/dev/null'], {
|
|
55
|
+
env,
|
|
56
|
+
});
|
|
57
|
+
}, {
|
|
58
|
+
retryOptions: {
|
|
59
|
+
// There's 30 * 60 seconds in 30 minutes, which is the timeout.
|
|
60
|
+
retries: 30 * 60,
|
|
61
|
+
retryIntervalMs: 1000,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
IosSimulatorUtils.waitForReadyAsync = waitForReadyAsync;
|
|
66
|
+
async function deleteAsync({ udid, env, }) {
|
|
67
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'shutdown', udid], { env });
|
|
68
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'delete', udid], { env });
|
|
69
|
+
}
|
|
70
|
+
IosSimulatorUtils.deleteAsync = deleteAsync;
|
|
71
|
+
async function startScreenRecordingAsync({ udid, env, }) {
|
|
72
|
+
const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-screen-recording-'));
|
|
73
|
+
const outputPath = node_path_1.default.join(outputDir, `${udid}.mov`);
|
|
74
|
+
const recordingSpawn = (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'recordVideo', '-f', outputPath], {
|
|
75
|
+
env,
|
|
76
|
+
});
|
|
77
|
+
const stdout = recordingSpawn.child.stdout;
|
|
78
|
+
const stderr = recordingSpawn.child.stderr;
|
|
79
|
+
if (!stdout || !stderr) {
|
|
80
|
+
// No stdout/stderr means the process failed to start, so awaiting it will throw an error.
|
|
81
|
+
await recordingSpawn;
|
|
82
|
+
throw new Error('Recording process failed to start.');
|
|
83
|
+
}
|
|
84
|
+
let outputAggregated = '';
|
|
85
|
+
// Listen to both stdout and stderr since "Recording started" might come from either
|
|
86
|
+
stdout.on('data', (data) => {
|
|
87
|
+
const output = data.toString();
|
|
88
|
+
outputAggregated += output;
|
|
89
|
+
});
|
|
90
|
+
stderr.on('data', (data) => {
|
|
91
|
+
const output = data.toString();
|
|
92
|
+
outputAggregated += output;
|
|
93
|
+
});
|
|
94
|
+
let isRecordingStarted = false;
|
|
95
|
+
for (let i = 0; i < 20; i++) {
|
|
96
|
+
// Check if recording started message appears in either stdout or stderr
|
|
97
|
+
if (outputAggregated.includes('Recording started')) {
|
|
98
|
+
isRecordingStarted = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
await (0, promises_1.setTimeout)(1000);
|
|
102
|
+
}
|
|
103
|
+
if (!isRecordingStarted) {
|
|
104
|
+
throw new Error('Recording not started in time.');
|
|
105
|
+
}
|
|
106
|
+
return { recordingSpawn, outputPath };
|
|
107
|
+
}
|
|
108
|
+
IosSimulatorUtils.startScreenRecordingAsync = startScreenRecordingAsync;
|
|
109
|
+
async function stopScreenRecordingAsync({ recordingSpawn, }) {
|
|
110
|
+
recordingSpawn.child.kill(2);
|
|
111
|
+
await recordingSpawn;
|
|
112
|
+
}
|
|
113
|
+
IosSimulatorUtils.stopScreenRecordingAsync = stopScreenRecordingAsync;
|
|
114
|
+
})(IosSimulatorUtils || (exports.IosSimulatorUtils = IosSimulatorUtils = {}));
|
|
115
|
+
function parseUdidFromBootstatusStdout(stdout) {
|
|
116
|
+
const matches = stdout.match(/^Monitoring boot status for .+ \((.+)\)\.$/m);
|
|
117
|
+
if (!matches) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return matches[1];
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=IosSimulatorUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IosSimulatorUtils.js","sourceRoot":"","sources":["../../src/utils/IosSimulatorUtils.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport { setTimeout } from 'node:timers/promises';\n\nimport spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';\nimport { z } from 'zod';\n\nimport { retryAsync } from './retry';\n\nexport type IosSimulatorUuid = string & z.BRAND<'IosSimulatorUuid'>;\nexport type IosSimulatorName = string & z.BRAND<'IosSimulatorName'>;\n\nexport namespace IosSimulatorUtils {\n type XcrunSimctlDevice = {\n availabilityError?: string;\n /** e.g. /Users/sjchmiela/Library/Developer/CoreSimulator/Devices/8272DEB1-42B5-4F78-AB2D-0BC5F320B822/data */\n dataPath: string;\n /** e.g. 18341888 */\n dataPathSize: number;\n /** e.g. /Users/sjchmiela/Library/Logs/CoreSimulator/8272DEB1-42B5-4F78-AB2D-0BC5F320B822 */\n logPath: string;\n /** e.g. 8272DEB1-42B5-4F78-AB2D-0BC5F320B822 */\n udid: IosSimulatorUuid;\n isAvailable: boolean;\n /** e.g. com.apple.CoreSimulator.SimDeviceType.iPhone-13-mini */\n deviceTypeIdentifier: string;\n state: 'Shutdown' | 'Booted';\n /** e.g. iPhone 15 */\n name: IosSimulatorName;\n /** e.g. 2024-01-22T19:28:56Z */\n lastBootedAt?: string;\n };\n\n type SimulatorDevice = XcrunSimctlDevice & { runtime: string; displayName: string };\n\n type XcrunSimctlListDevicesJsonOutput = {\n devices: {\n [runtime: string]: XcrunSimctlDevice[];\n };\n };\n\n export async function getAvailableDevicesAsync({\n env,\n filter,\n }: {\n env: NodeJS.ProcessEnv;\n filter: 'available' | 'booted';\n }): Promise<SimulatorDevice[]> {\n const result = await spawn(\n 'xcrun',\n ['simctl', 'list', 'devices', '--json', '--no-escape-slashes', filter],\n { env }\n );\n const xcrunData = JSON.parse(result.stdout) as XcrunSimctlListDevicesJsonOutput;\n\n const allAvailableDevices: SimulatorDevice[] = [];\n for (const [runtime, devices] of Object.entries(xcrunData.devices)) {\n allAvailableDevices.push(\n ...devices.map((device) => ({\n ...device,\n runtime,\n displayName: `${device.name} (${device.udid}) on ${runtime}`,\n }))\n );\n }\n\n return allAvailableDevices;\n }\n\n export async function getDeviceAsync({\n udid,\n env,\n }: {\n env: NodeJS.ProcessEnv;\n udid: IosSimulatorUuid;\n }): Promise<SimulatorDevice | null> {\n const devices = await getAvailableDevicesAsync({ env, filter: 'available' });\n return devices.find((device) => device.udid === udid) ?? null;\n }\n\n export async function cloneAsync({\n sourceDeviceIdentifier,\n destinationDeviceName,\n env,\n }: {\n sourceDeviceIdentifier: IosSimulatorName | IosSimulatorUuid;\n destinationDeviceName: IosSimulatorName;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n await spawn('xcrun', ['simctl', 'clone', sourceDeviceIdentifier, destinationDeviceName], {\n env,\n });\n }\n\n export async function startAsync({\n deviceIdentifier,\n env,\n }: {\n deviceIdentifier: IosSimulatorUuid | IosSimulatorName;\n env: NodeJS.ProcessEnv;\n }): Promise<{ udid: IosSimulatorUuid }> {\n const bootstatusResult = await spawn(\n 'xcrun',\n ['simctl', 'bootstatus', deviceIdentifier, '-b'],\n {\n env,\n }\n );\n\n const udid = parseUdidFromBootstatusStdout(bootstatusResult.stdout);\n if (!udid) {\n throw new Error('Failed to parse UDID from bootstatus result.');\n }\n\n return { udid };\n }\n\n export async function waitForReadyAsync({\n udid,\n env,\n }: {\n udid: IosSimulatorUuid;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n await retryAsync(\n async () => {\n await spawn('xcrun', ['simctl', 'io', udid, 'screenshot', '/dev/null'], {\n env,\n });\n },\n {\n retryOptions: {\n // There's 30 * 60 seconds in 30 minutes, which is the timeout.\n retries: 30 * 60,\n retryIntervalMs: 1_000,\n },\n }\n );\n }\n\n export async function deleteAsync({\n udid,\n env,\n }: {\n udid: IosSimulatorUuid;\n env: NodeJS.ProcessEnv;\n }): Promise<void> {\n await spawn('xcrun', ['simctl', 'shutdown', udid], { env });\n await spawn('xcrun', ['simctl', 'delete', udid], { env });\n }\n\n export async function startScreenRecordingAsync({\n udid,\n env,\n }: {\n udid: IosSimulatorUuid;\n env: NodeJS.ProcessEnv;\n }): Promise<{\n recordingSpawn: SpawnPromise<SpawnResult>;\n outputPath: string;\n }> {\n const outputDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ios-screen-recording-'));\n const outputPath = path.join(outputDir, `${udid}.mov`);\n const recordingSpawn = spawn('xcrun', ['simctl', 'io', udid, 'recordVideo', '-f', outputPath], {\n env,\n });\n\n const stdout = recordingSpawn.child.stdout;\n const stderr = recordingSpawn.child.stderr;\n if (!stdout || !stderr) {\n // No stdout/stderr means the process failed to start, so awaiting it will throw an error.\n await recordingSpawn;\n throw new Error('Recording process failed to start.');\n }\n\n let outputAggregated = '';\n\n // Listen to both stdout and stderr since \"Recording started\" might come from either\n stdout.on('data', (data) => {\n const output = data.toString();\n outputAggregated += output;\n });\n\n stderr.on('data', (data) => {\n const output = data.toString();\n outputAggregated += output;\n });\n\n let isRecordingStarted = false;\n for (let i = 0; i < 20; i++) {\n // Check if recording started message appears in either stdout or stderr\n if (outputAggregated.includes('Recording started')) {\n isRecordingStarted = true;\n break;\n }\n await setTimeout(1000);\n }\n\n if (!isRecordingStarted) {\n throw new Error('Recording not started in time.');\n }\n\n return { recordingSpawn, outputPath };\n }\n\n export async function stopScreenRecordingAsync({\n recordingSpawn,\n }: {\n recordingSpawn: SpawnPromise<SpawnResult>;\n }): Promise<void> {\n recordingSpawn.child.kill(2);\n await recordingSpawn;\n }\n}\n\nfunction parseUdidFromBootstatusStdout(stdout: string): IosSimulatorUuid | null {\n const matches = stdout.match(/^Monitoring boot status for .+ \\((.+)\\)\\.$/m);\n if (!matches) {\n return null;\n }\n return matches[1] as IosSimulatorUuid;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.196",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
"watch": "tsc --watch --preserveWatchOutput",
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"prepack": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"jest-unit": "jest --config jest/unit-config.ts",
|
|
18
|
+
"jest-integration": "jest --config jest/integration-config.ts",
|
|
19
|
+
"jest-unit-watch": "jest --config jest/unit-config.ts --watch",
|
|
20
|
+
"jest-integration-watch": "jest --config jest/integration-config.ts --watch",
|
|
19
21
|
"clean": "rm -rf node_modules dist coverage"
|
|
20
22
|
},
|
|
21
23
|
"author": "Expo <support@expo.io>",
|
|
@@ -76,5 +78,5 @@
|
|
|
76
78
|
"node": "20.14.0",
|
|
77
79
|
"yarn": "1.22.21"
|
|
78
80
|
},
|
|
79
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "2ff30a5e119b024e70975114f884a714647ecaf2"
|
|
80
82
|
}
|