@expo/build-tools 21.8.0 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/builders/common.js +4 -0
  2. package/dist/builders/ios.js +11 -6
  3. package/dist/steps/easFunctions.js +2 -0
  4. package/dist/steps/functions/installMaestro.d.ts +6 -1
  5. package/dist/steps/functions/installMaestro.js +181 -55
  6. package/dist/steps/functions/maestroBackend.d.ts +11 -0
  7. package/dist/steps/functions/maestroBackend.js +14 -0
  8. package/dist/steps/functions/maestroResultParser.d.ts +30 -0
  9. package/dist/steps/functions/maestroResultParser.js +78 -5
  10. package/dist/steps/functions/maestroScreenshots.d.ts +6 -0
  11. package/dist/steps/functions/maestroScreenshots.js +123 -0
  12. package/dist/steps/functions/maestroTests.js +187 -69
  13. package/dist/steps/functions/restoreBuildCache.js +2 -1
  14. package/dist/steps/functions/saveBuildCache.js +4 -2
  15. package/dist/steps/functions/saveCache.js +3 -0
  16. package/dist/steps/functions/startAgentDeviceRemoteSession.js +7 -0
  17. package/dist/steps/functions/startAppiumRemoteSession.d.ts +17 -0
  18. package/dist/steps/functions/startAppiumRemoteSession.js +244 -0
  19. package/dist/steps/functions/startArgentRemoteSession.js +7 -0
  20. package/dist/steps/functions/startServeSimRemoteSession.js +10 -1
  21. package/dist/steps/utils/appiumEvents.d.ts +10 -0
  22. package/dist/steps/utils/appiumEvents.js +175 -0
  23. package/dist/steps/utils/remoteDeviceRunSession.d.ts +2 -1
  24. package/dist/steps/utils/remoteDeviceRunSession.js +72 -46
  25. package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
  26. package/dist/utils/AndroidEmulatorUtils.js +46 -4
  27. package/dist/utils/sourceMaps.d.ts +5 -0
  28. package/dist/utils/sourceMaps.js +105 -0
  29. package/package.json +8 -8
