@serverkgg/bridge 0.1.11 → 0.1.12
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/src/runtime/bridgeLineSpacing.test.ts +132 -0
- package/src/runtime/bridgeLineSpacing.ts +49 -0
- package/src/runtime/bridgeSupervisor.ts +5 -3
- package/src/steam/steamcmdNarrator.test.ts +18 -5
- package/src/steam/steamcmdNarrator.ts +26 -2
- package/src/wine/protonCommand.test.ts +17 -1
- package/src/wine/protonCommand.ts +7 -1
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createLineSpacing } from "./bridgeLineSpacing";
|
|
3
|
+
|
|
4
|
+
const shown = (lines: string[]) => {
|
|
5
|
+
const spacing = createLineSpacing();
|
|
6
|
+
|
|
7
|
+
return lines.flatMap((line) => spacing(line));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("createLineSpacing", () => {
|
|
11
|
+
it("drops the blank a game prints after every line once the pattern is clear", () => {
|
|
12
|
+
expect(
|
|
13
|
+
shown([
|
|
14
|
+
"[LOG] one",
|
|
15
|
+
"",
|
|
16
|
+
"[LOG] two",
|
|
17
|
+
"",
|
|
18
|
+
"[LOG] three",
|
|
19
|
+
"",
|
|
20
|
+
"[LOG] four",
|
|
21
|
+
"",
|
|
22
|
+
"[LOG] five",
|
|
23
|
+
"",
|
|
24
|
+
]),
|
|
25
|
+
).toEqual([
|
|
26
|
+
"[LOG] one",
|
|
27
|
+
"",
|
|
28
|
+
"[LOG] two",
|
|
29
|
+
"",
|
|
30
|
+
"[LOG] three",
|
|
31
|
+
"[LOG] four",
|
|
32
|
+
"[LOG] five",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps a single blank line a single-spaced game uses to separate blocks", () => {
|
|
37
|
+
expect(
|
|
38
|
+
shown([
|
|
39
|
+
"Loading world",
|
|
40
|
+
"Loading players",
|
|
41
|
+
"",
|
|
42
|
+
"Server ready",
|
|
43
|
+
]),
|
|
44
|
+
).toEqual([
|
|
45
|
+
"Loading world",
|
|
46
|
+
"Loading players",
|
|
47
|
+
"",
|
|
48
|
+
"Server ready",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("collapses a run of blank lines into one", () => {
|
|
53
|
+
expect(
|
|
54
|
+
shown([
|
|
55
|
+
"banner",
|
|
56
|
+
"",
|
|
57
|
+
"",
|
|
58
|
+
"",
|
|
59
|
+
"first log line",
|
|
60
|
+
]),
|
|
61
|
+
).toEqual([
|
|
62
|
+
"banner",
|
|
63
|
+
"",
|
|
64
|
+
"first log line",
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("still shows a deliberate break inside a double-spaced stream", () => {
|
|
69
|
+
expect(
|
|
70
|
+
shown([
|
|
71
|
+
"a",
|
|
72
|
+
"",
|
|
73
|
+
"b",
|
|
74
|
+
"",
|
|
75
|
+
"c",
|
|
76
|
+
"",
|
|
77
|
+
"d",
|
|
78
|
+
"",
|
|
79
|
+
"",
|
|
80
|
+
"e",
|
|
81
|
+
]),
|
|
82
|
+
).toEqual([
|
|
83
|
+
"a",
|
|
84
|
+
"",
|
|
85
|
+
"b",
|
|
86
|
+
"",
|
|
87
|
+
"c",
|
|
88
|
+
"d",
|
|
89
|
+
"",
|
|
90
|
+
"e",
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("drops blank lines before the first line and whitespace-only lines", () => {
|
|
95
|
+
expect(
|
|
96
|
+
shown([
|
|
97
|
+
"",
|
|
98
|
+
" ",
|
|
99
|
+
"first",
|
|
100
|
+
]),
|
|
101
|
+
).toEqual([
|
|
102
|
+
"first",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("goes back to keeping single blanks when the game starts writing lines back to back", () => {
|
|
107
|
+
expect(
|
|
108
|
+
shown([
|
|
109
|
+
"a",
|
|
110
|
+
"",
|
|
111
|
+
"b",
|
|
112
|
+
"",
|
|
113
|
+
"c",
|
|
114
|
+
"",
|
|
115
|
+
"d",
|
|
116
|
+
"e",
|
|
117
|
+
"",
|
|
118
|
+
"f",
|
|
119
|
+
]),
|
|
120
|
+
).toEqual([
|
|
121
|
+
"a",
|
|
122
|
+
"",
|
|
123
|
+
"b",
|
|
124
|
+
"",
|
|
125
|
+
"c",
|
|
126
|
+
"d",
|
|
127
|
+
"e",
|
|
128
|
+
"",
|
|
129
|
+
"f",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DOUBLE_SPACED_RUN = 3;
|
|
2
|
+
|
|
3
|
+
export type BridgeLineSpacing = (line: string) => string[];
|
|
4
|
+
|
|
5
|
+
export const createLineSpacing = (): BridgeLineSpacing => {
|
|
6
|
+
const gaps: number[] = [];
|
|
7
|
+
|
|
8
|
+
let pending = 0;
|
|
9
|
+
let started = false;
|
|
10
|
+
|
|
11
|
+
const doubleSpaced = () => {
|
|
12
|
+
return gaps.length === DOUBLE_SPACED_RUN && gaps.every((gap) => gap === 1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (line) => {
|
|
16
|
+
if (line.trim().length === 0) {
|
|
17
|
+
if (started) {
|
|
18
|
+
pending += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const gap = pending;
|
|
25
|
+
const first = !started;
|
|
26
|
+
|
|
27
|
+
if (started) {
|
|
28
|
+
gaps.push(gap);
|
|
29
|
+
|
|
30
|
+
if (gaps.length > DOUBLE_SPACED_RUN) {
|
|
31
|
+
gaps.shift();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
started = true;
|
|
36
|
+
pending = 0;
|
|
37
|
+
|
|
38
|
+
if (first || gap === 0 || (gap === 1 && doubleSpaced())) {
|
|
39
|
+
return [
|
|
40
|
+
line,
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
"",
|
|
46
|
+
line,
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -4,6 +4,7 @@ import { BridgeLifecycleSubject } from "../modules/bridgeLifecycleSubject";
|
|
|
4
4
|
import { BRIDGE_STOP_SIGNALS, type BridgeStopSignal } from "../modules/bridgeStopSignal";
|
|
5
5
|
import { BRIDGE_CRASH_LINE_LIMIT, BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
|
|
6
6
|
import { stripAnsi } from "../utils";
|
|
7
|
+
import { createLineSpacing } from "./bridgeLineSpacing";
|
|
7
8
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
8
9
|
import { errorText } from "./bridgeLog";
|
|
9
10
|
import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
|
|
@@ -306,6 +307,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
306
307
|
const pump = async (stream: ReadableStream<Uint8Array>, kind: BridgeStream) => {
|
|
307
308
|
const decoder = new TextDecoder();
|
|
308
309
|
const reader = stream.getReader();
|
|
310
|
+
const spacing = createLineSpacing();
|
|
309
311
|
|
|
310
312
|
let buffer = "";
|
|
311
313
|
|
|
@@ -325,7 +327,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
325
327
|
|
|
326
328
|
buffer = split.rest;
|
|
327
329
|
|
|
328
|
-
for (const line of split.lines) {
|
|
330
|
+
for (const line of split.lines.flatMap(spacing)) {
|
|
329
331
|
emit({
|
|
330
332
|
stream: kind,
|
|
331
333
|
line,
|
|
@@ -333,10 +335,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
333
335
|
}
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
|
|
338
|
+
for (const line of spacing(buffer)) {
|
|
337
339
|
emit({
|
|
338
340
|
stream: kind,
|
|
339
|
-
line
|
|
341
|
+
line,
|
|
340
342
|
});
|
|
341
343
|
}
|
|
342
344
|
};
|
|
@@ -20,7 +20,7 @@ const recorder = () => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
describe("steamcmdNarrator", () => {
|
|
23
|
-
it("reports download progress in ten percent steps", () => {
|
|
23
|
+
it("reports download progress in ten percent steps, each line with the percent steamcmd printed", () => {
|
|
24
24
|
const { log, lines, fields } = recorder();
|
|
25
25
|
const narrate = steamcmdNarrator(log, "palworld");
|
|
26
26
|
|
|
@@ -36,10 +36,10 @@ describe("steamcmdNarrator", () => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
expect(lines).toEqual([
|
|
39
|
-
"downloading palworld",
|
|
40
|
-
"downloading palworld",
|
|
41
|
-
"downloading palworld",
|
|
42
|
-
"downloading palworld",
|
|
39
|
+
"downloading palworld 2%",
|
|
40
|
+
"downloading palworld 11%",
|
|
41
|
+
"downloading palworld 25%",
|
|
42
|
+
"downloading palworld 99%",
|
|
43
43
|
]);
|
|
44
44
|
expect(fields).toEqual([
|
|
45
45
|
{
|
|
@@ -74,6 +74,19 @@ describe("steamcmdNarrator", () => {
|
|
|
74
74
|
]);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("says how much of the download is done when steamcmd prints the byte counts", () => {
|
|
78
|
+
const { log, lines } = recorder();
|
|
79
|
+
const narrate = steamcmdNarrator(log, "palworld");
|
|
80
|
+
|
|
81
|
+
narrate("Update state (0x61) downloading, progress: 1.11 (127644881 / 11530459441)");
|
|
82
|
+
narrate("Update state (0x61) downloading, progress: 40.02 (4614180864 / 11530459441)");
|
|
83
|
+
|
|
84
|
+
expect(lines).toEqual([
|
|
85
|
+
"downloading palworld 1% (122 MB of 10.7 GB)",
|
|
86
|
+
"downloading palworld 40% (4.3 GB of 10.7 GB)",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
77
90
|
it("passes notable lines through with the ansi stripped", () => {
|
|
78
91
|
const { log, lines } = recorder();
|
|
79
92
|
const narrate = steamcmdNarrator(log, "rust");
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import type { BridgeLog } from "../context";
|
|
2
2
|
import { stripAnsi } from "../utils";
|
|
3
3
|
|
|
4
|
-
const PROGRESS =
|
|
4
|
+
const PROGRESS =
|
|
5
|
+
/Update state \(0x[0-9a-f]+\)\s*(?<phase>[^,]+),\s*progress:\s*(?<percent>\d+)\.\d+(?:\s*\((?<done>\d+)\s*\/\s*(?<total>\d+)\))?/i;
|
|
6
|
+
|
|
7
|
+
const GIGABYTE = 1024 ** 3;
|
|
8
|
+
|
|
9
|
+
const MEGABYTE = 1024 ** 2;
|
|
5
10
|
|
|
6
11
|
const PROGRESS_STEP = 10;
|
|
7
12
|
|
|
8
13
|
const NOTABLE = /^(Error|Warning|Failed|Success|Logging in|Connecting|Update state \(0x\d+\) (?!downloading))/i;
|
|
9
14
|
|
|
15
|
+
const byteSizeOf = (bytes: number) => {
|
|
16
|
+
if (bytes >= GIGABYTE) {
|
|
17
|
+
return `${(bytes / GIGABYTE).toFixed(1)} GB`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `${Math.round(bytes / MEGABYTE)} MB`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const progressLineOf = (phase: string, label: string, percent: number, done: number, total: number) => {
|
|
24
|
+
if (total < MEGABYTE) {
|
|
25
|
+
return `${phase} ${label} ${percent}%`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `${phase} ${label} ${percent}% (${byteSizeOf(done)} of ${byteSizeOf(total)})`;
|
|
29
|
+
};
|
|
30
|
+
|
|
10
31
|
export const steamcmdNarrator = (log: BridgeLog, label: string) => {
|
|
11
32
|
let phase = "";
|
|
12
33
|
let reported = -1;
|
|
@@ -28,7 +49,10 @@ export const steamcmdNarrator = (log: BridgeLog, label: string) => {
|
|
|
28
49
|
if (step > reported) {
|
|
29
50
|
reported = step;
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
const done = Number(progress.groups.done ?? 0);
|
|
53
|
+
const total = Number(progress.groups.total ?? 0);
|
|
54
|
+
|
|
55
|
+
log(progressLineOf(phase, label, percent, done, total), {
|
|
32
56
|
percent: step,
|
|
33
57
|
});
|
|
34
58
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
|
-
import { protonCommand, protonCompatPath } from "./protonCommand";
|
|
3
|
+
import { ProtonVerb, protonCommand, protonCompatPath } from "./protonCommand";
|
|
4
4
|
|
|
5
5
|
const ensured: string[] = [];
|
|
6
6
|
|
|
@@ -41,6 +41,22 @@ describe("protonCommand", () => {
|
|
|
41
41
|
]);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it("runs straight in the prefix when asked, so the game keeps the container's stdout", async () => {
|
|
45
|
+
expect(
|
|
46
|
+
(
|
|
47
|
+
await protonCommand(context, {
|
|
48
|
+
appId: "2394010",
|
|
49
|
+
exe: "Pal/Binaries/Win64/PalServer-Win64-Shipping-Cmd.exe",
|
|
50
|
+
verb: ProtonVerb.RunInPrefix,
|
|
51
|
+
})
|
|
52
|
+
).slice(-3),
|
|
53
|
+
).toEqual([
|
|
54
|
+
"/opt/proton/proton",
|
|
55
|
+
"runinprefix",
|
|
56
|
+
"Pal/Binaries/Win64/PalServer-Win64-Shipping-Cmd.exe",
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
44
60
|
it("ensures the compat and steam directories", async () => {
|
|
45
61
|
ensured.length = 0;
|
|
46
62
|
|
|
@@ -7,6 +7,11 @@ export const PROTON_COMPAT_DIRECTORY = ".proton";
|
|
|
7
7
|
|
|
8
8
|
export const STEAM_COMPAT_CLIENT_DIRECTORY = ".steam";
|
|
9
9
|
|
|
10
|
+
export enum ProtonVerb {
|
|
11
|
+
Run = "run",
|
|
12
|
+
RunInPrefix = "runinprefix",
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export interface ProtonCommandOptions {
|
|
11
16
|
appId: string;
|
|
12
17
|
exe: string;
|
|
@@ -16,6 +21,7 @@ export interface ProtonCommandOptions {
|
|
|
16
21
|
steamDir?: string;
|
|
17
22
|
home?: string;
|
|
18
23
|
environment?: Record<string, string>;
|
|
24
|
+
verb?: ProtonVerb;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export const protonCompatPath = (appId: string, compatDir: string = PROTON_COMPAT_DIRECTORY) => {
|
|
@@ -42,7 +48,7 @@ export const protonCommand = async (context: BridgeContext, options: ProtonComma
|
|
|
42
48
|
"env",
|
|
43
49
|
...Object.entries(environment).map(([key, value]) => `${key}=${value}`),
|
|
44
50
|
`${protonDir}/proton`,
|
|
45
|
-
|
|
51
|
+
options.verb ?? ProtonVerb.Run,
|
|
46
52
|
options.exe,
|
|
47
53
|
...(options.args ?? []),
|
|
48
54
|
];
|