@serverkgg/bridge 0.1.12 → 0.1.13
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/package.json +1 -1
- package/rules/package.md +2 -1
- package/src/bot/botDeploy.test.ts +57 -0
- package/src/bot/botEnvironment.test.ts +21 -0
- package/src/bot/botEnvironment.ts +10 -0
- package/src/bot/botRun.test.ts +296 -0
- package/src/bot/botRun.ts +109 -0
- package/src/bot/botState.test.ts +34 -1
- package/src/bot/botState.ts +1 -0
- package/src/bot/botVariables.test.ts +22 -0
- package/src/bot/botVariables.ts +3 -0
- package/src/bot/detect/detect.test.ts +172 -0
- package/src/bot/detect/detect.ts +31 -0
- package/src/bot/detect/detectResult.ts +1 -0
- package/src/bot/detect/detectTable.test.ts +25 -2
- package/src/bot/detect/detectTable.ts +24 -1
- package/src/bot/detect/keys.test.ts +7 -0
- package/src/bot/detect/keys.ts +3 -0
- package/src/bot/detect/serverkJson.test.ts +21 -0
- package/src/bot/detect/serverkJson.ts +13 -1
- package/src/bot/index.ts +1 -0
- package/src/cli/link.ts +3 -3
- package/src/cli/main.ts +8 -0
- package/src/cli/readme.test.ts +470 -0
- package/src/cli/readme.ts +570 -0
- package/src/cli/validate.test.ts +41 -1
- package/src/cli/validate.ts +25 -0
- package/src/context/bridgeContext.ts +1 -0
- package/src/modules/bridgeModule.ts +1 -0
- package/src/protocol/bridgeManifest.ts +1 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +7 -2
- package/src/runtime/bridgeExec.test.ts +150 -0
- package/src/runtime/bridgeExec.ts +67 -19
- package/src/runtime/bridgeLog.ts +9 -0
- package/src/runtime/bridgeRedaction.test.ts +28 -2
- package/src/runtime/bridgeRedaction.ts +7 -1
- package/src/runtime/bridgeRegistry.test.ts +30 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/runtime/bridgeScopedContext.ts +3 -0
- package/src/runtime/bridgeSetupHandlers.test.ts +3 -0
- package/src/runtime/bridgeSupervisor.ts +2 -6
- package/src/runtime/serve.ts +9 -2
- package/src/wire/panel.test.ts +32 -0
- package/src/wire/panel.ts +1 -0
|
@@ -66,6 +66,9 @@ describe("the context's variable maps", () => {
|
|
|
66
66
|
supervisor,
|
|
67
67
|
{
|
|
68
68
|
onLog: log,
|
|
69
|
+
onPrint() {
|
|
70
|
+
return;
|
|
71
|
+
},
|
|
69
72
|
onEmit() {
|
|
70
73
|
return;
|
|
71
74
|
},
|
|
@@ -155,6 +158,9 @@ describe("the context's ports and address", () => {
|
|
|
155
158
|
supervisor,
|
|
156
159
|
{
|
|
157
160
|
onLog: log,
|
|
161
|
+
onPrint() {
|
|
162
|
+
return;
|
|
163
|
+
},
|
|
158
164
|
onEmit() {
|
|
159
165
|
return;
|
|
160
166
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
|
-
import type
|
|
2
|
+
import { type BridgeActivity, BridgeStream, type BridgeValues } from "../core";
|
|
3
3
|
import type { BridgeConfig, BridgeSetupRuntime } from "../protocol";
|
|
4
4
|
import { stripAnsi } from "../utils";
|
|
5
5
|
import { createBridgeCodecs } from "./bridgeCodec";
|
|
@@ -8,7 +8,7 @@ import { createBridgeEgress } from "./bridgeEgress";
|
|
|
8
8
|
import { createBridgeExec } from "./bridgeExec";
|
|
9
9
|
import { createBridgeFiles } from "./bridgeFiles";
|
|
10
10
|
import { type BridgeLiveConfig, createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
11
|
-
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
11
|
+
import type { BridgeLeveledLogger, BridgePrinter } from "./bridgeLog";
|
|
12
12
|
import { createBridgeNet } from "./bridgeNet";
|
|
13
13
|
import { createBridgeProbes } from "./bridgeProbe";
|
|
14
14
|
import { createBridgeRcons } from "./bridgeRcon";
|
|
@@ -16,6 +16,7 @@ import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
|
16
16
|
|
|
17
17
|
export interface ContextHooks {
|
|
18
18
|
onLog: BridgeLeveledLogger;
|
|
19
|
+
onPrint: BridgePrinter;
|
|
19
20
|
onEmit: (event: string, payload?: BridgeValues) => void;
|
|
20
21
|
onSetup: (runtime: BridgeSetupRuntime | null) => void;
|
|
21
22
|
onActivity: (entry: BridgeActivity) => void;
|
|
@@ -178,5 +179,9 @@ export const createContext = (
|
|
|
178
179
|
},
|
|
179
180
|
},
|
|
180
181
|
),
|
|
182
|
+
|
|
183
|
+
print(line, stream = BridgeStream.Stdout) {
|
|
184
|
+
hooks.onPrint(line, stream);
|
|
185
|
+
},
|
|
181
186
|
};
|
|
182
187
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { BridgeStream } from "../core";
|
|
6
|
+
import { createBridgeExec } from "./bridgeExec";
|
|
7
|
+
|
|
8
|
+
const SHELL = "/bin/sh";
|
|
9
|
+
|
|
10
|
+
const GROUP_GONE_WAIT_MS = 2000;
|
|
11
|
+
|
|
12
|
+
const GROUP_POLL_MS = 100;
|
|
13
|
+
|
|
14
|
+
const TIMING_TEST_TIMEOUT_MS = 20_000;
|
|
15
|
+
|
|
16
|
+
const groupAlive = (group: number) => {
|
|
17
|
+
try {
|
|
18
|
+
process.kill(-group, 0);
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const groupGone = async (group: number) => {
|
|
27
|
+
for (let waited = 0; waited < GROUP_GONE_WAIT_MS; waited += GROUP_POLL_MS) {
|
|
28
|
+
if (!groupAlive(group)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await Bun.sleep(GROUP_POLL_MS);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return !groupAlive(group);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("createBridgeExec", () => {
|
|
39
|
+
let directory = "";
|
|
40
|
+
|
|
41
|
+
const execOf = () => {
|
|
42
|
+
return createBridgeExec({
|
|
43
|
+
cwd: directory,
|
|
44
|
+
uid: process.getuid?.() ?? 0,
|
|
45
|
+
gid: process.getgid?.() ?? 0,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
directory = await mkdtemp(join(tmpdir(), "serverk-exec-"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
await rm(directory, {
|
|
55
|
+
force: true,
|
|
56
|
+
recursive: true,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns the output of a child that writes and exits", async () => {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
const result = await execOf()(
|
|
64
|
+
[
|
|
65
|
+
SHELL,
|
|
66
|
+
"-c",
|
|
67
|
+
"echo hello; echo oops >&2",
|
|
68
|
+
],
|
|
69
|
+
{
|
|
70
|
+
onOutput(line: string, stream: BridgeStream) {
|
|
71
|
+
lines.push(`${stream} ${line}`);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
stdout: "hello\n",
|
|
78
|
+
stderr: "oops\n",
|
|
79
|
+
code: 0,
|
|
80
|
+
});
|
|
81
|
+
expect(lines.toSorted()).toEqual([
|
|
82
|
+
"stderr oops",
|
|
83
|
+
"stdout hello",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it(
|
|
88
|
+
"kills every process of the command on the deadline and returns at once",
|
|
89
|
+
async () => {
|
|
90
|
+
const startedAt = performance.now();
|
|
91
|
+
|
|
92
|
+
const result = await execOf()(
|
|
93
|
+
[
|
|
94
|
+
SHELL,
|
|
95
|
+
"-c",
|
|
96
|
+
"sleep 30 & sleep 30",
|
|
97
|
+
],
|
|
98
|
+
{
|
|
99
|
+
timeoutMs: 300,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.code).not.toBe(0);
|
|
104
|
+
expect(performance.now() - startedAt).toBeLessThan(2500);
|
|
105
|
+
},
|
|
106
|
+
TIMING_TEST_TIMEOUT_MS,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
it(
|
|
110
|
+
"kills what a command left running in its group once it returns",
|
|
111
|
+
async () => {
|
|
112
|
+
const startedAt = performance.now();
|
|
113
|
+
|
|
114
|
+
const result = await execOf()([
|
|
115
|
+
SHELL,
|
|
116
|
+
"-c",
|
|
117
|
+
"echo $$; sleep 30 &",
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
const group = Number(result.stdout.trim());
|
|
121
|
+
|
|
122
|
+
expect(result.code).toBe(0);
|
|
123
|
+
expect(Number.isSafeInteger(group) && group > 0).toBe(true);
|
|
124
|
+
expect(performance.now() - startedAt).toBeLessThan(5000);
|
|
125
|
+
expect(await groupGone(group)).toBe(true);
|
|
126
|
+
},
|
|
127
|
+
TIMING_TEST_TIMEOUT_MS,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
it.skipIf(!Bun.which("setsid"))(
|
|
131
|
+
"returns after a short grace when a process that left the group still holds the output open",
|
|
132
|
+
async () => {
|
|
133
|
+
const startedAt = performance.now();
|
|
134
|
+
|
|
135
|
+
const result = await execOf()([
|
|
136
|
+
SHELL,
|
|
137
|
+
"-c",
|
|
138
|
+
"echo started; setsid sleep 6 &",
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
expect(result).toEqual({
|
|
142
|
+
stdout: "started\n",
|
|
143
|
+
stderr: "",
|
|
144
|
+
code: 0,
|
|
145
|
+
});
|
|
146
|
+
expect(performance.now() - startedAt).toBeLessThan(5000);
|
|
147
|
+
},
|
|
148
|
+
TIMING_TEST_TIMEOUT_MS,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BridgeExecOptions, BridgeExecResult } from "../context";
|
|
2
2
|
import { BridgeStream } from "../core";
|
|
3
3
|
import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
|
|
4
|
+
import { readProcessGroup, signalGroup } from "./bridgeSignal";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_EXEC_TIMEOUT_MS = 900_000;
|
|
6
7
|
|
|
@@ -8,6 +9,8 @@ const OUTPUT_LIMIT = 256 * 1024;
|
|
|
8
9
|
|
|
9
10
|
const EXIT_GRACE_MS = 3000;
|
|
10
11
|
|
|
12
|
+
const DRAIN_GRACE_MS = 3000;
|
|
13
|
+
|
|
11
14
|
const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
12
15
|
|
|
13
16
|
const inheritedPath = () => {
|
|
@@ -45,8 +48,10 @@ const capped = (text: string) => {
|
|
|
45
48
|
const drain = async (
|
|
46
49
|
stream: ReadableStream<Uint8Array>,
|
|
47
50
|
report: ((line: string) => void) | undefined,
|
|
51
|
+
signal: AbortSignal,
|
|
48
52
|
): Promise<string> => {
|
|
49
53
|
const decoder = new TextDecoder();
|
|
54
|
+
const reader = stream.getReader();
|
|
50
55
|
|
|
51
56
|
let collected = "";
|
|
52
57
|
let pending = "";
|
|
@@ -59,28 +64,46 @@ const drain = async (
|
|
|
59
64
|
}
|
|
60
65
|
};
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
67
|
+
const cancel = () => {
|
|
68
|
+
void reader.cancel().catch(() => undefined);
|
|
69
|
+
};
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
signal.addEventListener("abort", cancel, {
|
|
72
|
+
once: true,
|
|
73
|
+
});
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
try {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const chunk = await reader.read();
|
|
78
|
+
|
|
79
|
+
if (chunk.done) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const text = decoder.decode(chunk.value, {
|
|
84
|
+
stream: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (collected.length < OUTPUT_LIMIT) {
|
|
88
|
+
collected += text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!report) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
pending += text;
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
const lines = pending.split("\n");
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
pending = lines.pop() ?? "";
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
emit(line);
|
|
103
|
+
}
|
|
83
104
|
}
|
|
105
|
+
} finally {
|
|
106
|
+
signal.removeEventListener("abort", cancel);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
emit(pending);
|
|
@@ -107,6 +130,7 @@ export const createBridgeExec = (target: BridgeExecTarget) => {
|
|
|
107
130
|
HOME: target.cwd,
|
|
108
131
|
...environment,
|
|
109
132
|
},
|
|
133
|
+
detached: true,
|
|
110
134
|
stderr: "pipe",
|
|
111
135
|
stdin: "ignore",
|
|
112
136
|
stdout: "pipe",
|
|
@@ -131,15 +155,33 @@ export const createBridgeExec = (target: BridgeExecTarget) => {
|
|
|
131
155
|
}
|
|
132
156
|
}
|
|
133
157
|
|
|
158
|
+
const group = await readProcessGroup(child.pid);
|
|
159
|
+
|
|
134
160
|
const timer = setTimeout(() => {
|
|
135
|
-
child.
|
|
161
|
+
if (!signalGroup(child.pid, group, "SIGKILL")) {
|
|
162
|
+
child.kill("SIGKILL");
|
|
163
|
+
}
|
|
136
164
|
}, timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS);
|
|
137
165
|
|
|
166
|
+
const drained = new AbortController();
|
|
167
|
+
|
|
168
|
+
let grace: ReturnType<typeof setTimeout> | null = null;
|
|
169
|
+
|
|
170
|
+
const exited = async () => {
|
|
171
|
+
const code = await child.exited;
|
|
172
|
+
|
|
173
|
+
grace = setTimeout(() => {
|
|
174
|
+
drained.abort();
|
|
175
|
+
}, DRAIN_GRACE_MS);
|
|
176
|
+
|
|
177
|
+
return code;
|
|
178
|
+
};
|
|
179
|
+
|
|
138
180
|
try {
|
|
139
181
|
const [stdout, stderr, code] = await Promise.all([
|
|
140
|
-
drain(child.stdout, onOutput && ((line) => onOutput(line, BridgeStream.Stdout))),
|
|
141
|
-
drain(child.stderr, onOutput && ((line) => onOutput(line, BridgeStream.Stderr))),
|
|
142
|
-
|
|
182
|
+
drain(child.stdout, onOutput && ((line) => onOutput(line, BridgeStream.Stdout)), drained.signal),
|
|
183
|
+
drain(child.stderr, onOutput && ((line) => onOutput(line, BridgeStream.Stderr)), drained.signal),
|
|
184
|
+
exited(),
|
|
143
185
|
]);
|
|
144
186
|
|
|
145
187
|
return {
|
|
@@ -149,6 +191,12 @@ export const createBridgeExec = (target: BridgeExecTarget) => {
|
|
|
149
191
|
};
|
|
150
192
|
} finally {
|
|
151
193
|
clearTimeout(timer);
|
|
194
|
+
|
|
195
|
+
if (grace !== null) {
|
|
196
|
+
clearTimeout(grace);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
signalGroup(child.pid, group, "SIGKILL");
|
|
152
200
|
}
|
|
153
201
|
};
|
|
154
202
|
};
|
package/src/runtime/bridgeLog.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { BridgeStream } from "../core";
|
|
1
2
|
import { BRIDGE_LOG_SOURCE } from "../protocol/bridgeLine";
|
|
2
3
|
import { BridgeTerminalLevel } from "../terminal";
|
|
3
4
|
|
|
4
5
|
export type BridgeLogger = (message: string, fields?: Record<string, unknown>) => void;
|
|
5
6
|
|
|
7
|
+
export type BridgePrinter = (line: string, stream: BridgeStream) => void;
|
|
8
|
+
|
|
6
9
|
export interface BridgeLeveledLogger extends BridgeLogger {
|
|
7
10
|
warn: BridgeLogger;
|
|
8
11
|
error: BridgeLogger;
|
|
@@ -33,6 +36,12 @@ export const log: BridgeLeveledLogger = Object.assign(
|
|
|
33
36
|
},
|
|
34
37
|
);
|
|
35
38
|
|
|
39
|
+
export const print: BridgePrinter = (line, stream) => {
|
|
40
|
+
const target = stream === BridgeStream.Stderr ? process.stderr : process.stdout;
|
|
41
|
+
|
|
42
|
+
target.write(`${line}\n`);
|
|
43
|
+
};
|
|
44
|
+
|
|
36
45
|
export const errorText = (error: unknown) => {
|
|
37
46
|
return error instanceof Error ? error.message : String(error);
|
|
38
47
|
};
|
|
@@ -2,13 +2,20 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { BridgeKind } from "../core";
|
|
5
|
+
import { BridgeKind, BridgeStream } from "../core";
|
|
6
6
|
import type { BridgeDriver } from "../modules";
|
|
7
7
|
import { BridgeControl, BridgeFormTarget, BridgeLayout, type BridgeTab } from "../panel";
|
|
8
8
|
import type { BridgeConfig } from "../protocol";
|
|
9
9
|
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
10
10
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createBridgeRedaction,
|
|
13
|
+
REDACTED,
|
|
14
|
+
redactLocalized,
|
|
15
|
+
redactLogger,
|
|
16
|
+
redactPrinter,
|
|
17
|
+
secretFieldKeys,
|
|
18
|
+
} from "./bridgeRedaction";
|
|
12
19
|
|
|
13
20
|
const text = (value: string) => ({
|
|
14
21
|
ar: value,
|
|
@@ -309,6 +316,25 @@ describe("redacting what the bridge reports", () => {
|
|
|
309
316
|
},
|
|
310
317
|
]);
|
|
311
318
|
});
|
|
319
|
+
|
|
320
|
+
it("covers a raw console line and keeps its stream", () => {
|
|
321
|
+
const lines: unknown[] = [];
|
|
322
|
+
const printer = redactPrinter((line, stream) => {
|
|
323
|
+
lines.push({
|
|
324
|
+
line,
|
|
325
|
+
stream,
|
|
326
|
+
});
|
|
327
|
+
}, redact);
|
|
328
|
+
|
|
329
|
+
printer(`token=${TOKEN}`, BridgeStream.Stderr);
|
|
330
|
+
|
|
331
|
+
expect(lines).toEqual([
|
|
332
|
+
{
|
|
333
|
+
line: `token=${REDACTED}`,
|
|
334
|
+
stream: BridgeStream.Stderr,
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
337
|
+
});
|
|
312
338
|
});
|
|
313
339
|
|
|
314
340
|
describe("redaction over the live config", () => {
|
|
@@ -2,7 +2,7 @@ import { BridgeSetupPromptKind, type LocalizedText } from "../core";
|
|
|
2
2
|
import type { BridgeDriver } from "../modules";
|
|
3
3
|
import { BridgeControl, BridgeLayout } from "../panel";
|
|
4
4
|
import type { BridgeSetupAction, BridgeSetupPrompt, BridgeSetupRuntime } from "../protocol";
|
|
5
|
-
import type { BridgeLeveledLogger, BridgeLogger } from "./bridgeLog";
|
|
5
|
+
import type { BridgeLeveledLogger, BridgeLogger, BridgePrinter } from "./bridgeLog";
|
|
6
6
|
|
|
7
7
|
export const REDACTED = "••••";
|
|
8
8
|
|
|
@@ -212,3 +212,9 @@ export const redactLogger = (logger: BridgeLeveledLogger, redact: BridgeRedact):
|
|
|
212
212
|
error: redactingWriter(logger.error, redact),
|
|
213
213
|
});
|
|
214
214
|
};
|
|
215
|
+
|
|
216
|
+
export const redactPrinter = (printer: BridgePrinter, redact: BridgeRedact): BridgePrinter => {
|
|
217
|
+
return (line, stream) => {
|
|
218
|
+
printer(redact(line), stream);
|
|
219
|
+
};
|
|
220
|
+
};
|
|
@@ -91,6 +91,9 @@ const harnessFor = (driver: BridgeDriver, variables: Record<string, string> = {}
|
|
|
91
91
|
|
|
92
92
|
const context = createContext(settings, supervisor, {
|
|
93
93
|
onLog: log,
|
|
94
|
+
onPrint() {
|
|
95
|
+
return;
|
|
96
|
+
},
|
|
94
97
|
onEmit(event, payload) {
|
|
95
98
|
ring.emit(event, payload);
|
|
96
99
|
},
|
|
@@ -773,6 +776,33 @@ describe("the setup singleton", () => {
|
|
|
773
776
|
});
|
|
774
777
|
});
|
|
775
778
|
|
|
779
|
+
describe("the terminal block", () => {
|
|
780
|
+
const terminalDriver = (shell?: boolean): BridgeDriver => ({
|
|
781
|
+
...baseDriver(),
|
|
782
|
+
terminal: {
|
|
783
|
+
kind: BridgeKind.Terminal,
|
|
784
|
+
commands: [],
|
|
785
|
+
...(shell === undefined
|
|
786
|
+
? {}
|
|
787
|
+
: {
|
|
788
|
+
shell,
|
|
789
|
+
}),
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("advertises a terminal that runs its input as a shell command", () => {
|
|
794
|
+
expect(registryFor(terminalDriver(true)).manifest().terminal).toEqual({
|
|
795
|
+
commands: [],
|
|
796
|
+
rules: [],
|
|
797
|
+
shell: true,
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it("advertises a terminal that declares no shell flag as one that writes to the process", () => {
|
|
802
|
+
expect(registryFor(terminalDriver()).manifest().terminal?.shell).toBe(false);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
776
806
|
describe("protected action dispatch", () => {
|
|
777
807
|
it("advertises protection and refuses old callers before touching files", async () => {
|
|
778
808
|
let calls = 0;
|
|
@@ -6,7 +6,7 @@ import { BRIDGE_CRASH_LINE_LIMIT, BridgeCrashPhase, type BridgeCrashRecord } fro
|
|
|
6
6
|
import { stripAnsi } from "../utils";
|
|
7
7
|
import { createLineSpacing } from "./bridgeLineSpacing";
|
|
8
8
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
9
|
-
import { errorText } from "./bridgeLog";
|
|
9
|
+
import { errorText, print } from "./bridgeLog";
|
|
10
10
|
import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
|
|
11
11
|
import { readProcessGroup, signalGroup } from "./bridgeSignal";
|
|
12
12
|
|
|
@@ -287,11 +287,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
287
287
|
}
|
|
288
288
|
: raw;
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
process.stderr.write(`${entry.line}\n`);
|
|
292
|
-
} else {
|
|
293
|
-
process.stdout.write(`${entry.line}\n`);
|
|
294
|
-
}
|
|
290
|
+
print(entry.line, entry.stream);
|
|
295
291
|
|
|
296
292
|
tail.push(entry);
|
|
297
293
|
|
package/src/runtime/serve.ts
CHANGED
|
@@ -19,9 +19,15 @@ import { validateDriver } from "./bridgeDriver";
|
|
|
19
19
|
import { createEventRing } from "./bridgeEvents";
|
|
20
20
|
import { decideInstall, recountHold } from "./bridgeInstall";
|
|
21
21
|
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
22
|
-
import { errorDetail, errorText, log as stdoutLog } from "./bridgeLog";
|
|
22
|
+
import { errorDetail, errorText, print, log as stdoutLog } from "./bridgeLog";
|
|
23
23
|
import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
createBridgeRedaction,
|
|
26
|
+
redactLocalized,
|
|
27
|
+
redactLogger,
|
|
28
|
+
redactPrinter,
|
|
29
|
+
secretFieldKeys,
|
|
30
|
+
} from "./bridgeRedaction";
|
|
25
31
|
import { createRegistry } from "./bridgeRegistry";
|
|
26
32
|
import { createServer } from "./bridgeServer";
|
|
27
33
|
import { setupSummaryOf } from "./bridgeSetup";
|
|
@@ -182,6 +188,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
182
188
|
supervisor,
|
|
183
189
|
{
|
|
184
190
|
onLog: log,
|
|
191
|
+
onPrint: redactPrinter(print, redact),
|
|
185
192
|
onEmit(event, payload) {
|
|
186
193
|
ring.emit(event, payload);
|
|
187
194
|
},
|
package/src/wire/panel.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
BridgePlace,
|
|
14
14
|
type BridgeSetupManifest,
|
|
15
15
|
BridgeSetupStepKind,
|
|
16
|
+
type BridgeTerminalManifest,
|
|
16
17
|
BridgeUploadMode,
|
|
17
18
|
} from "@serverkgg/bridge/protocol";
|
|
18
19
|
import { bridgeAvailabilitySchema, bridgeCellsSchema, bridgeManifestSchema } from "./panel";
|
|
@@ -783,3 +784,34 @@ describe("the availability schema", () => {
|
|
|
783
784
|
expect(parsed.modules[0]?.actions).toContain(BridgeAction.Availability);
|
|
784
785
|
});
|
|
785
786
|
});
|
|
787
|
+
|
|
788
|
+
const terminalManifest = (shell: boolean): BridgeManifest => ({
|
|
789
|
+
version: "1.0.0",
|
|
790
|
+
panel: null,
|
|
791
|
+
terminal: {
|
|
792
|
+
commands: [],
|
|
793
|
+
rules: [],
|
|
794
|
+
shell,
|
|
795
|
+
},
|
|
796
|
+
setup: null,
|
|
797
|
+
modules: [],
|
|
798
|
+
events: [],
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
describe("the terminal mirror", () => {
|
|
802
|
+
it("carries the shell flag through unchanged", () => {
|
|
803
|
+
expect(bridgeManifestSchema.parse(terminalManifest(true)).terminal?.shell).toBe(true);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("defaults a terminal from an older bridge that carries no shell flag to false", () => {
|
|
807
|
+
const older = terminalManifest(true);
|
|
808
|
+
|
|
809
|
+
delete (older.terminal as Partial<BridgeTerminalManifest>).shell;
|
|
810
|
+
|
|
811
|
+
expect(bridgeManifestSchema.parse(older).terminal).toEqual({
|
|
812
|
+
commands: [],
|
|
813
|
+
rules: [],
|
|
814
|
+
shell: false,
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
});
|
package/src/wire/panel.ts
CHANGED
|
@@ -318,6 +318,7 @@ export const bridgeTerminalRuleSchema: z.ZodType<BridgeTerminalRuleManifest> = z
|
|
|
318
318
|
export const bridgeTerminalManifestSchema: z.ZodType<BridgeTerminalManifest> = z.object({
|
|
319
319
|
commands: z.array(bridgeTerminalCommandSchema).max(128),
|
|
320
320
|
rules: z.array(bridgeTerminalRuleSchema).max(64),
|
|
321
|
+
shell: z.boolean().default(false),
|
|
321
322
|
});
|
|
322
323
|
|
|
323
324
|
const setupStepBase = {
|