@@ -0,0 +1,244 @@
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.createStartAppiumRemoteSessionBuildFunction = createStartAppiumRemoteSessionBuildFunction;
7
+ exports.resolveAppium3VersionSpec = resolveAppium3VersionSpec;
8
+ exports.resolveAppiumDeviceAsync = resolveAppiumDeviceAsync;
9
+ const eas_build_job_1 = require("@expo/eas-build-job");
10
+ const steps_1 = require("@expo/steps");
11
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_os_1 = __importDefault(require("node:os"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const semver_1 = __importDefault(require("semver"));
16
+ const zod_1 = require("zod");
17
+ const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
18
+ const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
19
+ const retry_1 = require("../../utils/retry");
20
+ const turtleFetch_1 = require("../../utils/turtleFetch");
21
+ const appiumEvents_1 = require("../utils/appiumEvents");
22
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
23
+ const APPIUM_HOST = '127.0.0.1';
24
+ const APPIUM_PORT = 4723;
25
+ const APPIUM_STARTUP_TIMEOUT_MS = 120_000;
26
+ const DEFAULT_APPIUM_VERSION = '^3';
27
+ const AppiumInstalledDriversSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.object({ installed: zod_1.z.boolean().optional() }).passthrough());
28
+ function createStartAppiumRemoteSessionBuildFunction(ctx) {
29
+ return new steps_1.BuildFunction({
30
+ namespace: 'eas',
31
+ id: 'start_appium_remote_session',
32
+ name: 'Start Appium remote session',
33
+ __metricsId: 'eas/start_appium_remote_session',
34
+ inputProviders: [
35
+ steps_1.BuildStepInput.createProvider({
36
+ id: 'package_version',
37
+ required: false,
38
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
39
+ }),
40
+ steps_1.BuildStepInput.createProvider({
41
+ id: 'max_idle_time_minutes',
42
+ required: false,
43
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
+ }),
45
+ ],
46
+ fn: async ({ logger, global }, { inputs, env, signal }) => {
47
+ const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
48
+ const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
49
+ const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
50
+ const packageVersion = inputs.package_version.value;
51
+ const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
52
+ const { runtimePlatform } = global;
53
+ const versionSpec = resolveAppium3VersionSpec(packageVersion);
54
+ logger.info(`Starting Appium remote session (version: ${versionSpec}, runtime: ${runtimePlatform}).`);
55
+ const device = await resolveAppiumDeviceAsync({ runtimePlatform, env, logger });
56
+ const { appiumHome, appiumBinPath, appiumEnv } = await installAppiumAsync({
57
+ versionSpec,
58
+ driverName: device.driverName,
59
+ env,
60
+ logger,
61
+ });
62
+ const appiumProcess = (0, remoteDeviceRunSession_1.spawnDetached)({
63
+ command: appiumBinPath,
64
+ args: [
65
+ '--address',
66
+ APPIUM_HOST,
67
+ '--port',
68
+ String(APPIUM_PORT),
69
+ '--base-path',
70
+ '/',
71
+ '--log-level',
72
+ 'error',
73
+ // Appium 3 gates session listing (GET /appium/sessions) behind the
74
+ // session_discovery insecure feature. We rely on it to poll for
75
+ // Appium Event Timings, so enable it for all drivers.
76
+ '--allow-insecure',
77
+ '*:session_discovery',
78
+ '--default-capabilities',
79
+ JSON.stringify({ 'appium:eventTimings': true }),
80
+ ],
81
+ env: appiumEnv,
82
+ });
83
+ try {
84
+ await waitForAppiumReadyAsync({ appiumProcess, logger });
85
+ }
86
+ catch (error) {
87
+ await appiumProcess.stopAsync();
88
+ await node_fs_1.default.promises.rm(appiumHome, { recursive: true, force: true });
89
+ throw error;
90
+ }
91
+ const eventCollection = await (0, appiumEvents_1.startAppiumEventCollectionAsync)({
92
+ ctx,
93
+ deviceRunSessionId,
94
+ appiumUrl: `http://${APPIUM_HOST}:${APPIUM_PORT}/`,
95
+ logger,
96
+ });
97
+ let appiumTunnel;
98
+ let serveSim;
99
+ try {
100
+ appiumTunnel = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
101
+ port: APPIUM_PORT,
102
+ subdomainPrefix: 'appium',
103
+ baseDomain: ngrokTunnelDomain,
104
+ authtoken: ngrokAuthtoken,
105
+ logger,
106
+ });
107
+ switch (runtimePlatform) {
108
+ case steps_1.BuildRuntimePlatform.DARWIN:
109
+ serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
110
+ baseDomain: ngrokTunnelDomain,
111
+ env,
112
+ logger,
113
+ timeoutMs: APPIUM_STARTUP_TIMEOUT_MS,
114
+ });
115
+ break;
116
+ case steps_1.BuildRuntimePlatform.LINUX:
117
+ break;
118
+ }
119
+ await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
120
+ ctx,
121
+ deviceRunSessionId,
122
+ remoteConfig: {
123
+ appiumUrl: appiumTunnel.url,
124
+ capabilities: {
125
+ platformName: device.platformName,
126
+ 'appium:automationName': device.automationName,
127
+ 'appium:udid': device.udid,
128
+ },
129
+ ...(serveSim ? { webPreviewUrl: serveSim.previewUrl } : {}),
130
+ },
131
+ logger,
132
+ });
133
+ await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
134
+ ctx,
135
+ deviceRunSessionId,
136
+ logger,
137
+ signal,
138
+ idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
139
+ ? {
140
+ maxIdleTimeMinutes,
141
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
142
+ }
143
+ : undefined,
144
+ });
145
+ }
146
+ finally {
147
+ if (serveSim) {
148
+ await serveSim.stopAsync();
149
+ }
150
+ if (appiumTunnel) {
151
+ await appiumTunnel.stopAsync();
152
+ }
153
+ await eventCollection.stopAsync();
154
+ await appiumProcess.stopAsync();
155
+ await node_fs_1.default.promises.rm(appiumHome, { recursive: true, force: true });
156
+ }
157
+ },
158
+ });
159
+ }
160
+ function resolveAppium3VersionSpec(packageVersion) {
161
+ const versionSpec = packageVersion ?? DEFAULT_APPIUM_VERSION;
162
+ const range = semver_1.default.validRange(versionSpec);
163
+ if (!range || !semver_1.default.subset(range, '>=3.0.0 <4.0.0-0')) {
164
+ throw new eas_build_job_1.SystemError(`Appium 3 is required for EAS Simulator sessions. Received package version "${versionSpec}".`);
165
+ }
166
+ return versionSpec;
167
+ }
168
+ async function resolveAppiumDeviceAsync({ runtimePlatform, env, logger, }) {
169
+ switch (runtimePlatform) {
170
+ case steps_1.BuildRuntimePlatform.DARWIN: {
171
+ await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
172
+ const [bootedDevice] = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableDevicesAsync({
173
+ env,
174
+ filter: 'booted',
175
+ });
176
+ if (!bootedDevice) {
177
+ throw new eas_build_job_1.SystemError('Could not find a booted iOS simulator for the Appium session.');
178
+ }
179
+ return {
180
+ platformName: 'iOS',
181
+ automationName: 'XCUITest',
182
+ driverName: 'xcuitest',
183
+ udid: bootedDevice.udid,
184
+ };
185
+ }
186
+ case steps_1.BuildRuntimePlatform.LINUX: {
187
+ const attachedDevices = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.getAttachedDevicesAsync({ env });
188
+ const bootedDevice = attachedDevices.find(device => device.state === 'device');
189
+ if (!bootedDevice) {
190
+ throw new eas_build_job_1.SystemError('Could not find a booted Android emulator for the Appium session.');
191
+ }
192
+ return {
193
+ platformName: 'Android',
194
+ automationName: 'UiAutomator2',
195
+ driverName: 'uiautomator2',
196
+ udid: bootedDevice.serialId,
197
+ };
198
+ }
199
+ }
200
+ }
201
+ async function installAppiumAsync({ versionSpec, driverName, env, logger, }) {
202
+ const appiumHome = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-appium-home-'));
203
+ await node_fs_1.default.promises.writeFile(node_path_1.default.join(appiumHome, 'package.json'), `${JSON.stringify({ name: 'eas-appium-home', private: true })}\n`);
204
+ const appiumEnv = { ...env, APPIUM_HOME: appiumHome };
205
+ const appiumBinPath = node_path_1.default.join(appiumHome, 'node_modules', '.bin', 'appium');
206
+ logger.info(`Installing appium@${versionSpec}.`);
207
+ await (0, turtle_spawn_1.default)('npm', ['install', '--prefix', appiumHome, `appium@${versionSpec}`], {
208
+ env: appiumEnv,
209
+ logger,
210
+ });
211
+ const { stdout } = await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'list', '--installed', '--json'], {
212
+ env: appiumEnv,
213
+ stdio: 'pipe',
214
+ });
215
+ const installedDrivers = AppiumInstalledDriversSchema.parse(JSON.parse(stdout));
216
+ if (installedDrivers[driverName]?.installed) {
217
+ logger.info(`Updating the installed Appium ${driverName} driver.`);
218
+ await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'update', driverName], { env: appiumEnv, logger });
219
+ }
220
+ else {
221
+ logger.info(`Installing the Appium ${driverName} driver.`);
222
+ await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'install', driverName], { env: appiumEnv, logger });
223
+ }
224
+ return { appiumHome, appiumBinPath, appiumEnv };
225
+ }
226
+ async function waitForAppiumReadyAsync({ appiumProcess, logger, }) {
227
+ const deadline = Date.now() + APPIUM_STARTUP_TIMEOUT_MS;
228
+ while (Date.now() < deadline) {
229
+ try {
230
+ const response = await (0, turtleFetch_1.turtleFetch)(`http://${APPIUM_HOST}:${APPIUM_PORT}/status`, 'GET', {
231
+ timeout: 2_000,
232
+ retries: 0,
233
+ logger,
234
+ });
235
+ if (response.ok) {
236
+ return;
237
+ }
238
+ }
239
+ catch { }
240
+ await (0, retry_1.sleepAsync)(1_000);
241
+ }
242
+ const output = appiumProcess.getOutput();
243
+ throw new eas_build_job_1.SystemError(`Timed out waiting for Appium to become ready.${output ? `\nAppium output:\n${output}` : ''}`);
244
+ }
@@ -57,6 +57,11 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
57
57
  required: false,
