fauxnix-cli 0.7.1 → 0.9.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/README.md +6 -0
- package/dist/cli.js +11 -1
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +356 -89
- package/dist/commands/install-all.js +7 -4
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.d.ts +2 -1
- package/dist/commands/text-filters.js +67 -7
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +95 -4
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +83 -13
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +79 -18
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +46 -2
- package/dist/ps-host.js +181 -17
- package/dist/registry.d.ts +54 -0
- package/dist/registry.js +182 -0
- package/dist/translator.js +65 -7
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -33,15 +33,46 @@ Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), e
|
|
|
33
33
|
|
|
34
34
|
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
35
35
|
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
36
|
-
Not supported: heredocs, while/until/case, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
|
|
36
|
+
Not supported: heredocs, while/until/case, env -i/--ignore-environment, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
|
|
37
37
|
|
|
38
38
|
CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is started when the MCP session begins (and after reset), so the first bash tool call is already warm.
|
|
39
|
-
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
39
|
+
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout, 130 cancelled). The tool also returns structuredContent (schemaVersion 1) with stdout/stderr/exitCode/timedOut/cancelled/truncated/sessionId.
|
|
40
40
|
|
|
41
41
|
Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
|
|
42
|
+
export function formatBashText(r) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (r.stdout.length)
|
|
45
|
+
parts.push(r.stdout.replace(/\n$/, ''));
|
|
46
|
+
if (r.stderr.length)
|
|
47
|
+
parts.push(r.stderr.replace(/\n$/, ''));
|
|
48
|
+
if (r.cancelled)
|
|
49
|
+
parts.push('Cancelled');
|
|
50
|
+
else if (r.timedOut)
|
|
51
|
+
parts.push('Exit code: 124');
|
|
52
|
+
else if (r.exitCode !== 0)
|
|
53
|
+
parts.push('Exit code: ' + r.exitCode);
|
|
54
|
+
return parts.length ? parts.join('\n') : '(no output)';
|
|
55
|
+
}
|
|
56
|
+
export function bashToolResult(r, sessionId, infra) {
|
|
57
|
+
const structuredContent = {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
stdout: r.stdout,
|
|
60
|
+
stderr: r.stderr,
|
|
61
|
+
exitCode: r.exitCode,
|
|
62
|
+
timedOut: r.timedOut,
|
|
63
|
+
cancelled: r.cancelled,
|
|
64
|
+
truncated: r.truncated,
|
|
65
|
+
sessionId,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: formatBashText(r) }],
|
|
69
|
+
structuredContent,
|
|
70
|
+
...(infra ? { isError: true } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
42
73
|
export async function startMcpServer() {
|
|
43
74
|
const server = new McpServer({ name: 'fauxnix', version: packageVersion }, { capabilities: { tools: {} } });
|
|
44
|
-
|
|
75
|
+
const session = new FauxnixSession();
|
|
45
76
|
await session.prewarm();
|
|
46
77
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
47
78
|
command: z.string().describe('The bash-style command line to run'),
|
|
@@ -52,23 +83,26 @@ export async function startMcpServer() {
|
|
|
52
83
|
.max(600_000)
|
|
53
84
|
.optional()
|
|
54
85
|
.describe('Timeout in milliseconds (default 120000)'),
|
|
55
|
-
}, EXEC_ANNOTATIONS, async ({ command, timeout_ms }) => {
|
|
86
|
+
}, EXEC_ANNOTATIONS, async ({ command, timeout_ms }, extra) => {
|
|
56
87
|
try {
|
|
57
88
|
const plans = translateCommandList(parseCommand(command));
|
|
58
|
-
const result = await session.run(plans, {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (result.exitCode !== 0)
|
|
65
|
-
parts.push('Exit code: ' + result.exitCode);
|
|
66
|
-
const text = parts.length ? parts.join('\n') : '(no output)';
|
|
67
|
-
return { content: [{ type: 'text', text }] };
|
|
89
|
+
const result = await session.run(plans, {
|
|
90
|
+
timeoutMs: timeout_ms,
|
|
91
|
+
signal: extra.signal,
|
|
92
|
+
});
|
|
93
|
+
const infra = result.spawnError === 'ENOENT' || result.spawnError === 'START';
|
|
94
|
+
return bashToolResult(result, session.id, infra);
|
|
68
95
|
}
|
|
69
96
|
catch (e) {
|
|
70
97
|
const msg = e instanceof Error ? e.message : String(e);
|
|
71
|
-
return {
|
|
98
|
+
return bashToolResult({
|
|
99
|
+
stdout: '',
|
|
100
|
+
stderr: msg,
|
|
101
|
+
exitCode: 2,
|
|
102
|
+
timedOut: false,
|
|
103
|
+
cancelled: false,
|
|
104
|
+
truncated: false,
|
|
105
|
+
}, session.id, true);
|
|
72
106
|
}
|
|
73
107
|
});
|
|
74
108
|
server.tool('fauxnix_translate', 'Translate a bash-style command into the equivalent PowerShell script WITHOUT executing it. Useful for learning/debugging what fauxnix does under the hood.', { command: z.string().describe('The bash-style command line to translate (never executed)') }, TRANSLATE_ANNOTATIONS, async ({ command }) => {
|
|
@@ -90,18 +124,45 @@ export async function startMcpServer() {
|
|
|
90
124
|
.describe('"status" shows the session state (cwd, tracked env keys); "reset" clears it back to a fresh shell'),
|
|
91
125
|
}, SESSION_ANNOTATIONS, async ({ action }) => {
|
|
92
126
|
if (action === 'reset') {
|
|
93
|
-
await session.
|
|
94
|
-
session = new FauxnixSession();
|
|
95
|
-
await session.prewarm();
|
|
127
|
+
await session.reset();
|
|
96
128
|
return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
|
|
97
129
|
}
|
|
98
130
|
const envKeys = Object.keys(session.env).sort();
|
|
99
131
|
const text = 'cwd: ' + (session.cwd ?? '(inherit from server start)') +
|
|
100
132
|
'\nenv keys: ' + (envKeys.length ? envKeys.join(', ') : '(none tracked)') +
|
|
133
|
+
'\nsession: ' + session.id +
|
|
101
134
|
'\ncommands registered: ' + registeredNames().length;
|
|
102
135
|
return { content: [{ type: 'text', text }] };
|
|
103
136
|
});
|
|
104
137
|
const transport = new StdioServerTransport();
|
|
138
|
+
let shuttingDown = false;
|
|
139
|
+
const shutdown = async () => {
|
|
140
|
+
if (shuttingDown)
|
|
141
|
+
return;
|
|
142
|
+
shuttingDown = true;
|
|
143
|
+
await session.dispose();
|
|
144
|
+
try {
|
|
145
|
+
await server.close();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* ignore */
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
process.stdin.on('end', () => {
|
|
152
|
+
void shutdown();
|
|
153
|
+
});
|
|
154
|
+
process.stdin.on('close', () => {
|
|
155
|
+
void shutdown();
|
|
156
|
+
});
|
|
157
|
+
process.on('SIGINT', () => {
|
|
158
|
+
void shutdown();
|
|
159
|
+
});
|
|
160
|
+
process.on('SIGTERM', () => {
|
|
161
|
+
void shutdown();
|
|
162
|
+
});
|
|
163
|
+
transport.onclose = () => {
|
|
164
|
+
void shutdown();
|
|
165
|
+
};
|
|
105
166
|
await server.connect(transport);
|
|
106
167
|
}
|
|
107
168
|
// keep referenced for tree-shaking clarity
|
package/dist/parser.js
CHANGED
|
@@ -586,28 +586,62 @@ export function parseCommand(input) {
|
|
|
586
586
|
let pos = 0;
|
|
587
587
|
const peek = () => tokens[pos];
|
|
588
588
|
const next = () => tokens[pos++];
|
|
589
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
590
|
+
/** Consume `;` / newline / `&&` / `||`. Trailing `&&`/`||` and `;;` fail loud (bash). */
|
|
591
|
+
const consumeListOp = (stops) => {
|
|
592
|
+
const t = peek();
|
|
593
|
+
if (t.type !== 'OP')
|
|
594
|
+
return null;
|
|
595
|
+
if (!(t.op === '&&' || t.op === '||' || isListSep(t.op)))
|
|
596
|
+
return null;
|
|
597
|
+
if (t.op === ';') {
|
|
598
|
+
next();
|
|
599
|
+
if (peek().type === 'OP' && peek().op === ';') {
|
|
600
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
601
|
+
}
|
|
602
|
+
return ';';
|
|
603
|
+
}
|
|
604
|
+
if (t.op === '\n') {
|
|
605
|
+
next();
|
|
606
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
607
|
+
next();
|
|
608
|
+
return ';';
|
|
609
|
+
}
|
|
610
|
+
const sep = t.op;
|
|
611
|
+
next();
|
|
612
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
613
|
+
next();
|
|
614
|
+
const n = peek();
|
|
615
|
+
const kw = peekKw();
|
|
616
|
+
if (n.type === 'OP' && n.op === ';') {
|
|
617
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
618
|
+
}
|
|
619
|
+
if (n.type === 'EOF' || (stops && kw && stops.has(kw))) {
|
|
620
|
+
throw new FauxnixParseError(n.type === 'EOF'
|
|
621
|
+
? 'fauxnix: syntax error: unexpected end of file after `' + sep + "'"
|
|
622
|
+
: "fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
623
|
+
}
|
|
624
|
+
return sep;
|
|
625
|
+
};
|
|
589
626
|
const parseList = () => {
|
|
590
627
|
const segments = [];
|
|
591
628
|
let op = ';';
|
|
592
|
-
|
|
593
|
-
|
|
629
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
630
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
631
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
632
|
+
}
|
|
594
633
|
next();
|
|
634
|
+
}
|
|
595
635
|
while (peek().type !== 'EOF') {
|
|
596
636
|
const pipeline = parsePipeline();
|
|
597
637
|
segments.push({ pipeline, op });
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
603
|
-
next();
|
|
604
|
-
}
|
|
605
|
-
else if (t.type === 'EOF') {
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
638
|
+
const nextOp = consumeListOp();
|
|
639
|
+
if (nextOp === null) {
|
|
640
|
+
if (peek().type === 'EOF')
|
|
641
|
+
break;
|
|
609
642
|
throw new FauxnixParseError('fauxnix: unexpected token after pipeline');
|
|
610
643
|
}
|
|
644
|
+
op = nextOp;
|
|
611
645
|
}
|
|
612
646
|
if (segments.length === 0)
|
|
613
647
|
throw new FauxnixParseError('fauxnix: empty command');
|
|
@@ -672,25 +706,22 @@ export function parseCommand(input) {
|
|
|
672
706
|
const stop = new Set(stops);
|
|
673
707
|
const segments = [];
|
|
674
708
|
let op = ';';
|
|
675
|
-
|
|
676
|
-
|
|
709
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
710
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
711
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
712
|
+
}
|
|
677
713
|
next();
|
|
714
|
+
}
|
|
678
715
|
while (peek().type !== 'EOF') {
|
|
679
716
|
const kw = peekKw();
|
|
680
717
|
if (kw && stop.has(kw))
|
|
681
718
|
break;
|
|
682
719
|
const pipeline = parsePipeline();
|
|
683
720
|
segments.push({ pipeline, op });
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
op = t.op === '\n' ? ';' : t.op;
|
|
687
|
-
next();
|
|
688
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
689
|
-
next();
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
721
|
+
const nextOp = consumeListOp(stop);
|
|
722
|
+
if (nextOp === null)
|
|
692
723
|
break;
|
|
693
|
-
|
|
724
|
+
op = nextOp;
|
|
694
725
|
}
|
|
695
726
|
if (segments.length === 0) {
|
|
696
727
|
throw new FauxnixParseError('fauxnix: empty command');
|
package/dist/ps-host.d.ts
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
export declare const PS_MISSING_MESSAGE: string;
|
|
2
|
+
export declare const DEFAULT_STDOUT_LIMIT = 8388608;
|
|
3
|
+
export declare const DEFAULT_STDERR_LIMIT = 1048576;
|
|
2
4
|
export interface HostInvokeResult {
|
|
3
5
|
stdout: Buffer;
|
|
4
6
|
stderr: Buffer;
|
|
5
7
|
exitCode: number;
|
|
6
8
|
timedOut: boolean;
|
|
9
|
+
cancelled: boolean;
|
|
10
|
+
truncated: boolean;
|
|
7
11
|
spawnError?: 'ENOENT' | 'START';
|
|
8
12
|
spawnMessage?: string;
|
|
9
13
|
}
|
|
10
14
|
export interface HostRequestEnv {
|
|
11
15
|
[key: string]: string;
|
|
12
16
|
}
|
|
13
|
-
export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv
|
|
17
|
+
export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv, opts?: {
|
|
18
|
+
v?: number;
|
|
19
|
+
stdoutLimit?: number;
|
|
20
|
+
stderrLimit?: number;
|
|
21
|
+
}): string;
|
|
22
|
+
export type HostV2Frame = {
|
|
23
|
+
v: 2;
|
|
24
|
+
type: 'ready';
|
|
25
|
+
capabilities?: {
|
|
26
|
+
cancel?: boolean;
|
|
27
|
+
maxChunkBytes?: number;
|
|
28
|
+
stderrMarker?: boolean;
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
v: 2;
|
|
32
|
+
type: 'stdout' | 'stderr';
|
|
33
|
+
id: string;
|
|
34
|
+
seq: number;
|
|
35
|
+
dataB64: string;
|
|
36
|
+
} | {
|
|
37
|
+
v: 2;
|
|
38
|
+
type: 'end';
|
|
39
|
+
id: string;
|
|
40
|
+
exitCode: number;
|
|
41
|
+
timedOut?: boolean;
|
|
42
|
+
cancelled?: boolean;
|
|
43
|
+
truncated?: boolean;
|
|
44
|
+
};
|
|
45
|
+
export declare function parseHostLine(line: string): {
|
|
46
|
+
v1Ready?: boolean;
|
|
47
|
+
v2?: HostV2Frame;
|
|
48
|
+
v1?: ReturnType<typeof decodeHostResponse>;
|
|
49
|
+
};
|
|
14
50
|
export declare function decodeHostResponse(line: string): {
|
|
15
51
|
id: string;
|
|
16
52
|
stdout: Buffer;
|
|
@@ -36,11 +72,17 @@ export declare class PowerShellHost {
|
|
|
36
72
|
private closed;
|
|
37
73
|
private startLock;
|
|
38
74
|
private invokeLock;
|
|
75
|
+
protocol: 1 | 2;
|
|
39
76
|
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
40
77
|
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
41
78
|
ready(): Promise<HostInvokeResult | null>;
|
|
42
|
-
invoke(script: string, env: HostRequestEnv, timeoutMs: number
|
|
79
|
+
invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: {
|
|
80
|
+
stdoutLimit?: number;
|
|
81
|
+
stderrLimit?: number;
|
|
82
|
+
}): Promise<HostInvokeResult>;
|
|
83
|
+
drainNativeStderr(): Buffer;
|
|
43
84
|
stop(): Promise<void>;
|
|
85
|
+
private cancelledResult;
|
|
44
86
|
private invokeSerial;
|
|
45
87
|
private ensureStarted;
|
|
46
88
|
private deadRestart;
|
|
@@ -49,5 +91,7 @@ export declare class PowerShellHost {
|
|
|
49
91
|
private nextLine;
|
|
50
92
|
private nextReadyLine;
|
|
51
93
|
private nextJsonLine;
|
|
94
|
+
private collectV2;
|
|
95
|
+
private waitNativeMarker;
|
|
52
96
|
private failWaiters;
|
|
53
97
|
}
|
package/dist/ps-host.js
CHANGED
|
@@ -6,12 +6,29 @@ const READY_TIMEOUT_MS = 30_000;
|
|
|
6
6
|
export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
7
7
|
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
8
8
|
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
|
-
export
|
|
10
|
-
|
|
9
|
+
export const DEFAULT_STDOUT_LIMIT = 8_388_608;
|
|
10
|
+
export const DEFAULT_STDERR_LIMIT = 1_048_576;
|
|
11
|
+
export function encodeHostRequest(id, script, env, opts) {
|
|
12
|
+
const body = {
|
|
11
13
|
id,
|
|
12
14
|
scriptB64: Buffer.from(script, 'utf8').toString('base64'),
|
|
13
15
|
env,
|
|
14
|
-
}
|
|
16
|
+
};
|
|
17
|
+
if (opts?.v === 2) {
|
|
18
|
+
body.v = 2;
|
|
19
|
+
body.type = 'run';
|
|
20
|
+
body.stdoutLimit = opts.stdoutLimit ?? DEFAULT_STDOUT_LIMIT;
|
|
21
|
+
body.stderrLimit = opts.stderrLimit ?? DEFAULT_STDERR_LIMIT;
|
|
22
|
+
}
|
|
23
|
+
return JSON.stringify(body);
|
|
24
|
+
}
|
|
25
|
+
export function parseHostLine(line) {
|
|
26
|
+
const j = JSON.parse(line);
|
|
27
|
+
if (j && j.v === 2 && typeof j.type === 'string')
|
|
28
|
+
return { v2: j };
|
|
29
|
+
if (j && j.ready === true)
|
|
30
|
+
return { v1Ready: true };
|
|
31
|
+
return { v1: decodeHostResponse(line) };
|
|
15
32
|
}
|
|
16
33
|
export function decodeHostResponse(line) {
|
|
17
34
|
const j = JSON.parse(line);
|
|
@@ -42,6 +59,7 @@ export class PowerShellHost {
|
|
|
42
59
|
closed = false;
|
|
43
60
|
startLock = null;
|
|
44
61
|
invokeLock = Promise.resolve();
|
|
62
|
+
protocol = 1;
|
|
45
63
|
constructor(hostFile, envFn) {
|
|
46
64
|
this.hostFile = hostFile;
|
|
47
65
|
this.envFn = envFn;
|
|
@@ -50,11 +68,18 @@ export class PowerShellHost {
|
|
|
50
68
|
async ready() {
|
|
51
69
|
return this.ensureStarted();
|
|
52
70
|
}
|
|
53
|
-
async invoke(script, env, timeoutMs) {
|
|
54
|
-
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
|
|
71
|
+
async invoke(script, env, timeoutMs, signal, limits) {
|
|
72
|
+
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal, limits));
|
|
55
73
|
this.invokeLock = run.then(() => undefined, () => undefined);
|
|
56
74
|
return run;
|
|
57
75
|
}
|
|
76
|
+
drainNativeStderr() {
|
|
77
|
+
if (!this.stderrChunks.length)
|
|
78
|
+
return Buffer.alloc(0);
|
|
79
|
+
const b = Buffer.concat(this.stderrChunks);
|
|
80
|
+
this.stderrChunks = [];
|
|
81
|
+
return b;
|
|
82
|
+
}
|
|
58
83
|
async stop() {
|
|
59
84
|
const proc = this.proc;
|
|
60
85
|
this.proc = null;
|
|
@@ -82,12 +107,28 @@ export class PowerShellHost {
|
|
|
82
107
|
});
|
|
83
108
|
});
|
|
84
109
|
}
|
|
85
|
-
|
|
110
|
+
cancelledResult() {
|
|
111
|
+
return {
|
|
112
|
+
stdout: Buffer.alloc(0),
|
|
113
|
+
stderr: Buffer.alloc(0),
|
|
114
|
+
exitCode: 130,
|
|
115
|
+
timedOut: false,
|
|
116
|
+
cancelled: true,
|
|
117
|
+
truncated: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async invokeSerial(script, env, timeoutMs, signal, limits) {
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
await this.stop();
|
|
123
|
+
return this.cancelledResult();
|
|
124
|
+
}
|
|
86
125
|
const started = await this.ensureStarted();
|
|
87
126
|
if (started)
|
|
88
|
-
return started;
|
|
127
|
+
return { ...started, cancelled: false, truncated: false };
|
|
89
128
|
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
90
|
-
const line = encodeHostRequest(id, script, env
|
|
129
|
+
const line = encodeHostRequest(id, script, env, this.protocol === 2
|
|
130
|
+
? { v: 2, stdoutLimit: limits?.stdoutLimit, stderrLimit: limits?.stderrLimit }
|
|
131
|
+
: undefined);
|
|
91
132
|
try {
|
|
92
133
|
this.proc.stdin.write(line + '\n');
|
|
93
134
|
}
|
|
@@ -98,24 +139,48 @@ export class PowerShellHost {
|
|
|
98
139
|
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
|
|
99
140
|
exitCode: 1,
|
|
100
141
|
timedOut: false,
|
|
142
|
+
cancelled: false,
|
|
143
|
+
truncated: false,
|
|
101
144
|
spawnMessage: e.message,
|
|
102
145
|
};
|
|
103
146
|
}
|
|
147
|
+
let cancelled = false;
|
|
148
|
+
const onAbort = () => {
|
|
149
|
+
cancelled = true;
|
|
150
|
+
void this.stop();
|
|
151
|
+
};
|
|
152
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
153
|
try {
|
|
154
|
+
if (this.protocol === 2) {
|
|
155
|
+
return await this.collectV2(id, timeoutMs);
|
|
156
|
+
}
|
|
105
157
|
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
106
158
|
const msg = decodeHostResponse(raw);
|
|
159
|
+
const native = this.drainNativeStderr();
|
|
107
160
|
return {
|
|
108
161
|
stdout: msg.stdout,
|
|
109
|
-
stderr: msg.stderr,
|
|
162
|
+
stderr: native.length ? Buffer.concat([msg.stderr, native]) : msg.stderr,
|
|
110
163
|
exitCode: msg.exitCode,
|
|
111
164
|
timedOut: false,
|
|
165
|
+
cancelled: false,
|
|
166
|
+
truncated: false,
|
|
112
167
|
};
|
|
113
168
|
}
|
|
114
169
|
catch (e) {
|
|
115
170
|
const timedOut = e.timedOut === true;
|
|
116
171
|
await this.stop();
|
|
172
|
+
this.drainNativeStderr();
|
|
173
|
+
if (cancelled || signal?.aborted)
|
|
174
|
+
return this.cancelledResult();
|
|
117
175
|
if (timedOut) {
|
|
118
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
stdout: Buffer.alloc(0),
|
|
178
|
+
stderr: Buffer.alloc(0),
|
|
179
|
+
exitCode: 124,
|
|
180
|
+
timedOut: true,
|
|
181
|
+
cancelled: false,
|
|
182
|
+
truncated: false,
|
|
183
|
+
};
|
|
119
184
|
}
|
|
120
185
|
if (this.closeErr && this.closeErr.code === 'ENOENT') {
|
|
121
186
|
return {
|
|
@@ -123,6 +188,8 @@ export class PowerShellHost {
|
|
|
123
188
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
124
189
|
exitCode: 127,
|
|
125
190
|
timedOut: false,
|
|
191
|
+
cancelled: false,
|
|
192
|
+
truncated: false,
|
|
126
193
|
spawnError: 'ENOENT',
|
|
127
194
|
};
|
|
128
195
|
}
|
|
@@ -134,8 +201,13 @@ export class PowerShellHost {
|
|
|
134
201
|
'\n', 'utf8'),
|
|
135
202
|
exitCode: code === 0 ? 1 : code,
|
|
136
203
|
timedOut: false,
|
|
204
|
+
cancelled: false,
|
|
205
|
+
truncated: false,
|
|
137
206
|
};
|
|
138
207
|
}
|
|
208
|
+
finally {
|
|
209
|
+
signal?.removeEventListener('abort', onAbort);
|
|
210
|
+
}
|
|
139
211
|
}
|
|
140
212
|
async ensureStarted() {
|
|
141
213
|
if (this.proc && !this.closed)
|
|
@@ -154,6 +226,8 @@ export class PowerShellHost {
|
|
|
154
226
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
155
227
|
exitCode: 127,
|
|
156
228
|
timedOut: false,
|
|
229
|
+
cancelled: false,
|
|
230
|
+
truncated: false,
|
|
157
231
|
spawnError: 'ENOENT',
|
|
158
232
|
};
|
|
159
233
|
}
|
|
@@ -162,6 +236,8 @@ export class PowerShellHost {
|
|
|
162
236
|
stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
|
|
163
237
|
exitCode: 127,
|
|
164
238
|
timedOut: false,
|
|
239
|
+
cancelled: false,
|
|
240
|
+
truncated: false,
|
|
165
241
|
spawnError: 'START',
|
|
166
242
|
spawnMessage: err.message,
|
|
167
243
|
};
|
|
@@ -189,14 +265,26 @@ export class PowerShellHost {
|
|
|
189
265
|
windowsHide: true,
|
|
190
266
|
});
|
|
191
267
|
this.proc = child;
|
|
192
|
-
child.stdout.on('data', (d) =>
|
|
193
|
-
|
|
268
|
+
child.stdout.on('data', (d) => {
|
|
269
|
+
if (this.proc !== child)
|
|
270
|
+
return;
|
|
271
|
+
this.onStdout(d);
|
|
272
|
+
});
|
|
273
|
+
child.stderr.on('data', (d) => {
|
|
274
|
+
if (this.proc !== child)
|
|
275
|
+
return;
|
|
276
|
+
this.stderrChunks.push(d);
|
|
277
|
+
});
|
|
194
278
|
child.on('error', (e) => {
|
|
279
|
+
if (this.proc !== child)
|
|
280
|
+
return;
|
|
195
281
|
this.closeErr = e;
|
|
196
282
|
this.closed = true;
|
|
197
283
|
this.failWaiters(e);
|
|
198
284
|
});
|
|
199
285
|
child.on('close', (c) => {
|
|
286
|
+
if (this.proc !== child)
|
|
287
|
+
return;
|
|
200
288
|
this.closeCode = c;
|
|
201
289
|
this.closed = true;
|
|
202
290
|
this.proc = null;
|
|
@@ -204,10 +292,13 @@ export class PowerShellHost {
|
|
|
204
292
|
});
|
|
205
293
|
try {
|
|
206
294
|
const readyLine = await this.nextReadyLine(READY_TIMEOUT_MS);
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
295
|
+
const parsed = parseHostLine(readyLine);
|
|
296
|
+
if (parsed.v2?.type === 'ready')
|
|
297
|
+
this.protocol = 2;
|
|
298
|
+
else if (parsed.v1Ready || decodeHostResponse(readyLine).ready)
|
|
299
|
+
this.protocol = 1;
|
|
300
|
+
else
|
|
209
301
|
throw new Error('fauxnix: powershell host handshake failed');
|
|
210
|
-
}
|
|
211
302
|
}
|
|
212
303
|
catch (e) {
|
|
213
304
|
await this.stop();
|
|
@@ -267,8 +358,10 @@ export class PowerShellHost {
|
|
|
267
358
|
if (!line.trim())
|
|
268
359
|
continue;
|
|
269
360
|
try {
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
361
|
+
const parsed = parseHostLine(line);
|
|
362
|
+
if (parsed.v2?.type === 'ready' || parsed.v1Ready)
|
|
363
|
+
return line;
|
|
364
|
+
if (parsed.v1?.ready)
|
|
272
365
|
return line;
|
|
273
366
|
}
|
|
274
367
|
catch {
|
|
@@ -300,6 +393,77 @@ export class PowerShellHost {
|
|
|
300
393
|
err.timedOut = true;
|
|
301
394
|
throw err;
|
|
302
395
|
}
|
|
396
|
+
async collectV2(id, timeoutMs) {
|
|
397
|
+
const deadline = Date.now() + timeoutMs;
|
|
398
|
+
const out = [];
|
|
399
|
+
const err = [];
|
|
400
|
+
let outSeq = 0;
|
|
401
|
+
let errSeq = 0;
|
|
402
|
+
let end = null;
|
|
403
|
+
while (!end) {
|
|
404
|
+
const line = await this.nextLine(Math.max(1, deadline - Date.now()));
|
|
405
|
+
if (!line.trim())
|
|
406
|
+
continue;
|
|
407
|
+
let parsed;
|
|
408
|
+
try {
|
|
409
|
+
parsed = parseHostLine(line);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const f = parsed.v2;
|
|
415
|
+
if (!f)
|
|
416
|
+
continue;
|
|
417
|
+
if (f.type === 'stdout' && f.id === id) {
|
|
418
|
+
if (f.seq !== outSeq)
|
|
419
|
+
throw new Error('fauxnix: host stdout seq gap');
|
|
420
|
+
out.push(Buffer.from(f.dataB64 ?? '', 'base64'));
|
|
421
|
+
outSeq++;
|
|
422
|
+
}
|
|
423
|
+
else if (f.type === 'stderr' && f.id === id) {
|
|
424
|
+
if (f.seq !== errSeq)
|
|
425
|
+
throw new Error('fauxnix: host stderr seq gap');
|
|
426
|
+
err.push(Buffer.from(f.dataB64 ?? '', 'base64'));
|
|
427
|
+
errSeq++;
|
|
428
|
+
}
|
|
429
|
+
else if (f.type === 'end' && f.id === id) {
|
|
430
|
+
end = f;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let native = Buffer.alloc(0);
|
|
434
|
+
try {
|
|
435
|
+
native = Buffer.from(await this.waitNativeMarker(id, 2000));
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
native = Buffer.from(this.drainNativeStderr());
|
|
439
|
+
}
|
|
440
|
+
const capturedErr = Buffer.from(Buffer.concat(err));
|
|
441
|
+
const n = Number(end.exitCode);
|
|
442
|
+
return {
|
|
443
|
+
stdout: Buffer.from(Buffer.concat(out)),
|
|
444
|
+
stderr: native.length ? Buffer.from(Buffer.concat([capturedErr, native])) : capturedErr,
|
|
445
|
+
exitCode: Number.isFinite(n) ? n : 0,
|
|
446
|
+
timedOut: end.timedOut === true,
|
|
447
|
+
cancelled: end.cancelled === true,
|
|
448
|
+
truncated: end.truncated === true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async waitNativeMarker(id, timeoutMs) {
|
|
452
|
+
const needle = Buffer.from('FAUXNIX_ERR_END:' + id + '\n', 'utf8');
|
|
453
|
+
const deadline = Date.now() + timeoutMs;
|
|
454
|
+
while (Date.now() < deadline) {
|
|
455
|
+
const buf = Buffer.concat(this.stderrChunks);
|
|
456
|
+
const idx = buf.indexOf(needle);
|
|
457
|
+
if (idx >= 0) {
|
|
458
|
+
const before = buf.subarray(0, idx);
|
|
459
|
+
const after = buf.subarray(idx + needle.length);
|
|
460
|
+
this.stderrChunks = after.length ? [Buffer.from(after)] : [];
|
|
461
|
+
return Buffer.from(before);
|
|
462
|
+
}
|
|
463
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
464
|
+
}
|
|
465
|
+
throw new Error('fauxnix: native stderr marker missing');
|
|
466
|
+
}
|
|
303
467
|
failWaiters(err) {
|
|
304
468
|
const ws = this.waiters.splice(0);
|
|
305
469
|
for (const w of ws) {
|