querysub 0.244.0 β 0.246.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/package.json +1 -1
- package/src/deployManager/MachinesPage.tsx +1 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +22 -13
- package/src/deployManager/machineApplyMainCode.ts +15 -14
- package/src/deployManager/machineController.ts +0 -2
- package/src/deployManager/spec.txt +1 -8
- package/src/diagnostics/logs/ObjectDisplay.tsx +1 -1
- package/src/diagnostics/logs/ansiFormat.ts +26 -2
- package/src/functional/runCommand.ts +1 -2
- package/src/library-components/StickyBottomScroll.tsx +41 -0
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@ export class MachinesPage extends qreact.Component {
|
|
|
42
42
|
|
|
43
43
|
return <div className={css.vbox(0).fillBoth.pad2(20)}>
|
|
44
44
|
{this.renderTabs()}
|
|
45
|
-
<div className={css.flexGrow(1).overflowAuto.pad2(20, 10)}>
|
|
45
|
+
<div className={css.flexGrow(1).overflowAuto.pad2(20, 10).fillWidth}>
|
|
46
46
|
{currentViewParam.value === "services" && <ServicesListPage />}
|
|
47
47
|
{currentViewParam.value === "machines" && <MachinesListPage />}
|
|
48
48
|
{currentViewParam.value === "service-detail" && <ServiceDetailPage />}
|
|
@@ -14,6 +14,9 @@ import { isDefined } from "../../misc";
|
|
|
14
14
|
import { watchScreenOutput, stopWatchingScreenOutput } from "../machineController";
|
|
15
15
|
import { getPathStr2 } from "../../path";
|
|
16
16
|
import { ScrollOnMount } from "../../library-components/ScrollOnMount";
|
|
17
|
+
import { StickyBottomScroll } from "../../library-components/StickyBottomScroll";
|
|
18
|
+
import { PrimitiveDisplay } from "../../diagnostics/logs/ObjectDisplay";
|
|
19
|
+
import { parseAnsiColors, rgbToHsl } from "../../diagnostics/logs/ansiFormat";
|
|
17
20
|
|
|
18
21
|
// Type declarations for Monaco editor
|
|
19
22
|
declare global {
|
|
@@ -126,7 +129,7 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
126
129
|
onData: async (data: string) => {
|
|
127
130
|
Querysub.localCommit(() => {
|
|
128
131
|
let fullData = this.state.watchingOutputs[outputKey].data + data;
|
|
129
|
-
fullData = fullData.slice(-
|
|
132
|
+
fullData = fullData.slice(-10_000_000);
|
|
130
133
|
this.state.watchingOutputs[outputKey].data = fullData;
|
|
131
134
|
});
|
|
132
135
|
}
|
|
@@ -304,9 +307,9 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
304
307
|
}}
|
|
305
308
|
/>
|
|
306
309
|
{/* Machine Status */}
|
|
307
|
-
{configT.parameters.deploy && <div className={css.vbox(8)}>
|
|
310
|
+
{configT.parameters.deploy && <div className={css.vbox(8).fillWidth}>
|
|
308
311
|
<h3>Deployed Machines ({config.machineIds.length})</h3>
|
|
309
|
-
<div className={css.vbox(4)}>
|
|
312
|
+
<div className={css.vbox(4).fillWidth}>
|
|
310
313
|
{machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError, index }) => {
|
|
311
314
|
if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
|
|
312
315
|
|
|
@@ -322,10 +325,10 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
322
325
|
let key = config.parameters.key;
|
|
323
326
|
const outputKey = getPathStr2(key, index + "");
|
|
324
327
|
const isWatching = this.state.watchingOutputs[outputKey].isWatching;
|
|
325
|
-
|
|
328
|
+
let outputData = this.state.watchingOutputs[outputKey].data;
|
|
326
329
|
|
|
327
330
|
return <div key={machineId}
|
|
328
|
-
className={css.pad2(12).vbox(10).bord2(0, 0, 20) + backgroundColor}
|
|
331
|
+
className={css.pad2(12).vbox(10).bord2(0, 0, 20).fillWidth + backgroundColor}
|
|
329
332
|
>
|
|
330
333
|
<div
|
|
331
334
|
className={css.hbox(12)}
|
|
@@ -424,17 +427,23 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
424
427
|
css.pad2(8).bord2(0, 0, 10).hsl(0, 0, 10).colorhsl(0, 0, 100)
|
|
425
428
|
.whiteSpace("pre-wrap").fontFamily("monospace")
|
|
426
429
|
.overflowAuto
|
|
427
|
-
.
|
|
430
|
+
.height("60vh")
|
|
431
|
+
.vbox0
|
|
428
432
|
.fillWidth
|
|
429
433
|
}
|
|
430
|
-
// Gives the user the cursor, which makes navigating nice
|
|
431
|
-
contentEditable
|
|
432
434
|
onClick={e => e.stopPropagation()}
|
|
433
|
-
|
|
434
435
|
>
|
|
435
|
-
{
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
<div className={css.flexShrink0}>
|
|
437
|
+
{(() => {
|
|
438
|
+
let parts = parseAnsiColors(outputData);
|
|
439
|
+
return parts.map(({ text, color }) => {
|
|
440
|
+
if (!color) return <span>{text}</span>;
|
|
441
|
+
let hue = rgbToHsl(color).h;
|
|
442
|
+
return <span className={css.hsl(hue, 60, 30)}>{text}</span>;
|
|
443
|
+
});
|
|
444
|
+
})()}
|
|
445
|
+
</div>
|
|
446
|
+
<StickyBottomScroll debugText={`Screen ${outputKey}`} time={Date.now()} />
|
|
438
447
|
</div>
|
|
439
448
|
}
|
|
440
449
|
</div>;
|
|
@@ -453,7 +462,7 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
453
462
|
configT.parameters.deploy = newDeployStatus;
|
|
454
463
|
void this.updateUnsavedChanges(configT);
|
|
455
464
|
}}>
|
|
456
|
-
{configT.parameters.deploy ? "
|
|
465
|
+
{configT.parameters.deploy ? "βΈοΈ Disable" : "π Enable"}
|
|
457
466
|
</button>
|
|
458
467
|
|
|
459
468
|
<button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 80)}
|
|
@@ -87,7 +87,6 @@ export async function streamScreenOutput(config: {
|
|
|
87
87
|
|
|
88
88
|
async function stop() {
|
|
89
89
|
if (stopped) return;
|
|
90
|
-
console.log(`Stopping stream output for ${screenName}`);
|
|
91
90
|
stopped = true;
|
|
92
91
|
|
|
93
92
|
if (childProcess) {
|
|
@@ -100,7 +99,7 @@ export async function streamScreenOutput(config: {
|
|
|
100
99
|
const pipeFile = `${root}${screenName}/pipe.txt`;
|
|
101
100
|
|
|
102
101
|
// Use tail -f to follow the pipe file
|
|
103
|
-
childProcess = spawn(`tail`, ["-f", pipeFile, "--
|
|
102
|
+
childProcess = spawn(`tail`, ["-f", pipeFile, "--lines", "1000"], {
|
|
104
103
|
stdio: "pipe",
|
|
105
104
|
});
|
|
106
105
|
|
|
@@ -108,7 +107,6 @@ export async function streamScreenOutput(config: {
|
|
|
108
107
|
|
|
109
108
|
childProcess.stdout?.on("data", (data) => {
|
|
110
109
|
if (stopped) return;
|
|
111
|
-
console.log(`Captured data for ${screenName}: ${data.length}`);
|
|
112
110
|
started.resolve();
|
|
113
111
|
void onDataWrapped(data.toString());
|
|
114
112
|
});
|
|
@@ -299,18 +297,21 @@ ${config.command}
|
|
|
299
297
|
let pipeScript = path.resolve(config.folder + "../pipe.sh");
|
|
300
298
|
await fs.promises.writeFile(pipeScript, `#!/bin/bash
|
|
301
299
|
line_count=0
|
|
300
|
+
max_lines_limit=$((${PIPE_FILE_LINE_LIMIT} * 100))
|
|
301
|
+
keep_lines_count=$((${PIPE_FILE_LINE_LIMIT} * 50))
|
|
302
|
+
|
|
302
303
|
while IFS= read -r line; do
|
|
303
304
|
echo "$line" >> "${pipeFile}"
|
|
304
305
|
((line_count++))
|
|
305
306
|
|
|
306
|
-
# Check
|
|
307
|
+
# Check line count every ${PIPE_FILE_LINE_LIMIT} lines to avoid too much overhead
|
|
307
308
|
if (( line_count % ${PIPE_FILE_LINE_LIMIT} == 0 )); then
|
|
308
309
|
if [ -f "${pipeFile}" ]; then
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
if [ "$
|
|
312
|
-
# Keep only the last
|
|
313
|
-
tail -n $
|
|
310
|
+
# Count total lines in file
|
|
311
|
+
total_lines=$(wc -l < "${pipeFile}")
|
|
312
|
+
if [ "$total_lines" -gt "$max_lines_limit" ]; then
|
|
313
|
+
# Keep only the last N lines when file gets too many lines
|
|
314
|
+
tail -n "$keep_lines_count" "${pipeFile}" > "${pipeFile}.tmp" && mv "${pipeFile}.tmp" "${pipeFile}"
|
|
314
315
|
fi
|
|
315
316
|
fi
|
|
316
317
|
fi
|
|
@@ -391,6 +392,10 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
391
392
|
for (let config of relevantConfigs) {
|
|
392
393
|
let matchedCount = config.machineIds.filter(id => id === machineId).length;
|
|
393
394
|
for (let i = 0; i < matchedCount; i++) {
|
|
395
|
+
let screenName = getScreenName({
|
|
396
|
+
serviceKey: config.parameters.key,
|
|
397
|
+
index: i,
|
|
398
|
+
});
|
|
394
399
|
|
|
395
400
|
let launchCount = launchesPerService.get(config.serviceId) || 0;
|
|
396
401
|
let lastLaunchedTime = lastLaunchedTimePerService.get(config.serviceId) || 0;
|
|
@@ -400,11 +405,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
400
405
|
totalTimesLaunched: launchCount,
|
|
401
406
|
};
|
|
402
407
|
try {
|
|
403
|
-
let folder = root +
|
|
404
|
-
let screenName = getScreenName({
|
|
405
|
-
serviceKey: config.parameters.key,
|
|
406
|
-
index: i,
|
|
407
|
-
});
|
|
408
|
+
let folder = root + screenName + "/";
|
|
408
409
|
screenNamesUsed.add(screenName);
|
|
409
410
|
let gitFolder = folder + "git/";
|
|
410
411
|
await fs.promises.mkdir(gitFolder, { recursive: true });
|
|
@@ -133,7 +133,6 @@ class MachineControllerClientBase {
|
|
|
133
133
|
}): Promise<void> {
|
|
134
134
|
let forwardToNodeId = forwardedCallbacks.get(config.callbackId);
|
|
135
135
|
if (forwardToNodeId) {
|
|
136
|
-
console.log(`onScreenOutput forward ${config.callbackId} ${config.data.length}`);
|
|
137
136
|
await MachineControllerClient.nodes[forwardToNodeId].onScreenOutput({
|
|
138
137
|
key: config.key,
|
|
139
138
|
index: config.index,
|
|
@@ -142,7 +141,6 @@ class MachineControllerClientBase {
|
|
|
142
141
|
});
|
|
143
142
|
return;
|
|
144
143
|
}
|
|
145
|
-
console.log(`onScreenOutput ${config.callbackId} ${config.data.length}`);
|
|
146
144
|
|
|
147
145
|
let callback = callbacks.get(config.callbackId);
|
|
148
146
|
if (!callback) {
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
todonext;
|
|
2
|
-
Syncing isn't updating?
|
|
3
|
-
|
|
4
|
-
3) Hmm... being able to watch the output for a service would be useful, and... it should be... trivial to do?
|
|
5
|
-
- Buttons per key + index, which we can figure out clientside. Stream results under the button, and we can stream multiple at once?
|
|
6
|
-
- OH, just for superusers, so the browser can connect directly!
|
|
7
|
-
|
|
8
1
|
2) Verify when the file is truncated it still works
|
|
9
2
|
|
|
10
|
-
|
|
3
|
+
2) Apply ansi coloring / whatever type we use
|
|
11
4
|
|
|
12
5
|
4) Destroy our testing digital ocean server
|
|
13
6
|
|
|
@@ -30,7 +30,7 @@ const basicToRGB: Record<number, { r: number; g: number; b: number }> = {
|
|
|
30
30
|
export function parseAnsiColors(input: string): ColorGroup[] {
|
|
31
31
|
const result: ColorGroup[] = [];
|
|
32
32
|
let currentIndex = 0;
|
|
33
|
-
const regex = /\x1b\[(38;2;(\d+);(\d+);(\d+)m|(\d+)m)/g;
|
|
33
|
+
const regex = /\x1b\[(38;2;(\d+);(\d+);(\d+)m|38;5;(\d+)m|(\d+)m)/g;
|
|
34
34
|
let match;
|
|
35
35
|
|
|
36
36
|
while ((match = regex.exec(input)) !== null) {
|
|
@@ -49,8 +49,9 @@ export function parseAnsiColors(input: string): ColorGroup[] {
|
|
|
49
49
|
const text = input.slice(colorStart, textEnd);
|
|
50
50
|
|
|
51
51
|
if (text) {
|
|
52
|
-
const [, fullColor, r, g, b, basicCode] = match;
|
|
52
|
+
const [, fullColor, r, g, b, paletteIndex, basicCode] = match;
|
|
53
53
|
if (r && g && b) {
|
|
54
|
+
// 24-bit RGB color (38;2;r;g;b)
|
|
54
55
|
result.push({
|
|
55
56
|
text,
|
|
56
57
|
color: {
|
|
@@ -59,7 +60,30 @@ export function parseAnsiColors(input: string): ColorGroup[] {
|
|
|
59
60
|
b: parseInt(b)
|
|
60
61
|
}
|
|
61
62
|
});
|
|
63
|
+
} else if (paletteIndex) {
|
|
64
|
+
// 216-color palette (38;5;index)
|
|
65
|
+
const index = parseInt(paletteIndex);
|
|
66
|
+
if (index >= 16 && index < 232) {
|
|
67
|
+
// Convert 216-color palette index back to RGB
|
|
68
|
+
const paletteValue = index - 16;
|
|
69
|
+
const rIndex = Math.floor(paletteValue / 36);
|
|
70
|
+
const gIndex = Math.floor((paletteValue % 36) / 6);
|
|
71
|
+
const bIndex = paletteValue % 6;
|
|
72
|
+
|
|
73
|
+
result.push({
|
|
74
|
+
text,
|
|
75
|
+
color: {
|
|
76
|
+
r: Math.round(rIndex / 5 * 255),
|
|
77
|
+
g: Math.round(gIndex / 5 * 255),
|
|
78
|
+
b: Math.round(bIndex / 5 * 255)
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
// Handle other palette ranges if needed (16 basic colors, grayscale, etc.)
|
|
83
|
+
result.push({ text });
|
|
84
|
+
}
|
|
62
85
|
} else if (basicCode) {
|
|
86
|
+
// Basic color codes (30-37, 90-97)
|
|
63
87
|
const code = parseInt(basicCode);
|
|
64
88
|
const rgbColor = basicToRGB[code];
|
|
65
89
|
result.push({
|
|
@@ -14,8 +14,7 @@ export async function runPromise(command: string, config?: {
|
|
|
14
14
|
const childProc = child_process.spawn(command, {
|
|
15
15
|
shell: true,
|
|
16
16
|
cwd: config?.cwd,
|
|
17
|
-
|
|
18
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
let fullOutput = "";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { css } from "../4-dom/css";
|
|
2
|
+
import { qreact } from "../4-dom/qreact";
|
|
3
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
4
|
+
/** MOST of the time ScrollOnMount will work. But sometimes it won't, in which case this case work IF the holder size is fixed.
|
|
5
|
+
* - This also works well with cases where your content starts small, as this forces you to always have overflow, and also a bit of buffer at the bottom.
|
|
6
|
+
*
|
|
7
|
+
* Put directly inside of the scrollable parent, that has vbox0, and put flexShrink0 on the other content.
|
|
8
|
+
*/
|
|
9
|
+
export class StickyBottomScroll extends qreact.Component<{
|
|
10
|
+
debugText: string;
|
|
11
|
+
// Pass so we re-render every time the parent re-renders
|
|
12
|
+
time: number;
|
|
13
|
+
}> {
|
|
14
|
+
scrollElement: HTMLElement | undefined;
|
|
15
|
+
render() {
|
|
16
|
+
this.props.time;
|
|
17
|
+
const elem = this.scrollElement;
|
|
18
|
+
let debugText = this.props.debugText;
|
|
19
|
+
if (elem) {
|
|
20
|
+
let isScrolledToBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight < 50;
|
|
21
|
+
if (isScrolledToBottom) {
|
|
22
|
+
Querysub.afterAllRendersFinished(() => {
|
|
23
|
+
console.log(`StickyBottomScroll scrolling to bottom (${debugText})`);
|
|
24
|
+
elem.scrollTop = elem.scrollHeight;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<div
|
|
31
|
+
key={this.props.debugText + "StickyBottomScroll"}
|
|
32
|
+
className={css.size(1, `calc(100% + 5px)`)}
|
|
33
|
+
ref2={e => {
|
|
34
|
+
this.scrollElement = e.parentElement!;
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
<div className={css.size(1, 50).flexShrink0} />
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|