@expo/build-tools 22.4.0 → 22.6.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.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { uploadWithSignedUrl } from './storage/uploadWithSignedUrl';
6
6
  export type { SignedUrl, UploadWithSignedUrlParams } from './storage/uploadWithSignedUrl';
7
7
  export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
8
8
  export { PackageManager } from './utils/packageManager';
9
+ export * as TurtleSshSession from './utils/turtleSshSession';
10
+ export { formatSecondsForLog } from './utils/formatDuration';
9
11
  export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
10
12
  export { Hook, runHookIfPresent } from './utils/hooks';
11
13
  export { parseGradleProfile, formatGradleProfileReport } from './android/gradleProfile';
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.HttpLogStream = exports.Builders = void 0;
42
+ exports.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.formatSecondsForLog = exports.TurtleSshSession = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.HttpLogStream = exports.Builders = void 0;
43
43
  const Builders = __importStar(require("./builders"));
44
44
  exports.Builders = Builders;
45
45
  const HttpLogStream_1 = __importDefault(require("./logging/HttpLogStream"));
@@ -53,6 +53,9 @@ Object.defineProperty(exports, "BuildContext", { enumerable: true, get: function
53
53
  Object.defineProperty(exports, "SkipNativeBuildError", { enumerable: true, get: function () { return context_1.SkipNativeBuildError; } });
54
54
  var packageManager_1 = require("./utils/packageManager");
55
55
  Object.defineProperty(exports, "PackageManager", { enumerable: true, get: function () { return packageManager_1.PackageManager; } });
56
+ exports.TurtleSshSession = __importStar(require("./utils/turtleSshSession"));
57
+ var formatDuration_1 = require("./utils/formatDuration");
58
+ Object.defineProperty(exports, "formatSecondsForLog", { enumerable: true, get: function () { return formatDuration_1.formatSecondsForLog; } });
56
59
  var xcodeBuildLogs_1 = require("./ios/xcodeBuildLogs");
57
60
  Object.defineProperty(exports, "findAndUploadXcodeBuildLogsAsync", { enumerable: true, get: function () { return xcodeBuildLogs_1.findAndUploadXcodeBuildLogsAsync; } });
58
61
  var hooks_1 = require("./utils/hooks");
@@ -8,6 +8,8 @@ exports.launchApplicationAsync = launchApplicationAsync;
8
8
  const eas_build_job_1 = require("@expo/eas-build-job");
9
9
  const steps_1 = require("@expo/steps");
10
10
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ const IOS_URL_SCHEME_APPROVAL_DOMAIN = 'com.apple.launchservices.schemeapproval';
12
+ const IOS_URL_SCHEME_APPROVAL_KEY_PREFIX = 'com.apple.CoreSimulator.CoreSimulatorBridge-->';
11
13
  function createLaunchApplicationFunction() {
12
14
  return new steps_1.BuildFunction({
13
15
  namespace: 'eas',
@@ -63,6 +65,7 @@ async function launchApplicationAsync({ applicationIdentifier, activityName, lau
63
65
  logger,
64
66
  });
65
67
  if (openUrl) {
68
+ await preapproveIosUrlSchemeAsync({ applicationIdentifier, openUrl, env, logger });
66
69
  logger.info(`Opening ${openUrl} in ${applicationIdentifier}.`);
67
70
  await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'openurl', 'booted', openUrl], { env, logger });
68
71
  }
@@ -93,6 +96,28 @@ async function launchApplicationAsync({ applicationIdentifier, activityName, lau
93
96
  ], { env, logger });
94
97
  }
95
98
  }
99
+ async function preapproveIosUrlSchemeAsync({ applicationIdentifier, openUrl, env, logger, }) {
100
+ const urlScheme = new URL(openUrl).protocol.slice(0, -1);
101
+ if (urlScheme === 'http' || urlScheme === 'https') {
102
+ return;
103
+ }
104
+ try {
105
+ await (0, turtle_spawn_1.default)('xcrun', [
106
+ 'simctl',
107
+ 'spawn',
108
+ 'booted',
109
+ 'defaults',
110
+ 'write',
111
+ IOS_URL_SCHEME_APPROVAL_DOMAIN,
112
+ `${IOS_URL_SCHEME_APPROVAL_KEY_PREFIX}${urlScheme}`,
113
+ '-string',
114
+ applicationIdentifier,
115
+ ], { env, logger });
116
+ }
117
+ catch (error) {
118
+ logger.warn({ err: error }, `Could not preapprove the ${urlScheme} URL scheme for ${applicationIdentifier}. Opening the URL anyway; the Simulator might require manual confirmation.`);
119
+ }
120
+ }
96
121
  function logApplicationLaunch(logger, applicationIdentifier, launchArgs) {
97
122
  const argumentsDescription = launchArgs.length > 0 ? ` with arguments ${JSON.stringify(launchArgs)}` : '';
98
123
  logger.info(`Launching ${applicationIdentifier}${argumentsDescription}.`);
@@ -0,0 +1,9 @@
1
+ import { type AppiumCommand } from './appiumCommands.generated';
2
+ export declare const APPIUM_COMMAND_SUMMARIES: Record<AppiumCommand, string>;
3
+ /**
4
+ * Translate a raw Appium command name into a short, human-readable summary.
5
+ *
6
+ * If a command has no curated summary, the raw command name is returned
7
+ * unchanged — we intentionally do not guess a phrasing.
8
+ */
9
+ export declare function humanizeAppiumCommand(command: string): string;
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APPIUM_COMMAND_SUMMARIES = void 0;
4
+ exports.humanizeAppiumCommand = humanizeAppiumCommand;
5
+ exports.APPIUM_COMMAND_SUMMARIES = {
6
+ // Session lifecycle
7
+ createSession: 'Started the session',
8
+ deleteSession: 'Ended the session',
9
+ getSession: 'Read the session details',
10
+ getStatus: 'Checked the server status',
11
+ getAppiumSessions: 'Listed Appium sessions',
12
+ getAppiumSessionCapabilities: 'Read the session capabilities',
13
+ getTimeouts: 'Read timeouts',
14
+ timeouts: 'Set timeouts',
15
+ getSettings: 'Read Appium settings',
16
+ updateSettings: 'Updated Appium settings',
17
+ listCommands: 'Listed available commands',
18
+ listExtensions: 'Listed available extensions',
19
+ receiveAsyncResponse: 'Received an async response',
20
+ // Screen and media
21
+ getScreenshot: 'Took a screenshot',
22
+ getElementScreenshot: 'Took a screenshot of an element',
23
+ getPageSource: 'Read the screen contents',
24
+ printPage: 'Printed the page',
25
+ // Window and orientation
26
+ getWindowRect: 'Read the screen size',
27
+ setWindowRect: 'Resized the window',
28
+ maximizeWindow: 'Maximized the window',
29
+ minimizeWindow: 'Minimized the window',
30
+ fullScreenWindow: 'Made the window full screen',
31
+ getWindowHandle: 'Read the current window',
32
+ getWindowHandles: 'Listed open windows',
33
+ setWindow: 'Switched window',
34
+ closeWindow: 'Closed the window',
35
+ createNewWindow: 'Opened a new window',
36
+ setFrame: 'Switched to a frame',
37
+ switchToParentFrame: 'Switched to the parent frame',
38
+ getOrientation: 'Read the screen orientation',
39
+ setOrientation: 'Changed the screen orientation',
40
+ getRotation: 'Read the device rotation',
41
+ setRotation: 'Rotated the device',
42
+ // Device
43
+ getDeviceTime: 'Read the device time',
44
+ getGeoLocation: 'Read the device location',
45
+ setGeoLocation: 'Set the device location',
46
+ getNetworkConnection: 'Read the network connection',
47
+ setNetworkConnection: 'Changed the network connection',
48
+ setDevicePosture: 'Set the device posture',
49
+ clearDevicePosture: 'Cleared the device posture',
50
+ setPermissions: 'Set app permissions',
51
+ // Element discovery
52
+ findElement: 'Found an element',
53
+ findElements: 'Found elements',
54
+ findElementFromElement: 'Found a nested element',
55
+ findElementsFromElement: 'Found nested elements',
56
+ findElementFromShadowRoot: 'Found an element in a shadow root',
57
+ findElementsFromShadowRoot: 'Found elements in a shadow root',
58
+ elementShadowRoot: 'Read an element shadow root',
59
+ active: 'Read the focused element',
60
+ // Element interaction
61
+ click: 'Tapped an element',
62
+ clear: 'Cleared an element',
63
+ setValue: 'Typed into an element',
64
+ getText: 'Read element text',
65
+ getName: 'Read an element tag',
66
+ getAttribute: 'Read an element attribute',
67
+ getProperty: 'Read an element property',
68
+ getCssProperty: 'Read an element style',
69
+ getComputedLabel: 'Read an element label',
70
+ getComputedRole: 'Read an element role',
71
+ getElementRect: 'Read an element position and size',
72
+ elementDisplayed: 'Checked if an element was visible',
73
+ elementEnabled: 'Checked if an element was enabled',
74
+ elementSelected: 'Checked if an element was selected',
75
+ // Gestures
76
+ performActions: 'Performed a gesture',
77
+ releaseActions: 'Finished a gesture',
78
+ // App management
79
+ installApp: 'Installed an app',
80
+ removeApp: 'Removed an app',
81
+ isAppInstalled: 'Checked if an app was installed',
82
+ activateApp: 'Activated an app',
83
+ terminateApp: 'Terminated an app',
84
+ queryAppState: 'Read the app state',
85
+ background: 'Sent the app to the background',
86
+ // Contexts
87
+ getContexts: 'Listed available contexts',
88
+ getCurrentContext: 'Read the current context',
89
+ setContext: 'Switched context',
90
+ // Web navigation
91
+ setUrl: 'Opened a URL',
92
+ getUrl: 'Read the current URL',
93
+ back: 'Navigated back',
94
+ forward: 'Navigated forward',
95
+ refresh: 'Refreshed the page',
96
+ title: 'Read the page title',
97
+ // Cookies
98
+ getCookie: 'Read a cookie',
99
+ getCookies: 'Read all cookies',
100
+ setCookie: 'Set a cookie',
101
+ deleteCookie: 'Deleted a cookie',
102
+ deleteCookies: 'Deleted all cookies',
103
+ // Alerts
104
+ getAlertText: 'Read an alert',
105
+ setAlertText: 'Typed into an alert',
106
+ postAcceptAlert: 'Accepted an alert',
107
+ postDismissAlert: 'Dismissed an alert',
108
+ // Keyboard and input methods
109
+ hideKeyboard: 'Hid the keyboard',
110
+ isKeyboardShown: 'Checked if the keyboard was shown',
111
+ activateIMEEngine: 'Activated an input method',
112
+ deactivateIMEEngine: 'Deactivated the input method',
113
+ getActiveIMEEngine: 'Read the active input method',
114
+ availableIMEEngines: 'Listed input methods',
115
+ isIMEActivated: 'Checked if an input method was active',
116
+ // Clipboard (driver-level, not declared by base-driver)
117
+ getClipboard: 'Read the clipboard',
118
+ setClipboard: 'Wrote to the clipboard',
119
+ // Files
120
+ pushFile: 'Pushed a file to the device',
121
+ pullFile: 'Pulled a file from the device',
122
+ pullFolder: 'Pulled a folder from the device',
123
+ // Logs
124
+ getLog: 'Read device logs',
125
+ getLogTypes: 'Listed log types',
126
+ getLogEvents: 'Read logged events',
127
+ logCustomEvent: 'Logged a custom event',
128
+ generateTestReport: 'Generated a test report',
129
+ // Scripts
130
+ execute: 'Ran a script command',
131
+ executeAsync: 'Ran an async script command',
132
+ executeCdp: 'Ran a Chrome DevTools command',
133
+ // Web authentication (virtual authenticators)
134
+ addVirtualAuthenticator: 'Added a virtual authenticator',
135
+ removeVirtualAuthenticator: 'Removed a virtual authenticator',
136
+ addAuthCredential: 'Added an auth credential',
137
+ getAuthCredential: 'Read auth credentials',
138
+ removeAuthCredential: 'Removed an auth credential',
139
+ removeAllAuthCredentials: 'Removed all auth credentials',
140
+ setUserAuthVerified: 'Set the user verification state',
141
+ // Federated sign-in (FedCM)
142
+ fedCMGetAccounts: 'Listed federated sign-in accounts',
143
+ fedCMSelectAccount: 'Selected a federated sign-in account',
144
+ fedCMGetDialogType: 'Read the federated sign-in dialog type',
145
+ fedCMGetTitle: 'Read the federated sign-in dialog title',
146
+ fedCMClickDialogButton: 'Clicked a federated sign-in dialog button',
147
+ fedCMCancelDialog: 'Canceled the federated sign-in dialog',
148
+ fedCMResetCooldown: 'Reset the federated sign-in cooldown',
149
+ fedCMSetDelayEnabled: 'Toggled the federated sign-in delay',
150
+ // Virtual sensors and pressure sources
151
+ createVirtualSensor: 'Created a virtual sensor',
152
+ updateVirtualSensorReading: 'Updated a virtual sensor reading',
153
+ getVirtualSensorInfo: 'Read virtual sensor info',
154
+ deleteVirtualSensor: 'Removed a virtual sensor',
155
+ createVirtualPressureSource: 'Created a virtual pressure source',
156
+ updateVirtualPressureSource: 'Updated a virtual pressure source',
157
+ deleteVirtualPressureSource: 'Removed a virtual pressure source',
158
+ // Privacy / experimental web platform
159
+ getGlobalPrivacyControl: 'Read the Global Privacy Control setting',
160
+ setGlobalPrivacyControl: 'Set the Global Privacy Control setting',
161
+ setStorageAccess: 'Set storage access',
162
+ setRPHRegistrationMode: 'Set the protocol handler registration mode',
163
+ setSPCTransactionMode: 'Set the payment transaction mode',
164
+ // Driver routes and compatibility commands
165
+ asyncScriptTimeout: 'Set the async script timeout',
166
+ closeApp: 'Closed the app',
167
+ disableConditionInducer: 'Disabled the condition inducer',
168
+ enableConditionInducer: 'Enabled the condition inducer',
169
+ fingerprint: 'Simulated a fingerprint',
170
+ getCurrentActivity: 'Read the current activity',
171
+ getCurrentPackage: 'Read the current package',
172
+ getDisplayDensity: 'Read the display density',
173
+ getLocation: 'Read an element location',
174
+ getLocationInView: 'Read an element location in view',
175
+ getPerformanceData: 'Read performance data',
176
+ getPerformanceDataTypes: 'Listed performance data types',
177
+ getScreenInfo: 'Read screen information',
178
+ getSize: 'Read an element size',
179
+ getStrings: 'Read app strings',
180
+ getSystemBars: 'Read system bar information',
181
+ getViewportRect: 'Read the viewport size',
182
+ getViewportScreenshot: 'Took a viewport screenshot',
183
+ getWindowSize: 'Read the window size',
184
+ gsmCall: 'Simulated a phone call',
185
+ gsmSignal: 'Changed the cellular signal',
186
+ gsmVoice: 'Changed the cellular voice state',
187
+ implicitWait: 'Set the element lookup timeout',
188
+ isLocationServicesEnabled: 'Checked if location services were enabled',
189
+ isLocked: 'Checked if the device was locked',
190
+ keyevent: 'Sent a key event',
191
+ keys: 'Sent keyboard input',
192
+ launchApp: 'Launched the app',
193
+ listConditionInducers: 'Listed condition inducers',
194
+ lock: 'Locked the device',
195
+ longPressKeyCode: 'Long-pressed a key',
196
+ networkSpeed: 'Changed the network speed',
197
+ openNotifications: 'Opened notifications',
198
+ powerAC: 'Changed the charging state',
199
+ powerCapacity: 'Changed the battery level',
200
+ pressKeyCode: 'Pressed a key',
201
+ replaceValue: 'Replaced an element value',
202
+ reset: 'Reset the app',
203
+ sendSMS: 'Simulated a text message',
204
+ sensorSet: 'Changed a sensor value',
205
+ setStylusHandwriting: 'Changed stylus handwriting mode',
206
+ setValueImmediate: 'Typed into an element immediately',
207
+ startActivity: 'Started an activity',
208
+ startAudioRecording: 'Started audio recording',
209
+ startRecordingScreen: 'Started screen recording',
210
+ stopAudioRecording: 'Stopped audio recording',
211
+ stopRecordingScreen: 'Stopped screen recording',
212
+ submit: 'Submitted an element',
213
+ toggleData: 'Toggled mobile data',
214
+ toggleEnrollTouchId: 'Changed Touch ID enrollment',
215
+ toggleFlightMode: 'Toggled airplane mode',
216
+ toggleLocationServices: 'Toggled location services',
217
+ toggleWiFi: 'Toggled Wi-Fi',
218
+ touchId: 'Simulated Touch ID',
219
+ unlock: 'Unlocked the device',
220
+ // Driver mobile commands
221
+ mobileAcceptAlert: 'Accepted an alert',
222
+ mobileActivateApp: 'Activated an app',
223
+ mobileBackgroundApp: 'Sent the app to the background',
224
+ mobileBluetooth: 'Changed Bluetooth state',
225
+ mobileBroadcast: 'Sent an Android broadcast',
226
+ mobileCalibrateWebToRealCoordinatesTranslation: 'Calibrated web coordinates',
227
+ mobileChangePermissions: 'Changed app permissions',
228
+ mobileClearApp: 'Cleared app data',
229
+ mobileClearKeychains: 'Cleared keychains',
230
+ mobileClickGesture: 'Performed a click gesture',
231
+ mobileConfigureLocalization: 'Changed device localization',
232
+ mobileDeepLink: 'Opened a deep link',
233
+ mobileDeleteFile: 'Deleted a device file',
234
+ mobileDeleteFolder: 'Deleted a device folder',
235
+ mobileDeviceidle: 'Changed device idle state',
236
+ mobileDisableVoiceOver: 'Disabled VoiceOver',
237
+ mobileDismissAlert: 'Dismissed an alert',
238
+ mobileDoubleClickGesture: 'Performed a double-click gesture',
239
+ mobileDoubleTap: 'Double-tapped the screen',
240
+ mobileDragFromToForDuration: 'Dragged between screen points',
241
+ mobileDragFromToWithVelocity: 'Dragged with a set velocity',
242
+ mobileDragGesture: 'Performed a drag gesture',
243
+ mobileEnableVoiceOver: 'Enabled VoiceOver',
244
+ mobileEnrollBiometric: 'Changed biometric enrollment',
245
+ mobileExecEmuConsoleCommand: 'Ran an emulator console command',
246
+ mobileExpectNotification: 'Waited for a notification',
247
+ mobileFingerprint: 'Simulated a fingerprint',
248
+ mobileFlingGesture: 'Performed a fling gesture',
249
+ mobileForcePress: 'Force-pressed the screen',
250
+ mobileGetActionHistory: 'Read scheduled action history',
251
+ mobileGetActiveAppInfo: 'Read active app information',
252
+ mobileGetAppearance: 'Read the device appearance',
253
+ mobileGetBatteryInfo: 'Read battery information',
254
+ mobileGetChromeCapabilities: 'Read Chrome capabilities',
255
+ mobileGetConnectivity: 'Read device connectivity',
256
+ mobileGetContentSize: 'Read scrollable content size',
257
+ mobileGetContexts: 'Listed available contexts',
258
+ mobileGetDeviceInfo: 'Read device information',
259
+ mobileGetDeviceTime: 'Read the device time',
260
+ mobileGetDeclaredOrientation: 'Read the declared device orientation',
261
+ mobileGetGeolocation: 'Read the device location',
262
+ mobileGetIncreaseContrast: 'Read the Increase Contrast setting',
263
+ mobileGetNotifications: 'Read notifications',
264
+ mobileGetPasteboard: 'Read the pasteboard',
265
+ mobileGetPerformanceData: 'Read app performance data',
266
+ mobileGetPermission: 'Read an app permission',
267
+ mobileGetPermissions: 'Read app permissions',
268
+ mobileGetSimulatedLocation: 'Read the simulated location',
269
+ mobileGetSource: 'Read the screen contents',
270
+ mobileGetUiMode: 'Read the Android UI mode',
271
+ mobileGetXctestScreenRecordingInfo: 'Read XCTest screen recording information',
272
+ mobileGsmCall: 'Simulated a phone call',
273
+ mobileGsmSignal: 'Changed the cellular signal',
274
+ mobileGsmVoice: 'Changed the cellular voice state',
275
+ mobileHandleAlert: 'Handled an alert',
276
+ mobileHideKeyboard: 'Hid the keyboard',
277
+ mobileInjectEmulatorCameraImage: 'Injected an emulator camera image',
278
+ mobileInstallApp: 'Installed an app',
279
+ mobileInstallCertificate: 'Installed a certificate',
280
+ mobileInstallMultipleApks: 'Installed multiple APKs',
281
+ mobileInstallXCTestBundle: 'Installed an XCTest bundle',
282
+ mobileIsAppInstalled: 'Checked if an app was installed',
283
+ mobileIsBiometricEnrolled: 'Checked biometric enrollment',
284
+ mobileIsMediaProjectionRecordingRunning: 'Checked media projection recording',
285
+ mobileIsVoiceOverEnabled: 'Checked if VoiceOver was enabled',
286
+ mobileKeys: 'Sent keyboard input',
287
+ mobileKillApp: 'Stopped an app',
288
+ mobileLaunchApp: 'Launched an app',
289
+ mobileListApps: 'Listed installed apps',
290
+ mobileListCertificates: 'Listed certificates',
291
+ mobileListDisplays: 'Listed displays',
292
+ mobileListSms: 'Listed text messages',
293
+ mobileListWindows: 'Listed app windows',
294
+ mobileListXCTestBundles: 'Listed XCTest bundles',
295
+ mobileLongClickGesture: 'Performed a long-click gesture',
296
+ mobileNetworkSpeed: 'Changed the network speed',
297
+ mobileNfc: 'Changed NFC state',
298
+ mobilePerformAccessibilityAudit: 'Ran an accessibility audit',
299
+ mobilePerformEditorAction: 'Performed a keyboard editor action',
300
+ mobilePerformHandGesture: 'Performed a hand gesture',
301
+ mobilePerformIndigoHidEvent: 'Performed an Indigo HID event',
302
+ mobilePerformIoHidEvent: 'Performed an IOHID event',
303
+ mobilePerformStatusBarCommand: 'Performed a status bar command',
304
+ mobilePinch: 'Pinched the screen',
305
+ mobilePinchCloseGesture: 'Performed a pinch-close gesture',
306
+ mobilePinchOpenGesture: 'Performed a pinch-open gesture',
307
+ mobilePowerAc: 'Changed the charging state',
308
+ mobilePowerCapacity: 'Changed the battery level',
309
+ mobilePressButton: 'Pressed a device button',
310
+ mobilePressKey: 'Pressed a key',
311
+ mobilePullFile: 'Pulled a file from the device',
312
+ mobilePullFolder: 'Pulled a folder from the device',
313
+ mobilePushFile: 'Pushed a file to the device',
314
+ mobilePushNotification: 'Sent a push notification',
315
+ mobileQueryAppState: 'Read the app state',
316
+ mobileRefreshGpsCache: 'Refreshed the GPS cache',
317
+ mobileRemoveApp: 'Removed an app',
318
+ mobileRemoveCertificate: 'Removed a certificate',
319
+ mobileReplaceElementValue: 'Replaced an element value',
320
+ mobileResetAccessibilityCache: 'Reset the accessibility cache',
321
+ mobileResetGeolocation: 'Reset the device location',
322
+ mobileResetLocationService: 'Reset location services',
323
+ mobileResetPermission: 'Reset an app permission',
324
+ mobileResetSimulatedLocation: 'Reset the simulated location',
325
+ mobileRotateDigitalCrown: 'Rotated the Digital Crown',
326
+ mobileRotateElement: 'Rotated an element',
327
+ mobileRunXCTest: 'Ran an XCTest bundle',
328
+ mobileScheduleAction: 'Scheduled a device action',
329
+ mobileScreenshots: 'Took display screenshots',
330
+ mobileScroll: 'Scrolled the screen',
331
+ mobileScrollBackTo: 'Scrolled back to an element',
332
+ mobileScrollGesture: 'Performed a scroll gesture',
333
+ mobileScrollToElement: 'Scrolled to an element',
334
+ mobileSelectPickerWheelValue: 'Changed a picker wheel value',
335
+ mobileSendBiometricMatch: 'Simulated biometric authentication',
336
+ mobileSendMemoryWarning: 'Sent a memory warning',
337
+ mobileSendSms: 'Simulated a text message',
338
+ mobileSendTrimMemory: 'Sent an Android memory trim event',
339
+ mobileSetAppearance: 'Changed the device appearance',
340
+ mobileSetConnectivity: 'Changed device connectivity',
341
+ mobileSetContentSize: 'Changed the content size',
342
+ mobileSetGeolocation: 'Set the device location',
343
+ mobileSetIncreaseContrast: 'Changed the Increase Contrast setting',
344
+ mobileSetPasteboard: 'Wrote to the pasteboard',
345
+ mobileSetPermissions: 'Set app permissions',
346
+ mobileSetSimulatedLocation: 'Set a simulated location',
347
+ mobileSetUiMode: 'Changed the Android UI mode',
348
+ mobileShake: 'Shook the device',
349
+ mobileShell: 'Ran an Android shell command',
350
+ mobileSimctl: 'Ran a simctl command',
351
+ mobileSiriCommand: 'Sent a Siri command',
352
+ mobileStartActivity: 'Started an activity',
353
+ mobileStartLogsBroadcast: 'Started log streaming',
354
+ mobileStartMediaProjectionRecording: 'Started media projection recording',
355
+ mobileStartNetworkMonitor: 'Started network monitoring',
356
+ mobileStartPerfRecord: 'Started performance recording',
357
+ mobileStartScreenRecording: 'Started screen recording',
358
+ mobileStartScreenStreaming: 'Started screen streaming',
359
+ mobileStartService: 'Started a service',
360
+ mobileStartSystemMonitor: 'Started system monitoring',
361
+ mobileStartXctestScreenRecording: 'Started XCTest screen recording',
362
+ mobileStopLogsBroadcast: 'Stopped log streaming',
363
+ mobileStopMediaProjectionRecording: 'Stopped media projection recording',
364
+ mobileStopNetworkMonitor: 'Stopped network monitoring',
365
+ mobileStopPerfRecord: 'Stopped performance recording',
366
+ mobileStopScreenRecording: 'Stopped screen recording',
367
+ mobileStopScreenStreaming: 'Stopped screen streaming',
368
+ mobileStopService: 'Stopped a service',
369
+ mobileStopSystemMonitor: 'Stopped system monitoring',
370
+ mobileStopXctestScreenRecording: 'Stopped XCTest screen recording',
371
+ mobileSwipe: 'Swiped the screen',
372
+ mobileSwipeGesture: 'Performed a swipe gesture',
373
+ mobileTap: 'Tapped the screen',
374
+ mobileTapWithNumberOfTaps: 'Tapped the screen multiple times',
375
+ mobileTerminateApp: 'Terminated an app',
376
+ mobileTouchAndHold: 'Touched and held the screen',
377
+ mobileTwoFingerTap: 'Tapped with two fingers',
378
+ mobileType: 'Typed text',
379
+ mobileUnlock: 'Unlocked the device',
380
+ mobileUnscheduleAction: 'Canceled a scheduled action',
381
+ mobileUpdateSafariPreferences: 'Updated Safari preferences',
382
+ mobileViewPortRect: 'Read the viewport size',
383
+ mobileViewportScreenshot: 'Took a viewport screenshot',
384
+ mobileVoiceOverCurrentSpeech: 'Read current VoiceOver speech',
385
+ mobileVoiceOverMove: 'Moved the VoiceOver cursor',
386
+ };
387
+ /**
388
+ * Translate a raw Appium command name into a short, human-readable summary.
389
+ *
390
+ * If a command has no curated summary, the raw command name is returned
391
+ * unchanged — we intentionally do not guess a phrasing.
392
+ */
393
+ function humanizeAppiumCommand(command) {
394
+ return Object.hasOwn(exports.APPIUM_COMMAND_SUMMARIES, command)
395
+ ? exports.APPIUM_COMMAND_SUMMARIES[command]
396
+ : command;
397
+ }
@@ -0,0 +1,4 @@
1
+ export type BaseDriverAppiumCommand = 'activateApp' | 'activateIMEEngine' | 'active' | 'addAuthCredential' | 'addVirtualAuthenticator' | 'availableIMEEngines' | 'back' | 'clear' | 'clearDevicePosture' | 'click' | 'closeWindow' | 'createNewWindow' | 'createSession' | 'createVirtualPressureSource' | 'createVirtualSensor' | 'deactivateIMEEngine' | 'deleteCookie' | 'deleteCookies' | 'deleteSession' | 'deleteVirtualPressureSource' | 'deleteVirtualSensor' | 'elementDisplayed' | 'elementEnabled' | 'elementSelected' | 'elementShadowRoot' | 'execute' | 'executeAsync' | 'executeCdp' | 'fedCMCancelDialog' | 'fedCMClickDialogButton' | 'fedCMGetAccounts' | 'fedCMGetDialogType' | 'fedCMGetTitle' | 'fedCMResetCooldown' | 'fedCMSelectAccount' | 'fedCMSetDelayEnabled' | 'findElement' | 'findElementFromElement' | 'findElementFromShadowRoot' | 'findElements' | 'findElementsFromElement' | 'findElementsFromShadowRoot' | 'forward' | 'fullScreenWindow' | 'generateTestReport' | 'getActiveIMEEngine' | 'getAlertText' | 'getAppiumSessionCapabilities' | 'getAppiumSessions' | 'getAttribute' | 'getAuthCredential' | 'getComputedLabel' | 'getComputedRole' | 'getContexts' | 'getCookie' | 'getCookies' | 'getCssProperty' | 'getCurrentContext' | 'getDeviceTime' | 'getElementRect' | 'getElementScreenshot' | 'getGeoLocation' | 'getGlobalPrivacyControl' | 'getLog' | 'getLogEvents' | 'getLogTypes' | 'getName' | 'getNetworkConnection' | 'getOrientation' | 'getPageSource' | 'getProperty' | 'getRotation' | 'getScreenshot' | 'getSession' | 'getSettings' | 'getStatus' | 'getText' | 'getTimeouts' | 'getUrl' | 'getVirtualSensorInfo' | 'getWindowHandle' | 'getWindowHandles' | 'getWindowRect' | 'hideKeyboard' | 'installApp' | 'isAppInstalled' | 'isIMEActivated' | 'isKeyboardShown' | 'listCommands' | 'listExtensions' | 'logCustomEvent' | 'maximizeWindow' | 'minimizeWindow' | 'performActions' | 'postAcceptAlert' | 'postDismissAlert' | 'printPage' | 'pullFile' | 'pullFolder' | 'pushFile' | 'queryAppState' | 'receiveAsyncResponse' | 'refresh' | 'releaseActions' | 'removeAllAuthCredentials' | 'removeApp' | 'removeAuthCredential' | 'removeVirtualAuthenticator' | 'setAlertText' | 'setContext' | 'setCookie' | 'setDevicePosture' | 'setFrame' | 'setGeoLocation' | 'setGlobalPrivacyControl' | 'setNetworkConnection' | 'setOrientation' | 'setPermissions' | 'setRPHRegistrationMode' | 'setRotation' | 'setSPCTransactionMode' | 'setStorageAccess' | 'setUrl' | 'setUserAuthVerified' | 'setValue' | 'setWindow' | 'setWindowRect' | 'switchToParentFrame' | 'terminateApp' | 'timeouts' | 'title' | 'updateSettings' | 'updateVirtualPressureSource' | 'updateVirtualSensorReading';
2
+ export type XCUITestAppiumCommand = 'asyncScriptTimeout' | 'background' | 'closeApp' | 'disableConditionInducer' | 'enableConditionInducer' | 'getClipboard' | 'getLocation' | 'getLocationInView' | 'getScreenInfo' | 'getSize' | 'getStrings' | 'getViewportRect' | 'getViewportScreenshot' | 'getWindowSize' | 'implicitWait' | 'isLocked' | 'keys' | 'launchApp' | 'listConditionInducers' | 'lock' | 'mobileActivateApp' | 'mobileCalibrateWebToRealCoordinatesTranslation' | 'mobileClearApp' | 'mobileClearKeychains' | 'mobileConfigureLocalization' | 'mobileDeepLink' | 'mobileDeleteFile' | 'mobileDeleteFolder' | 'mobileDisableVoiceOver' | 'mobileDoubleTap' | 'mobileDragFromToForDuration' | 'mobileDragFromToWithVelocity' | 'mobileEnableVoiceOver' | 'mobileEnrollBiometric' | 'mobileExpectNotification' | 'mobileForcePress' | 'mobileGetActiveAppInfo' | 'mobileGetAppearance' | 'mobileGetBatteryInfo' | 'mobileGetContentSize' | 'mobileGetContexts' | 'mobileGetDeviceInfo' | 'mobileGetDeviceTime' | 'mobileGetIncreaseContrast' | 'mobileGetPasteboard' | 'mobileGetPermission' | 'mobileGetSimulatedLocation' | 'mobileGetSource' | 'mobileGetXctestScreenRecordingInfo' | 'mobileHandleAlert' | 'mobileHideKeyboard' | 'mobileInstallApp' | 'mobileInstallCertificate' | 'mobileInstallXCTestBundle' | 'mobileIsAppInstalled' | 'mobileIsBiometricEnrolled' | 'mobileIsVoiceOverEnabled' | 'mobileKeys' | 'mobileKillApp' | 'mobileLaunchApp' | 'mobileListApps' | 'mobileListCertificates' | 'mobileListXCTestBundles' | 'mobilePerformAccessibilityAudit' | 'mobilePerformHandGesture' | 'mobilePerformIndigoHidEvent' | 'mobilePerformIoHidEvent' | 'mobilePinch' | 'mobilePressButton' | 'mobilePullFile' | 'mobilePullFolder' | 'mobilePushFile' | 'mobilePushNotification' | 'mobileQueryAppState' | 'mobileRemoveApp' | 'mobileRemoveCertificate' | 'mobileResetLocationService' | 'mobileResetPermission' | 'mobileResetSimulatedLocation' | 'mobileRotateDigitalCrown' | 'mobileRotateElement' | 'mobileRunXCTest' | 'mobileScroll' | 'mobileScrollToElement' | 'mobileSelectPickerWheelValue' | 'mobileSendBiometricMatch' | 'mobileSendMemoryWarning' | 'mobileSetAppearance' | 'mobileSetContentSize' | 'mobileSetIncreaseContrast' | 'mobileSetPasteboard' | 'mobileSetPermissions' | 'mobileSetSimulatedLocation' | 'mobileShake' | 'mobileSimctl' | 'mobileSiriCommand' | 'mobileStartLogsBroadcast' | 'mobileStartNetworkMonitor' | 'mobileStartPerfRecord' | 'mobileStartScreenRecording' | 'mobileStartSystemMonitor' | 'mobileStartXctestScreenRecording' | 'mobileStopLogsBroadcast' | 'mobileStopNetworkMonitor' | 'mobileStopPerfRecord' | 'mobileStopScreenRecording' | 'mobileStopSystemMonitor' | 'mobileStopXctestScreenRecording' | 'mobileSwipe' | 'mobileTap' | 'mobileTapWithNumberOfTaps' | 'mobileTerminateApp' | 'mobileTouchAndHold' | 'mobileTwoFingerTap' | 'mobileUpdateSafariPreferences' | 'mobileVoiceOverCurrentSpeech' | 'mobileVoiceOverMove' | 'reset' | 'setClipboard' | 'setValueImmediate' | 'startAudioRecording' | 'startRecordingScreen' | 'stopAudioRecording' | 'stopRecordingScreen' | 'submit' | 'toggleEnrollTouchId' | 'touchId' | 'unlock';
3
+ export type UiAutomator2AppiumCommand = 'background' | 'fingerprint' | 'getClipboard' | 'getCurrentActivity' | 'getCurrentPackage' | 'getDisplayDensity' | 'getLocation' | 'getLocationInView' | 'getPerformanceData' | 'getPerformanceDataTypes' | 'getSize' | 'getStrings' | 'getSystemBars' | 'getWindowSize' | 'gsmCall' | 'gsmSignal' | 'gsmVoice' | 'implicitWait' | 'isLocationServicesEnabled' | 'isLocked' | 'keyevent' | 'keys' | 'lock' | 'longPressKeyCode' | 'mobileAcceptAlert' | 'mobileBackgroundApp' | 'mobileBluetooth' | 'mobileBroadcast' | 'mobileChangePermissions' | 'mobileClearApp' | 'mobileClickGesture' | 'mobileDeepLink' | 'mobileDeleteFile' | 'mobileDeviceidle' | 'mobileDismissAlert' | 'mobileDoubleClickGesture' | 'mobileDragGesture' | 'mobileExecEmuConsoleCommand' | 'mobileFingerprint' | 'mobileFlingGesture' | 'mobileGetActionHistory' | 'mobileGetBatteryInfo' | 'mobileGetChromeCapabilities' | 'mobileGetConnectivity' | 'mobileGetContexts' | 'mobileGetDeclaredOrientation' | 'mobileGetDeviceInfo' | 'mobileGetDeviceTime' | 'mobileGetGeolocation' | 'mobileGetNotifications' | 'mobileGetPerformanceData' | 'mobileGetPermissions' | 'mobileGetUiMode' | 'mobileGsmCall' | 'mobileGsmSignal' | 'mobileGsmVoice' | 'mobileInjectEmulatorCameraImage' | 'mobileInstallApp' | 'mobileInstallMultipleApks' | 'mobileIsAppInstalled' | 'mobileIsMediaProjectionRecordingRunning' | 'mobileListApps' | 'mobileListDisplays' | 'mobileListSms' | 'mobileListWindows' | 'mobileLongClickGesture' | 'mobileNetworkSpeed' | 'mobileNfc' | 'mobilePerformEditorAction' | 'mobilePerformStatusBarCommand' | 'mobilePinchCloseGesture' | 'mobilePinchOpenGesture' | 'mobilePowerAc' | 'mobilePowerCapacity' | 'mobilePressKey' | 'mobileRefreshGpsCache' | 'mobileRemoveApp' | 'mobileReplaceElementValue' | 'mobileResetAccessibilityCache' | 'mobileResetGeolocation' | 'mobileScheduleAction' | 'mobileScreenshots' | 'mobileScroll' | 'mobileScrollBackTo' | 'mobileScrollGesture' | 'mobileSendSms' | 'mobileSendTrimMemory' | 'mobileSetConnectivity' | 'mobileSetGeolocation' | 'mobileSetUiMode' | 'mobileShell' | 'mobileStartActivity' | 'mobileStartLogsBroadcast' | 'mobileStartMediaProjectionRecording' | 'mobileStartScreenStreaming' | 'mobileStartService' | 'mobileStopLogsBroadcast' | 'mobileStopMediaProjectionRecording' | 'mobileStopScreenStreaming' | 'mobileStopService' | 'mobileSwipeGesture' | 'mobileTerminateApp' | 'mobileType' | 'mobileUnlock' | 'mobileUnscheduleAction' | 'mobileViewPortRect' | 'mobileViewportScreenshot' | 'networkSpeed' | 'openNotifications' | 'powerAC' | 'powerCapacity' | 'pressKeyCode' | 'replaceValue' | 'sendSMS' | 'sensorSet' | 'setClipboard' | 'setStylusHandwriting' | 'setValueImmediate' | 'startActivity' | 'startRecordingScreen' | 'stopRecordingScreen' | 'toggleData' | 'toggleFlightMode' | 'toggleLocationServices' | 'toggleWiFi' | 'unlock';
4
+ export type AppiumCommand = BaseDriverAppiumCommand | XCUITestAppiumCommand | UiAutomator2AppiumCommand;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
10
10
  const promises_1 = require("node:timers/promises");
11
11
  const zod_1 = require("zod");
12
12
  const turtleFetch_1 = require("../../utils/turtleFetch");
13
+ const appiumCommandSummary_1 = require("./appiumCommandSummary");
13
14
  const deviceRunSessionEvents_1 = require("./deviceRunSessionEvents");
14
15
  const APPIUM_REQUEST_TIMEOUT_MS = 10_000;
15
16
  const POLL_INTERVAL_MS = 1_000;
@@ -127,7 +128,7 @@ function createAppiumEventSource(eventFile) {
127
128
  type: 'operation.completed',
128
129
  operationId,
129
130
  durationMs: Math.max(0, command.endTime - command.startTime),
130
- summary: command.cmd,
131
+ summary: (0, appiumCommandSummary_1.humanizeAppiumCommand)(command.cmd),
131
132
  data: {
132
133
  command: command.cmd,
133
134
  appiumSessionId: command.appiumSessionId,
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { CustomBuildContext } from '../../customBuildContext';
4
4
  declare const ArgentArtifactSchema: z.ZodObject<{
5
5
  id: z.ZodString;
6
+ kind: z.ZodOptional<z.ZodString>;
6
7
  filename: z.ZodString;
7
8
  mimeType: z.ZodString;
8
9
  isDirectory: z.ZodOptional<z.ZodBoolean>;
@@ -21,11 +21,11 @@ const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
21
21
  const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
22
22
  const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
23
23
  const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
24
- // Kinds the EAS dashboard groups device run session artifacts by. Only media types the dashboard
25
- // renders specially are mapped; anything else stays unclassified and lands in its "Other" group.
26
- // Note that these kinds only affect grouping and labelling. The dashboard gates its inline video
27
- // player on the `__eas_screen_recording` metadata flag, which Argent artifacts deliberately do not
28
- // set, so a `screen-recording` kind here does not add a player to the session page.
24
+ // Fallback kinds for Argent versions that do not include a semantic artifact kind. Only media types
25
+ // the EAS dashboard renders specially can be inferred safely. Note that these kinds only affect
26
+ // grouping and labelling. The dashboard gates its inline video player on the
27
+ // `__eas_screen_recording` metadata flag, which Argent artifacts deliberately do not set, so a
28
+ // `screen-recording` kind here does not add a player to the session page.
29
29
  const ARGENT_ARTIFACT_KIND_BY_MIME_TYPE = new Map([
30
30
  ['image/png', 'screenshot'],
31
31
  ['image/jpeg', 'screenshot'],
@@ -34,6 +34,9 @@ const ARGENT_ARTIFACT_KIND_BY_MIME_TYPE = new Map([
34
34
  ]);
35
35
  const ArgentArtifactSchema = zod_1.z.object({
36
36
  id: zod_1.z.string(),
37
+ // Optional for compatibility with older Argent tool servers. Keep this open to new kinds so a
38
+ // newer Argent version can add a category without requiring an EAS worker release first.
39
+ kind: zod_1.z.string().optional(),
37
40
  filename: zod_1.z.string(),
38
41
  mimeType: zod_1.z.string(),
39
42
  isDirectory: zod_1.z.boolean().optional(),
@@ -42,8 +45,12 @@ const ArgentArtifactsListResponseSchema = zod_1.z.object({
42
45
  artifacts: zod_1.z.array(ArgentArtifactSchema),
43
46
  });
44
47
  function getArgentArtifactKind(artifact) {
48
+ if (artifact.kind) {
49
+ return artifact.kind;
50
+ }
45
51
  // Directories are repackaged as a tarball before upload, so the reported media type describes the
46
- // contents rather than the file we actually store.
52
+ // contents rather than the file we actually store. A semantic kind above remains valid because it
53
+ // describes what the artifact represents, not its transport format.
47
54
  if (artifact.isDirectory) {
48
55
  return undefined;
49
56
  }
@@ -73,7 +73,7 @@ const SERVE_SIM_PACKAGE_NAME = '@expo/serve-sim';
73
73
  const SERVE_SIM_HOST = '127.0.0.1';
74
74
  const SERVE_SIM_MAX_DIMENSION = '1280';
75
75
  const SERVE_SIM_MJPEG_QUALITY = '0.55';
76
- const SERVE_SIM_VIDEO_BITRATE = '3000000';
76
+ const SERVE_SIM_VIDEO_BITRATE = '6000000';
77
77
  const SERVE_SIM_VIDEO_FPS = '60';
78
78
  const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
79
79
  mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
@@ -0,0 +1 @@
1
+ export declare function formatSecondsForLog(totalSeconds: number): string;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatSecondsForLog = formatSecondsForLog;
4
+ function formatSecondsForLog(totalSeconds) {
5
+ const hours = Math.floor(totalSeconds / 3600);
6
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
7
+ const seconds = totalSeconds % 60;
8
+ const parts = [];
9
+ if (hours > 0) {
10
+ parts.push(hours === 1 ? '1 hour' : `${hours} hours`);
11
+ }
12
+ if (minutes > 0) {
13
+ parts.push(minutes === 1 ? '1 minute' : `${minutes} minutes`);
14
+ }
15
+ if (seconds > 0 || parts.length === 0) {
16
+ parts.push(seconds === 1 ? '1 second' : `${seconds} seconds`);
17
+ }
18
+ return parts.join(' ');
19
+ }
@@ -1,2 +1,9 @@
1
+ import { ChildProcess } from 'node:child_process';
2
+ export declare function isChildProcessAlive(child: ChildProcess): boolean;
3
+ /**
4
+ * Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
5
+ * children cannot survive after the parent is gone (e.g. across upterm redial).
6
+ */
7
+ export declare function killProcessGroup(child: ChildProcess): void;
1
8
  export declare function getParentAndDescendantProcessPidsAsync(ppid: number): Promise<number[]>;
2
9
  export declare function isProcessDescendantOfAsync(pid: number, ancestorPid: number): Promise<boolean>;
@@ -3,9 +3,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isChildProcessAlive = isChildProcessAlive;
7
+ exports.killProcessGroup = killProcessGroup;
6
8
  exports.getParentAndDescendantProcessPidsAsync = getParentAndDescendantProcessPidsAsync;
7
9
  exports.isProcessDescendantOfAsync = isProcessDescendantOfAsync;
8
10
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ function isChildProcessAlive(child) {
12
+ return child.exitCode === null && child.signalCode === null && !child.killed;
13
+ }
14
+ /**
15
+ * Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
16
+ * children cannot survive after the parent is gone (e.g. across upterm redial).
17
+ */
18
+ function killProcessGroup(child) {
19
+ if (child.pid == null) {
20
+ return;
21
+ }
22
+ try {
23
+ process.kill(-child.pid, 'SIGTERM');
24
+ }
25
+ catch {
26
+ child.kill();
27
+ }
28
+ }
9
29
  async function getChildrenPidsAsync(parentPids) {
10
30
  try {
11
31
  const result = await (0, turtle_spawn_1.default)('pgrep', ['-P', parentPids.join(',')], {
@@ -0,0 +1,28 @@
1
+ import { Job, SshSettings } from '@expo/eas-build-job';
2
+ import { bunyan } from '@expo/logger';
3
+ import { BuildContext } from '../context';
4
+ export type TurtleSshTarget = {
5
+ type: 'BUILD' | 'JOB_RUN';
6
+ id: string;
7
+ };
8
+ export declare function isSshEnabled(job: Pick<Job, 'ssh'>): boolean;
9
+ export declare function getSshIdleTimeoutSeconds(job: Pick<Job, 'ssh'>): number;
10
+ export declare function getSshRelayServerUrl(job: Pick<Job, 'ssh'>): string;
11
+ export type SshSessionHandle = {
12
+ getConnectedClientCountAsync: () => Promise<number>;
13
+ ensureConnectedAsync: () => Promise<void>;
14
+ stopAsync: () => Promise<void>;
15
+ };
16
+ export type StartedSshSession = {
17
+ handle: SshSessionHandle;
18
+ idleTimeoutSeconds: number;
19
+ };
20
+ export declare function startSshSessionAsync(ctx: BuildContext, { target, relayServerUrl, idleTimeoutSeconds: requestedIdleTimeoutSeconds, }: {
21
+ target: TurtleSshTarget;
22
+ } & SshSettings): Promise<StartedSshSession>;
23
+ export declare function superviseSshSessionAsync({ handle, idleTimeoutSeconds, hasJobFinished, logger, }: {
24
+ handle: SshSessionHandle;
25
+ idleTimeoutSeconds: number;
26
+ hasJobFinished: () => boolean;
27
+ logger: bunyan;
28
+ }): Promise<void>;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSshEnabled = isSshEnabled;
4
+ exports.getSshIdleTimeoutSeconds = getSshIdleTimeoutSeconds;
5
+ exports.getSshRelayServerUrl = getSshRelayServerUrl;
6
+ exports.startSshSessionAsync = startSshSessionAsync;
7
+ exports.superviseSshSessionAsync = superviseSshSessionAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
+ const gql_tada_1 = require("gql.tada");
10
+ const formatDuration_1 = require("./formatDuration");
11
+ const retry_1 = require("./retry");
12
+ const upterm_1 = require("./upterm");
13
+ const sentry_1 = require("../sentry");
14
+ const MAX_SSH_REDIALS = 10;
15
+ const REDIAL_BACKOFF_MS = 6_000;
16
+ const CLIENT_COUNT_POLL_INTERVAL_MS = 5_000;
17
+ const MAX_SSH_IDLE_TIMEOUT_SECONDS = 3600;
18
+ const DEFAULT_SSH_IDLE_TIMEOUT_SECONDS = 0;
19
+ const CREATE_OR_UPDATE_TURTLE_SSH_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
20
+ mutation CreateOrUpdateTurtleSshSession(
21
+ $target: TurtleSshTargetInput!
22
+ $connectionConfig: TurtleSshConnectionConfigInput!
23
+ $sessionSettings: TurtleSshSessionSettingsInput!
24
+ ) {
25
+ turtleSshSession {
26
+ createOrUpdateTurtleSshSession(
27
+ target: $target
28
+ connectionConfig: $connectionConfig
29
+ sessionSettings: $sessionSettings
30
+ ) {
31
+ id
32
+ sessionSettings {
33
+ idleTimeoutSeconds
34
+ }
35
+ }
36
+ }
37
+ }
38
+ `);
39
+ function isSshEnabled(job) {
40
+ return job.ssh != null;
41
+ }
42
+ function getSshIdleTimeoutSeconds(job) {
43
+ const idleTimeoutSeconds = job.ssh?.idleTimeoutSeconds ?? DEFAULT_SSH_IDLE_TIMEOUT_SECONDS;
44
+ if (!Number.isInteger(idleTimeoutSeconds) ||
45
+ idleTimeoutSeconds < 0 ||
46
+ idleTimeoutSeconds > MAX_SSH_IDLE_TIMEOUT_SECONDS) {
47
+ throw new eas_build_job_1.SystemError(`SSH idle timeout must be an integer between 0 and ${MAX_SSH_IDLE_TIMEOUT_SECONDS} seconds, got ${idleTimeoutSeconds}.`, { trackingCode: 'SSH_IDLE_TIMEOUT_INVALID' });
48
+ }
49
+ return idleTimeoutSeconds;
50
+ }
51
+ function getSshRelayServerUrl(job) {
52
+ const relayServerUrl = job.ssh?.relayServerUrl;
53
+ if (!relayServerUrl) {
54
+ throw new eas_build_job_1.SystemError('SSH is enabled but no relay server URL was configured on the job.', {
55
+ trackingCode: 'SSH_RELAY_SERVER_URL_MISSING',
56
+ });
57
+ }
58
+ return relayServerUrl;
59
+ }
60
+ async function createOrUpdateSessionAsync(ctx, { target, connectionConfig, idleTimeoutSeconds, }) {
61
+ const result = await ctx.graphqlClient
62
+ .mutation(CREATE_OR_UPDATE_TURTLE_SSH_SESSION_MUTATION, {
63
+ target,
64
+ connectionConfig: {
65
+ ...connectionConfig,
66
+ type: 'UPTERM_V1',
67
+ },
68
+ sessionSettings: { idleTimeoutSeconds },
69
+ })
70
+ .toPromise();
71
+ if (result.error || !result.data) {
72
+ throw new eas_build_job_1.SystemError(`Failed to create or update the SSH session: ${result.error?.message ?? 'no data returned'}`, { cause: result.error });
73
+ }
74
+ const session = result.data.turtleSshSession.createOrUpdateTurtleSshSession;
75
+ return { idleTimeoutSeconds: session.sessionSettings.idleTimeoutSeconds };
76
+ }
77
+ async function startSshSessionAsync(ctx, { target, relayServerUrl, idleTimeoutSeconds: requestedIdleTimeoutSeconds, }) {
78
+ const logger = ctx.logger;
79
+ const host = await (0, upterm_1.startUptermHostAsync)(ctx, { relayServerUrl });
80
+ let idleTimeoutSeconds;
81
+ try {
82
+ ({ idleTimeoutSeconds } = await createOrUpdateSessionAsync(ctx, {
83
+ target,
84
+ connectionConfig: { ...host.connectionConfig, reconnecting: false },
85
+ idleTimeoutSeconds: requestedIdleTimeoutSeconds,
86
+ }));
87
+ }
88
+ catch (err) {
89
+ await host.stopAsync().catch(() => { });
90
+ throw err;
91
+ }
92
+ const ensureConnectedAsync = async () => {
93
+ if (host.isAlive()) {
94
+ return;
95
+ }
96
+ for (let attempt = 1; attempt <= MAX_SSH_REDIALS; attempt++) {
97
+ try {
98
+ if (!host.isAlive()) {
99
+ logger.warn('The SSH relay connection dropped. Reconnecting...');
100
+ await createOrUpdateSessionAsync(ctx, {
101
+ target,
102
+ connectionConfig: { ...host.connectionConfig, reconnecting: true },
103
+ idleTimeoutSeconds: requestedIdleTimeoutSeconds,
104
+ }).catch(() => { });
105
+ await host.redialAsync();
106
+ }
107
+ await createOrUpdateSessionAsync(ctx, {
108
+ target,
109
+ connectionConfig: { ...host.connectionConfig, reconnecting: false },
110
+ idleTimeoutSeconds: requestedIdleTimeoutSeconds,
111
+ });
112
+ logger.info('The SSH relay connection was restored.');
113
+ return;
114
+ }
115
+ catch (err) {
116
+ logger.warn({ err }, `SSH reconnect attempt ${attempt} of ${MAX_SSH_REDIALS} failed.`);
117
+ if (attempt < MAX_SSH_REDIALS) {
118
+ await (0, retry_1.sleepAsync)(REDIAL_BACKOFF_MS);
119
+ }
120
+ }
121
+ }
122
+ throw new eas_build_job_1.SystemError(`The SSH relay connection dropped and could not be restored after ${MAX_SSH_REDIALS} attempts.`);
123
+ };
124
+ return {
125
+ handle: {
126
+ getConnectedClientCountAsync: () => host.getConnectedClientCountAsync(),
127
+ ensureConnectedAsync,
128
+ stopAsync: () => host.stopAsync(),
129
+ },
130
+ idleTimeoutSeconds,
131
+ };
132
+ }
133
+ async function superviseSshSessionAsync({ handle, idleTimeoutSeconds, hasJobFinished, logger, }) {
134
+ const idleTimeoutMs = idleTimeoutSeconds * 1_000;
135
+ let idleSince = null;
136
+ let previousClientCount = 0;
137
+ for (;;) {
138
+ try {
139
+ await handle.ensureConnectedAsync();
140
+ }
141
+ catch (err) {
142
+ logger.warn({ err }, 'Could not restore the SSH relay connection. Closing the session.');
143
+ sentry_1.Sentry.capture('Could not restore the SSH relay connection', err instanceof Error ? err : undefined, {
144
+ tags: { trackingCode: 'SSH_RELAY_RECONNECT_FAILED' },
145
+ });
146
+ return;
147
+ }
148
+ let connectedClientCount;
149
+ try {
150
+ connectedClientCount = await handle.getConnectedClientCountAsync();
151
+ }
152
+ catch (err) {
153
+ logger.warn({ err }, 'Could not read the SSH client count. Closing the session.');
154
+ sentry_1.Sentry.capture('Could not read the SSH client count', err instanceof Error ? err : undefined, {
155
+ tags: { trackingCode: 'SSH_CLIENT_COUNT_UNREADABLE' },
156
+ });
157
+ return;
158
+ }
159
+ if (connectedClientCount !== previousClientCount) {
160
+ logger.info(`SSH clients connected: ${connectedClientCount}`);
161
+ previousClientCount = connectedClientCount;
162
+ }
163
+ const jobHasFinished = hasJobFinished();
164
+ if (connectedClientCount > 0 || !jobHasFinished) {
165
+ idleSince = null;
166
+ }
167
+ else if (idleSince === null) {
168
+ idleSince = Date.now();
169
+ }
170
+ if (jobHasFinished && connectedClientCount === 0) {
171
+ if (idleTimeoutSeconds === 0) {
172
+ logger.info('The job finished and no SSH client is connected. Closing the session.');
173
+ return;
174
+ }
175
+ if (idleSince !== null && Date.now() - idleSince >= idleTimeoutMs) {
176
+ logger.info(`No SSH client connected for ${(0, formatDuration_1.formatSecondsForLog)(idleTimeoutSeconds)} after the job finished. Closing the session.`);
177
+ return;
178
+ }
179
+ }
180
+ await (0, retry_1.sleepAsync)(CLIENT_COUNT_POLL_INTERVAL_MS);
181
+ }
182
+ }
@@ -0,0 +1,38 @@
1
+ import { Env } from '@expo/eas-build-job';
2
+ import { z } from 'zod';
3
+ import { BuildContext } from '../context';
4
+ export declare function resolveUptermGcsObjectName(platform?: NodeJS.Platform, arch?: string): string;
5
+ export type SshConnectionConfig = {
6
+ type: 'upterm-v1';
7
+ host: string;
8
+ secret: string;
9
+ };
10
+ export type UptermHost = {
11
+ connectionConfig: SshConnectionConfig;
12
+ getConnectedClientCountAsync: () => Promise<number>;
13
+ isAlive: () => boolean;
14
+ redialAsync: () => Promise<SshConnectionConfig>;
15
+ stopAsync: () => Promise<void>;
16
+ };
17
+ declare const UptermSessionJsonZ: z.ZodObject<{
18
+ sessionId: z.ZodString;
19
+ host: z.ZodString;
20
+ clientCount: z.ZodOptional<z.ZodNumber>;
21
+ }, z.core.$strip>;
22
+ type UptermSessionJson = z.infer<typeof UptermSessionJsonZ>;
23
+ export declare function connectionConfigFromUptermSession(parsed: Pick<UptermSessionJson, 'sessionId' | 'host'>): SshConnectionConfig | null;
24
+ /**
25
+ * Strip upterm session secrets out of text before it reaches a log or an error message. upterm
26
+ * prints the session id as the userinfo of the connect line it advertises
27
+ * (`upterm proxy ws(s)://<sessionId>@host`) and repeats it bare as the ssh destination on the
28
+ * same line. Control characters are dropped first so they cannot split a token mid-match. The
29
+ * loop lifts the id out of the proxy URL and removes every occurrence of it; the final replace
30
+ * blanks userinfo in any other URL as a catch-all.
31
+ */
32
+ export declare function redactConnectionSecrets(text: string): string;
33
+ export declare function redactSpawnErrorForLog(err: unknown): unknown;
34
+ export declare function resolveUptermPathAsync(env: Env): Promise<string>;
35
+ export declare function startUptermHostAsync(ctx: BuildContext, { relayServerUrl }: {
36
+ relayServerUrl: string;
37
+ }): Promise<UptermHost>;
38
+ export {};
@@ -0,0 +1,257 @@
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.resolveUptermGcsObjectName = resolveUptermGcsObjectName;
7
+ exports.connectionConfigFromUptermSession = connectionConfigFromUptermSession;
8
+ exports.redactConnectionSecrets = redactConnectionSecrets;
9
+ exports.redactSpawnErrorForLog = redactSpawnErrorForLog;
10
+ exports.resolveUptermPathAsync = resolveUptermPathAsync;
11
+ exports.startUptermHostAsync = startUptermHostAsync;
12
+ const eas_build_job_1 = require("@expo/eas-build-job");
13
+ const downloader_1 = __importDefault(require("@expo/downloader"));
14
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
15
+ const promises_1 = __importDefault(require("node:fs/promises"));
16
+ const node_os_1 = __importDefault(require("node:os"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const zod_1 = require("zod");
19
+ const processes_1 = require("./processes");
20
+ const retry_1 = require("./retry");
21
+ const CONTROL_CHARACTERS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
22
+ function resolveUptermGcsObjectName(platform = process.platform, arch = process.arch) {
23
+ if (platform === 'darwin' && arch === 'arm64') {
24
+ return 'upterm-darwin-arm64';
25
+ }
26
+ if (platform === 'linux' && arch === 'x64') {
27
+ return 'upterm-linux-amd64';
28
+ }
29
+ throw new eas_build_job_1.SystemError(`SSH upterm is only available on darwin/arm64 and linux/x64 (got ${platform}/${arch}).`);
30
+ }
31
+ const UPTERM_GCS_BASE_URL = 'https://storage.googleapis.com/turtle-v2/upterm';
32
+ const UPTERM_DOWNLOAD_TIMEOUT_MS = 60_000;
33
+ const UPTERM_KEEPALIVE_SLEEP_SECONDS = 6 * 60 * 60;
34
+ const CONNECTION_POLL_INTERVAL_MS = 500;
35
+ const CONNECTION_STARTUP_TIMEOUT_MS = 60_000;
36
+ const PROCESS_EXIT_TIMEOUT_MS = 5_000;
37
+ const CLIENT_COUNT_READ_ATTEMPTS = 4;
38
+ const CLIENT_COUNT_READ_RETRY_MS = 500;
39
+ const DEFAULT_SSH_PORT = '22';
40
+ const UptermSessionJsonZ = zod_1.z.object({
41
+ sessionId: zod_1.z.string().min(1),
42
+ host: zod_1.z.string().min(1),
43
+ clientCount: zod_1.z.number().optional(),
44
+ });
45
+ function connectionConfigFromUptermSession(parsed) {
46
+ let host = parsed.host;
47
+ if (host.includes('://')) {
48
+ let url;
49
+ try {
50
+ url = new URL(host);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ host = url.port && url.port !== DEFAULT_SSH_PORT ? `${url.hostname}:${url.port}` : url.hostname;
56
+ }
57
+ if (!host) {
58
+ return null;
59
+ }
60
+ return { type: 'upterm-v1', host, secret: parsed.sessionId };
61
+ }
62
+ /**
63
+ * Strip upterm session secrets out of text before it reaches a log or an error message. upterm
64
+ * prints the session id as the userinfo of the connect line it advertises
65
+ * (`upterm proxy ws(s)://<sessionId>@host`) and repeats it bare as the ssh destination on the
66
+ * same line. Control characters are dropped first so they cannot split a token mid-match. The
67
+ * loop lifts the id out of the proxy URL and removes every occurrence of it; the final replace
68
+ * blanks userinfo in any other URL as a catch-all.
69
+ */
70
+ function redactConnectionSecrets(text) {
71
+ let redacted = text.replace(CONTROL_CHARACTERS, '');
72
+ for (const [, token] of redacted.matchAll(/upterm proxy wss?:\/\/([^@\s]+)@/g)) {
73
+ redacted = redacted.split(token).join('<redacted>');
74
+ }
75
+ return redacted.replace(/([a-z][a-z0-9+.-]*:\/\/)[^@\s/]+@/gi, '$1<redacted>@');
76
+ }
77
+ function redactSpawnErrorForLog(err) {
78
+ if (!err || typeof err !== 'object') {
79
+ return err;
80
+ }
81
+ const spawnErr = err;
82
+ return {
83
+ ...spawnErr,
84
+ ...(typeof spawnErr.message === 'string'
85
+ ? { message: redactConnectionSecrets(spawnErr.message) }
86
+ : {}),
87
+ ...(typeof spawnErr.stdout === 'string'
88
+ ? { stdout: redactConnectionSecrets(spawnErr.stdout) }
89
+ : {}),
90
+ ...(typeof spawnErr.stderr === 'string'
91
+ ? { stderr: redactConnectionSecrets(spawnErr.stderr) }
92
+ : {}),
93
+ };
94
+ }
95
+ async function resolveUptermPathAsync(env) {
96
+ try {
97
+ await (0, turtle_spawn_1.default)('upterm', ['version'], { stdio: 'pipe', env });
98
+ return 'upterm';
99
+ }
100
+ catch { }
101
+ const objectName = resolveUptermGcsObjectName();
102
+ const downloadDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-upterm-'));
103
+ const uptermPath = node_path_1.default.join(downloadDir, objectName);
104
+ const url = `${UPTERM_GCS_BASE_URL}/${objectName}`;
105
+ try {
106
+ await (0, downloader_1.default)(url, uptermPath, { retry: 3, timeout: UPTERM_DOWNLOAD_TIMEOUT_MS });
107
+ await promises_1.default.chmod(uptermPath, 0o755);
108
+ }
109
+ catch (err) {
110
+ await promises_1.default.rm(downloadDir, { recursive: true, force: true }).catch(() => { });
111
+ throw new eas_build_job_1.SystemError(`The upterm SSH client was not on PATH and could not be downloaded from ${url}. ${err instanceof Error ? err.message : String(err)}`);
112
+ }
113
+ return uptermPath;
114
+ }
115
+ async function findAdminSocketPathAsync(uptermSocketDir) {
116
+ // Use this dial's own admin socket, not upterm's default, which can still point at a previous
117
+ // dial's session after a redial and break client-count reads.
118
+ const entries = await promises_1.default.readdir(uptermSocketDir).catch(() => []);
119
+ const socketName = entries.find(entry => entry.endsWith('.sock'));
120
+ return socketName ? node_path_1.default.join(uptermSocketDir, socketName) : null;
121
+ }
122
+ async function readCurrentSessionJsonAsync(uptermPath, adminSocketPath) {
123
+ try {
124
+ const result = await (0, turtle_spawn_1.default)(uptermPath, ['session', 'current', '--admin-socket', adminSocketPath, '--output', 'json'], { stdio: 'pipe' });
125
+ const parsed = UptermSessionJsonZ.safeParse(JSON.parse(result.stdout));
126
+ return parsed.success ? parsed.data : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ async function waitForConnectionConfigAsync(uptermPath, uptermSocketDir, getHostOutput) {
133
+ const deadline = Date.now() + CONNECTION_STARTUP_TIMEOUT_MS;
134
+ for (;;) {
135
+ const adminSocketPath = await findAdminSocketPathAsync(uptermSocketDir);
136
+ if (adminSocketPath) {
137
+ const session = await readCurrentSessionJsonAsync(uptermPath, adminSocketPath);
138
+ if (session) {
139
+ const connectionConfig = connectionConfigFromUptermSession(session);
140
+ if (connectionConfig) {
141
+ return connectionConfig;
142
+ }
143
+ }
144
+ }
145
+ if (Date.now() >= deadline) {
146
+ throw new eas_build_job_1.SystemError(`The upterm client did not register with the relay within ${CONNECTION_STARTUP_TIMEOUT_MS / 1_000}s. Output:\n${redactConnectionSecrets(getHostOutput())}`);
147
+ }
148
+ await (0, retry_1.sleepAsync)(CONNECTION_POLL_INTERVAL_MS);
149
+ }
150
+ }
151
+ async function startUptermHostAsync(ctx, { relayServerUrl }) {
152
+ const uptermPath = await resolveUptermPathAsync(ctx.env);
153
+ const stateDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-ssh-'));
154
+ const hostKeyPath = node_path_1.default.join(stateDir, 'id_host');
155
+ const forceCommandPath = node_path_1.default.join(stateDir, 'join.sh');
156
+ const uptermSocketDir = node_path_1.default.join(stateDir, 'upterm');
157
+ await (0, turtle_spawn_1.default)('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', hostKeyPath, '-q'], {
158
+ logger: ctx.logger,
159
+ });
160
+ await promises_1.default.writeFile(forceCommandPath, '#!/usr/bin/env bash\nexec bash -l\n', { mode: 0o755 });
161
+ let currentProcess = null;
162
+ const stopCurrentProcessAsync = async () => {
163
+ const previousProcess = currentProcess;
164
+ currentProcess = null;
165
+ if (!previousProcess) {
166
+ return;
167
+ }
168
+ (0, processes_1.killProcessGroup)(previousProcess.child);
169
+ await Promise.race([
170
+ previousProcess.catch(() => { }),
171
+ (0, retry_1.sleepAsync)(PROCESS_EXIT_TIMEOUT_MS).then(() => {
172
+ ctx.logger.debug('The previous upterm host process did not exit in time.');
173
+ }),
174
+ ]);
175
+ };
176
+ const getConnectedClientCountAsync = async () => {
177
+ for (let attempt = 1; attempt <= CLIENT_COUNT_READ_ATTEMPTS; attempt++) {
178
+ const adminSocketPath = await findAdminSocketPathAsync(uptermSocketDir);
179
+ const session = adminSocketPath
180
+ ? await readCurrentSessionJsonAsync(uptermPath, adminSocketPath)
181
+ : null;
182
+ if (session && typeof session.clientCount === 'number') {
183
+ return session.clientCount;
184
+ }
185
+ if (attempt < CLIENT_COUNT_READ_ATTEMPTS) {
186
+ await (0, retry_1.sleepAsync)(CLIENT_COUNT_READ_RETRY_MS);
187
+ }
188
+ }
189
+ throw new eas_build_job_1.SystemError('Could not read the SSH client count from the upterm admin socket.', {
190
+ trackingCode: 'SSH_CLIENT_COUNT_UNREADABLE',
191
+ });
192
+ };
193
+ const dialAsync = async () => {
194
+ await stopCurrentProcessAsync();
195
+ await promises_1.default.rm(uptermSocketDir, { recursive: true, force: true }).catch(err => {
196
+ ctx.logger.debug({ err }, 'Failed to clear the previous SSH socket directory.');
197
+ });
198
+ ctx.logger.debug('Connecting to the SSH relay.');
199
+ // --force-command is what each connecting SSH client runs (a login shell). The `sleep` after
200
+ // `--` is the host-side process that keeps `upterm host` up while nobody is connected.
201
+ const uptermProcess = (0, turtle_spawn_1.default)(uptermPath, [
202
+ 'host',
203
+ '--server',
204
+ relayServerUrl,
205
+ '--accept',
206
+ '--skip-host-key-check',
207
+ '-i',
208
+ hostKeyPath,
209
+ '--force-command',
210
+ forceCommandPath,
211
+ '--',
212
+ 'bash',
213
+ '-lc',
214
+ `sleep ${UPTERM_KEEPALIVE_SLEEP_SECONDS}`,
215
+ ], {
216
+ // upterm puts its admin socket under XDG_RUNTIME_DIR; point it at our state dir so we can
217
+ // find it for `session current` and clean it up on stop/redial.
218
+ env: { ...ctx.env, XDG_RUNTIME_DIR: stateDir },
219
+ stdio: ['ignore', 'pipe', 'pipe'],
220
+ detached: true,
221
+ });
222
+ uptermProcess.catch(err => ctx.logger.debug({ err: redactSpawnErrorForLog(err) }, 'The upterm host process exited.'));
223
+ uptermProcess.child.unref();
224
+ currentProcess = uptermProcess;
225
+ let output = '';
226
+ const appendChunk = (chunk) => {
227
+ output += chunk.toString();
228
+ };
229
+ uptermProcess.child.stdout?.on('data', appendChunk);
230
+ uptermProcess.child.stderr?.on('data', appendChunk);
231
+ return await waitForConnectionConfigAsync(uptermPath, uptermSocketDir, () => output);
232
+ };
233
+ const stopAsync = async () => {
234
+ await stopCurrentProcessAsync();
235
+ await promises_1.default.rm(stateDir, { recursive: true, force: true });
236
+ };
237
+ let connectionConfig;
238
+ try {
239
+ connectionConfig = await dialAsync();
240
+ }
241
+ catch (err) {
242
+ await stopAsync();
243
+ throw err;
244
+ }
245
+ return {
246
+ get connectionConfig() {
247
+ return connectionConfig;
248
+ },
249
+ getConnectedClientCountAsync,
250
+ isAlive: () => currentProcess != null && (0, processes_1.isChildProcessAlive)(currentProcess.child),
251
+ redialAsync: async () => {
252
+ connectionConfig = await dialAsync();
253
+ return connectionConfig;
254
+ },
255
+ stopAsync,
256
+ };
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "22.4.0",
3
+ "version": "22.6.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -25,6 +25,7 @@
25
25
  "build": "tsc",
26
26
  "build:record-sim": "mkdir -p bin && record_sim_bin_path=$(swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build --show-bin-path) && swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build && cp \"$record_sim_bin_path/record-sim\" bin/record-sim && chmod +x bin/record-sim",
27
27
  "typecheck": "tsc",
28
+ "generate-appium-commands": "mise exec node@22.22.0 -- node scripts/generate-appium-commands.js",
28
29
  "prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
29
30
  "jest-unit": "jest --config jest/unit-config.ts",
30
31
  "jest-integration": "jest --config jest/integration-config.ts",
@@ -102,5 +103,5 @@
102
103
  "typescript": "^5.5.4",
103
104
  "uuid": "^9.0.1"
104
105
  },
105
- "gitHead": "39c39ad4bfdd1e7e74417bb6f7178b011ce61bbe"
106
+ "gitHead": "89fe2cb3e1bba6e153752f1acf26b7817974b4a4"
106
107
  }