remote-codex 0.11.43 → 0.11.45
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/README.md +9 -0
- package/apps/relay-server/dist/index.js +17 -1
- package/apps/supervisor-api/dist/index.js +2189 -1798
- package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +1 -0
- package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +22 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-Dmrigdek.js → thread-ui-BWC_ljvN.js} +11 -11
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows.md +81 -0
- package/package.json +14 -3
- package/packages/claude/src/runtimeAdapter.test.ts +32 -4
- package/packages/claude/src/runtimeAdapter.ts +21 -22
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/index.ts +12 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BT0SM9C-.js +0 -21
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import crossSpawn from 'cross-spawn';
|
|
6
|
+
|
|
7
|
+
export interface SpawnProcessOptions {
|
|
8
|
+
command: string;
|
|
9
|
+
args?: readonly string[];
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
stdio?: SpawnOptions['stdio'];
|
|
13
|
+
detached?: boolean;
|
|
14
|
+
windowsHide?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunProcessOptions extends Omit<SpawnProcessOptions, 'stdio' | 'detached'> {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
maxOutputBytes?: number;
|
|
20
|
+
input?: string | Buffer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProcessResult {
|
|
24
|
+
code: number | null;
|
|
25
|
+
signal: NodeJS.Signals | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
timedOut: boolean;
|
|
29
|
+
outputTruncated: boolean;
|
|
30
|
+
spawnError: Error | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParsedCommandLine {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a persisted/display command into argv without evaluating a shell.
|
|
42
|
+
* Supports the quoting used by the built-in runtime install commands. Shell
|
|
43
|
+
* operators and environment expansion are intentionally treated as literals.
|
|
44
|
+
*/
|
|
45
|
+
export function parseCommandLine(value: string): ParsedCommandLine {
|
|
46
|
+
const tokens: string[] = [];
|
|
47
|
+
let token = '';
|
|
48
|
+
let quote: 'single' | 'double' | null = null;
|
|
49
|
+
let tokenStarted = false;
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
52
|
+
const character = value[index]!;
|
|
53
|
+
if (quote === 'single') {
|
|
54
|
+
if (character === "'") {
|
|
55
|
+
quote = null;
|
|
56
|
+
} else {
|
|
57
|
+
token += character;
|
|
58
|
+
}
|
|
59
|
+
tokenStarted = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (quote === 'double') {
|
|
63
|
+
if (character === '"') {
|
|
64
|
+
quote = null;
|
|
65
|
+
} else if (
|
|
66
|
+
character === '\\' &&
|
|
67
|
+
(value[index + 1] === '"' || value[index + 1] === '\\')
|
|
68
|
+
) {
|
|
69
|
+
token += value[index + 1];
|
|
70
|
+
index += 1;
|
|
71
|
+
} else {
|
|
72
|
+
token += character;
|
|
73
|
+
}
|
|
74
|
+
tokenStarted = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (/\s/.test(character)) {
|
|
79
|
+
if (tokenStarted) {
|
|
80
|
+
tokens.push(token);
|
|
81
|
+
token = '';
|
|
82
|
+
tokenStarted = false;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === "'") {
|
|
87
|
+
quote = 'single';
|
|
88
|
+
tokenStarted = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === '"') {
|
|
92
|
+
quote = 'double';
|
|
93
|
+
tokenStarted = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
token += character;
|
|
97
|
+
tokenStarted = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (quote) {
|
|
101
|
+
throw new Error('Command contains an unterminated quoted argument.');
|
|
102
|
+
}
|
|
103
|
+
if (tokenStarted) {
|
|
104
|
+
tokens.push(token);
|
|
105
|
+
}
|
|
106
|
+
const [command, ...args] = tokens;
|
|
107
|
+
if (!command) {
|
|
108
|
+
throw new Error('Command is empty.');
|
|
109
|
+
}
|
|
110
|
+
return { command, args };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Spawn an executable without constructing a shell command string.
|
|
115
|
+
* cross-spawn is used so Windows PATHEXT and npm .cmd shims work consistently.
|
|
116
|
+
*/
|
|
117
|
+
export function spawnProcess(options: SpawnProcessOptions): ChildProcess {
|
|
118
|
+
return crossSpawn(options.command, [...(options.args ?? [])], {
|
|
119
|
+
cwd: options.cwd,
|
|
120
|
+
env: options.env,
|
|
121
|
+
stdio: options.stdio ?? 'pipe',
|
|
122
|
+
detached: options.detached,
|
|
123
|
+
windowsHide: options.windowsHide ?? true,
|
|
124
|
+
shell: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function runProcess(options: RunProcessOptions): Promise<ProcessResult> {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const child = spawnProcess({
|
|
131
|
+
...options,
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
});
|
|
134
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
135
|
+
let stdout: Buffer = Buffer.alloc(0);
|
|
136
|
+
let stderr: Buffer = Buffer.alloc(0);
|
|
137
|
+
let timedOut = false;
|
|
138
|
+
let outputTruncated = false;
|
|
139
|
+
let spawnError: Error | null = null;
|
|
140
|
+
let settled = false;
|
|
141
|
+
|
|
142
|
+
const append = (current: Buffer, chunk: Buffer | string) => {
|
|
143
|
+
const nextChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
144
|
+
const remaining = Math.max(0, maxOutputBytes - current.byteLength);
|
|
145
|
+
if (nextChunk.byteLength > remaining) {
|
|
146
|
+
outputTruncated = true;
|
|
147
|
+
}
|
|
148
|
+
return remaining === 0
|
|
149
|
+
? current
|
|
150
|
+
: Buffer.concat([current, nextChunk.subarray(0, remaining)]);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
child.stdout?.on('data', (chunk: Buffer | string) => {
|
|
154
|
+
stdout = append(stdout, chunk);
|
|
155
|
+
});
|
|
156
|
+
child.stderr?.on('data', (chunk: Buffer | string) => {
|
|
157
|
+
stderr = append(stderr, chunk);
|
|
158
|
+
});
|
|
159
|
+
child.on('error', (error) => {
|
|
160
|
+
spawnError = error;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const finish = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
164
|
+
if (settled) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
settled = true;
|
|
168
|
+
if (timer) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
resolve({
|
|
172
|
+
code: spawnError ? null : code,
|
|
173
|
+
signal,
|
|
174
|
+
stdout: stdout.toString('utf8'),
|
|
175
|
+
stderr: stderr.toString('utf8'),
|
|
176
|
+
timedOut,
|
|
177
|
+
outputTruncated,
|
|
178
|
+
spawnError,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
child.on('close', finish);
|
|
183
|
+
const timer = options.timeoutMs && options.timeoutMs > 0
|
|
184
|
+
? setTimeout(() => {
|
|
185
|
+
timedOut = true;
|
|
186
|
+
child.kill('SIGTERM');
|
|
187
|
+
}, options.timeoutMs)
|
|
188
|
+
: null;
|
|
189
|
+
|
|
190
|
+
if (options.input !== undefined) {
|
|
191
|
+
child.stdin?.end(options.input);
|
|
192
|
+
} else {
|
|
193
|
+
child.stdin?.end();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pathEnvironmentValue(env: NodeJS.ProcessEnv) {
|
|
199
|
+
const entry = Object.entries(env).find(([key]) => key.toLowerCase() === 'path');
|
|
200
|
+
return entry?.[1] ?? '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function windowsExtensions(command: string, env: NodeJS.ProcessEnv) {
|
|
204
|
+
if (path.extname(command)) {
|
|
205
|
+
return [''];
|
|
206
|
+
}
|
|
207
|
+
const pathExtEntry = Object.entries(env).find(([key]) => key.toLowerCase() === 'pathext');
|
|
208
|
+
const extensions = (pathExtEntry?.[1] ?? '.COM;.EXE;.BAT;.CMD')
|
|
209
|
+
.split(';')
|
|
210
|
+
.map((extension) => extension.trim())
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
return ['', ...extensions];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function isFile(candidate: string) {
|
|
216
|
+
try {
|
|
217
|
+
return (await fs.stat(candidate)).isFile();
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Resolve a command for diagnostics only. Execution must still use spawnProcess. */
|
|
224
|
+
export async function resolveExecutable(
|
|
225
|
+
command: string,
|
|
226
|
+
options: {
|
|
227
|
+
cwd?: string;
|
|
228
|
+
env?: NodeJS.ProcessEnv;
|
|
229
|
+
platform?: NodeJS.Platform;
|
|
230
|
+
} = {},
|
|
231
|
+
): Promise<string | null> {
|
|
232
|
+
const env = options.env ?? process.env;
|
|
233
|
+
const platform = options.platform ?? process.platform;
|
|
234
|
+
const cwd = options.cwd ?? process.cwd();
|
|
235
|
+
const hasPathSeparator = command.includes('/') || command.includes('\\');
|
|
236
|
+
const searchDirectories = hasPathSeparator
|
|
237
|
+
? ['']
|
|
238
|
+
: pathEnvironmentValue(env).split(path.delimiter).filter(Boolean);
|
|
239
|
+
const extensions = platform === 'win32' ? windowsExtensions(command, env) : [''];
|
|
240
|
+
|
|
241
|
+
for (const directory of searchDirectories) {
|
|
242
|
+
const base = hasPathSeparator
|
|
243
|
+
? (path.isAbsolute(command) ? command : path.resolve(cwd, command))
|
|
244
|
+
: path.join(directory, command);
|
|
245
|
+
for (const extension of extensions) {
|
|
246
|
+
const candidate = `${base}${extension}`;
|
|
247
|
+
if (await isFile(candidate)) {
|
|
248
|
+
return path.resolve(candidate);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
@@ -64,6 +64,15 @@ export interface RuntimeConfigDto {
|
|
|
64
64
|
port: number;
|
|
65
65
|
workspaceRoot: string;
|
|
66
66
|
environment: string;
|
|
67
|
+
platform?: string;
|
|
68
|
+
architecture?: string;
|
|
69
|
+
nodeVersion?: string;
|
|
70
|
+
capabilities?: {
|
|
71
|
+
terminal: boolean;
|
|
72
|
+
tmux: boolean;
|
|
73
|
+
managedSignals: boolean;
|
|
74
|
+
windowsTaskScheduler: boolean;
|
|
75
|
+
};
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export interface AuthSessionDto {
|
|
@@ -1066,6 +1075,9 @@ export interface PluginManifestDto {
|
|
|
1066
1075
|
export interface PluginDto extends PluginManifestDto {
|
|
1067
1076
|
enabled: boolean;
|
|
1068
1077
|
source?: 'builtin' | 'imported' | null;
|
|
1078
|
+
available?: boolean;
|
|
1079
|
+
unavailableReasonCode?: 'unsupported_platform' | null;
|
|
1080
|
+
unavailableReason?: string | null;
|
|
1069
1081
|
}
|
|
1070
1082
|
|
|
1071
1083
|
export interface UpdatePluginInput {
|
|
@@ -2,9 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import net from 'node:net';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const repoRoot = path.resolve(scriptDir, '..');
|
|
@@ -67,6 +68,12 @@ async function startService() {
|
|
|
67
68
|
await assertTcpPortAvailable(apiHost, apiPort, 'API');
|
|
68
69
|
await assertTcpPortAvailable(serviceHost, servicePort, 'Web');
|
|
69
70
|
|
|
71
|
+
const apiInstanceId = crypto.randomUUID();
|
|
72
|
+
const apiControlToken = crypto.randomBytes(32).toString('base64url');
|
|
73
|
+
const apiControlEndpoint = process.platform === 'win32'
|
|
74
|
+
? `\\\\.\\pipe\\remote-codex-service-${crypto.createHash('sha256').update(serviceDir).digest('hex').slice(0, 24)}`
|
|
75
|
+
: path.join(serviceDir, 'api-control.sock');
|
|
76
|
+
|
|
70
77
|
const apiPid = spawnDetached(process.execPath, [apiEntry], apiLogPath, {
|
|
71
78
|
NODE_ENV: 'production',
|
|
72
79
|
HOST: apiHost,
|
|
@@ -76,6 +83,9 @@ async function startService() {
|
|
|
76
83
|
REMOTE_CODEX_PACKAGE_ROOT: repoRoot,
|
|
77
84
|
REMOTE_CODEX_DISABLE_BUILD_RESTART:
|
|
78
85
|
process.env.REMOTE_CODEX_DISABLE_BUILD_RESTART ?? (supportsSourceRestart ? 'false' : 'true'),
|
|
86
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: apiControlEndpoint,
|
|
87
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: apiControlToken,
|
|
88
|
+
REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: apiInstanceId,
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
try {
|
|
@@ -108,11 +118,14 @@ async function startService() {
|
|
|
108
118
|
apiHost,
|
|
109
119
|
apiPort,
|
|
110
120
|
apiPid,
|
|
121
|
+
apiInstanceId,
|
|
122
|
+
apiControlToken,
|
|
123
|
+
apiControlEndpoint,
|
|
111
124
|
webPid,
|
|
112
125
|
apiLogPath,
|
|
113
126
|
webLogPath,
|
|
114
127
|
};
|
|
115
|
-
await
|
|
128
|
+
await writePrivateState(state);
|
|
116
129
|
|
|
117
130
|
console.log(`Started supervisor service.`);
|
|
118
131
|
console.log(`Web: http://${serviceHost}:${servicePort} (pid ${webPid})`);
|
|
@@ -305,8 +318,14 @@ async function probeHttp(url) {
|
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
async function stopState(state) {
|
|
308
|
-
|
|
309
|
-
|
|
321
|
+
stopPid(state.webPid);
|
|
322
|
+
let apiShutdownRequested = false;
|
|
323
|
+
if (state.apiControlEndpoint && state.apiControlToken && state.apiInstanceId) {
|
|
324
|
+
const response = await requestLifecycleControl(state, 'shutdown').catch(() => null);
|
|
325
|
+
apiShutdownRequested = response?.ok === true && response.instanceId === state.apiInstanceId;
|
|
326
|
+
}
|
|
327
|
+
if (!apiShutdownRequested) {
|
|
328
|
+
stopPid(state.apiPid);
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
const deadline = Date.now() + 5_000;
|
|
@@ -331,6 +350,44 @@ async function readState() {
|
|
|
331
350
|
}
|
|
332
351
|
}
|
|
333
352
|
|
|
353
|
+
async function writePrivateState(state) {
|
|
354
|
+
await fsp.mkdir(serviceDir, { recursive: true, mode: 0o700 });
|
|
355
|
+
await fsp.chmod(serviceDir, 0o700).catch(() => undefined);
|
|
356
|
+
const temporaryPath = `${stateFile}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
357
|
+
let handle = null;
|
|
358
|
+
try {
|
|
359
|
+
handle = await fsp.open(temporaryPath, 'wx', 0o600);
|
|
360
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
361
|
+
await handle.sync();
|
|
362
|
+
await handle.close();
|
|
363
|
+
handle = null;
|
|
364
|
+
await fsp.rename(temporaryPath, stateFile);
|
|
365
|
+
if (process.platform === 'win32') {
|
|
366
|
+
const username = process.env.USERDOMAIN && process.env.USERNAME
|
|
367
|
+
? `${process.env.USERDOMAIN}\\${process.env.USERNAME}`
|
|
368
|
+
: process.env.USERNAME ?? os.userInfo().username;
|
|
369
|
+
spawnSync('icacls.exe', [
|
|
370
|
+
serviceDir,
|
|
371
|
+
'/inheritance:r',
|
|
372
|
+
'/grant:r',
|
|
373
|
+
`${username}:(OI)(CI)F`,
|
|
374
|
+
'SYSTEM:(OI)(CI)F',
|
|
375
|
+
], { windowsHide: true, stdio: 'ignore' });
|
|
376
|
+
spawnSync('icacls.exe', [
|
|
377
|
+
stateFile,
|
|
378
|
+
'/inheritance:r',
|
|
379
|
+
'/grant:r',
|
|
380
|
+
`${username}:F`,
|
|
381
|
+
'SYSTEM:F',
|
|
382
|
+
], { windowsHide: true, stdio: 'ignore' });
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
await handle?.close().catch(() => undefined);
|
|
386
|
+
await fsp.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
334
391
|
function serviceStateAlive(state) {
|
|
335
392
|
return [state.apiPid, state.webPid].some((pid) => isProcessAlive(pid));
|
|
336
393
|
}
|
|
@@ -340,6 +397,14 @@ function stopPid(pid) {
|
|
|
340
397
|
return;
|
|
341
398
|
}
|
|
342
399
|
|
|
400
|
+
if (process.platform === 'win32') {
|
|
401
|
+
spawn('taskkill.exe', ['/PID', String(pid), '/T'], {
|
|
402
|
+
windowsHide: true,
|
|
403
|
+
stdio: 'ignore',
|
|
404
|
+
}).unref();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
343
408
|
try {
|
|
344
409
|
process.kill(-pid, 'SIGTERM');
|
|
345
410
|
return;
|
|
@@ -359,6 +424,14 @@ function forceStopPid(pid) {
|
|
|
359
424
|
return;
|
|
360
425
|
}
|
|
361
426
|
|
|
427
|
+
if (process.platform === 'win32') {
|
|
428
|
+
spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], {
|
|
429
|
+
windowsHide: true,
|
|
430
|
+
stdio: 'ignore',
|
|
431
|
+
}).unref();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
362
435
|
try {
|
|
363
436
|
process.kill(-pid, 'SIGKILL');
|
|
364
437
|
return;
|
|
@@ -412,3 +485,38 @@ function sleep(milliseconds) {
|
|
|
412
485
|
setTimeout(resolve, milliseconds);
|
|
413
486
|
});
|
|
414
487
|
}
|
|
488
|
+
|
|
489
|
+
function requestLifecycleControl(state, action, timeoutMs = 2_000) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const socket = net.createConnection(state.apiControlEndpoint);
|
|
492
|
+
const timer = setTimeout(() => {
|
|
493
|
+
socket.destroy();
|
|
494
|
+
reject(new Error('Lifecycle control request timed out.'));
|
|
495
|
+
}, timeoutMs);
|
|
496
|
+
let output = '';
|
|
497
|
+
socket.setEncoding('utf8');
|
|
498
|
+
socket.once('connect', () => {
|
|
499
|
+
socket.write(`${JSON.stringify({
|
|
500
|
+
action,
|
|
501
|
+
token: state.apiControlToken,
|
|
502
|
+
instanceId: state.apiInstanceId,
|
|
503
|
+
})}\n`);
|
|
504
|
+
});
|
|
505
|
+
socket.on('data', (chunk) => {
|
|
506
|
+
output += chunk;
|
|
507
|
+
const newline = output.indexOf('\n');
|
|
508
|
+
if (newline < 0) return;
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
socket.end();
|
|
511
|
+
try {
|
|
512
|
+
resolve(JSON.parse(output.slice(0, newline)));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
reject(error);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
socket.once('error', (error) => {
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
reject(error);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|