58
58
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
59
59
  }),
60
+ steps_1.BuildStepInput.createProvider({
61
+ id: 'max_duration_seconds',
62
+ required: false,
63
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
64
+ }),
60
65
  ],
61
66
  fn: async ({ logger, global }, { inputs, env, signal }) => {
62
67
  // Fail fast before any expensive setup if the injected env
@@ -69,6 +74,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
69
74
  const packageVersion = inputs.package_version.value;
70
75
  // A missing or non-positive value disables the idle timeout (opt-in feature).
71
76
  const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
77
+ const maxDurationSeconds = inputs.max_duration_seconds?.value;
72
78
  warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger });
73
79
  const versionSpec = packageVersion ?? 'latest';
74
80
  const { runtimePlatform } = global;
@@ -178,6 +184,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
178
184
  ctx,
179
185
  deviceRunSessionId,
180
186
  logger,
187
+ maxDurationSeconds,
181
188
  signal,
182
189
  idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
183
190
  ? {
@@ -11,9 +11,17 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
11
11
  name: 'Start serve-sim remote session',
12
12
  __metricsId: 'eas/start_serve_sim_remote_session',
13
13
  supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
14
- fn: async ({ logger }, { env, signal }) => {
14
+ inputProviders: [
15
+ steps_1.BuildStepInput.createProvider({
16
+ id: 'max_duration_seconds',
17
+ required: false,
18
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
19
+ }),
20
+ ],
21
+ fn: async ({ logger }, { inputs, env, signal }) => {
15
22
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
16
23
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
24
+ const maxDurationSeconds = inputs.max_duration_seconds?.value;
17
25
  logger.info('Starting serve-sim remote session.');
18
26
  await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
19
27
  const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
@@ -34,6 +42,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
34
42
  ctx,
35
43
  deviceRunSessionId,
36
44
  logger,
45
+ maxDurationSeconds,
37
46
  signal,
38
47
  });
39
48
  }
