@sharpe-jupyter/connect 0.4.1 → 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 +102 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -232,7 +232,11 @@ function installPackages(packages) {
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
function startJupyter(port2) {
|
|
235
|
-
const env = {
|
|
235
|
+
const env = {
|
|
236
|
+
...process.env,
|
|
237
|
+
PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}`,
|
|
238
|
+
SHARPE_ENV_DIR: process.cwd()
|
|
239
|
+
};
|
|
236
240
|
return spawn(
|
|
237
241
|
venvBin("jupyter"),
|
|
238
242
|
[
|
|
@@ -254,7 +258,11 @@ function isUvProject() {
|
|
|
254
258
|
return existsSync4(join4(process.cwd(), "uv.lock"));
|
|
255
259
|
}
|
|
256
260
|
function startJupyterWithUv(port2, uvBin) {
|
|
257
|
-
const env = {
|
|
261
|
+
const env = {
|
|
262
|
+
...process.env,
|
|
263
|
+
PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}`,
|
|
264
|
+
SHARPE_ENV_DIR: process.cwd()
|
|
265
|
+
};
|
|
258
266
|
return spawn(
|
|
259
267
|
uvBin,
|
|
260
268
|
[
|
|
@@ -325,6 +333,7 @@ function parseJupyterLine(line) {
|
|
|
325
333
|
// src/app.tsx
|
|
326
334
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
327
335
|
var MAX_EVENTS = 10;
|
|
336
|
+
var MAX_PROCESS_LOGS = 12;
|
|
328
337
|
var LOGO = ` ..
|
|
329
338
|
.:
|
|
330
339
|
.@.
|
|
@@ -378,11 +387,23 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
378
387
|
const [depsPrompt, setDepsPrompt] = useState(null);
|
|
379
388
|
const [depsStatus, setDepsStatus] = useState(null);
|
|
380
389
|
const depsResolverRef = useRef(null);
|
|
390
|
+
const [processLogs, setProcessLogs] = useState([]);
|
|
381
391
|
const didWeSpawnJupyter = useRef(false);
|
|
382
392
|
const jupyterProc = useRef(null);
|
|
383
393
|
const tunnelProc = useRef(null);
|
|
384
394
|
const cleanedUp = useRef(false);
|
|
395
|
+
const processLogsRef = useRef([]);
|
|
385
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
|
+
}, []);
|
|
386
407
|
const pushEvent = useCallback((message) => {
|
|
387
408
|
setEvents((prev) => {
|
|
388
409
|
const next = [...prev, { time: formatTime(), message }];
|
|
@@ -431,20 +452,28 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
431
452
|
jupyterProc.current = null;
|
|
432
453
|
}
|
|
433
454
|
setJupyterRunning(false);
|
|
455
|
+
setProcessLogs([]);
|
|
456
|
+
processLogsRef.current = [];
|
|
434
457
|
pushEvent("Restarting notebook server...");
|
|
435
458
|
const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
|
|
436
459
|
jupyterProc.current = proc;
|
|
437
460
|
didWeSpawnJupyter.current = true;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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);
|
|
448
477
|
proc.on("error", () => {
|
|
449
478
|
setJupyterRunning(false);
|
|
450
479
|
pushEvent("Notebook server failed to start");
|
|
@@ -463,12 +492,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
463
492
|
clearInterval(poll);
|
|
464
493
|
setJupyterRunning(true);
|
|
465
494
|
pushEvent("Notebook server started");
|
|
466
|
-
} else if (attempts >=
|
|
495
|
+
} else if (attempts >= 40) {
|
|
467
496
|
clearInterval(poll);
|
|
468
497
|
pushEvent("Notebook server not responding");
|
|
469
498
|
}
|
|
470
499
|
}, 1e3);
|
|
471
|
-
}, [port2, pushEvent]);
|
|
500
|
+
}, [port2, pushEvent, pushProcessLog]);
|
|
472
501
|
useInput(
|
|
473
502
|
(_input, key) => {
|
|
474
503
|
if (_input === "r" || _input === "R") {
|
|
@@ -493,6 +522,8 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
493
522
|
setTunnelConnected(false);
|
|
494
523
|
setDepsPrompt(null);
|
|
495
524
|
setDepsStatus(null);
|
|
525
|
+
setProcessLogs([]);
|
|
526
|
+
processLogsRef.current = [];
|
|
496
527
|
depsResolverRef.current?.(false);
|
|
497
528
|
depsResolverRef.current = null;
|
|
498
529
|
}, []);
|
|
@@ -535,9 +566,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
535
566
|
const proc = startJupyterWithUv(port2, uvPath);
|
|
536
567
|
jupyterProc.current = proc;
|
|
537
568
|
didWeSpawnJupyter.current = true;
|
|
538
|
-
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
569
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
|
|
539
570
|
if (cancelled) return;
|
|
540
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
571
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
541
572
|
if (errorInfo) {
|
|
542
573
|
setJupyterStep("error");
|
|
543
574
|
setError(errorInfo);
|
|
@@ -621,9 +652,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
621
652
|
const proc = startJupyter(port2);
|
|
622
653
|
jupyterProc.current = proc;
|
|
623
654
|
didWeSpawnJupyter.current = true;
|
|
624
|
-
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
655
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
|
|
625
656
|
if (cancelled) return;
|
|
626
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
657
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
627
658
|
if (errorInfo) {
|
|
628
659
|
setJupyterStep("error");
|
|
629
660
|
setError(errorInfo);
|
|
@@ -811,6 +842,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
811
842
|
),
|
|
812
843
|
/* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
|
|
813
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)) }),
|
|
814
846
|
depsPrompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
815
847
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
816
848
|
"Found ",
|
|
@@ -896,43 +928,64 @@ function StatusBadge({ status }) {
|
|
|
896
928
|
] });
|
|
897
929
|
}
|
|
898
930
|
}
|
|
899
|
-
function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
|
|
931
|
+
function waitForJupyterHealthy(proc, port2, pushEvent, pushLog, isCancelled) {
|
|
900
932
|
return new Promise((resolve) => {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
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}`);
|
|
914
946
|
}
|
|
915
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);
|
|
916
968
|
let attempts = 0;
|
|
917
969
|
const poll = setInterval(async () => {
|
|
918
970
|
if (isCancelled()) {
|
|
919
|
-
|
|
920
|
-
resolve("cancelled");
|
|
971
|
+
done("cancelled");
|
|
921
972
|
return;
|
|
922
973
|
}
|
|
923
974
|
attempts++;
|
|
924
975
|
const healthy = await checkJupyterHealth(port2);
|
|
925
976
|
if (healthy) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
clearInterval(poll);
|
|
930
|
-
resolve("timeout");
|
|
977
|
+
done(null);
|
|
978
|
+
} else if (attempts >= 40) {
|
|
979
|
+
done("timeout");
|
|
931
980
|
}
|
|
932
981
|
}, 1e3);
|
|
933
982
|
});
|
|
934
983
|
}
|
|
935
|
-
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")}` : "";
|
|
936
989
|
if (spawnError === "port-conflict") {
|
|
937
990
|
return {
|
|
938
991
|
message: "Could not start the notebook server.",
|
|
@@ -945,13 +998,20 @@ Close whatever is using it and try again, or use a different port:
|
|
|
945
998
|
if (spawnError === "timeout") {
|
|
946
999
|
return {
|
|
947
1000
|
message: "The notebook server isn't responding.",
|
|
948
|
-
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."
|
|
949
1009
|
};
|
|
950
1010
|
}
|
|
951
1011
|
if (spawnError && spawnError !== "cancelled") {
|
|
952
1012
|
return {
|
|
953
1013
|
message: "Could not start the notebook server.",
|
|
954
|
-
hint:
|
|
1014
|
+
hint: `${spawnError}${logTail}`
|
|
955
1015
|
};
|
|
956
1016
|
}
|
|
957
1017
|
return null;
|