@expo/build-tools 24.3.0 → 24.5.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 (33) hide show
  1. package/dist/common/projectSources.js +3 -0
  2. package/dist/steps/functions/startIosSimulator.js +34 -2
  3. package/dist/steps/functions/startLocalEgress.d.ts +4 -3
  4. package/dist/steps/functions/startLocalEgress.js +10 -6
  5. package/dist/steps/functions/startSandbox.js +5 -2
  6. package/dist/steps/utils/localEgress.d.ts +24 -0
  7. package/dist/steps/utils/localEgress.js +59 -1
  8. package/dist/steps/utils/localEgressGuard.d.ts +179 -0
  9. package/dist/steps/utils/localEgressGuard.js +537 -0
  10. package/dist/steps/utils/localEgressSession.js +4 -0
  11. package/dist/steps/utils/sandboxCommandImplementations.d.ts +12 -0
  12. package/dist/steps/utils/sandboxCommandImplementations.js +33 -0
  13. package/dist/steps/utils/sandboxDaemon.d.ts +2 -0
  14. package/dist/steps/utils/sandboxDaemon.js +70 -3
  15. package/dist/steps/utils/shellSessionManager.d.ts +29 -0
  16. package/dist/steps/utils/shellSessionManager.js +269 -0
  17. package/dist/utils/IosSimulatorUtils.d.ts +34 -0
  18. package/dist/utils/IosSimulatorUtils.js +62 -0
  19. package/dist/utils/processes.d.ts +4 -1
  20. package/dist/utils/processes.js +7 -4
  21. package/package.json +7 -4
  22. package/resources/egress-guard/README.md +83 -0
  23. package/resources/egress-guard/build.sh +30 -0
  24. package/resources/egress-guard/check.c +112 -0
  25. package/resources/egress-guard/guard.c +274 -0
  26. package/resources/egress-guard/policy.c +164 -0
  27. package/resources/egress-guard/policy.h +60 -0
  28. package/resources/egress-guard/tests/guard_insert_test.c +291 -0
  29. package/resources/egress-guard/tests/guard_test.c +177 -0
  30. package/resources/egress-guard/tests/nettest.swift +172 -0
  31. package/resources/egress-guard/tests/policy_test.c +143 -0
  32. package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
  33. package/resources/egress-guard/tests/run-policy-tests.sh +7 -0
@@ -7,6 +7,7 @@ exports.startSandboxDaemonAsync = startSandboxDaemonAsync;
7
7
  const eas_build_job_1 = require("@expo/eas-build-job");
8
8
  const promises_1 = require("node:timers/promises");
9
9
  const ws_1 = __importDefault(require("ws"));
