@expo/build-tools 24.4.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/steps/functions/startSandbox.js +5 -2
- 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/processes.d.ts +4 -1
- package/dist/utils/processes.js +7 -4
- package/package.json +5 -4
|
@@ -31,8 +31,9 @@ function createStartSandboxBuildFunction(ctx) {
|
|
|
31
31
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
32
32
|
}),
|
|
33
33
|
],
|
|
34
|
-
fn: async (stepCtx, { inputs, signal }) => {
|
|
35
|
-
|
|
34
|
+
fn: async (stepCtx, { inputs, signal, env }) => {
|
|
35
|
+
// The daemon needs this credential, but shell commands do not.
|
|
36
|
+
const { __EAS_SANDBOX_MCP_TOKEN: sandboxToken, ...commandEnv } = env;
|
|
36
37
|
if (!sandboxToken) {
|
|
37
38
|
throw new eas_build_job_1.SystemError('__EAS_SANDBOX_MCP_TOKEN is required to start the sandbox daemon.');
|
|
38
39
|
}
|
|
@@ -47,6 +48,8 @@ function createStartSandboxBuildFunction(ctx) {
|
|
|
47
48
|
reconnectDelayMs: RECONNECT_DELAY_MS,
|
|
48
49
|
logger: stepCtx.logger,
|
|
49
50
|
signal,
|
|
51
|
+
workingDirectory: stepCtx.workingDirectory,
|
|
52
|
+
env: commandEnv,
|
|
50
53
|
});
|
|
51
54
|
try {
|
|
52
55
|
await daemon.ready;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type SandboxDaemonCommandParams, type SandboxDaemonCommandResult, type SandboxDaemonMethod } from '@expo/eas-build-job';
|
|
2
|
+
export type SandboxDaemonCommandImplementations = {
|
|
3
|
+
[Method in SandboxDaemonMethod]: (params: SandboxDaemonCommandParams<Method>) => Promise<SandboxDaemonCommandResult<Method>>;
|
|
4
|
+
};
|
|
5
|
+
export declare function createSandboxCommandImplementations({ workingDirectory, env, signal, }: {
|
|
6
|
+
workingDirectory: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
signal: AbortSignal;
|
|
9
|
+
}): {
|
|
10
|
+
commandImplementations: SandboxDaemonCommandImplementations;
|
|
11
|
+
stoppedPromise: Promise<void>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSandboxCommandImplementations = createSandboxCommandImplementations;
|
|
4
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
5
|
+
const shellSessionManager_1 = require("./shellSessionManager");
|
|
6
|
+
const DEFAULT_EXEC_YIELD_TIME_MS = 10_000;
|
|
7
|
+
const DEFAULT_WRITE_YIELD_TIME_MS = 250;
|
|
8
|
+
function createSandboxCommandImplementations({ workingDirectory, env, signal, }) {
|
|
9
|
+
const sessions = new shellSessionManager_1.ShellSessionManager({ workingDirectory, env, signal });
|
|
10
|
+
return {
|
|
11
|
+
commandImplementations: {
|
|
12
|
+
async execCommand(params) {
|
|
13
|
+
const callStartedAt = node_perf_hooks_1.performance.now();
|
|
14
|
+
const sessionId = await sessions.startAsync({
|
|
15
|
+
cmd: params.cmd,
|
|
16
|
+
workdir: params.workdir,
|
|
17
|
+
tty: params.tty,
|
|
18
|
+
});
|
|
19
|
+
const result = await sessions.readAsync(sessionId, params.yieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS);
|
|
20
|
+
return { ...result, wallTimeSeconds: (node_perf_hooks_1.performance.now() - callStartedAt) / 1_000 };
|
|
21
|
+
},
|
|
22
|
+
async writeStdin(params) {
|
|
23
|
+
const callStartedAt = node_perf_hooks_1.performance.now();
|
|
24
|
+
if (params.chars !== undefined) {
|
|
25
|
+
sessions.write(params.sessionId, params.chars);
|
|
26
|
+
}
|
|
27
|
+
const result = await sessions.readAsync(params.sessionId, params.yieldTimeMs ?? DEFAULT_WRITE_YIELD_TIME_MS);
|
|
28
|
+
return { ...result, wallTimeSeconds: (node_perf_hooks_1.performance.now() - callStartedAt) / 1_000 };
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
stoppedPromise: sessions.stoppedPromise,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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>",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"@expo/config": "55.0.10",
|
|
43
43
|
"@expo/config-plugins": "55.0.7",
|
|
44
44
|
"@expo/downloader": "24.0.0",
|
|
45
|
-
"@expo/eas-build-job": "24.
|
|
45
|
+
"@expo/eas-build-job": "24.5.0",
|
|
46
46
|
"@expo/env": "^0.4.0",
|
|
47
47
|
"@expo/logger": "24.0.0",
|
|
48
48
|
"@expo/package-manager": "1.9.10",
|
|
49
49
|
"@expo/plist": "^0.3.5",
|
|
50
50
|
"@expo/results": "^1.0.0",
|
|
51
51
|
"@expo/spawn-async": "1.7.2",
|
|
52
|
-
"@expo/steps": "24.
|
|
52
|
+
"@expo/steps": "24.5.0",
|
|
53
53
|
"@expo/template-file": "24.0.0",
|
|
54
54
|
"@expo/turtle-spawn": "24.0.0",
|
|
55
55
|
"@expo/xcpretty": "^4.3.1",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"lodash": "^4.18.1",
|
|
68
68
|
"node-fetch": "^2.7.0",
|
|
69
69
|
"node-forge": "^1.3.1",
|
|
70
|
+
"node-pty": "1.2.0-beta.15",
|
|
70
71
|
"node-stream-zip": "1.15.0",
|
|
71
72
|
"nullthrows": "^1.1.1",
|
|
72
73
|
"plist": "^3.1.0",
|
|
@@ -106,5 +107,5 @@
|
|
|
106
107
|
"typescript": "^5.5.4",
|
|
107
108
|
"uuid": "^9.0.1"
|
|
108
109
|
},
|
|
109
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "43068db22a079c67198fc7f7ccb5e85286ba3207"
|
|
110
111
|
}
|