GemStone Manager: one screen for the GemStone environment - #454
Conversation
Three places knew that a stone needs a gigabyte, and each rederived it from the same two magic divisors: `isSharedMemoryConfigured`, which the Start Stone preflight gates on; Quick Setup, which warns before it builds anything; and the OS view's own status node, which also formats the reading for display. Three copies of `shmmax / 2^30`, `shmall / 2^18` and `>= 1`, in two files. They are one question — does this reading clear the threshold, and how do we say how much there is — so `sharedMemoryStatus` answers it once and the three ask. An unreadable sysctl was already "not configured" everywhere; the helper returns that, which is how the view's node loses its if/else entirely. No behaviour changes: the divisors, the comparison and the label are the ones the status node already produced, and its tests pin every branch of them.
`fetchAvailableVersions` did two jobs in one pass: scrape the downloads page, then work out from the disk what is downloaded, extracted, or a local build. Only the first needs the network, and only the second changes between two calls a minute apart. A caller wanting the cheap half had no way to ask, so it paid for a round trip — and got nothing at all when the machine was offline, which is exactly when knowing what is already installed matters most. The two are now separate. `fetchCatalog` answers what the downloads page lists. `versionsFrom` turns a catalog plus the disk into rows, reading the disk afresh every time, so a caller may hold one catalog and ask as often as it likes. `fetchAvailableVersions` is the two together and is unchanged for its callers. `getInstalledVersions` is then `versionsFrom([])`: a catalog listing nothing already means every extracted version becomes a row, since none can be in it. Saying it that way rather than mapping the rows a second time is what makes the two lists agree — the offline answer is now filtered and ordered exactly like the online one, where a hand-rolled second pass had left it in whatever order the directory happened to be read in, carrying releases too old for Jasper to drive that the catalog answer drops. The row builder behind both is `installedVersion`, which is the fifteen lines the two extracted-version loops each used to carry. It sets `local` only for a symlinked build; a real directory leaves the flag absent, which is the shape that pass has always produced.
Getting from a fresh install to a live session means visiting four separate sidebar trees — OS prerequisites, versions, databases, logins — each showing a slice of the answer and none of them showing what to do next. The manager is one editor tab that renders all four, ordered by what is actually blocking you: a machine that cannot run a stone leads with Operating System, a machine with no release installed leads with Versions, and once both are settled they sink below Connect and Databases. It is a read/coordinate surface, not a reimplementation. Every mutating action dispatches an existing `gemstone.*` command, so the confirmation modals, progress notifications and tree refreshes come from the code that already owns them. It does not trust the webview either: session actions are matched against a fixed list rather than executed on trust, and a file it is asked to open or restore from is re-checked against its own database directory first. It keeps itself current. Logins live in a setting, and the editor that writes them saves long after the command which opened it returned, so Connect follows `gemstone.logins` rather than the moment of dispatch. Everything else follows the admin views' own change events, coalesced so one command that refreshes several views costs one rebuild — and a rebuild is quiet, because the panel is refreshing under someone who is reading it. A change arriving while its tab is hidden waits until the tab comes back. A refresh renders twice: once from what is already on disk, then again when the download catalog answers, so the panel is usable while the catalog is in flight and stays usable when it never arrives. The catalog is then held for as long as the panel is open — what changes between two rebuilds is on disk, and a network round trip behind every stone start is a wait for nothing. Refresh drops it, so the button still means what it says. A command that rejects is reported rather than dropped: a dispatch is fire-and-forget from here, so without that a failed start reads as a click that never registered — nothing said, nothing logged, the panel unchanged. Listing a database's files asks the filesystem once per directory, too. Stating each name in turn costs a `wsl.exe` spawn per file where paths route through WSL, which a directory of stone logs turns into a frozen window on every rebuild. What the reader has said stands. Which sections start open follows what needs attention, which is right on arrival and wrong afterwards: the panel redraws whenever anything changes, so a section opened to work through would otherwise snap shut under them. And a live session outranks anything guessed about its stone — the session is first-hand knowledge that the stone is up, where "running" is read from a process list this machine may have no installed version to consult, so a login that is connected is no longer offered the button that starts its stone. Whether a stone is up now falls back to that process list for a login whose database was not made here, rather than reading as stopped. Two things it learned from being used: `startstone` and `startnetldi` exit non-zero when the server is already running, so it starts only what is down — the sidebar never hit that because its Start actions only appeared on a stopped row. And gslist reports a start time to the minute, so a stone that came up seconds ago reads "just started" rather than "running 0 min". It follows the two settings it depends on, `gemstone.logins` and `gemstone.rootPath` — the second being the folder every database and installed version is found under, and the one the panel itself offers a button to change. The icons are the codicon font, which a webview inherits nothing of, so `codicon.css` and `codicon.ttf` ship in the `.vsix` behind two `!` lines holding them out of the wholesale `node_modules/**` exclusion. That makes them third-party files this extension distributes: the icons are CC BY 4.0, and THIRD-PARTY.md carries the attribution beside the vendored AST it already covers. Both files are pinned by a guard, since losing the whitelist or the production dependency that leaves them there costs nothing at compile time and nothing in the dev host — the panel renders with every glyph a blank box, and only once packaged. The behavior lives in gemstoneManagerView.js, read at module load and injected as a nonce'd <script> under a strict CSP, as debuggerPanel.ts does. The `*View.js` suffix is not decoration: a webview script sharing this module's basename would shadow it, since Vite resolves an extensionless import to the .js of such a pair. The codicon font ships with the extension because a webview inherits none.
The panel could say only that shared memory was short, and offer Quick Setup. Every other prerequisite the Configure OS view knew about — RemoveIPC, the WSL version, mirrored networking, the two gs64ldi services entries, the hosts entry — was invisible here, and so were the eight commands that fix them. On Windows that is the whole story of why a stone will not start. The OS section is now a checklist: one row per prerequisite this machine actually has, each saying what the machine reports — '2.0 GB', 'mirrored', 'gs64ldi missing' — and carrying its own remedy only when it is not ok. That is how the tree read, a status with the thing that fixes it underneath, and it answers "which part is wrong" rather than only "something is". Which rows appear follows the machine: shared memory everywhere, RemoveIPC on Linux and on the Linux inside WSL, and the WSL set only where paths route through WSL. The hosts entry appears only while networking is NAT, since that is the only time it matters. The shared-memory row reports headroom, not just the limit. shmall is a ceiling for the machine, not for one stone: caches other stones already hold count against it, so this laptop cleared the 1 GB threshold with 123 MB free and no stone would start. The row now reads what is allocated against the limit, from ipcs, and warns below 100 MB free — what a database Jasper creates asks for (SHR_PAGE_CACHE_SIZE_KB = 100000). Reading ipcs means knowing which lines are segments: macOS opens one with the type letter and Linux with the hex key, and taking any line with enough columns counts macOS's title line, whose last field is the year. Refresh forgets the WSL answers before rebuilding. They are cached and the checklist reads them, so a machine could stay "WSL not reachable" after the user had made it reachable — a checklist that cannot recover from being right once. `gemstone.refreshVersions` already did this, which is why the trees recovered and the panel did not. The probes were all there — getRemoveIpcConfigured, getWslInfo, getWslNetworkInfoCached, and the two services checks, which are exported now rather than private to the module that used to draw them. Remedies dispatch through an allow-list, exactly as session actions do: the webview sends a command name, and a name that is not a known remedy is dropped rather than run.
Fourteen actions the trees had and the panel did not, each dispatched to the command that already owns it. Logins gain Delete and Duplicate beside Edit, and selecting a session goes through `gemstone.selectSession` rather than the session manager directly, so everything else showing the selection follows. Export Classes and Full Logical Backup join the session actions — the second is a different backup from the extent one already here, and it belongs to a session rather than to a database. Versions gain a terminal for a product folder, the Windows client trio, and the walkthrough. The client actions need to know whether a client is extracted, so the row carries that now; they and Copy Host appear only where paths route through WSL, since they mean nothing anywhere else. Processes gain Copy Host and, on a process that stopped responding, clearing its stale lock — the row already knew it was stale, it just had nothing to offer about it. Databases gain Install Server Support. Whether a stone has the Enhanced Inspector and refactoring support is a property of that database, and until now the only way to find out was an inspector behaving differently. The command resolves the session it needs and says so when there is none, so the panel offers it without second-guessing. Create Database now dispatches its command instead of calling the database manager itself. Doing it here skipped that command's check for whether a login already targets the new stone, so a database made from the panel could end up with a second DataCurator login and no message. That was the last thing the panel did for itself rather than delegating, and dropping it takes the whole DatabaseManager dependency out of the panel's bag.
The panel had unit tests and nothing that had ever opened it. Those tests know what the host posts and what the webview draws from a state handed to it; what neither of them touches is the panel actually rendering inside VS Code, in a webview two frames deep, wired to commands that reach a live database. Three scenarios, in a chapter of their own so the storyboard reads as the screen's own section of the manual: the four sections are there on opening; the operating-system checklist says what this machine reports, read from its real sysctl and ipcs; and a login listed under Connect logs in, leaving the row offering to log out again and naming the session the editor now works through. Writing them found three things the unit tests could not. A section opened to read through snapped shut on the next redraw, because which sections start open is decided fresh each render — now the reader's answer outlives it. A login whose stone Jasper cannot see running was offered "Start & log in" even while connected to it, because the row asked about the stone before asking whether a session was already open on it. And whether a stone is up fell back to reading as "no" for any database this machine did not make here, rather than asking the process list. Each has a unit test of its own alongside the fix. The Connect scenario reaches the database through "Start & log in" rather than "Log in": the container's GemStone lives outside the folder Jasper scans, so it finds no installed version to run gslist from and cannot see the stone at all. That is the honest picture of a login to a stone made elsewhere, and the button starts only what is down, so it is the way in either way.
A mutation run over the panel — break a line of production code, see whether any test notices — found fourteen changes the suite let through. They fall into three groups, and none of them was a test that failed to assert; they were guarantees nothing had been written about at all. The two allow-lists are the worst of it. Both are described in this code as the panel's defence against running a command name the webview invented, and neither had a test that the list is ever consulted: the fixture held no sessions, so nothing reached the session-action path, and deleting both checks outright left the suite green. There are now four tests — an offered action runs, one that is not offered does not, for each list — and a fifth asserting every id in them is a command this extension registers, since a rename elsewhere would leave a button silently doing nothing. The panel's own machinery was the second group. It is a singleton, and building a second one instead of revealing the first went unnoticed, as did never releasing it on close; two panels would show the same environment side by side and drift apart the moment either acted. Its one-scan-at-a-time rule was in the same position: running scans concurrently, or dropping the pass queued behind one, both passed — and the first is how an older scan comes to post its state last. The rest were single guarantees: markup in a name has to arrive as text, a process belongs to a database only if the version matches too, a change of session redraws Connect, a tab being shown is not a reason to rescan, and a start time that is not a date answers nothing. Every one of the fourteen now fails a test.
Every user-facing change in the file carries an entry; this is the largest one since the refactoring engine.
b15d286 to
7e1c6ab
Compare
…te scripting' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
The autofix for the cross-site-scripting finding added `ev.source !== window` to the webview's message listener, which drops every message the panel exists to receive: VS Code relays the extension host's messages in from the frame around this one, so `ev.source` is never this window. The panel renders its loading skeleton and then nothing, for ever. Nothing local caught it. The unit tests call `render(state)` directly and never go near the listener; compile, lint and the packaging check are all indifferent. The three acceptance scenarios, which drive a real editor window, went from green to timing out on a section that never appears — which is the whole reason they exist. The two type guards that came with it are kept: they cost nothing and a message without a `state` is not one this panel can draw. The finding itself is about `innerHTML` in `render`, and checking the event's source sanitizes nothing — it only cuts the path the analysis was following. What answers it is `esc`, which every interpolation goes through, with tests pinning that markup in a database name and in a login label arrives as text.
GemStone Search docks a webview of its own in the panel as soon as a session exists, so from the login scenario onward `iframe.webview` matches two frames and Playwright refuses the ambiguity rather than guessing. Its QuickPick form is the same feature with no second webview, and this suite is not about search. It sits with the other settings that quiet the workbench for a clean trace.
Auditing every interpolation in the view for the cross-site-scripting finding turned up two that skipped the escaper: a process's pid and port. Both come from parsing gslist output, so neither is a plausible carrier, but they were the only values reaching the markup unescaped and the rule this file follows is that all of them go through `esc`. This does not answer the finding, which is about the `innerHTML` in `render` rather than any particular value. It closes the gap the audit for it found.
MatiasFernandez
left a comment
There was a problem hiding this comment.
Left a few comments, I want to spend some time testing the new view. We discussed with Eric in the past that the sidebar would require some improvements but we haven't settle on a path forward, so I want to play around with this proposal to see how it feels
| * would otherwise need, in the order that works. The stone must answer before | ||
| * the login is attempted, so the start is awaited rather than fired alongside. | ||
| */ | ||
| /** Run one of the allowed session commands against a live session. */ |
There was a problem hiding this comment.
💭 Three doc blocks have stacked up on this one method, and the two above it belong to methods further down: "Connect as a specific login..." is connectLogin's, and "Bring a stopped database up and then log in..." is startAndConnect's -- both of which now sit below with no doc of their own. Someone hovering runSessionAction is told it connects a login.
Same shape at line 759: installNewVersion carries installVersion's "Installing a chosen release is a single action: fetch the archive, then extract it" ahead of its own block.
Looks like a reorder that left the comments behind -- can you move each block back onto the method it describes? It matters a bit more than usual here since runSessionAction is one of the two allow-list gates, so it's a method people will read before touching.
| const { panel } = openPanel(); | ||
|
|
||
| changeSetting('editor.fontSize'); | ||
| await new Promise((resolve) => setTimeout(resolve, 0)); |
There was a problem hiding this comment.
💭 I think this one passes whether the code is right or wrong. markStale schedules the rebuild on the 200 ms coalescing timer, so at setTimeout(..., 0) nothing has been posted yet even if editor.fontSize had wrongly triggered a rebuild -- the assertion is satisfied by the timer not having fired, not by the setting being ignored.
await settle() (400 ms, as the neighbouring tests use) would make it fail if the filter regressed. Worth calling out given the mutation audit in the description: this is exactly the shape that survives one -- delete both affectsConfiguration checks and this test stays green.
| * `gs64ldi <port>/tcp` entry? Routed through wslExecSync so the check | ||
| * inspects the Linux distro, not any Windows file of the same name. */ | ||
| function wslServicesHasGs64ldi(): boolean { | ||
| /** Does the WSL distro's /etc/services carry the gs64ldi entry? */ |
There was a problem hiding this comment.
💭 The original block comment is still sitting above this one, so the function now carries two docs -- and TS surfaces only the nearest, which means the tooltip loses "Routed through wslExecSync so the check inspects the Linux distro, not any Windows file of the same name". That was the part explaining why the function exists rather than just reading the file. Same at line 48 for windowsServicesHasGs64ldi.
Was the intent to replace them? If so, should the routing rationale fold into the new one-liner rather than being dropped?
Related, while you're in here: isSharedMemoryConfigured (line 149) lost its doc entirely in this pass. "Treats an unreadable sysctl as not configured" is still true and still not obvious from the body.
| } | ||
|
|
||
| /** Long enough for the panel's coalescing window to close and a rebuild to land. */ | ||
| function settle(): Promise<void> { |
There was a problem hiding this comment.
💭 settle() is a flat 400 ms sleep and it's awaited around 20 times in this file, so roughly eight seconds of the suite is spent waiting out a 200 ms timer -- and it's the kind of margin that gets thin on a loaded CI box.
Would vi.useFakeTimers() plus await vi.advanceTimersByTimeAsync(COALESCE_MS + 1) work here? It would make the "exactly one more pass" and "rebuilds once for a burst" tests deterministic rather than merely likely, which is where the real value is -- those are the ones asserting an exact count. The tests that just wait for something to arrive already use vi.waitFor.
| if (a.iconOnly) { | ||
| return `<button type="button" class="icon-btn" data-action="${action}"${data}${title} aria-label="${esc(label)}">${icon(iconKey)}</button>`; | ||
| } | ||
| const variant = cls === 'btn-primary' ? ' btn-primary' : ''; |
There was a problem hiding this comment.
💭 btn() only honours 'btn-primary' -- every other value of cls is silently discarded. 'btn-ghost' is passed at 24 call sites and 'btn-secondary' at one, and neither class appears in the CSS block in gemstoneManager.ts, so today they read as styling intent that never lands.
Should the argument just be dropped where it isn't btn-primary, or was .btn-ghost a rule meant to ship with the stylesheet? Either way it would stop the next reader assuming those buttons already render differently from the default secondary treatment.
| // fails however comfortably the limit itself clears 1 GB. | ||
| const shmallBytes = mem ? mem.shmall * 4096 : undefined; | ||
| const free = shmallBytes !== undefined && inUse !== undefined ? shmallBytes - inUse : undefined; | ||
| const roomForACache = free === undefined || free >= 100 * 1024 * 1024; |
There was a problem hiding this comment.
💭 This 100 MB is the cache size databaseManager.ts:252 writes into every generated conf as SHR_PAGE_CACHE_SIZE_KB = 100000, but the two are independent literals -- and they don't quite agree, since 100000 KB is 102,400,000 bytes and this is 104,857,600.
Should the conf value become a shared constant both can read? The comment just above already names the coupling, which is what makes it worth pinning: if someone bumps the cache in databaseManager, this check keeps warning against the old figure and nothing fails.
| @@ -0,0 +1,1509 @@ | |||
| // GemStone Manager — a single, consolidated editor-tab webview that manages the | |||
There was a problem hiding this comment.
💭 I see that all the logic for the gemstoneManager is sitting at the root of the client workspace. We've been working with Eric on reorganizing the code a bit to keep the code for big features grouped together. Could you create a gemstoneManager folder in client/src to group the logic and the related tests there?
|
|
||
| --- | ||
|
|
||
| ## Microsoft codicons (icon font) |
There was a problem hiding this comment.
💭 Why are we including this here if we are not vendoring codicons?
| expect(whitelist.has(`!${file}`)).toBe(true); | ||
| }); | ||
|
|
||
| // vsce prunes devDependencies before it packages, so a whitelist over one |
There was a problem hiding this comment.
💭 I don't understand the purpose of this test; parsing the package.json feels weird. And the comment mentions devDependencies but the package is imported as a normal dependency. What do we lose if we remove this?
| !node_modules/koffi/index.d.ts | ||
| !node_modules/koffi/LICENSE.txt | ||
| !node_modules/koffi/build/** | ||
| # The GemStone Manager webview draws every icon with the codicon font, which a |
There was a problem hiding this comment.
💭 Eric has been working on multiple webviews for different features, but this new one seems to be using a different styling/UI design. Are there specific limitations you found with the current established font/icons/styling, or is this just a preference? I'm worried about the visual consistency across the different webviews of Jasper.
Yes, and I'd like to give it a try too. |
Running GemStone means keeping four things in order: an operating system
configured to hand out shared memory, a version installed from it, a database
made from that version, and a login that reaches the database. Each has its own
sidebar view, which is fine once everything is up — and four separate lists to
walk when it isn't.
This adds GemStone Admin: GemStone Manager, an editor tab that puts all four
on one screen in the order they matter. A machine that cannot hand out enough
shared memory says so before anything else; a machine with nothing installed
leads with the versions; once neither is true the screen leads with connecting,
because that is what it is usually opened to do.
Reading it
Eight commits. The first two stand on their own without the panel as their
excuse, and are worth taking first:
shmmax / 2^30,shmall / 2^18and>= 1from scratch — the Start Stonepreflight, Quick Setup, and the OS view's own status node, which also formats
the reading for display. One helper now answers it, and the status node loses
its if/else entirely.
fetchCatalogdoesthe scrape;
versionsFromturns a catalog plus the disk into rows;fetchAvailableVersionsis the two together and is unchanged for its callers.getInstalledVersionsbecomesversionsFrom([]), which is what makes theoffline list filtered and ordered like the online one — it had not been, so the
panel's first paint showed releases Jasper cannot drive, which then vanished.
Then the panel itself, the two commits that complete it, the acceptance chapter,
a test-hardening pass, and the changelog note.
What the screen offers
actually has (shared memory with how much is still free rather than just the
limit, RemoveIPC, the WSL version, mirrored networking, both
gs64ldiservicesentries, the hosts entry), each saying what the machine reports and carrying
its own remedy only while it is failing.
log in, start-and-log-in, edit, delete, duplicate, select a session, plus the
session actions including Export Classes and Full Logical Backup.
local builds, open the folder or a terminal, the Windows client trio, the
walkthrough.
and ports, logins, extent chooser, log/conf/backup files; create, delete,
start/stop, replace extent, back up, restore, install server support, terminal,
reveal, create and edit logins, clear a stale lock, copy host.
Everything the four sidebar views offered, the manager offers. The panel accepts
47 kinds of message, two of which fan out through an allow-list — 8 session
actions and 9 operating-system remedies — and each was checked against the
contributions of the view that used to own it.
How it is built
It dispatches the existing
gemstone.*commands rather than reimplementing them,so confirmations, progress and tree refreshes come from the code that already
owns them. It does not trust the webview: session actions and OS remedies are
matched against allow-lists, and a file path is re-checked against its database
before anything is opened or restored from. It keeps itself current from the
admin views' own change events — coalesced, quiet, one scan at a time, and idle
while its tab is hidden.
Two things worth calling out
@vscode/codiconsis a production dependency. The panel draws every iconwith the codicon font, which a webview inherits nothing of, and
vsceprunesdevDependencies — so a
.vscodeignorewhitelist over one would find nothing leftto package. Two files ship (
codicon.css,codicon.ttf), the icons areCC BY 4.0, and THIRD-PARTY.md carries the attribution. Both files and the
dependency are pinned by a guard, because losing either costs nothing at compile
time and nothing in the dev host: the panel renders with every glyph a blank box,
and only once packaged.
The manager is palette-only for now. An icon beside the GEMSTONE title needs
viewContainer/title, which is a proposed API the workbench only honours forworkbench.view.debug. A gear on the container becomes available once thesidebar collapses toward a single view.
Verified
lint,format:check,compile,lint:supply-chain,lint:lockfileclean.npm testgreen — 5,421 client, 322 server, 92 mcp, against a live test stone.A new acceptance chapter runs green headless in the container
(
npm run test:acceptance:docker): the four sections on opening, theoperating-system checklist read from the machine's real
sysctlandipcs, andlogging in to a database from Connect. Writing it found three bugs the unit tests
could not — a section snapping shut on redraw, a live session offered "Start &
log in", and a stone this machine did not create always reading as stopped.
The tests were then audited by mutation rather than by reading: break a line of
production code, see whether anything fails. Fourteen changes broke nothing, the
worst being that neither allow-list had a test that the list is ever consulted —
the fixture held no sessions, so nothing reached that path, and deleting both
checks left the suite green. All fourteen now fail a test.
Known gaps
Four
project-managementacceptance scenarios are red, and were red before thisbranch — verified by stashing its work and re-running. Diagnosed separately: the
shared
Given I am logged in to a databaseasserts on a sidebar that logging inhas replaced, since the Explorer is its own view container.
The Connect scenario reaches the database through "Start & log in" rather than
"Log in": the container's GemStone lives outside the folder Jasper scans, so it
finds no installed version to run
gslistfrom and cannot see the stone at all.That is an honest picture of a login to a stone made elsewhere, but it leaves the
plain "Log in" path without acceptance coverage.