10
+ const sandboxCommandImplementations_1 = require("./sandboxCommandImplementations");
10
11
  async function startSandboxDaemonAsync(options) {
11
12
  options.signal?.throwIfAborted();
12
13
  let socket;
@@ -14,6 +15,11 @@ async function startSandboxDaemonAsync(options) {
14
15
  let hasConnected = false;
15
16
  let resolveConnected;
16
17
  let rejectConnected;
18
+ const { commandImplementations, stoppedPromise: commandsStoppedPromise } = (0, sandboxCommandImplementations_1.createSandboxCommandImplementations)({
19
+ workingDirectory: options.workingDirectory,
20
+ env: options.env,
21
+ signal: abortController.signal,
22
+ });
17
23
  const connected = new Promise((resolve, reject) => {
18
24
  resolveConnected = resolve;
19
25
  rejectConnected = reject;
@@ -26,15 +32,39 @@ async function startSandboxDaemonAsync(options) {
26
32
  const connectionLoop = (async () => {
27
33
  while (!abortController.signal.aborted) {
28
34
  try {
29
- socket = new ws_1.default(new URL('/sandbox/connect', options.serverUrl), {
35
+ const connectedSocket = new ws_1.default(new URL('/sandbox/connect', options.serverUrl), {
30
36
  handshakeTimeout: 10_000,
31
37
  headers: { Authorization: `Bearer ${options.credential}` },
32
38
  });
33
- await waitForOpen(socket);
39
+ socket = connectedSocket;
40
+ await waitForOpen(connectedSocket);
34
41
  options.logger.info('Sandbox MCP server connected.');
42
+ connectedSocket.on('message', async (message) => {
43
+ try {
44
+ const response = await handleMessageAsync(commandImplementations, message.toString());
45
+ // MCP fails pending calls on disconnect. Responses belong to this socket only.
46
+ if (connectedSocket.readyState !== ws_1.default.OPEN) {
47
+ options.logger.warn('Sandbox command response was lost after disconnect. The command may have run and output may have been consumed.');
48
+ return;
49
+ }
50
+ await new Promise((resolve, reject) => {
51
+ connectedSocket.send(JSON.stringify(response), error => {
52
+ if (error) {
53
+ reject(error);
54
+ }
55
+ else {
56
+ resolve();
57
+ }
58
+ });
59
+ });
60
+ }
61
+ catch (error) {
62
+ options.logger.warn({ err: error }, 'Could not send sandbox command response.');
63
+ }
64
+ });
35
65
  hasConnected = true;
36
66
  resolveConnected();
37
- await waitForClose(socket);
67
+ await waitForClose(connectedSocket);
38
68
  }
39
69
  catch (error) {
40
70
  if (!hasConnected) {
@@ -68,6 +98,7 @@ async function startSandboxDaemonAsync(options) {
68
98
  async stopAsync() {
69
99
  options.signal?.removeEventListener('abort', stop);
70
100
  stop();
101
+ await commandsStoppedPromise;
71
102
  await connectionLoop;
72
103
  },
73
104
  };
@@ -88,3 +119,39 @@ function waitForClose(socket) {
88
119
  socket.once('error', reject);
89
120
  });
90
121
  }
122
+ async function handleMessageAsync(commandImplementations, message) {
123
+ let rawRequest;
124
+ try {
125
+ rawRequest = JSON.parse(message);
126
+ }
127
+ catch {
128
+ return { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } };
129
+ }
130
+ const request = eas_build_job_1.SandboxDaemonRequestZ.safeParse(rawRequest);
131
+ if (!request.success) {
132
+ return { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid request' } };
133
+ }
134
+ const { id, method, params } = request.data;
135
+ if (!Object.hasOwn(eas_build_job_1.SandboxDaemonCommands, method)) {
136
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } };
137
+ }
138
+ const commandMethod = method;
139
+ const parsedParams = eas_build_job_1.SandboxDaemonCommands[commandMethod].params.safeParse(params);
140
+ if (!parsedParams.success) {
141
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params' } };
142
+ }
143
+ try {
144
+ return {
145
+ jsonrpc: '2.0',
146
+ id,
147
+ result: await commandImplementations[commandMethod](parsedParams.data),
148
+ };
149
+ }
150
+ catch (error) {
151
+ return {
152
+ jsonrpc: '2.0',
153
+ id,
154
+ error: { code: -32603, message: error instanceof Error ? error.message : 'Internal error' },
155
+ };
156
+ }
157
+ }
@@ -0,0 +1,29 @@
1
+ export declare class ShellSessionManager {
2
+ private readonly options;
3
+ private readonly sessions;
4
+ private nextSessionId;
5
+ readonly stoppedPromise: Promise<void>;
6
+ constructor(options: {
7
+ workingDirectory: string;
8
+ env: NodeJS.ProcessEnv;
9
+ signal: AbortSignal;
10
+ });
11
+ startAsync({ cmd, workdir, tty, }: {
12
+ cmd: string;
13
+ workdir?: string;
14
+ tty?: boolean;
15
+ }): Promise<number>;
16
+ write(sessionId: number, chars: string): void;
17
+ readAsync(sessionId: number, yieldTimeMs: number): Promise<{
18
+ output: string;
19
+ } & ({
20
+ exitCode: number;
21
+ } | {
22
+ terminationSignal: string;
23
+ } | {
24
+ sessionId: number;
25
+ })>;
26
+ private stopAsync;
27
+ private throwIfStopped;
28
+ private getSession;
29
+ }
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ShellSessionManager = void 0;
40
+ const pty = __importStar(require("node-pty"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const promises_1 = __importDefault(require("node:fs/promises"));
43
+ const node_os_1 = require("node:os");
44
+ const node_path_1 = __importDefault(require("node:path"));
45
+ const processes_1 = require("../../utils/processes");
46
+ const PROCESS_STOP_GRACE_PERIOD_MS = 1_000;
47
+ const SIGNAL_NAMES = new Map(Object.entries(node_os_1.constants.signals)
48
+ .reverse()
49
+ .map(([name, number]) => [number, name]));
50
+ class ShellSessionManager {
51
+ options;
52
+ // Keep completed sessions so callers can still read their output and exit status.
53
+ sessions = new Map();
54
+ nextSessionId = 1;
55
+ stoppedPromise;
56
+ constructor(options) {
57
+ this.options = options;
58
+ this.stoppedPromise = (async () => {
59
+ if (!options.signal.aborted) {
60
+ await new Promise(resolve => {
61
+ options.signal.addEventListener('abort', () => resolve(), { once: true });
62
+ });
63
+ }
64
+ await this.stopAsync();
65
+ })();
66
+ // Cleanup can fail before the owner awaits it.
67
+ this.stoppedPromise.catch(() => { });
68
+ }
69
+ async startAsync({ cmd, workdir, tty, }) {
70
+ this.throwIfStopped();
71
+ const workingDirectory = workdir
72
+ ? node_path_1.default.resolve(this.options.workingDirectory, workdir)
73
+ : this.options.workingDirectory;
74
+ await validateWorkingDirectoryAsync(workingDirectory);
75
+ this.throwIfStopped();
76
+ const sessionId = this.nextSessionId++;
77
+ const session = tty
78
+ ? startPtyCommand({ command: cmd, workingDirectory, env: this.options.env })
79
+ : startPipeCommand({ command: cmd, workingDirectory, env: this.options.env });
80
+ this.sessions.set(sessionId, session);
81
+ return sessionId;
82
+ }
83
+ write(sessionId, chars) {
84
+ this.throwIfStopped();
85
+ const session = this.getSession(sessionId);
86
+ if (!session.isCompleted && !session.error) {
87
+ session.write(chars);
88
+ }
89
+ }
90
+ async readAsync(sessionId, yieldTimeMs) {
91
+ this.throwIfStopped();
92
+ const session = this.getSession(sessionId);
93
+ await waitForSessionsAsync([session], yieldTimeMs);
94
+ if (session.error) {
95
+ throw session.error;
96
+ }
97
+ const output = session.output;
98
+ session.output = '';
99
+ const result = {
100
+ output,
101
+ };
102
+ if (session.terminationSignal !== undefined) {
103
+ return { ...result, terminationSignal: session.terminationSignal };
104
+ }
105
+ if (session.exitCode !== undefined) {
106
+ return { ...result, exitCode: session.exitCode };
107
+ }
108
+ return { ...result, sessionId };
109
+ }
110
+ async stopAsync() {
111
+ const sessions = [...this.sessions.values()];
112
+ // Only signal groups while their leaders are alive. After a leader exits, its PID may
113
+ // be reused. Any surviving descendants are left to VM teardown.
114
+ for (const session of sessions) {
115
+ if (!session.hasLeaderExited) {
116
+ (0, processes_1.killProcessGroup)(session.process, 'SIGTERM');
117
+ }
118
+ }
119
+ await waitForSessionsAsync(sessions, PROCESS_STOP_GRACE_PERIOD_MS);
120
+ for (const session of sessions) {
121
+ if (!session.hasLeaderExited) {
122
+ (0, processes_1.killProcessGroup)(session.process, 'SIGKILL');
123
+ }
124
+ }
125
+ await waitForSessionsAsync(sessions, PROCESS_STOP_GRACE_PERIOD_MS);
126
+ this.sessions.clear();
127
+ }
128
+ throwIfStopped() {
129
+ this.options.signal.throwIfAborted();
130
+ }
131
+ getSession(sessionId) {
132
+ const session = this.sessions.get(sessionId);
133
+ if (!session) {
134
+ throw new Error(`Command session ${sessionId} does not exist.`);
135
+ }
136
+ return session;
137
+ }
138
+ }
139
+ exports.ShellSessionManager = ShellSessionManager;
140
+ function startPipeCommand({ command, workingDirectory, env, }) {
141
+ const child = (0, node_child_process_1.spawn)(command, {
142
+ cwd: workingDirectory,
143
+ env,
144
+ shell: env.SHELL ?? true,
145
+ detached: true,
146
+ });
147
+ const session = createSession(child);
148
+ session.write = (chars) => {
149
+ child.stdin.write(chars);
150
+ };
151
+ child.stdout.setEncoding('utf8');
152
+ child.stderr.setEncoding('utf8');
153
+ child.stdout.on('data', data => {
154
+ session.output += data;
155
+ });
156
+ child.stderr.on('data', data => {
157
+ session.output += data;
158
+ });
159
+ child.stdin.on('error', () => { });
160
+ child.once('exit', () => {
161
+ session.hasLeaderExited = true;
162
+ });
163
+ child.once('error', error => {
164
+ if (!session.isCompleted) {
165
+ session.hasLeaderExited = true;
166
+ session.isCompleted = true;
167
+ session.error = error;
168
+ session.resolveCompleted();
169
+ }
170
+ });
171
+ child.once('close', (exitCode, signal) => {
172
+ if (!session.isCompleted) {
173
+ session.isCompleted = true;
174
+ if (signal) {
175
+ session.terminationSignal = signal;
176
+ }
177
+ else {
178
+ session.exitCode = exitCode ?? 1;
179
+ }
180
+ session.resolveCompleted();
181
+ }
182
+ });
183
+ return session;
184
+ }
185
+ function startPtyCommand({ command, workingDirectory, env, }) {
186
+ const shell = env.SHELL ?? '/bin/sh';
187
+ const terminal = pty.spawn(shell, ['-c', command], {
188
+ cwd: workingDirectory,
189
+ env,
190
+ name: 'xterm-256color',
191
+ cols: 80,
192
+ rows: 24,
193
+ });
194
+ const session = createSession(terminal);
195
+ session.write = (chars) => {
196
+ terminal.write(chars);
197
+ };
198
+ terminal.onData(data => {
199
+ session.output += data;
200
+ });
201
+ terminal.onExit(({ exitCode, signal }) => {
202
+ session.hasLeaderExited = true;
203
+ session.isCompleted = true;
204
+ if (signal) {
205
+ session.terminationSignal = SIGNAL_NAMES.get(signal) ?? `SIGNAL_${signal}`;
206
+ }
207
+ else {
208
+ session.exitCode = exitCode;
209
+ }
210
+ session.resolveCompleted();
211
+ });
212
+ return session;
213
+ }
214
+ function createSession(process) {
215
+ let resolveCompleted;
216
+ const completed = new Promise(resolve => {
217
+ resolveCompleted = resolve;
218
+ });
219
+ return {
220
+ process,
221
+ completed,
222
+ resolveCompleted,
223
+ isCompleted: false,
224
+ hasLeaderExited: false,
225
+ output: '',
226
+ write: () => { },
227
+ };
228
+ }
229
+ async function validateWorkingDirectoryAsync(workingDirectory) {
230
+ let stats;
231
+ try {
232
+ stats = await promises_1.default.stat(workingDirectory);
233
+ }
234
+ catch (error) {
235
+ if (error?.code === 'ENOENT') {
236
+ throw new Error(`Working directory does not exist: ${workingDirectory}`);
237
+ }
238
+ if (error?.code === 'EACCES') {
239
+ throw new Error(`Working directory is not accessible: ${workingDirectory}`);
240
+ }
241
+ throw error;
242
+ }
243
+ if (!stats.isDirectory()) {
244
+ throw new Error(`Working directory is not a directory: ${workingDirectory}`);
245
+ }
246
+ try {
247
+ await promises_1.default.access(workingDirectory, promises_1.default.constants.R_OK | promises_1.default.constants.X_OK);
248
+ }
249
+ catch (error) {
250
+ if (error?.code === 'EACCES') {
251
+ throw new Error(`Working directory is not accessible: ${workingDirectory}`);
252
+ }
253
+ throw error;
254
+ }
255
+ }
256
+ async function waitForSessionsAsync(sessions, timeoutMs) {
257
+ let timer;
258
+ try {
259
+ await Promise.race([
260
+ Promise.all(sessions.map(session => session.completed)),
261
+ new Promise(resolve => {
262
+ timer = setTimeout(resolve, timeoutMs);
263
+ }),
264
+ ]);
265
+ }
266
+ finally {
267
+ clearTimeout(timer);
268
+ }
269
+ }
@@ -52,6 +52,29 @@ export declare namespace IosSimulatorUtils {
52
52
  deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
53
53
  env: NodeJS.ProcessEnv;
54
54
  }): Promise<void>;
55
+ /**
56
+ * The UDID for a device name or UDID. A name picks the first available
57
+ * device with that name, as `simctl` itself does.
58
+ */
59
+ export function resolveUdidAsync({ deviceIdentifier, env, }: {
60
+ deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
61
+ env: NodeJS.ProcessEnv;
62
+ }): Promise<IosSimulatorUuid>;
63
+ /**
64
+ * Start booting without waiting for boot to complete; follow with
65
+ * `startAsync` to wait for it. `launchdEnvironment` is handed to the
66
+ * simulator's launchd before it spawns anything: `simctl` forwards every
67
+ * `SIMCTL_CHILD_`-prefixed variable of its own environment to the process
68
+ * it starts, and for `boot` that process is launchd itself. This is the only
69
+ * way to give the first processes of a boot an environment; `launchctl
70
+ * setenv` after boot only reaches processes started later. A device that is
71
+ * already booted keeps its environment.
72
+ */
73
+ export function bootAsync({ deviceIdentifier, env, launchdEnvironment, }: {
74
+ deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
75
+ env: NodeJS.ProcessEnv;
76
+ launchdEnvironment?: Record<string, string>;
77
+ }): Promise<void>;
55
78
  export function startAsync({ deviceIdentifier, env, }: {
56
79
  deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
57
80
  env: NodeJS.ProcessEnv;
@@ -66,6 +89,17 @@ export declare namespace IosSimulatorUtils {
66
89
  udid: IosSimulatorUuid;
67
90
  env: NodeJS.ProcessEnv;
68
91
  }): Promise<void>;
92
+ /**
93
+ * Set environment variables in the Simulator's launchd. Every process that
94
+ * launchd spawns afterwards inherits them: apps launched by SpringBoard
95
+ * (deep links, taps, WebDriverAgent) as well as by `simctl launch`.
96
+ * Processes that are already running keep their environment.
97
+ */
98
+ export function setLaunchdEnvironmentAsync({ udid, env, variables, }: {
99
+ udid: IosSimulatorUuid;
100
+ env: NodeJS.ProcessEnv;
101
+ variables: Record<string, string>;
102
+ }): Promise<void>;
69
103
  export function collectLogsAsync({ deviceIdentifier, env, }: {
70
104
  deviceIdentifier: IosSimulatorName | IosSimulatorUuid;
71
105
  env: NodeJS.ProcessEnv;
@@ -87,6 +87,50 @@ var IosSimulatorUtils;
87
87
  }
88
88
  }
89
89
  IosSimulatorUtils.enableAccessibilitySettingsAsync = enableAccessibilitySettingsAsync;
90
+ const UDID_PATTERN = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
91
+ /**
92
+ * The UDID for a device name or UDID. A name picks the first available
93
+ * device with that name, as `simctl` itself does.
94
+ */
95
+ async function resolveUdidAsync({ deviceIdentifier, env, }) {
96
+ if (UDID_PATTERN.test(deviceIdentifier)) {
97
+ return deviceIdentifier;
98
+ }
99
+ const devices = await getAvailableDevicesAsync({ env, filter: 'available' });
100
+ const device = devices.find(candidate => candidate.name === deviceIdentifier);
101
+ if (!device) {
102
+ throw new eas_build_job_1.UserError('EAS_IOS_SIMULATOR_NOT_FOUND', `No available iOS Simulator is named "${deviceIdentifier}". Run \`xcrun simctl list devices available\` on the device host to see the devices it offers.`);
103
+ }
104
+ return device.udid;
105
+ }
106
+ IosSimulatorUtils.resolveUdidAsync = resolveUdidAsync;
107
+ /**
108
+ * Start booting without waiting for boot to complete; follow with
109
+ * `startAsync` to wait for it. `launchdEnvironment` is handed to the
110
+ * simulator's launchd before it spawns anything: `simctl` forwards every
111
+ * `SIMCTL_CHILD_`-prefixed variable of its own environment to the process
112
+ * it starts, and for `boot` that process is launchd itself. This is the only
113
+ * way to give the first processes of a boot an environment; `launchctl
114
+ * setenv` after boot only reaches processes started later. A device that is
115
+ * already booted keeps its environment.
116
+ */
117
+ async function bootAsync({ deviceIdentifier, env, launchdEnvironment = {}, }) {
118
+ const bootEnv = { ...env };
119
+ for (const [name, value] of Object.entries(launchdEnvironment)) {
120
+ bootEnv[`SIMCTL_CHILD_${name}`] = value;
121
+ }
122
+ try {
123
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'boot', deviceIdentifier], { env: bootEnv, stdio: 'pipe' });
124
+ }
125
+ catch (err) {
126
+ const failed = err;
127
+ if (/current state: Booted/.test(failed.stderr ?? '')) {
128
+ return;
129
+ }
130
+ throw err;
131
+ }
132
+ }
133
+ IosSimulatorUtils.bootAsync = bootAsync;
90
134
  async function startAsync({ deviceIdentifier, env, }) {
91
135
  const bootstatusResult = await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'bootstatus', deviceIdentifier, '-b'], {
92
136
  env,
@@ -154,6 +198,24 @@ var IosSimulatorUtils;
154
198
  throw lastError ?? new eas_build_job_1.SystemError('Unable to disable apsd in the Simulator.');
155
199
  }
156
200
  IosSimulatorUtils.disableApsdAsync = disableApsdAsync;
201
+ /**
202
+ * Set environment variables in the Simulator's launchd. Every process that
203
+ * launchd spawns afterwards inherits them: apps launched by SpringBoard
204
+ * (deep links, taps, WebDriverAgent) as well as by `simctl launch`.
205
+ * Processes that are already running keep their environment.
206
+ */
207
+ async function setLaunchdEnvironmentAsync({ udid, env, variables, }) {
208
+ // One invocation for every variable: each `simctl spawn` costs a few
209
+ // hundred milliseconds on a device host, and this runs in the window
210
+ // between `simctl boot` returning and launchd spawning the boot's
211
+ // processes, which must inherit these.
212
+ const pairs = Object.entries(variables).flat();
213
+ if (pairs.length === 0) {
214
+ return;
215
+ }
216
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'setenv', ...pairs], { env });
217
+ }
218
+ IosSimulatorUtils.setLaunchdEnvironmentAsync = setLaunchdEnvironmentAsync;
157
219
  async function collectLogsAsync({ deviceIdentifier, env, }) {
158
220
  const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-simulator-logs-'));
159
221
  const outputPath = node_path_1.default.join(outputDir, `${deviceIdentifier}.logarchive`);
@@ -4,6 +4,9 @@ export declare function isChildProcessAlive(child: ChildProcess): boolean;
4
4
  * Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
5
5
  * children cannot survive after the parent is gone (e.g. across upterm redial).
6
6
  */
7
- export declare function killProcessGroup(child: ChildProcess): void;
7
+ export declare function killProcessGroup(child: {
8
+ pid?: number;
9
+ kill(signal?: NodeJS.Signals): void;
10
+ }, signal?: NodeJS.Signals): void;
8
11
  export declare function getParentAndDescendantProcessPidsAsync(ppid: number): Promise<number[]>;
9
12
  export declare function isProcessDescendantOfAsync(pid: number, ancestorPid: number): Promise<boolean>;
@@ -15,15 +15,18 @@ function isChildProcessAlive(child) {
15
15
  * Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
16
16
  * children cannot survive after the parent is gone (e.g. across upterm redial).
17
17
  */
18
- function killProcessGroup(child) {
18
+ function killProcessGroup(child, signal = 'SIGTERM') {
19
19
  if (child.pid == null) {
20
20
  return;
21
21
  }
22
22
  try {
23
- process.kill(-child.pid, 'SIGTERM');
23
+ process.kill(-child.pid, signal);
24
24
  }
25
- catch {
26
- child.kill();
25
+ catch (error) {
26
+ // ESRCH means the process group no longer exists, so there is nothing left to stop.
27
+ if (error?.code !== 'ESRCH') {
28
+ child.kill(signal);
29
+ }
27
30
  }
28
31
  }
29
32
  async function getChildrenPidsAsync(parentPids) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "24.3.0",
3
+ "version": "24.5.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -24,6 +24,8 @@
24
24
  "prebuild": "yarn gql",
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
+ "build:egress-guard": "resources/egress-guard/build.sh",
28
+ "test:egress-guard": "resources/egress-guard/tests/run-policy-tests.sh && resources/egress-guard/tests/run-guard-tests.sh",
27
29
  "typecheck": "tsc",
28
30
  "generate-appium-commands": "mise exec node@22.22.0 -- node scripts/generate-appium-commands.js",
29
31
  "prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
@@ -40,14 +42,14 @@
40
42
  "@expo/config": "55.0.10",
41
43
  "@expo/config-plugins": "55.0.7",
42
44
  "@expo/downloader": "24.0.0",
43
- "@expo/eas-build-job": "24.3.0",
45
+ "@expo/eas-build-job": "24.5.0",
44
46
  "@expo/env": "^0.4.0",
45
47
  "@expo/logger": "24.0.0",
46
48
  "@expo/package-manager": "1.9.10",
47
49
  "@expo/plist": "^0.3.5",
48
50
  "@expo/results": "^1.0.0",
49
51
  "@expo/spawn-async": "1.7.2",
50
- "@expo/steps": "24.3.0",
52
+ "@expo/steps": "24.5.0",
51
53
  "@expo/template-file": "24.0.0",
52
54
  "@expo/turtle-spawn": "24.0.0",
53
55
  "@expo/xcpretty": "^4.3.1",
@@ -65,6 +67,7 @@
65
67
  "lodash": "^4.18.1",
66
68
  "node-fetch": "^2.7.0",
67
69
  "node-forge": "^1.3.1",
70
+ "node-pty": "1.2.0-beta.15",
68
71
  "node-stream-zip": "1.15.0",
69
72
  "nullthrows": "^1.1.1",
70
73
  "plist": "^3.1.0",
@@ -104,5 +107,5 @@
104
107
  "typescript": "^5.5.4",
105
108
  "uuid": "^9.0.1"
106
109
  },
107
- "gitHead": "66cdccb0e7c3ec47054c3f093c25b9799d60ff36"
110
+ "gitHead": "43068db22a079c67198fc7f7ccb5e85286ba3207"
108
111
  }