@@ -0,0 +1,10 @@
1
+ import { type bunyan } from '@expo/logger';
2
+ import { type CustomBuildContext } from '../../customBuildContext';
3
+ import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
4
+ export declare function startAppiumEventCollectionAsync({ ctx, deviceRunSessionId, appiumUrl, logger, pollIntervalMs, }: {
5
+ ctx: CustomBuildContext;
6
+ deviceRunSessionId: string;
7
+ appiumUrl: string;
8
+ logger: bunyan;
9
+ pollIntervalMs?: number;
10
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -0,0 +1,175 @@
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.startAppiumEventCollectionAsync = startAppiumEventCollectionAsync;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const promises_1 = require("node:timers/promises");
11
+ const zod_1 = require("zod");
12
+ const turtleFetch_1 = require("../../utils/turtleFetch");
13
+ const deviceRunSessionEvents_1 = require("./deviceRunSessionEvents");
14
+ const APPIUM_REQUEST_TIMEOUT_MS = 10_000;
15
+ const POLL_INTERVAL_MS = 1_000;
16
+ const GET_LOG_EVENTS_COMMAND = 'getLogEvents';
17
+ const AppiumSessionsResponseSchema = zod_1.z.object({
18
+ value: zod_1.z.array(zod_1.z.object({ id: zod_1.z.string() }).passthrough()),
19
+ });
20
+ const AppiumCommandSchema = zod_1.z.object({
21
+ appiumSessionId: zod_1.z.string(),
22
+ cmd: zod_1.z.string(),
23
+ startTime: zod_1.z.number(),
24
+ endTime: zod_1.z.number(),
25
+ });
26
+ const AppiumEventsResponseSchema = zod_1.z.object({
27
+ value: zod_1.z.object({
28
+ commands: zod_1.z.array(AppiumCommandSchema.omit({ appiumSessionId: true })),
29
+ }),
30
+ });
31
+ async function startAppiumEventCollectionAsync({ ctx, deviceRunSessionId, appiumUrl, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
32
+ const eventDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-appium-events-'));
33
+ const eventFile = node_path_1.default.join(eventDirectory, 'commands.ndjson');
34
+ await node_fs_1.default.promises.writeFile(eventFile, '');
35
+ let eventCollection;
36
+ try {
37
+ eventCollection = await (0, deviceRunSessionEvents_1.startDeviceRunSessionEventCollectionAsync)({
38
+ ctx,
39
+ deviceRunSessionId,
40
+ logger,
41
+ pollIntervalMs,
42
+ source: createAppiumEventSource(eventFile),
43
+ });
44
+ }
45
+ catch (error) {
46
+ await node_fs_1.default.promises.rm(eventDirectory, { recursive: true, force: true });
47
+ throw error;
48
+ }
49
+ const observedCommandKeys = new Set();
50
+ const controller = new AbortController();
51
+ let didReportCollectionFailure = false;
52
+ const collectSafelyAsync = async () => {
53
+ try {
54
+ await collectAppiumCommandsAsync({
55
+ appiumUrl,
56
+ eventFile,
57
+ observedCommandKeys,
58
+ logger,
59
+ });
60
+ didReportCollectionFailure = false;
61
+ }
62
+ catch (error) {
63
+ if (!didReportCollectionFailure) {
64
+ didReportCollectionFailure = true;
65
+ logger.warn({ err: error }, 'Could not collect Appium events.');
66
+ }
67
+ }
68
+ };
69
+ const pollingPromise = (async () => {
70
+ while (!controller.signal.aborted) {
71
+ await collectSafelyAsync();
72
+ try {
73
+ await (0, promises_1.setTimeout)(pollIntervalMs, undefined, { signal: controller.signal });
74
+ }
75
+ catch (error) {
76
+ if (!controller.signal.aborted) {
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+ })().catch(error => {
82
+ logger.warn({ err: error }, 'Appium event collection poller failed.');
83
+ });
84
+ return {
85
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
86
+ stopAsync: async () => {
87
+ controller.abort();
88
+ await pollingPromise;
89
+ await collectSafelyAsync();
90
+ try {
91
+ await eventCollection.stopAsync();
92
+ }
93
+ finally {
94
+ await node_fs_1.default.promises.rm(eventDirectory, { recursive: true, force: true });
95
+ }
96
+ },
97
+ };
98
+ }
99
+ function createAppiumEventSource(eventFile) {
100
+ return {
101
+ producer: 'appium',
102
+ findEventFilesAsync: async () => [eventFile],
103
+ sourceKeyForFile: file => node_path_1.default.basename(file, node_path_1.default.extname(file)),
104
+ parseLine: ({ line, sourceKey, sequenceNumber, deviceRunSessionId }) => {
105
+ if (!line.trim()) {
106
+ return {};
107
+ }
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(line);
111
+ }
112
+ catch {
113
+ return { failure: 'invalid-json' };
114
+ }
115
+ const result = AppiumCommandSchema.safeParse(parsed);
116
+ if (!result.success) {
117
+ return { failure: 'invalid-event' };
118
+ }
119
+ const command = result.data;
120
+ const operationId = `${sourceKey}:${sequenceNumber}`;
121
+ return {
122
+ event: {
123
+ v: 1,
124
+ eventId: `appium:${deviceRunSessionId}:${operationId}`,
125
+ ts: new Date(command.endTime).toISOString(),
126
+ producer: 'appium',
127
+ type: 'operation.completed',
128
+ operationId,
129
+ durationMs: Math.max(0, command.endTime - command.startTime),
130
+ summary: command.cmd,
131
+ data: {
132
+ command: command.cmd,
133
+ appiumSessionId: command.appiumSessionId,
134
+ },
135
+ },
136
+ };
137
+ },
138
+ };
139
+ }
140
+ async function collectAppiumCommandsAsync({ appiumUrl, eventFile, observedCommandKeys, logger, }) {
141
+ const response = await (0, turtleFetch_1.turtleFetch)(new URL('appium/sessions', appiumUrl).toString(), 'GET', {
142
+ timeout: APPIUM_REQUEST_TIMEOUT_MS,
143
+ retries: 0,
144
+ logger,
145
+ });
146
+ const sessions = AppiumSessionsResponseSchema.parse(await response.json()).value;
147
+ const newCommands = [];
148
+ const pendingCommandKeys = new Set();
149
+ for (const { id: appiumSessionId } of sessions) {
150
+ const eventsResponse = await (0, turtleFetch_1.turtleFetch)(new URL(`session/${encodeURIComponent(appiumSessionId)}/appium/events`, appiumUrl).toString(), 'POST', {
151
+ json: {},
152
+ timeout: APPIUM_REQUEST_TIMEOUT_MS,
153
+ retries: 0,
154
+ logger,
155
+ });
156
+ const commands = AppiumEventsResponseSchema.parse(await eventsResponse.json()).value.commands;
157
+ for (const command of commands) {
158
+ const key = [appiumSessionId, command.cmd, command.startTime, command.endTime].join(':');
159
+ if (command.cmd === GET_LOG_EVENTS_COMMAND ||
160
+ observedCommandKeys.has(key) ||
161
+ pendingCommandKeys.has(key)) {
162
+ continue;
163
+ }
164
+ pendingCommandKeys.add(key);
165
+ newCommands.push({ key, command: { appiumSessionId, ...command } });
166
+ }
167
+ }
168
+ if (newCommands.length === 0) {
169
+ return;
170
+ }
171
+ await node_fs_1.default.promises.appendFile(eventFile, `${newCommands.map(({ command }) => JSON.stringify(command)).join('\n')}\n`);
172
+ for (const { key } of newCommands) {
173
+ observedCommandKeys.add(key);
174
+ }
175
+ }
@@ -25,10 +25,11 @@ export type DeviceRunSessionIdleTimeout = {
25
25
  */
26
26
  getLastEventObservedAt: () => Date | undefined;
27
27
  };
28
- export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }: {
28
+ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, maxDurationSeconds, signal: cancelSignal, idleTimeout, }: {
29
29
  ctx: CustomBuildContext;
30
30
  deviceRunSessionId: string;
31
31
  logger: bunyan;
32
+ maxDurationSeconds?: number;
32
33
  signal?: AbortSignal;
33
34
  idleTimeout?: DeviceRunSessionIdleTimeout;
34
35
  }): Promise<void>;
@@ -63,6 +63,7 @@ const zod_1 = require("zod");
63
63
  const node_crypto_1 = require("node:crypto");
64
64
  const node_fs_1 = __importDefault(require("node:fs"));
65
65
  const node_net_1 = require("node:net");
66
+ const node_timers_1 = require("node:timers");
66
67
  const promises_1 = require("node:timers/promises");
67
68
  const sentry_1 = require("../../sentry");
68
69
  const retry_1 = require("../../utils/retry");
@@ -152,58 +153,83 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
152
153
  stdio: ['ignore', 'pipe', 'pipe'],
153
154
  });
154
155
  }
