querysub 0.245.0 β†’ 0.247.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.245.0",
3
+ "version": "0.247.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -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(-10000);
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
- const outputData = this.state.watchingOutputs[outputKey].data;
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
- .maxHeight("60vh")
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
- {outputData && outputData || "Waiting for output..."}
436
- {/* <div className={css.size(1, `65vh`)} /> */}
437
- <ScrollOnMount debugText={`Screen ${outputKey}`} />
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 ? "πŸ”’ Disable" : "πŸš€ Enable"}
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, "--retry", "--lines", "1000"], {
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
  });
@@ -269,7 +267,12 @@ const runScreenCommand = measureWrap(async function runScreenCommand(config: {
269
267
  if (pid && await isScreenRunningProcess(pid)) {
270
268
  // It doesn't want to die. Wait longer, but it it just won't die, kill the screen
271
269
  console.warn(`Screen ${screenName} is not dying, giving it another 30 seconds`);
272
- await delay(timeInSecond * 30);
270
+ for (let i = 0; i < 6; i++) {
271
+ await delay(5);
272
+ if (!await isScreenRunningProcess(pid)) {
273
+ break;
274
+ }
275
+ }
273
276
  if (pid && await isScreenRunningProcess(pid)) {
274
277
  console.warn(`Screen ${screenName} is still running, killing it forcefully`);
275
278
  await killScreen({ screenName });
@@ -303,14 +306,14 @@ while IFS= read -r line; do
303
306
  echo "$line" >> "${pipeFile}"
304
307
  ((line_count++))
305
308
 
306
- # Check file size every 100 lines to avoid too much overhead
309
+ # Check line count every ${PIPE_FILE_LINE_LIMIT} lines to avoid too much overhead
307
310
  if (( line_count % ${PIPE_FILE_LINE_LIMIT} == 0 )); then
308
311
  if [ -f "${pipeFile}" ]; then
309
- # Get file size (works on both Linux and macOS)
310
- filesize=$(stat -f%z "${pipeFile}" 2>/dev/null || stat -c%s "${pipeFile}" 2>/dev/null || echo 0)
311
- if [ "$filesize" -gt 1048576 ]; then
312
- # Keep only the last 1000 lines when file gets too big
313
- tail -n ${PIPE_FILE_LINE_LIMIT} "${pipeFile}" > "${pipeFile}.tmp" && mv "${pipeFile}.tmp" "${pipeFile}"
312
+ # Count total lines in file
313
+ total_lines=$(wc -l < "${pipeFile}")
314
+ if [ "$total_lines" -gt ${PIPE_FILE_LINE_LIMIT} ]; then
315
+ # Keep only the last ${PIPE_FILE_LINE_LIMIT} lines when file gets too many lines
316
+ tail -n ${Math.floor(PIPE_FILE_LINE_LIMIT / 2)} "${pipeFile}" > "${pipeFile}.tmp" && mv "${pipeFile}.tmp" "${pipeFile}"
314
317
  fi
315
318
  fi
316
319
  fi
@@ -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) OH! I think if something is scrollable, and is scrolled down, it'll stick to the bottom. We just need it to start scrollable!
3
+ 2) Apply ansi coloring / whatever type we use
11
4
 
12
5
  4) Destroy our testing digital ocean server
13
6
 
@@ -126,7 +126,7 @@ export class ObjectDisplay extends qreact.Component<{
126
126
  }
127
127
  }
128
128
 
129
- class PrimitiveDisplay extends qreact.Component<{
129
+ export class PrimitiveDisplay extends qreact.Component<{
130
130
  value: unknown;
131
131
  }> {
132
132
  render() {
@@ -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
- // NOTE: Now we pipe stdin, as I think inheriting it was causing us to block sometimes?
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
+ }