feat(lsp): polish — graceful shutdown, log level, upstream error-format canaries - #10
Merged
Merged
Conversation
This was referenced Jun 20, 2026
klaidliadon
force-pushed
the
ridl-lsp-hardening/parse-pipeline
branch
from
June 20, 2026 07:05
5c318c4 to
855168c
Compare
klaidliadon
force-pushed
the
ridl-lsp-hardening/polish
branch
from
June 20, 2026 07:05
7940cb5 to
28a9590
Compare
Diagnostics depend on the exact wording/shape of upstream webrpc parser errors: isVersionOptionalSchemaError substring-matches the schema-validation message, and errorToDiagnostic regex-extracts line:col from the error text. There is no typed-error alternative upstream, so an upstream wording change would silently break import handling and collapse diagnostic positions to a line-1 smear (audit I4). - Name the matched substrings as constants documenting the coupling. - TestVersionRequiredErrorFormat pins the version-required message; TestUpstreamErrorFormatCanary pins that a positioned error still yields a line:col prefix errorToDiagnostic can parse. Either drifting fails CI.
main used context.Background() with no signal handling, and Shutdown/Exit were no-ops (audit S1). A supervised or containerized server (the Docker ENTRYPOINT) had no clean shutdown path, and the process always exited 0 regardless of protocol state. - Derive the root context from signal.NotifyContext(SIGINT, SIGTERM) and close the connection on signal. - Track whether shutdown was received; on a client-closed stream, exit non-zero if it was not (LSP spec), exit 0 after a signal.
Logging was hardcoded to zap production (info), with no way to quiet or to raise verbosity for field debugging (audit S2). Read the level from RIDL_LSP_LOG_LEVEL (debug/info/warn/error); an invalid value is reported and ignored rather than failing startup.
- overlayContents called docs.All() twice (double lock + alloc); call it once (audit S3). - memFileInfo.ModTime returned time.Now(), making in-memory overlay metadata non-deterministic; return a zero time, since only content is read (audit S4). - git describe --tags failed builds outside a tagged checkout (shallow clone, fork); fall back to --always then 'dev', and default Docker VERSION to dev (audit S5).
Self-review caught that the first S1 cut exited non-zero whenever the stream closed without a prior shutdown — but editors routinely tear down a stdio server by just closing the pipe (no shutdown/exit), so that flagged every normal close as an error in supervisor/CI logs. Per the LSP spec the exit-code rule belongs to the exit notification: the Exit handler now exits 0 if shutdown was received, else 1 (via an injectable exit func so the contract is unit-tested). A bare stream close or an OS signal exits 0.
Codex review (reproduced via subprocess) found the exit code was racy: protocol.Handlers runs every request through AsyncHandler in its own goroutine, so on 'exit' + immediate EOF, conn.Done() could return from main (exit 0) before the async Exit handler reached os.Exit(1). Dispatching only exit synchronously would still race an async 'shutdown' setting its flag. Dispatch both shutdown and exit synchronously in the read loop, in arrival order: shutdown sets the flag, then exit reads it and terminates — all before the trailing EOF. Add an end-to-end subprocess test asserting exit-without-shutdown=1 and shutdown-then-exit=0.
klaidliadon
force-pushed
the
ridl-lsp-hardening/polish
branch
from
June 20, 2026 07:09
28a9590 to
d558be9
Compare
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.
Final hardening layer: a supervised/containerized server now shuts down cleanly on a signal and reports a spec-correct exit code, logging is tunable for field debugging, and the fragile string-coupling to the upstream parser's error wording is pinned so an upstream bump fails CI loudly instead of silently breaking diagnostics.
Third of the prod-readiness stack. Depends on #8 and #9 — merge those first.
Changes (audit findings)
ridl/parser.go, tests): diagnostics depend on the exact wording/shape of upstream webrpc parser errors (aschema error: version is required…substring match, and aline:col:regex). There's no typed-error alternative upstream, so a wording change would silently break import handling and collapse diagnostic positions. Named the matched substrings as constants and added canaries (TestVersionRequiredErrorFormat,TestUpstreamErrorFormatCanary) that fail CI on drift.cmd/ridl-lsp/main.go,lsp/server.go): derive the root context fromsignal.NotifyContext(SIGINT, SIGTERM)so the DockerENTRYPOINTshuts down cleanly; theexithandler reports the LSP-spec exit code (0 ifshutdownwas received, else 1). Lifecycle methods are dispatched synchronously so the exit code can't lose a race with the transport EOF (see review note).cmd/ridl-lsp/main.go):RIDL_LSP_LOG_LEVEL(debug/info/warn/error); an invalid value is reported and ignored rather than failing startup.overlayContentssnapshotsdocs.All()once;memFileInfo.ModTimereturns a zero time (deterministic, parser ignores it);git describefalls back to--always/devand Docker defaultsVERSION=devfor untagged builds.Test plan
New tests: error-format canaries (
parser_test.go,upstream_error_format_test.go), exit-code contract (lifecycle_test.gounit +exit_e2e_test.gosubprocess), log-level parsing (main_test.go).Review
Self-review caught an Important issue: the first exit-code cut exited non-zero on any stream close without shutdown — wrong, because editors routinely tear down by just closing the pipe. Reworked so the exit code is owned by the
exitnotification. A Codex adversarial pass (via agent-comms, 2 rounds) then reproduced a subtler race —AsyncHandlerrunsexitin a goroutine that loses to EOF onconn.Done()— fixed by dispatchingshutdown+exitsynchronously in the read loop, with an end-to-end subprocess test. Security-review: no findings.Stack:
ridl-lsp-hardeningThis stack is managed with sdf.