Skip to content

Detect servers started outside Jasper's environment and offer to restart them under Jasper - #480

Draft
ericwinger wants to merge 3 commits into
mainfrom
eric/issue472-external-servers
Draft

Detect servers started outside Jasper's environment and offer to restart them under Jasper#480
ericwinger wants to merge 3 commits into
mainfrom
eric/issue472-external-servers

Conversation

@ericwinger

Copy link
Copy Markdown
Member

Closes #472.

The problem

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 somewhere Jasper does not look, and gslist alone reported it as Stopped while the process was plainly alive.

What the user hit:

  • The Databases tree could show Stone Running / NetLDI Stopped with both processes up, because one had registered where Jasper looks and the other had not.
  • The login failed as though nothing were running, and Jasper offered to start the database.
  • Accepting that offer collided with the servers already holding those names.
  • In some states a raw 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 gslist and 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/stopnetldi runs 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 gslist invocation 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

  • 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. classifyStartNeed leads with the same isConnectable predicate the tree derives its statuses from, so the two agree by construction rather than by coincidence.
  • Discovery no longer inherits GEMSTONE_NRS_ALL from the shell that launched the editor, whose #dir: and #netldi: components can point somewhere other than what Jasper manages.
  • deleteDatabase and replaceExtent could act under a live stone. Both gated on "appears in Jasper's gslist", 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.
  • "GemStone <version> not found. Please extract it first." now names the root Jasper searched and the directory it looked for, and points at both ways out.

Testing

npm run lint && npm run format:check && npm run compile && npm test all 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 same gslist binary, pointed at a different GEMSTONE_GLOBAL_DIR, report "no GemStone servers" while ps still 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:

  1. ps -eo was Linux-only — on BSD/Darwin -e means "also show the environment", not "all processes", so that form lists only processes with a controlling terminal, which a detached stoned never has. This would have made the whole feature silently inert on every Mac. Now -A, which selects on both; verified equivalent to -e on Linux, but the Darwin half is reasoned from the flag semantics rather than tested.
  2. macOS may not let ps eww read 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

  1. client/src/externalServerScan.ts — the parsing and identity rules, the load-bearing part
  2. client/src/processManager.tsgetExternalServers, stopExternalServer, killHostServer
  3. client/src/externalServerReconcile.ts — the decision flow and the wording
  4. Everything else is wiring

🤖 Generated with Claude Code

ericwinger and others added 3 commits August 21, 2026 16:43
…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
ericwinger marked this pull request as draft August 22, 2026 00:22
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.

Detect stones/netldis started outside Jasper's environment and offer to restart them under Jasper and connect

2 participants