@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.
- package/dist/common/projectSources.js +3 -0
- package/dist/steps/functions/startIosSimulator.js +34 -2
- package/dist/steps/functions/startLocalEgress.d.ts +4 -3
- package/dist/steps/functions/startLocalEgress.js +10 -6
- package/dist/steps/functions/startSandbox.js +5 -2
- package/dist/steps/utils/localEgress.d.ts +24 -0
- package/dist/steps/utils/localEgress.js +59 -1
- package/dist/steps/utils/localEgressGuard.d.ts +179 -0
- package/dist/steps/utils/localEgressGuard.js +537 -0
- package/dist/steps/utils/localEgressSession.js +4 -0
- package/dist/steps/utils/sandboxCommandImplementations.d.ts +12 -0
- package/dist/steps/utils/sandboxCommandImplementations.js +33 -0
- package/dist/steps/utils/sandboxDaemon.d.ts +2 -0
- package/dist/steps/utils/sandboxDaemon.js +70 -3
- package/dist/steps/utils/shellSessionManager.d.ts +29 -0
- package/dist/steps/utils/shellSessionManager.js +269 -0
- package/dist/utils/IosSimulatorUtils.d.ts +34 -0
- package/dist/utils/IosSimulatorUtils.js +62 -0
- package/dist/utils/processes.d.ts +4 -1
- package/dist/utils/processes.js +7 -4
- package/package.json +7 -4
- package/resources/egress-guard/README.md +83 -0
- package/resources/egress-guard/build.sh +30 -0
- package/resources/egress-guard/check.c +112 -0
- package/resources/egress-guard/guard.c +274 -0
- package/resources/egress-guard/policy.c +164 -0
- package/resources/egress-guard/policy.h +60 -0
- package/resources/egress-guard/tests/guard_insert_test.c +291 -0
- package/resources/egress-guard/tests/guard_test.c +177 -0
- package/resources/egress-guard/tests/nettest.swift +172 -0
- package/resources/egress-guard/tests/policy_test.c +143 -0
- package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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>;
|
package/dist/utils/processes.js
CHANGED
|
@@ -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,
|
|
23
|
+
process.kill(-child.pid, signal);
|
|
24
24
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
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": "
|
|
110
|
+
"gitHead": "43068db22a079c67198fc7f7ccb5e85286ba3207"
|
|
108
111
|
}
|