Skip to content

Fall back to an installed kernel when the bundled one cannot run - #526

Draft
samclark2015 wants to merge 1 commit into
sclark/win32-arm64-vsixfrom
sclark/kernel-probe-fallback
Draft

Fall back to an installed kernel when the bundled one cannot run#526
samclark2015 wants to merge 1 commit into
sclark/win32-arm64-vsixfrom
sclark/kernel-probe-fallback

Conversation

@samclark2015

Copy link
Copy Markdown

Stacked on #524, which is stacked on #523.

Problem

The bundled Linux kernels are built on ubuntu-latest (24.04) and ubuntu-24.04-arm, which puts a glibc 2.39 floor on them. Measured on the shipped ggsql_0.4.1_amd64.deb with objdump -p (real .gnu.version_r entries, both arches identical):

required from libc.so.6:      ... GLIBC_2.38, GLIBC_2.39
required from libstdc++.so.6: ... GLIBCXX_3.4.29, CXXABI_1.3.13

Positron supports Ubuntu 20+ and RHEL 9+, and Remote SSH / Posit Workbench on RHEL 8/9/10 + Ubuntu 22/24. So the bundled kernel does not run on Ubuntu 20.04, 22.04, RHEL 8, RHEL 9, Debian 12, Amazon Linux 2023 or SLES 15.

Nothing catches it. execve succeeds — the ELF is valid and the interpreter exists — so this is not ENOENT; the dynamic linker then fails and the process exits non-zero. isKernelAccessible() is stat + X_OK, which cannot see that. So the extension registers a runtime, and because bundled is the default strategy and wins outright, that dead runtime shadows a working kernel on PATH — a regression for anyone on those distros who has one today.

Rebuilding against an older glibc is the real fix and is not in this PR. This makes the extension degrade correctly regardless.

Change

  • Run a kernel before offering it. probeKernel() execs ggsql-jupyter --version with a 15s timeout and requires exit 0. Only the Bundled candidate is probed — a kernel the user installed keeps today's stat-only treatment.
  • ggsql-jupyter now accepts --version. The clap Args struct had no #[command(version)], so --version exited non-zero on a healthy binary — a --version probe would have rejected every kernel on every platform. Useful on its own.
  • Make the fallback reachable. selectKernelCandidates() returns a KernelSelection { strategy, candidates, fallback }. Under bundled the host lookup is a fallback() callback rather than a flat list, so the common case never pays for the which/where shell-out.
  • Cache successes only. Memoized in globalState against the extension version: one spawn per update, not per window. A failure is not cached — it is cheap to repeat, and a host that gains the missing libraries starts working without waiting for an update.
  • Gate the kernelspec write. writeKernelJson() ran for Bundled before any session started, so a broken kernel poisoned the user kernelspec directory for Quarto and plain Jupyter. It now runs only for a candidate that passed.

Prompting

No prompt on fallback, by agreement. Discovery runs on every window open, before the user has shown any interest in ggsql; and if the bundled kernel cannot exec and a host one can, there is no trade-off for the user to weigh. The runtime picker already discloses the source (plain ggsql for bundled vs ggsql (Path)/ggsql (System)), and the log records the handover.

The one case that interrupts is the dead end — nothing runnable anywhere. reportNoUsableKernel() shows a non-modal warning once per extension version with Install ggsql and Show Log. Its wording branches so it also covers a build that carries no kernel at all, which is the win32-arm64 VSIX added in #524. It is skipped under the path strategy, where the user named a binary and the log already reports it.

Verification

Command Result
cargo build --release --package ggsql-jupyter passed
./target/release/ggsql-jupyter --version ggsql-jupyter 0.4.1, exit 0
cargo fmt --all --check clean
cargo clippy --package ggsql-jupyter --all-targets no warnings
npx tsc --noEmit -p . clean
npm run lint clean
npm test 79 passing, 0 failing

New tests cover: bundled fails the probe and a host kernel takes over; bundled fails with nothing else present (zero runtimes, notification path); bundled passes and the host lookup is never performed; probe memoization; and probeKernel directly against real binaries.

Two honest caveats:

  1. npm test needs launchArgs: ['--user-data-dir', '/tmp/vsct-ud'] in .vscode-test.mjs to run from this worktree — the path makes the VS Code IPC socket exceed macOS's 103-char limit. Environmental, not a code failure; the change was reverted before committing and CI runs from a short path.
  2. RuntimeManagerOptions.probe was added as a test seam beyond the original plan. The existing test stubs write #!/bin/sh scripts named ggsql-jupyter.exe, which cannot be spawned on Windows, so a real probe by default would have broken the windows-latest leg of test-extension.yaml. It follows the existing kernelSpecDir seam.

Minor note: --version emits a tracing line on stderr before clap parses args, since tracing is initialised first in main. Harmless — the probe reads only the exit code — but relevant if anyone later parses the output.

🤖 Generated with Claude Code

A bundled kernel is built for a platform, not for every system it can be
installed on. One linked against newer shared libraries than the host
provides is exec'd successfully and then rejected by the dynamic linker,
so it passes every filesystem check discovery makes and still cannot
serve a session. On the Linux builds this is the common case rather than
an edge one: the kernels are built on Ubuntu 24.04 and need GLIBC_2.39,
while Positron supports back to Ubuntu 20.04 and RHEL 9.

Run the bundled kernel before offering it, and put the host locations
behind it as a fallback tier. selectKernelCandidates() now returns a
KernelSelection carrying that tier as a callback, so the common case --
a bundled kernel that runs -- never pays for the PATH lookup.

Only the bundled kernel is probed; a kernel the user installed is taken
at its word. A success is cached against the extension version, keeping
it to one spawn per update; a failure is not, so a host that gains the
missing libraries starts working without waiting for an update. The
Jupyter kernel spec is written only for a kernel that passed, because it
outlives the window, is what Quarto resolves, and has no fallback.

A fallback that succeeds stays silent: the runtime's name in the picker
already discloses where it came from. Only the dead end interrupts --
nothing runnable anywhere, whether the bundled kernel failed or the build
carries none -- with one non-modal notice per extension version.

ggsql-jupyter gains --version, which the probe uses and which had no way
to be asked before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant