@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.
Files changed (2) hide show
  1. package/dist/index.js +92 -40
  2. 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
- let stderrBuf = "";
447
- proc.stderr?.on("data", (data) => {
448
- stderrBuf += data.toString();
449
- const lines = stderrBuf.split("\n");
450
- stderrBuf = lines.pop() ?? "";
451
- for (const line of lines) {
452
- const msg = parseJupyterLine(line);
453
- if (msg) pushEvent(msg);
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 >= 20) {
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
- proc.on("error", (err) => resolve(err.message));
910
- let stderrBuf = "";
911
- proc.stderr?.on("data", (data) => {
912
- stderrBuf += data.toString();
913
- const lines = stderrBuf.split("\n");
914
- stderrBuf = lines.pop() ?? "";
915
- for (const line of lines) {
916
- if (line.includes("Address already in use")) {
917
- resolve("port-conflict");
918
- return;
919
- }
920
- const msg = parseJupyterLine(line);
921
- if (msg) pushEvent(msg);
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
- clearInterval(poll);
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
- clearInterval(poll);
935
- resolve(null);
936
- } else if (attempts >= 20) {
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: `Try restarting or check if port ${port2} is available.`
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: "Something went wrong starting Jupyter. Try running again."
1014
+ hint: `${spawnError}${logTail}`
963
1015
  };
964
1016
  }
965
1017
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sharpe-jupyter/connect",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Connect a local JupyterHub to Sharpe via Cloudflare Tunnel",
5
5
  "type": "module",
6
6
  "bin": {