155
- async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }) {
156
- logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
157
- if (idleTimeout) {
158
- logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
159
- }
160
- let pollErrorCount = 0;
161
- let lastActivityAt = new Date();
162
- while (!signal?.aborted) {
156
+ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, maxDurationSeconds, signal: cancelSignal, idleTimeout, }) {
157
+ const durationAbortController = new AbortController();
158
+ const signal = cancelSignal
159
+ ? AbortSignal.any([cancelSignal, durationAbortController.signal])
160
+ : durationAbortController.signal;
161
+ // Nothing to wait for if the step was already aborted before we started;
162
+ // return before logging so we don't claim to be polling a session we never poll.
163
+ if (signal.aborted) {
164
+ return;
165
+ }
166
+ const durationTimeout = maxDurationSeconds === undefined
167
+ ? undefined
168
+ : (0, node_timers_1.setTimeout)(() => {
169
+ logger.info(`Device run session ${deviceRunSessionId} reached its maximum duration.`);
170
+ durationAbortController.abort();
171
+ }, maxDurationSeconds * 1_000);
172
+ try {
173
+ logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
174
+ if (durationTimeout !== undefined) {
175
+ logger.info(`The device run session will stop automatically after ${maxDurationSeconds} seconds.`);
176
+ }
163
177
  if (idleTimeout) {
164
- const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
165
- if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
166
- lastActivityAt = lastEventObservedAt;
167
- }
168
- if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
169
- logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
170
- `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
171
- await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
172
- return;
173
- }
178
+ logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
174
179
  }
175
- try {
176
- const result = await ctx.graphqlClient
177
- .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
178
- .toPromise();
179
- if (result.error) {
180
- throw result.error;
181
- }
182
- const status = result.data?.deviceRunSessions?.byId?.status;
183
- if (!status) {
184
- throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
180
+ let pollErrorCount = 0;
181
+ let lastActivityAt = new Date();
182
+ while (!signal.aborted) {
183
+ if (idleTimeout) {
184
+ const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
185
+ if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
186
+ lastActivityAt = lastEventObservedAt;
187
+ }
188
+ if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
189
+ logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
190
+ `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
191
+ await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
192
+ return;
193
+ }
185
194
  }
