Detect servers started outside Jasper's environment and offer to restart them under Jasper - #480
Draft
ericwinger wants to merge 3 commits into
Draft
Detect servers started outside Jasper's environment and offer to restart them under Jasper#480ericwinger wants to merge 3 commits into
ericwinger wants to merge 3 commits into
Conversation
…them Jasper runs its own gslist against the root it manages, and a GemStone server registers in the locks/ directory of whatever GEMSTONE_GLOBAL_DIR it was started with. A stone or netldi started by hand, from a shell whose GemStone environment differs from Jasper's, therefore registers where Jasper does not look — and gslist alone reported it as Stopped while the process was plainly alive. The Databases tree could show Stone Running / NetLDI Stopped with both processes up, the login failed as though nothing were running, accepting the offer to start collided with the servers already holding those names, and in some states a raw "GEMSTONE environment variable is not defined" from a GemStone shell script was relayed verbatim. The user went off to debug a shell profile and an install that were both fine. Cross-check the host process table against Jasper's own gslist, mark such a server "Running outside Jasper" with its PID and registration directory, and offer — on connect, or from the row's own action — to stop it and start it again under Jasper's environment. The clean stopstone/stopnetldi runs against the directory the server actually registered in, which is what lets it stop cleanly rather than be killed, with the force-kill fallback behind it. Name alone cannot say whether a same-named server is really this database's, so identity is settled by the conf and log paths the process was started with; unconfirmed means Jasper does not touch it and says why. When a stop or kill fails it reports both names, the PIDs, the registration directory, and the gslist invocation that will show the servers where they are, rather than dead-ending. Two state-accuracy fixes from the same investigation: The tree no longer shows a healthy Running for a database a login cannot reach. Rows now distinguish Running, Stopped, not responding, not connectable (the stone is fine but nothing can traverse its NetLDI), and running outside Jasper; the tree and the login-failure recovery share one connectable predicate so they cannot disagree. Discovery no longer inherits GEMSTONE_NRS_ALL from the shell that launched the editor, whose #netldi: and #dir: components can point somewhere other than what Jasper manages. Also found on the way: deleteDatabase and replaceExtent gated on "appears in Jasper's gslist", which is false for a stone started outside Jasper even though that process has the extent open — so the files could be pulled out from under a running database. Both now refuse for a stone alive anywhere on the host. Closes #472 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…still ours
Three gaps in the previous commit's coverage.
An integration test runs the scan against the real test stone instead of
recorded ps lines. The unit tests prove the parsing rules; they cannot
prove the premises underneath them — that GemStone's servers really look
like that, that ps hands over a server's environment on this platform,
and above all that gslist really is blind to a live server registered
elsewhere. It reproduces the bug end to end: the same live servers and
the same gslist binary, pointed at a different GEMSTONE_GLOBAL_DIR,
report "no GemStone servers" while ps still sees them. Being in the CI
matrix, it runs against every release rather than one machine's install.
It also pinned down something worth knowing: gslist exits non-zero when
it has nothing to report, so an empty listing is indistinguishable from
a failure by exit code alone. ProcessManager already handled that by
catching and falling back to an empty list — that catch turns out to be
load-bearing rather than defensive, and now says so.
WSL identity matching was untested. A database path is a Windows UNC
path while the running server reports Linux paths, so identity has to be
judged after converting; without that it never confirms on Windows and
the restart offer silently never appears. Verified as a real tripwire by
removing the conversion and watching the test fail.
killHostServer re-checked that a PID was still a GemStone server, not
that it was still *this* one. The reconcile holds that PID across a
modal dialog a user can sit on indefinitely, so the number has had real
time to be recycled — possibly onto another stone, which the weaker
check waved through. It now matches on name and kind, both before
signalling and between SIGTERM and SIGKILL: anything else under that PID
means ours is already gone and escalating would hit a bystander.
Also closed two smaller gaps the earlier pass reasoned about but never
asserted: a zombie process ("[stoned] <defunct>") is not a running
server, and a path containing a space comes back truncated from ps —
which reads as pointing outside the database, so it refuses a stop
rather than risking the wrong one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Acting on a review of the external-server work. Three findings were real bugs, and the first two could each have made the feature actively harmful rather than merely absent. An unreadable gslist made every live server look external, confirmed, and killable. refreshProcesses leaves an empty process list for three different reasons — gslist found nothing, there was no extracted version to run it from, or the spawn threw — and only the first means "nothing is registered where Jasper looks". The cross-check could not tell them apart, so a broken gslist turned every running server, including ones Jasper started itself, into a candidate for Restart & Connect and then for kill -9. It now records whether the read actually happened and reports nothing unless it did, with a second, independent guard: a server registered in Jasper's own root is not outside it, whatever the listing said. `ps -eo pid=,command=` is Linux-only. On BSD/Darwin `-e` means "also show the environment" and is not a selection flag, so that form lists only the invoking user's processes that have a controlling terminal — which a detached stoned or netldid never has. The whole feature would have been silently inert on every Mac. Now `-A`, which selects on both. Verified equivalent to `-e` on Linux here; the Darwin half is reasoned from the flag semantics, not tested. A force-killed external server left its lock behind, and nothing cleaned it. The clean stop usually fails for an external server (the stock DataCurator password is rarely right), so the kill is the normal path — and it orphaned a lock on the database Jasper manages, in a directory Jasper had just positively identified. The user was left with gslist listing a dead server and a startstone that refuses. A confirmed server whose registration directory is known now gets that lock cleared, with what happened reported either way; unconfirmed identity or an unknown directory still gets left alone, which is what the original reasoning was actually true for. Also from the review: Two same-named live servers: the first match won and identity was judged on whichever that happened to be, so the server actually asked about could go unmentioned while Jasper reported "probably a different database" about its neighbour. All candidates are now collected, enriched from their own environments, and ranked by how well identity is established. A version learned only from a server's environment is now re-checked, so a same-named server from another install cannot stay in the finding. The reconcile's safety gate read a report while the loop stopped the servers in a separate argument, with nothing tying the two together. It now reads the same finding it acts on, through the one predicate that also produces the report's flag. The restart-from-the-row command aborted when only the netldi was external: startstone against the already-running stone exits non-zero, so the one server that needed starting stayed down and the user was told the restart failed. isConnectable claimed the tree and the login recovery shared it while neither called it. classifyStartNeed now leads with it, which makes the claim true rather than deleting it. A relative conf path (`-e gs64stone.conf`) was read as evidence the server belonged elsewhere — a confident false claim built from no evidence. Relative hints are ignored; paths are normalized, so `/./` and doubled separators no longer read as a different database. Timeouts on the ps calls, which run synchronously on the extension host from the tree's getChildren. A refused kill now says "not permitted" and points at sudo instead of blaming a stubborn process. Test fixes, several of which were tests that could not fail: which stop binary and argv each server kind gets (proved by swapping them and watching the suite stay green), the runCommand substitution of the GEMSTONE message, forceKillStone's own fixture using an unrealistic command line, a stale assertion on a message this branch had already replaced, and a lock assertion nothing in its code path could ever have violated. The integration test gains a positive control for the confirmed verdict — the only state that authorizes a kill — and skips with a reason when the harness stone carries no path to confirm against. Not addressed, and reasoned rather than tested: whether macOS exposes a process's environment to `ps eww` at all. If it does not, globalDir is always unknown there and Restart & Connect is never offered — only Connect as-is. The integration test asserts it loudly so a macOS runner says so rather than degrading quietly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ericwinger
marked this pull request as draft
August 22, 2026 00:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #472.
The problem
Jasper runs its own
gslistagainst the root it manages, and a GemStone server registers in thelocks/directory of whateverGEMSTONE_GLOBAL_DIRit was started with. A stone or NetLDI started by hand — from a shell whose GemStone environment differs from Jasper's — therefore registers somewhere Jasper does not look, andgslistalone reported it as Stopped while the process was plainly alive.What the user hit:
GEMSTONE environment variable is not defined— the GemStone shell script's own stderr, relayed verbatim — sent the user off to debug a shell profile and an install that were both fine.What this does
Cross-checks the host process table against Jasper's own
gslistand marks such a server Running outside Jasper, with its PID and registration directory in the row's tooltip. On connect — or from the row's new Restart Under Jasper's Environment action — Jasper offers to stop it and start it again under its own environment, worded as the environment mismatch it is rather than as a missing install.The clean
stopstone/stopnetldiruns against the directory the server actually registered in, which is what lets it stop cleanly instead of being killed; the force-kill sits behind that as a fallback.Identity is the careful part. Two databases can share a stone name, so Jasper stops a server only when the conf/log paths that server was started with place it inside the managed database's directory. When it cannot confirm, it says so plainly and does not offer the restart. If a stop or kill fails it reports both names, the PIDs, the registration directory, the
gslistinvocation that will show the servers where they actually are, and the lock files a kill leaves behind — rather than dead-ending.Folded in from the same investigation
classifyStartNeedleads with the sameisConnectablepredicate the tree derives its statuses from, so the two agree by construction rather than by coincidence.GEMSTONE_NRS_ALLfrom the shell that launched the editor, whose#dir:and#netldi:components can point somewhere other than what Jasper manages.deleteDatabaseandreplaceExtentcould act under a live stone. Both gated on "appears in Jasper'sgslist", which is false for an externally started stone even though that process has the extent open. Both now refuse for a stone alive anywhere on the host, and say what to do about it.Testing
npm run lint && npm run format:check && npm run compile && npm testall clean — 6,580 tests.Mostly unit-level, with an integration test (
externalServerScan.integration.test.ts) that runs the scan against a live stone and reproduces the bug end to end: the same servers and the samegslistbinary, pointed at a differentGEMSTONE_GLOBAL_DIR, report "no GemStone servers" whilepsstill sees them. It is in the CI matrix, so it runs against every release rather than one machine's install.Several fixes came from a review pass on the first two commits; the third commit's message has the detail. Two worth knowing about because they were tests that could not fail: which stop binary each server kind gets was asserted nowhere (proved by swapping them and watching the suite stay green), and the substitution of the
GEMSTONE-undefined message was uncovered.What I could not verify
macOS. Two things:
ps -eowas Linux-only — on BSD/Darwin-emeans "also show the environment", not "all processes", so that form lists only processes with a controlling terminal, which a detachedstonednever has. This would have made the whole feature silently inert on every Mac. Now-A, which selects on both; verified equivalent to-eon Linux, but the Darwin half is reasoned from the flag semantics rather than tested.ps ewwread another process's environment at all. If it does not, the registration directory is always unknown there, identity stays unconfirmed, and Restart & Connect is never offered — only Connect as-is. Detection itself still works. The integration test asserts this loudly so a macOS runner reports it rather than degrading quietly.Both are worth a few minutes on a Mac before this ships.
The UI itself. The reconcile dialog and the new tree states have not been driven in a real editor window — every branch of the flow is unit-tested, but nobody has looked at it.
Suggested review order
client/src/externalServerScan.ts— the parsing and identity rules, the load-bearing partclient/src/processManager.ts—getExternalServers,stopExternalServer,killHostServerclient/src/externalServerReconcile.ts— the decision flow and the wording🤖 Generated with Claude Code