@damurka/jovian 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -36,7 +36,7 @@ The pieces are named after moons of Jupiter:
36
36
  npm install @damurka/jovian
37
37
  ```
38
38
 
39
- The package ships **prebuilt** `themisto`, `elara` and `carpo` binaries — no compiler, CMake or vcpkg — for **Windows x64** (`win32-x64`), **Linux x64** (`linux-x64`) and **macOS on Apple Silicon** (`darwin-arm64`); Intel Macs and Linux on ARM are not published yet, so build from source there and point `JOVIAN_NATIVE_DIR` at `dist/native/Release`. npm installs the matching `@damurka/jovian-<os>-<cpu>` package automatically as an optional dependency, so do not install with `--omit=optional` / `--no-optional`. Node.js ≥ 22.4 is required (ES modules; global `fetch` and `WebSocket`).
39
+ The package ships **prebuilt** `themisto`, `elara` and `carpo` binaries — no compiler, CMake or vcpkg — for **Windows x64**, **Linux x64 and arm64**, and **macOS x64 (Intel) and arm64 (Apple Silicon)**. Windows on ARM and 32-bit ARM Linux are not supported (there is no ARM64 R for Windows yet); build from source elsewhere and point `JOVIAN_NATIVE_DIR` at `dist/native/Release`. npm installs the matching `@damurka/jovian-<os>-<cpu>` package automatically as an optional dependency, so do not install with `--omit=optional` / `--no-optional`. Node.js ≥ 22.13 is required. The package is an ES module: use `import` (in a project with `"type": "module"`, or `.mjs`/`.mts` files), or `require()` it from CommonJS on Node 22.13+.
40
40
 
41
41
  What you must already have on the machine:
42
42
 
@@ -21,9 +21,18 @@ import {
21
21
  Creates sessions and owns the one shared supervisor process (`themisto`) they all talk to. The supervisor is spawned lazily on the first `createSession()` and killed by `stopAll()` / `killAll()` (and, as a safety net, when the Node process exits).
22
22
 
23
23
  ```typescript
24
- const manager = new SessionManager();
24
+ const manager = new SessionManager(); // quiet
25
+ const verbose = new SessionManager({ logLevel: 'debug' }); // everything, plus the kernels' own output
25
26
  ```
26
27
 
28
+ **Logging.** The library is quiet by default: it prints only one-time setup notices (the install of the R packages on the first R session), warnings and errors. A kernel that fails to start reports its own error in the exception's message (`Kernel output: …`). To see more:
29
+
30
+ | Option / variable | Effect |
31
+ |---|---|
32
+ | `logLevel: 'trace' \| 'debug' \| 'info' \| 'notice' \| 'warn' \| 'error' \| 'silent'` (or env `JOVIAN_LOG_LEVEL`) | What the built-in console logger prints: this level and above. Default `'notice'`. `'debug'` and `'trace'` also print the kernels' start-up output. |
33
+ | `logger: (level, message, data?) => void` | Receives **every** message (do your own filtering) instead of the console. A session's own `logger` option takes precedence for that session. |
34
+ | `kernelOutput: boolean` (or env `JOVIAN_KERNEL_OUTPUT=1`) | Print the kernels' own `[elara]` / `[carpo]` start-up output to stderr as it happens. Default off (on for `debug`/`trace`). |
35
+
27
36
  | Member | Description |
28
37
  |---|---|
29
38
  | `createSession(options?: EngineOptions): Promise<Session>` | Spawns a kernel process (per `options.kernelType`), waits until it has registered and the session's WebSocket is ready, and resolves with the `Session`. Rejects with the supervisor's message if the kernel could not start: `workingDirectory does not exist or is not a directory: …`, `no kernel executable is configured for kernelType 'python' …`, or — when the kernel process itself failed (R or Python not found, …) — `Kernel process exited before it could register -- check its stderr output for the actual error.` The *actual* R/Python error is printed on stderr with an `[elara]` / `[carpo]` prefix (see [Troubleshooting](../troubleshooting.md)). |
@@ -88,8 +88,8 @@ The npm packages are built and published by `.github/workflows/release.yml` from
88
88
 
89
89
  ## Debugging
90
90
 
91
- - **Kernel logs.** Everything a kernel prints (`[R Interpreter] …`, `[carpo] …`, and anything R/Python writes outside an execution) appears on the *supervisor's* stderr, which `lib/` forwards to your process's stderr, prefixed `[elara]` / `[carpo]`. That is the first place to look when a session fails to start.
92
- - **Library logs.** Pass `logger` to `createSession()` (or read the default console output): `trace` shows queueing, request ids and timeouts.
91
+ - **Kernel logs.** Everything a kernel prints (`[R Interpreter] …`, `[carpo] …`, and anything R/Python writes outside an execution) appears on the *supervisor's* stderr, prefixed `[elara]` / `[carpo]`. `lib/` keeps it quiet by default (and puts the relevant lines in the error when a kernel fails to start); set `JOVIAN_LOG_LEVEL=debug` (or `JOVIAN_KERNEL_OUTPUT=1`, or `new SessionManager({ kernelOutput: true })`) to forward it to your stderr. That is the first place to look when a session fails to start.
92
+ - **Library logs.** Quiet by default; `JOVIAN_LOG_LEVEL=trace` (or `new SessionManager({ logLevel: 'trace' })`) shows queueing, request ids and timeouts, and `logger` receives every message.
93
93
  - **Run a kernel by hand.** Generate a kernelspec (`npm run jupyter:kernelspec`) and start it from `jupyter console --kernel elara`, or run `elara -f <connection-file> --r-home …` yourself — no supervisor involved.
94
94
  - **Talk to the supervisor directly.** Start `themisto` (it prints `{"type":"supervisorReady","httpPort":…,"wsPort":…}`) and use `curl` against [its HTTP API](protocol.md#2-themistos-http-api) and any WebSocket client against `ws://127.0.0.1:<wsPort>/sessions/<id>/messages`.
95
95
  - **One native test.** `dist/native/Release/session_registry_test.exe --gtest_filter=*Interrupt*` (kernel log noise goes to the same stdout; filter with `grep -v "^\[elara\]"`).
package/docs/releasing.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Releasing
2
2
 
3
- Jovian is published to npm as four packages under the `@damurka` scope (Windows x64, Linux x64 and macOS arm64; see [Adding a platform](#adding-a-platform) for the others):
3
+ Jovian is published to npm as six packages under the `@damurka` scope, one library and five platform packages (Windows x64, Linux x64 and arm64, macOS x64 and arm64; see [Platforms that are not supported](#platforms-that-are-not-supported)):
4
4
 
5
5
  | Package | Contents |
6
6
  |---|---|
7
7
  | `@damurka/jovian` | The compiled TypeScript library, the `hera` R package, docs. Lists the platform packages below as `optionalDependencies`. This is the one users install. |
8
8
  | `@damurka/jovian-win32-x64` | `themisto.exe`, `elara.exe`, `carpo.exe` and the DLLs they need. |
9
- | `@damurka/jovian-linux-x64`, `-darwin-arm64` | `themisto`, `elara`, `carpo`. |
9
+ | `@damurka/jovian-linux-x64`, `-linux-arm64`, `-darwin-x64`, `-darwin-arm64` | `themisto`, `elara`, `carpo`. |
10
10
 
11
11
  Each platform package declares `os` and `cpu`, so npm installs only the one that matches the machine. At run time the library finds the binaries in that package (see [`lib/session/native-paths.ts`](../lib/session/native-paths.ts): `JOVIAN_NATIVE_DIR`, then the platform package, then a source checkout's `dist/native/Release`). All four packages are published at the **same version**; the main package pins the platform packages to it.
12
12
 
@@ -69,20 +69,26 @@ node scripts/release-smoke.mjs --dir dist/release/tarballs --python
69
69
 
70
70
  Nothing there publishes. Publishing by hand is `npm publish <tarball> --access public`, platform packages first.
71
71
 
72
- ## Adding a platform
72
+ ## Platforms that are not supported
73
+
74
+ A platform is published only when there is a real R for it to pair with and a runner that can build and smoke-test it end to end.
75
+
76
+ - **Windows on ARM (`win32-arm64`).** R for Windows on ARM64 is experimental and not on CRAN (no binary packages either), so there is no R for a native ARM64 kernel to load. People on Windows on ARM run x64 R under emulation, which would need the x64 kernel, but npm installs the x64 package only on x64 CPUs. Not supported until R for Windows on ARM64 is released.
77
+ - **32-bit ARM Linux (`linux-armhf`).** GitHub's hosted runners cannot run 32-bit ARM code (the arm64 machines lack AArch32), so it can only be built by cross-compiling and tested under QEMU emulation, which is not set up.
73
78
 
74
- Only the platforms that have been built and smoke-tested end to end are published (Windows x64, Ubuntu x64 and macOS arm64, the three the CI matrix covers). To add `linux-arm64` or `darwin-x64`:
79
+ Their users get `jovian: there are no prebuilt kernels for <os>-<cpu>` and can build from source and set `JOVIAN_NATIVE_DIR`.
80
+
81
+ ## Adding a platform
75
82
 
76
83
  1. Add the target to `TARGETS` in `scripts/release.mjs` and to `SUPPORTED_PLATFORMS` in `lib/session/native-paths.ts` (a unit test fails if they differ).
77
- 2. Add it to the build matrix in `.github/workflows/release.yml` (`ubuntu-24.04-arm`, `macos-15-intel`); the job `env` already sets `MACOSX_DEPLOYMENT_TARGET: '14.0'` for macOS.
84
+ 2. Add it to the build matrix in `.github/workflows/release.yml`.
78
85
  3. Run the workflow by hand (a dry run) and fix what the smoke test finds before tagging a release.
79
-
80
- Until a platform is added, its users get `jovian: there are no prebuilt kernels for <os>-<cpu>` and can build from source and set `JOVIAN_NATIVE_DIR`.
86
+ 4. A new platform is a **new package on npm**, and npm cannot stage the first version of a package: publish it by hand once (see [The first release is published by hand](#the-first-release-is-published-by-hand)) before the main package that depends on it is approved. The workflow prints a warning naming each such package and does not fail.
81
87
 
82
88
  ## Things to know
83
89
 
84
90
  - **hera upgrades.** npm resets file modification times, so the kernel's usual "is the hera source newer than the installed one" check cannot fire for an npm install. The staged `hera` `DESCRIPTION` is stamped with `Config/jovian/release: <version>`, and the kernel reinstalls `hera` when that stamp differs from the installed copy's. Users therefore get the matching `hera` after upgrading the package, once.
85
- - **Linux binaries and glibc.** They are built on `ubuntu-24.04` (glibc 2.39) and run on any distribution with at least that glibc. Building on an older image would widen compatibility; a `linux-arm64` build would use `ubuntu-24.04-arm`.
91
+ - **Linux binaries and glibc.** They are built on `ubuntu-24.04` (glibc 2.39) and run on any distribution with at least that glibc. Building on an older image would widen compatibility; the `linux-arm64` build runs on `ubuntu-24.04-arm`.
86
92
  - **macOS.** The build sets `MACOSX_DEPLOYMENT_TARGET=14.0`. The binaries are not code-signed or notarized; binaries installed through npm are not quarantined by Gatekeeper, so this works, but bundling them into a downloaded `.app` would require signing.
87
93
  - **Windows.** The DLLs come from vcpkg's `x64-windows` triplet and are copied next to the executables; the binaries link the dynamic Visual C++ runtime (`MSVCP140.dll`, `VCRUNTIME140.dll`, checked with `dumpbin /dependents`), which is **not** bundled: users need the "Microsoft Visual C++ Redistributable" (x64, 2015–2022), and the README's Install section says so. Bundling the runtime DLLs into the package, or linking the runtime statically, would remove that requirement.
88
94
  - **Executable bit.** npm does not reliably preserve it; the library `chmod`s the kernels before starting them.
@@ -21,13 +21,13 @@ Messages below are quoted from the code. **First habit:** a kernel that fails to
21
21
  | Message | Meaning and fix |
22
22
  |---|---|
23
23
  | `jovian: the kernel binaries were not found. Expected the '@damurka/jovian-<os>-<cpu>' package …` | The platform package was not installed: it is an optional dependency, so `npm install --omit=optional` / `--no-optional` skips it (reinstall without the flag), or a lockfile made on another OS omitted it (`npm install` on this OS). In a source checkout: run `npm run build`, or set `$JOVIAN_NATIVE_DIR`. |
24
- | `jovian: there are no prebuilt kernels for <os>-<cpu> (supported: …)` | Prebuilt packages exist for `win32-x64`, `linux-x64` and `darwin-arm64` only (Intel Macs and Linux on ARM are not published yet). Elsewhere, [build from source](../README.md#requirements) and set `$JOVIAN_NATIVE_DIR` to `dist/native/Release`. |
24
+ | `jovian: there are no prebuilt kernels for <os>-<cpu> (supported: …)` | Prebuilt packages exist for `win32-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` and `darwin-arm64` only (Windows on ARM and 32-bit ARM Linux are not supported). Elsewhere, [build from source](../README.md#requirements) and set `$JOVIAN_NATIVE_DIR` to `dist/native/Release`. |
25
25
  | `version 'GLIBC_2.xx' not found` on Linux | The prebuilt Linux binaries need a glibc at least as new as the one they were built with (Ubuntu 24.04: 2.39). Build from source on the older system. |
26
26
  | `Supervisor process exited before it was ready (code 1)` | `themisto` refused to start — typically `[themisto] FATAL: kernel executable not found at … (pass --kernel-exe to override)`: `elara` is missing next to `themisto`. |
27
27
  | `no kernel executable is configured for kernelType 'python' …` | `carpo` was not built (or not found beside `themisto`). It is built by default; check `dist/native/Release/carpo[.exe]` and Themisto's `[themisto] NOTE: no Python kernel executable found` line. R sessions are unaffected. |
28
28
  | `workingDirectory does not exist or is not a directory: <path>` | Create the directory first, or fix the path. |
29
- | `Kernel process exited before it could register -- check its stderr output for the actual error.` | The kernel crashed at start-up. See the R / Python sections below and read the `[elara]` / `[carpo]` lines. |
30
- | `Did not receive kernel configuration within 60s -- the kernel process is still running but never registered. Check its stderr output for what it's doing.` | The kernel started but hung before registering — e.g. R blocked loading packages, a very slow disk, or a security product scanning the process. The stderr shows how far it got. |
29
+ | `Kernel process exited before it could register -- check its stderr output for the actual error.` | The kernel crashed at start-up. The exception's message ends with `Kernel output:` and the kernel's own error lines; for the full output run with `JOVIAN_LOG_LEVEL=debug` (or `new SessionManager({ kernelOutput: true })`). See the R / Python sections below. |
30
+ | `Did not receive kernel configuration within 60s -- the kernel process is still running but never registered. Check its stderr output for what it's doing.` | The kernel started but hung before registering — e.g. R blocked loading packages, a very slow disk, or a security product scanning the process. Run with `JOVIAN_LOG_LEVEL=debug` to print the kernel's output and see how far it got. |
31
31
  | `WebSocket connection to session <id> failed` / `Session <id> closed before it was ready` | The supervisor accepted the session but the WebSocket could not be established/kept — usually the supervisor died. Look for its exit. |
32
32
 
33
33
  ### R
@@ -38,6 +38,7 @@ Messages below are quoted from the code. **First habit:** a kernel that fails to
38
38
  | `R_HOME is not set -- elara needs a working R installation to run. …` | The library could not find R (it tries `$R_HOME`, `R RHOME`, and on Windows the registry): put R on `PATH` or pass `rHome` (`R RHOME` prints it). |
39
39
  | `No libpython3.*.so* … was found under '/lib' … Is Python installed at ''?` | The library could not find Python (it tries `$PYTHONHOME`, then `python3`/`python` on `PATH`): install Python 3 (`sudo apt install python3`) or pass `pythonHome`. |
40
40
  | `Could not set up the R packages the kernel needs: …` | The one-time install of `hera` and its dependencies (see the README's Install section) failed; the text after the colon is R's own reason (no internet access to CRAN, no compiler on Linux, a library that cannot be written to). Fix that and call `createSession()` again; it retries. `JOVIAN_SKIP_R_SETUP=1` skips the step. |
41
+ | `ERROR: failed to lock directory '<library>' for modifying. Try removing '<library>/00LOCK-<pkg>'` | An earlier install of that package was interrupted (Ctrl+C, a kill, a timeout) and left its lock folder, which makes R refuse every later install of it. The automatic setup removes locks older than 5 minutes itself; to clear one now: `rm -rf <library>/00LOCK-*` (Windows: delete the `00LOCK-*` folder), then run again. |
41
42
  | `unable to load shared object '/usr/lib/R/site-library/<pkg>/libs/<pkg>.so': undefined symbol: SETLENGTH` (or another `undefined symbol`) | Only when the automatic setup is skipped or you install by hand. Debian/Ubuntu: a package installed with `apt` (`r-cran-*`, in `/usr/lib/R/site-library`) was built for an older R than yours (the automatic setup reinstalls these itself). Reinstall it, and everything it needs, from CRAN into your own library, which R searches first. Needs a compiler (`sudo apt install build-essential`):<br>`wanted <- c("remotes","cli","evaluate","glue","IRdisplay","jsonlite","R6","repr","rlang")`<br>`all <- unique(c(wanted, unlist(tools::package_dependencies(wanted, db = available.packages(), recursive = TRUE), use.names = FALSE)))`<br>`ip <- installed.packages(); install.packages(setdiff(all, rownames(ip)[!is.na(ip[, "Priority"])]), repos = "https://cloud.r-project.org")` |
42
43
  | `WARNING: 'hera' package could not be loaded (status: install_failed: …)` | Elara could not install the bundled `hera` R package; the text after `install_failed:` is R's own reason. Usually one of `hera`'s CRAN dependencies is missing or cannot be built (install them first: `install.packages(c("remotes", "cli", "evaluate", "glue", "IRdisplay", "jsonlite", "R6", "repr", "rlang"))`), or the R library is not writable. To see the full output run `R CMD INSTALL node_modules/@damurka/jovian/packages/hera`. |
43
44
  | `Could not load …/lib/libR.so (…). Is R installed at '…'? If this R was built from source, it needs to have been configured with --enable-R-shlib, or no libR.so exists at all` | Use a distribution/CRAN R, or rebuild R with `--enable-R-shlib`. |
@@ -141,7 +141,7 @@ export class ExecutionQueue {
141
141
  break;
142
142
  case 'error':
143
143
  pending.output.push(message);
144
- this.logger?.error(`Execution ${message.parentMsgId} reported an R error`, { evalue: message.content?.evalue });
144
+ this.logger?.debug(`Execution ${message.parentMsgId} reported an R error`, { evalue: message.content?.evalue });
145
145
  if (pending.stopOnError) {
146
146
  this.abortQueued();
147
147
  }
@@ -7,7 +7,7 @@ export declare const PACKAGE_NAME = "@damurka/jovian";
7
7
  * the main package only where it matches (each declares `os`/`cpu`). These are
8
8
  * the platforms that get a package.
9
9
  */
10
- export declare const SUPPORTED_PLATFORMS: readonly ['win32-x64', 'linux-x64', 'darwin-arm64'];
10
+ export declare const SUPPORTED_PLATFORMS: readonly ['win32-x64', 'linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'];
11
11
  export declare function platformPackageName(platform?: string, arch?: string): string | undefined;
12
12
  export interface NativeLocation {
13
13
  /** Directory holding themisto, elara and carpo. */
@@ -11,7 +11,7 @@ export const PACKAGE_NAME = `${PACKAGE_SCOPE}/jovian`;
11
11
  * the main package only where it matches (each declares `os`/`cpu`). These are
12
12
  * the platforms that get a package.
13
13
  */
14
- export const SUPPORTED_PLATFORMS = ['win32-x64', 'linux-x64', 'darwin-arm64'];
14
+ export const SUPPORTED_PLATFORMS = ['win32-x64', 'linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'];
15
15
  export function platformPackageName(platform = process.platform, arch = process.arch) {
16
16
  const key = `${platform}-${arch}`;
17
17
  return SUPPORTED_PLATFORMS.includes(key) ? `${PACKAGE_NAME}-${key}` : undefined;
@@ -20,6 +20,7 @@ export declare const R_SETUP_SCRIPT: string;
20
20
  export interface SetupLogger {
21
21
  debug(message: string): void;
22
22
  info(message: string): void;
23
+ notice(message: string): void;
23
24
  }
24
25
  export interface SetupProcessOutcome {
25
26
  code: number | null;
@@ -24,6 +24,7 @@ import { createInterface } from 'node:readline';
24
24
  export const R_SETUP_SCRIPT = String.raw `
25
25
  src <- commandArgs(trailingOnly = TRUE)[1]
26
26
  say <- function(...) cat("JOVIAN_R_SETUP: ", ..., "\n", sep = "")
27
+ note <- function(...) cat("JOVIAN_R_SETUP_INFO: ", ..., "\n", sep = "")
27
28
  fail <- function(...) {
28
29
  cat("JOVIAN_R_SETUP_ERROR: ", ..., "\n", sep = "")
29
30
  quit(save = "no", status = 1)
@@ -41,7 +42,7 @@ hera_current <- function() {
41
42
  suppressWarnings(suppressMessages(requireNamespace("hera", quietly = TRUE)))
42
43
  }
43
44
  if (hera_current()) {
44
- say("hera is already installed")
45
+ note("hera is already installed")
45
46
  quit(save = "no", status = 0)
46
47
  }
47
48
 
@@ -59,6 +60,15 @@ if (is.na(lib)) {
59
60
  .libPaths(c(lib, .libPaths()))
60
61
  }
61
62
 
63
+ for (lock in Sys.glob(file.path(lib, "00LOCK-*"))) {
64
+ # An install that was killed (Ctrl+C, a timeout) leaves its lock behind, and
65
+ # every later install of that package is refused until it is removed.
66
+ if (difftime(Sys.time(), file.info(lock)$mtime, units = "mins") > 5) {
67
+ say("removing a stale lock left by an interrupted install: ", lock)
68
+ unlink(lock, recursive = TRUE)
69
+ }
70
+ }
71
+
62
72
  ip <- installed.packages()
63
73
  base_packages <- rownames(ip)[!is.na(ip[, "Priority"])]
64
74
  fields <- tryCatch(read.dcf(file.path(src, "DESCRIPTION"), fields = c("Depends", "Imports", "LinkingTo")), error = function(e) NULL)
@@ -90,9 +100,28 @@ if (length(missing) > 0) {
90
100
  error = function(e) fail("installing R packages failed: ", conditionMessage(e)))
91
101
  still_missing <- missing[!vapply(missing, usable, logical(1))]
92
102
  if (length(still_missing) > 0) {
103
+ # The warnings only say "non-zero exit status". Packages that need
104
+ # another one that failed fail too, so re-run the install of the ones
105
+ # that do not, on their own, and report what R and the compiler said.
106
+ needs <- tryCatch(tools::package_dependencies(still_missing, db = available, recursive = FALSE), error = function(e) list())
107
+ roots <- still_missing[vapply(still_missing, function(p) !any(needs[[p]] %in% still_missing), logical(1))]
108
+ reasons <- character()
109
+ for (p in utils::head(roots, 2)) {
110
+ why <- tryCatch({
111
+ tarball <- utils::download.packages(p, destdir = tempdir(), repos = getOption("repos"), type = "source", quiet = TRUE)[1, 2]
112
+ out <- suppressWarnings(system2(file.path(R.home("bin"), "R"), c("CMD", "INSTALL", paste0("--library=", shQuote(lib)), shQuote(tarball)), stdout = TRUE, stderr = TRUE))
113
+ if (is.null(attr(out, "status")) || identical(attr(out, "status"), 0L)) {
114
+ "it installed when retried on its own, so the failure may be temporary: try again"
115
+ } else {
116
+ important <- grep("error|ERROR|undefined|fatal|cannot|not found|No such file|Killed", out, value = TRUE)
117
+ substr(gsub("[[:space:]]+", " ", paste(utils::tail(if (length(important)) important else out, 5), collapse = " ; ")), 1, 700)
118
+ }
119
+ }, error = function(e) conditionMessage(e))
120
+ reasons <- c(reasons, paste0(p, ": ", why))
121
+ }
93
122
  fail("could not install these R packages: ", paste(still_missing, collapse = ", "),
94
- " -- on Linux they are compiled from source, which needs a compiler (Ubuntu: sudo apt install build-essential)",
95
- if (length(warnings_seen)) paste0(" | ", substr(paste(unique(warnings_seen), collapse = " | "), 1, 400)) else "")
123
+ if (length(reasons)) paste0(" -- ", paste(reasons, collapse = " | ")) else "",
124
+ " (on Linux packages are compiled from source and need a compiler: sudo apt install build-essential)")
96
125
  }
97
126
  }
98
127
 
@@ -187,7 +216,9 @@ async function setUp(rscript, heraSrcPath, rLibs, logger, deps) {
187
216
  if (line.startsWith('JOVIAN_R_SETUP_ERROR: '))
188
217
  failure = line.slice('JOVIAN_R_SETUP_ERROR: '.length);
189
218
  else if (line.startsWith('JOVIAN_R_SETUP: '))
190
- logger.info(`R setup: ${line.slice('JOVIAN_R_SETUP: '.length)}`);
219
+ logger.notice(`R setup: ${line.slice('JOVIAN_R_SETUP: '.length)}`);
220
+ else if (line.startsWith('JOVIAN_R_SETUP_INFO: '))
221
+ logger.info(`R setup: ${line.slice('JOVIAN_R_SETUP_INFO: '.length)}`);
191
222
  else if (line.trim())
192
223
  otherOutput.push(line.trim());
193
224
  };
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
2
+ import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, LoggerFunction, LogThreshold, SessionManagerOptions, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
3
3
  import type { ExecutionState } from '../types/messages.js';
4
4
  import { SupervisorClient, type SessionConnectionInfo } from './supervisor-client.js';
5
5
  import { Comm } from './comm.js';
@@ -27,7 +27,10 @@ export declare class Session extends EventEmitter {
27
27
  private readonly executionHistory;
28
28
  private readonly executionHistoryByMsgId;
29
29
  private readonly historyStreamChars;
30
- constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient);
30
+ constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient, logging?: {
31
+ level?: LogThreshold | undefined;
32
+ logger?: LoggerFunction | undefined;
33
+ });
31
34
  /**
32
35
  * (Re)establishes the WebSocket to this.info's session and resolves
33
36
  * once it's ready. Used both by the constructor and by restart() --
@@ -209,10 +212,18 @@ export declare class Session extends EventEmitter {
209
212
  kill(): void;
210
213
  }
211
214
  export declare class SessionManager {
215
+ private readonly logLevel;
216
+ private readonly customLogger;
212
217
  private readonly logger;
213
218
  private readonly supervisor;
214
219
  private readonly sessions;
215
220
  private exitHandlerRegistered;
221
+ /**
222
+ * Quiet by default: only one-time setup notices, warnings and errors are
223
+ * printed. See SessionManagerOptions for `logLevel`, `logger` and
224
+ * `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
225
+ */
226
+ constructor(options?: SessionManagerOptions);
216
227
  /** Creates a new R session in its own OS process and waits for it to be ready. */
217
228
  createSession(requested?: EngineOptions): Promise<Session>;
218
229
  /** Gracefully stops every session managed by this instance. */
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { randomUUID } from 'crypto';
3
- import { Logger } from '../utils/logger.js';
3
+ import { Logger, defaultLogLevel } from '../utils/logger.js';
4
4
  import { MessageRouter } from '../messaging/message-router.js';
5
5
  import { ExecutionQueue } from '../execution/execution-queue.js';
6
6
  import { MiddlewareChain } from '../middleware/middleware-chain.js';
@@ -89,12 +89,12 @@ export class Session extends EventEmitter {
89
89
  executionHistory = [];
90
90
  executionHistoryByMsgId = new Map();
91
91
  historyStreamChars = new WeakMap();
92
- constructor(info, options, supervisor) {
92
+ constructor(info, options, supervisor, logging = {}) {
93
93
  super();
94
94
  this.info = info;
95
95
  this.currentOptions = options;
96
96
  this.supervisor = supervisor;
97
- this.logger = new Logger(options.logger);
97
+ this.logger = new Logger(options.logger ?? logging.logger, logging.level);
98
98
  this.on('message', (message) => {
99
99
  this.recordExecutionHistory(message);
100
100
  this.settleRequest(message);
@@ -781,10 +781,25 @@ export class Session extends EventEmitter {
781
781
  }
782
782
  }
783
783
  export class SessionManager {
784
- logger = new Logger();
785
- supervisor = new SupervisorClient(this.logger);
784
+ logLevel;
785
+ customLogger;
786
+ logger;
787
+ supervisor;
786
788
  sessions = new Set();
787
789
  exitHandlerRegistered = false;
790
+ /**
791
+ * Quiet by default: only one-time setup notices, warnings and errors are
792
+ * printed. See SessionManagerOptions for `logLevel`, `logger` and
793
+ * `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
794
+ */
795
+ constructor(options = {}) {
796
+ this.logLevel = options.logLevel ?? defaultLogLevel();
797
+ this.customLogger = options.logger;
798
+ this.logger = new Logger(this.customLogger, this.logLevel);
799
+ const verbose = this.logLevel === 'trace' || this.logLevel === 'debug';
800
+ const forwardKernelOutput = options.kernelOutput ?? (Boolean(process.env.JOVIAN_KERNEL_OUTPUT) || verbose);
801
+ this.supervisor = new SupervisorClient(this.logger, { forwardKernelOutput });
802
+ }
788
803
  /** Creates a new R session in its own OS process and waits for it to be ready. */
789
804
  async createSession(requested = {}) {
790
805
  // Finds R / Python when rHome / pythonHome were not given (see runtimes.ts).
@@ -793,7 +808,7 @@ export class SessionManager {
793
808
  // First R session only: installs hera and what it needs (see r-setup.ts).
794
809
  await ensureRPackages(options, this.logger);
795
810
  const info = await this.supervisor.createSession(options);
796
- const session = new Session(info, options, this.supervisor);
811
+ const session = new Session(info, options, this.supervisor, { level: this.logLevel, logger: this.customLogger });
797
812
  this.sessions.add(session);
798
813
  this.registerExitHandler();
799
814
  try {
@@ -809,7 +824,7 @@ export class SessionManager {
809
824
  async stopAll() {
810
825
  await Promise.all([...this.sessions].map((session) => session.stop()));
811
826
  this.sessions.clear();
812
- this.supervisor.kill();
827
+ this.supervisor.kill(true);
813
828
  }
814
829
  /**
815
830
  * Forcibly terminates every session. Prefer stopAll(), but a session
@@ -10,7 +10,14 @@ export declare class SupervisorClient {
10
10
  private child;
11
11
  private readyPromise;
12
12
  private readonly logger;
13
- constructor(logger: Logger);
13
+ private readonly forwardKernelOutput;
14
+ private readonly recentOutput;
15
+ constructor(logger: Logger, options?: {
16
+ forwardKernelOutput?: boolean;
17
+ });
18
+ private rememberOutput;
19
+ /** The message plus what the kernels said just before, when that was not already printed. */
20
+ private withKernelOutput;
14
21
  private ensureStarted;
15
22
  private spawnSupervisor;
16
23
  createSession(options: EngineOptions): Promise<SessionConnectionInfo>;
@@ -30,7 +37,11 @@ export declare class SupervisorClient {
30
37
  * close this session and create a new one just to pick a different R.
31
38
  */
32
39
  restartSession(info: SessionConnectionInfo, options?: Partial<EngineOptions>): Promise<void>;
33
- /** Skips graceful per-session shutdown -- only for cleanup on the way out. */
34
- kill(): void;
40
+ /**
41
+ * Skips graceful per-session shutdown -- only for cleanup on the way out.
42
+ * `expected` is the normal end of stopAll(), after every session was
43
+ * stopped: not worth a warning.
44
+ */
45
+ kill(expected?: boolean): void;
35
46
  }
36
47
  //# sourceMappingURL=supervisor-client.d.ts.map
@@ -33,8 +33,29 @@ export class SupervisorClient {
33
33
  child;
34
34
  readyPromise;
35
35
  logger;
36
- constructor(logger) {
36
+ forwardKernelOutput;
37
+ // The supervisor's stderr is where every kernel's start-up output ends up
38
+ // ([elara] ..., [carpo] ...). It is kept, not printed, unless asked for,
39
+ // and attached to the error when a kernel fails to start.
40
+ recentOutput = [];
41
+ constructor(logger, options = {}) {
37
42
  this.logger = logger;
43
+ this.forwardKernelOutput = options.forwardKernelOutput ?? false;
44
+ }
45
+ rememberOutput(line) {
46
+ if (!line.trim())
47
+ return;
48
+ this.recentOutput.push(line);
49
+ if (this.recentOutput.length > 200)
50
+ this.recentOutput.shift();
51
+ }
52
+ /** The message plus what the kernels said just before, when that was not already printed. */
53
+ withKernelOutput(message) {
54
+ if (this.forwardKernelOutput || this.recentOutput.length === 0)
55
+ return message;
56
+ const notable = this.recentOutput.filter((line) => /error|fatal|warning|failed|cannot|not found|no such/i.test(line));
57
+ const lines = (notable.length > 0 ? notable : this.recentOutput).slice(-8);
58
+ return `${message}\nKernel output:\n ${lines.join('\n ')}`;
38
59
  }
39
60
  ensureStarted() {
40
61
  if (!this.readyPromise) {
@@ -48,7 +69,11 @@ export class SupervisorClient {
48
69
  this.logger.debug(`Spawning supervisor process from: ${exePath}`);
49
70
  const child = spawn(exePath, [], { stdio: ['ignore', 'pipe', 'pipe'] });
50
71
  this.child = child;
51
- child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
72
+ createInterface({ input: child.stderr }).on('line', (line) => {
73
+ this.rememberOutput(line);
74
+ if (this.forwardKernelOutput)
75
+ process.stderr.write(`${line}\n`);
76
+ });
52
77
  const rl = createInterface({ input: child.stdout });
53
78
  const onLine = (line) => {
54
79
  let message;
@@ -87,7 +112,7 @@ export class SupervisorClient {
87
112
  });
88
113
  const body = await res.json();
89
114
  if (!res.ok || !body.sessionId) {
90
- throw new Error(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`);
115
+ throw new Error(this.withKernelOutput(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`));
91
116
  }
92
117
  return { sessionId: body.sessionId, httpBase, wsBase: `ws://127.0.0.1:${wsPort}` };
93
118
  }
@@ -126,10 +151,18 @@ export class SupervisorClient {
126
151
  throw new Error(body.error ?? `Supervisor failed to restart session ${info.sessionId} (HTTP ${res.status})`);
127
152
  }
128
153
  }
129
- /** Skips graceful per-session shutdown -- only for cleanup on the way out. */
130
- kill() {
154
+ /**
155
+ * Skips graceful per-session shutdown -- only for cleanup on the way out.
156
+ * `expected` is the normal end of stopAll(), after every session was
157
+ * stopped: not worth a warning.
158
+ */
159
+ kill(expected = false) {
131
160
  if (this.child && !this.child.killed) {
132
- this.logger.warn(`Force-killing supervisor process (pid ${this.child.pid})`);
161
+ const message = `Force-killing supervisor process (pid ${this.child.pid})`;
162
+ if (expected)
163
+ this.logger.debug(message);
164
+ else
165
+ this.logger.warn(message);
133
166
  this.child.kill();
134
167
  }
135
168
  }
@@ -1,5 +1,29 @@
1
1
  import type { JupyterMessage } from './messages.js';
2
- export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
2
+ /**
3
+ * 'notice' is for the few things a user should be told even when the library
4
+ * is otherwise quiet (e.g. the one-time install of the R packages, which takes
5
+ * a while); it sits between 'info' and 'warn'.
6
+ */
7
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'notice' | 'warn' | 'error';
8
+ /** What the built-in console logger prints: this level and above; 'silent' prints nothing. */
9
+ export type LogThreshold = LogLevel | 'silent';
10
+ export interface SessionManagerOptions {
11
+ /**
12
+ * How much the library prints to the console (default 'notice': one-time
13
+ * setup messages, warnings and errors; env JOVIAN_LOG_LEVEL sets the
14
+ * default). Ignored for messages sent to `logger`, which receives all of them.
15
+ */
16
+ logLevel?: LogThreshold | undefined;
17
+ /** Receives every log message instead of the console. */
18
+ logger?: LoggerFunction | undefined;
19
+ /**
20
+ * Print the kernels' own start-up output (the `[elara]` / `[carpo]` lines)
21
+ * to stderr as it happens. Off by default -- it is included in the error
22
+ * when a kernel fails to start -- and on when logLevel is 'debug' or
23
+ * 'trace' or env JOVIAN_KERNEL_OUTPUT is set.
24
+ */
25
+ kernelOutput?: boolean | undefined;
26
+ }
3
27
  export type LoggerFunction = (level: LogLevel, message: string, data?: any) => void;
4
28
  export interface EngineOptions {
5
29
  /**
@@ -1,11 +1,22 @@
1
- import type { LoggerFunction } from '../types/index.js';
1
+ import type { LogLevel, LoggerFunction, LogThreshold } from '../types/index.js';
2
2
  export declare function timestamp(): string;
3
+ /** True when a message at `level` passes `threshold`. */
4
+ export declare function passes(threshold: LogThreshold, level: LogLevel): boolean;
5
+ /** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
6
+ export declare function defaultLogLevel(env?: Record<string, string | undefined>): LogThreshold;
3
7
  export declare class Logger {
4
- private customLogger?;
5
- constructor(customLogger?: LoggerFunction);
8
+ private readonly customLogger;
9
+ private readonly threshold;
10
+ /**
11
+ * A custom logger receives every message (it does its own filtering, as it
12
+ * always has); `threshold` only limits what is printed to the console.
13
+ */
14
+ constructor(customLogger?: LoggerFunction, threshold?: LogThreshold);
15
+ private emit;
6
16
  trace(message: string, data?: any): void;
7
17
  debug(message: string, data?: any): void;
8
18
  info(message: string, data?: any): void;
19
+ notice(message: string, data?: any): void;
9
20
  warn(message: string, data?: any): void;
10
21
  error(message: string, error?: any): void;
11
22
  }
@@ -9,50 +9,58 @@ export function timestamp() {
9
9
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
10
10
  `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
11
11
  }
12
+ const ORDER = { trace: 0, debug: 1, info: 2, notice: 3, warn: 4, error: 5, silent: 6 };
13
+ /** True when a message at `level` passes `threshold`. */
14
+ export function passes(threshold, level) {
15
+ return ORDER[level] >= ORDER[threshold];
16
+ }
17
+ /** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
18
+ export function defaultLogLevel(env = process.env) {
19
+ const wanted = env.JOVIAN_LOG_LEVEL?.trim().toLowerCase();
20
+ return wanted && wanted in ORDER ? wanted : 'notice';
21
+ }
12
22
  export class Logger {
13
23
  customLogger;
14
- constructor(customLogger) {
24
+ threshold;
25
+ /**
26
+ * A custom logger receives every message (it does its own filtering, as it
27
+ * always has); `threshold` only limits what is printed to the console.
28
+ */
29
+ constructor(customLogger, threshold = defaultLogLevel()) {
15
30
  this.customLogger = customLogger;
31
+ this.threshold = threshold;
16
32
  }
17
- trace(message, data) {
33
+ emit(level, message, data) {
18
34
  if (this.customLogger) {
19
- this.customLogger('trace', message, data);
20
- }
21
- else {
22
- console.log(`${timestamp()} [trace] ${message}`, data ? data : '');
35
+ this.customLogger(level, message, data);
36
+ return;
23
37
  }
38
+ if (!passes(this.threshold, level))
39
+ return;
40
+ const print = level === 'warn' ? console.warn : level === 'error' ? console.error : console.log;
41
+ const line = `${timestamp()} [${level}] ${message}`;
42
+ if (data)
43
+ print(line, data);
44
+ else
45
+ print(line);
46
+ }
47
+ trace(message, data) {
48
+ this.emit('trace', message, data);
24
49
  }
25
50
  debug(message, data) {
26
- if (this.customLogger) {
27
- this.customLogger('debug', message, data);
28
- }
29
- else {
30
- console.log(`${timestamp()} [debug] ${message}`, data ? data : '');
31
- }
51
+ this.emit('debug', message, data);
32
52
  }
33
53
  info(message, data) {
34
- if (this.customLogger) {
35
- this.customLogger('info', message, data);
36
- }
37
- else {
38
- console.log(`${timestamp()} [info] ${message}`, data ? data : '');
39
- }
54
+ this.emit('info', message, data);
55
+ }
56
+ notice(message, data) {
57
+ this.emit('notice', message, data);
40
58
  }
41
59
  warn(message, data) {
42
- if (this.customLogger) {
43
- this.customLogger('warn', message, data);
44
- }
45
- else {
46
- console.warn(`${timestamp()} [warn] ${message}`, data ? data : '');
47
- }
60
+ this.emit('warn', message, data);
48
61
  }
49
62
  error(message, error) {
50
- if (this.customLogger) {
51
- this.customLogger('error', message, error);
52
- }
53
- else {
54
- console.error(`${timestamp()} [error] ${message}`, error ? error : '');
55
- }
63
+ this.emit('error', message, error);
56
64
  }
57
65
  }
58
66
  //# sourceMappingURL=logger.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damurka/jovian",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "author": "David Kariuki",
6
6
  "repository": {
@@ -31,16 +31,18 @@
31
31
  "exports": {
32
32
  ".": {
33
33
  "types": "./lib/index.d.ts",
34
- "import": "./lib/index.js"
34
+ "import": "./lib/index.js",
35
+ "default": "./lib/index.js"
35
36
  },
36
37
  "./types": {
37
38
  "types": "./lib/types/index.d.ts",
38
- "import": "./lib/types/index.js"
39
+ "import": "./lib/types/index.js",
40
+ "default": "./lib/types/index.js"
39
41
  },
40
42
  "./package.json": "./package.json"
41
43
  },
42
44
  "engines": {
43
- "node": ">=22.4.0"
45
+ "node": ">=22.13.0"
44
46
  },
45
47
  "files": [
46
48
  "lib",
@@ -50,8 +52,10 @@
50
52
  "LICENSE"
51
53
  ],
52
54
  "optionalDependencies": {
53
- "@damurka/jovian-win32-x64": "0.1.1",
54
- "@damurka/jovian-linux-x64": "0.1.1",
55
- "@damurka/jovian-darwin-arm64": "0.1.1"
55
+ "@damurka/jovian-win32-x64": "0.1.2",
56
+ "@damurka/jovian-linux-x64": "0.1.2",
57
+ "@damurka/jovian-linux-arm64": "0.1.2",
58
+ "@damurka/jovian-darwin-x64": "0.1.2",
59
+ "@damurka/jovian-darwin-arm64": "0.1.2"
56
60
  }
57
61
  }
@@ -26,4 +26,4 @@ Imports:
26
26
  URL: https://github.com/damurka/jovian
27
27
  BugReports: https://github.com/damurka/jovian/issues
28
28
  Config/roxygen2/version: 8.0.0
29
- Config/jovian/release: 0.1.1
29
+ Config/jovian/release: 0.1.2