186
- pollErrorCount = 0;
187
- if (status === 'STOPPED') {
188
- logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
189
- return;
195
+ try {
196
+ const result = await ctx.graphqlClient
197
+ .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
198
+ .toPromise();
199
+ if (result.error) {
200
+ throw result.error;
201
+ }
202
+ const status = result.data?.deviceRunSessions?.byId?.status;
203
+ if (!status) {
204
+ throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
205
+ }
206
+ pollErrorCount = 0;
207
+ if (status === 'STOPPED') {
208
+ logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
209
+ return;
210
+ }
211
+ if (status === 'ERRORED') {
212
+ throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
213
+ }
190
214
  }
191
- if (status === 'ERRORED') {
192
- throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
215
+ catch (err) {
216
+ if (err instanceof eas_build_job_1.SystemError) {
217
+ throw err;
218
+ }
219
+ const error = err instanceof Error ? err : new Error(String(err));
220
+ pollErrorCount += 1;
221
+ if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
222
+ sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
223
+ logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
224
+ }
193
225
  }
226
+ await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
194
227
  }
195
- catch (err) {
196
- if (err instanceof eas_build_job_1.SystemError) {
197
- throw err;
198
- }
199
- const error = err instanceof Error ? err : new Error(String(err));
200
- pollErrorCount += 1;
201
- if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
202
- sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
203
- logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
204
- }
228
+ }
229
+ finally {
230
+ if (durationTimeout !== undefined) {
231
+ (0, node_timers_1.clearTimeout)(durationTimeout);
205
232
  }
206
- await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
207
233
  }
208
234
  }
209
235
  // Best effort: when this fails, the caller still tears the session down and the
@@ -529,7 +555,7 @@ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, time
529
555
  await waitForServeSimReadyAsync({ serveSim, port, timeoutMs });
530
556
  const tunnel = await startNgrokTunnelAsync({
531
557
  port,
532
- subdomainPrefix: 'serve-sim',
558
+ subdomainPrefix: 'web-preview',
533
559
  baseDomain,
534
560
  authtoken: getNgrokAuthtokenOrThrow(env),
535
561
  logger,
@@ -42,6 +42,7 @@ export declare namespace AndroidEmulatorUtils {
42
42
  emulatorPromise: SpawnPromise<SpawnResult>;
43
43
  serialId: AndroidDeviceSerialId;
44
44
  logcatOutputPath: string;
45
+ emulatorOutputPath: string;
45
46
  }>;
46
47
  function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
47
48
  serialId: AndroidDeviceSerialId;