@sharpe-jupyter/connect 0.4.2 → 0.4.3
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/dist/index.js +92 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -333,6 +333,7 @@ function parseJupyterLine(line) {
|
|
|
333
333
|
// src/app.tsx
|
|
334
334
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
335
335
|
var MAX_EVENTS = 10;
|
|
336
|
+
var MAX_PROCESS_LOGS = 12;
|
|
336
337
|
var LOGO = ` ..
|
|
337
338
|
.:
|
|
338
339
|
.@.
|
|
@@ -386,11 +387,23 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
386
387
|
const [depsPrompt, setDepsPrompt] = useState(null);
|
|
387
388
|
const [depsStatus, setDepsStatus] = useState(null);
|
|
388
389
|
const depsResolverRef = useRef(null);
|
|
390
|
+
const [processLogs, setProcessLogs] = useState([]);
|
|
389
391
|
const didWeSpawnJupyter = useRef(false);
|
|
390
392
|
const jupyterProc = useRef(null);
|
|
391
393
|
const tunnelProc = useRef(null);
|
|
392
394
|
const cleanedUp = useRef(false);
|
|
395
|
+
const processLogsRef = useRef([]);
|
|
393
396
|
const uptime = useUptime(startedAt);
|
|
397
|
+
const pushProcessLog = useCallback((line) => {
|
|
398
|
+
const trimmed = line.trimEnd();
|
|
399
|
+
if (!trimmed) return;
|
|
400
|
+
setProcessLogs((prev) => {
|
|
401
|
+
const next = [...prev, trimmed];
|
|
402
|
+
const sliced = next.length > MAX_PROCESS_LOGS ? next.slice(-MAX_PROCESS_LOGS) : next;
|
|
403
|
+
processLogsRef.current = sliced;
|
|
404
|
+
return sliced;
|
|
405
|
+
});
|
|
406
|
+
}, []);
|
|
394
407
|
const pushEvent = useCallback((message) => {
|
|
395
408
|
setEvents((prev) => {
|
|
396
409
|
const next = [...prev, { time: formatTime(), message }];
|
|
@@ -439,20 +452,28 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
439
452
|
jupyterProc.current = null;
|
|
440
453
|
}
|
|
441
454
|
setJupyterRunning(false);
|
|
455
|
+
setProcessLogs([]);
|
|
456
|
+
processLogsRef.current = [];
|
|
442
457
|
pushEvent("Restarting notebook server...");
|
|
443
458
|
const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
|
|
444
459
|
jupyterProc.current = proc;
|
|
445
460
|
didWeSpawnJupyter.current = true;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
461
|
+
const pipeStream = (stream) => {
|
|
462
|
+
if (!stream) return;
|
|
463
|
+
let buf = "";
|
|
464
|
+
stream.on("data", (data) => {
|
|
465
|
+
buf += data.toString();
|
|
466
|
+
const lines = buf.split("\n");
|
|
467
|
+
buf = lines.pop() ?? "";
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
pushProcessLog(line);
|
|
470
|
+
const msg = parseJupyterLine(line);
|
|
471
|
+
if (msg) pushEvent(msg);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
pipeStream(proc.stderr);
|
|
476
|
+
pipeStream(proc.stdout);
|
|
456
477
|
proc.on("error", () => {
|
|
457
478
|
setJupyterRunning(false);
|
|
458
479
|
pushEvent("Notebook server failed to start");
|
|
@@ -471,12 +492,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
471
492
|
clearInterval(poll);
|
|
472
493
|
setJupyterRunning(true);
|
|
473
494
|
pushEvent("Notebook server started");
|
|
474
|
-
} else if (attempts >=
|
|
495
|
+
} else if (attempts >= 40) {
|
|
475
496
|
clearInterval(poll);
|
|
476
497
|
pushEvent("Notebook server not responding");
|
|
477
498
|
}
|
|
478
499
|
}, 1e3);
|
|
479
|
-
}, [port2, pushEvent]);
|
|
500
|
+
}, [port2, pushEvent, pushProcessLog]);
|
|
480
501
|
useInput(
|
|
481
502
|
(_input, key) => {
|
|
482
503
|
if (_input === "r" || _input === "R") {
|
|
@@ -501,6 +522,8 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
501
522
|
setTunnelConnected(false);
|
|
502
523
|
setDepsPrompt(null);
|
|
503
524
|
setDepsStatus(null);
|
|
525
|
+
setProcessLogs([]);
|
|
526
|
+
processLogsRef.current = [];
|
|
504
527
|
depsResolverRef.current?.(false);
|
|
505
528
|
depsResolverRef.current = null;
|
|
506
529
|
}, []);
|
|
@@ -543,9 +566,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
543
566
|
const proc = startJupyterWithUv(port2, uvPath);
|
|
544
567
|
jupyterProc.current = proc;
|
|
545
568
|
didWeSpawnJupyter.current = true;
|
|
546
|
-
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
569
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
|
|
547
570
|
if (cancelled) return;
|
|
548
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
571
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
549
572
|
if (errorInfo) {
|
|
550
573
|
setJupyterStep("error");
|
|
551
574
|
setError(errorInfo);
|
|
@@ -629,9 +652,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
629
652
|
const proc = startJupyter(port2);
|
|
630
653
|
jupyterProc.current = proc;
|
|
631
654
|
didWeSpawnJupyter.current = true;
|
|
632
|
-
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
655
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
|
|
633
656
|
if (cancelled) return;
|
|
634
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
657
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
635
658
|
if (errorInfo) {
|
|
636
659
|
setJupyterStep("error");
|
|
637
660
|
setError(errorInfo);
|
|
@@ -819,6 +842,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
819
842
|
),
|
|
820
843
|
/* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
|
|
821
844
|
] }),
|
|
845
|
+
processLogs.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: processLogs.map((line, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate", children: line }, i)) }),
|
|
822
846
|
depsPrompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
823
847
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
824
848
|
"Found ",
|
|
@@ -904,43 +928,64 @@ function StatusBadge({ status }) {
|
|
|
904
928
|
] });
|
|
905
929
|
}
|
|
906
930
|
}
|
|
907
|
-
function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
|
|
931
|
+
function waitForJupyterHealthy(proc, port2, pushEvent, pushLog, isCancelled) {
|
|
908
932
|
return new Promise((resolve) => {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
|
|
933
|
+
let resolved = false;
|
|
934
|
+
const done = (result) => {
|
|
935
|
+
if (resolved) return;
|
|
936
|
+
resolved = true;
|
|
937
|
+
clearInterval(poll);
|
|
938
|
+
resolve(result);
|
|
939
|
+
};
|
|
940
|
+
proc.on("error", (err) => done(err.message));
|
|
941
|
+
proc.on("exit", (code, signal) => {
|
|
942
|
+
if (code !== null && code !== 0) {
|
|
943
|
+
done(`exit-${code}`);
|
|
944
|
+
} else if (signal) {
|
|
945
|
+
done(`signal-${signal}`);
|
|
922
946
|
}
|
|
923
947
|
});
|
|
948
|
+
const pipeOutput = (stream) => {
|
|
949
|
+
if (!stream) return;
|
|
950
|
+
let buf = "";
|
|
951
|
+
stream.on("data", (data) => {
|
|
952
|
+
buf += data.toString();
|
|
953
|
+
const lines = buf.split("\n");
|
|
954
|
+
buf = lines.pop() ?? "";
|
|
955
|
+
for (const line of lines) {
|
|
956
|
+
pushLog(line);
|
|
957
|
+
if (line.includes("Address already in use")) {
|
|
958
|
+
done("port-conflict");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const msg = parseJupyterLine(line);
|
|
962
|
+
if (msg) pushEvent(msg);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
};
|
|
966
|
+
pipeOutput(proc.stderr);
|
|
967
|
+
pipeOutput(proc.stdout);
|
|
924
968
|
let attempts = 0;
|
|
925
969
|
const poll = setInterval(async () => {
|
|
926
970
|
if (isCancelled()) {
|
|
927
|
-
|
|
928
|
-
resolve("cancelled");
|
|
971
|
+
done("cancelled");
|
|
929
972
|
return;
|
|
930
973
|
}
|
|
931
974
|
attempts++;
|
|
932
975
|
const healthy = await checkJupyterHealth(port2);
|
|
933
976
|
if (healthy) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
clearInterval(poll);
|
|
938
|
-
resolve("timeout");
|
|
977
|
+
done(null);
|
|
978
|
+
} else if (attempts >= 40) {
|
|
979
|
+
done("timeout");
|
|
939
980
|
}
|
|
940
981
|
}, 1e3);
|
|
941
982
|
});
|
|
942
983
|
}
|
|
943
|
-
function handleJupyterSpawnError(spawnError, port2) {
|
|
984
|
+
function handleJupyterSpawnError(spawnError, port2, lastLogs) {
|
|
985
|
+
const logTail = lastLogs.length > 0 ? `
|
|
986
|
+
|
|
987
|
+
Process output:
|
|
988
|
+
${lastLogs.slice(-8).join("\n")}` : "";
|
|
944
989
|
if (spawnError === "port-conflict") {
|
|
945
990
|
return {
|
|
946
991
|
message: "Could not start the notebook server.",
|
|
@@ -953,13 +998,20 @@ Close whatever is using it and try again, or use a different port:
|
|
|
953
998
|
if (spawnError === "timeout") {
|
|
954
999
|
return {
|
|
955
1000
|
message: "The notebook server isn't responding.",
|
|
956
|
-
hint: `
|
|
1001
|
+
hint: `Waited 40s but http://localhost:${port2}/api/status never returned OK.${logTail}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (spawnError?.startsWith("exit-")) {
|
|
1005
|
+
const code = spawnError.slice(5);
|
|
1006
|
+
return {
|
|
1007
|
+
message: `Notebook server process exited with code ${code}.`,
|
|
1008
|
+
hint: logTail || "No output was captured from the process."
|
|
957
1009
|
};
|
|
958
1010
|
}
|
|
959
1011
|
if (spawnError && spawnError !== "cancelled") {
|
|
960
1012
|
return {
|
|
961
1013
|
message: "Could not start the notebook server.",
|
|
962
|
-
hint:
|
|
1014
|
+
hint: `${spawnError}${logTail}`
|
|
963
1015
|
};
|
|
964
1016
|
}
|
|
965
1017
|
return null;
|