diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-11 08:34:41 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-11 08:34:41 +0300 |
| commit | e95f3fdf0a66ba05ba2c8fb7e755e107f9cf7991 (patch) | |
| tree | 412c7e21ec9c317beb99ed7fe0d4d93a7dacbe50 /internal/hexailsp/run.go | |
| parent | 73dadb573f92dca310036e8793932e94277abd62 (diff) | |
Thread context.Context through blocking I/O entry points
Accept ctx as the first parameter on the blocking I/O entry points and
propagate it to downstream blocking calls so the work is cancellable from
the process entry point:
- appconfig.Load / LoadWithOptions: honor ctx before the blocking file
reads, returning defaults on a cancelled context.
- LSP: lsp.Server.Run(ctx) ties the serve loop to the caller context via a
new watchParentContext bridge (cancels the server context, aborting
in-flight LLM work). Threaded through hexailsp.Run/RunWithConfig/
RunWithFactory and runtimeconfig.Store.Reload.
- MCP: mcp.Server.Run(ctx) stops accepting requests once ctx is cancelled;
threaded through hexaimcp.Run/RunWithFactory/RunBackfill.
- editor: RunEditor/OpenTempAndEdit/OpenFile take ctx and use
exec.CommandContext so a cancelled context kills the editor subprocess;
threaded through hexaicli, hexaiaction and askcli call sites.
Top-level callers (cmd/hexai-lsp-server, cmd/hexai-mcp-server) now build a
signal-cancelled context (SIGINT/SIGTERM) so shutdown tears the run down
cleanly. Updated comments to explain the cancellation flow and added
cancellation tests for the LSP/MCP loops, editor, and config load.
All tests pass with -race; cross-package coverage 86.2%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal/hexailsp/run.go')
| -rw-r--r-- | internal/hexailsp/run.go | 31 |
1 files changed, 19 insertions, 12 deletions
diff --git a/internal/hexailsp/run.go b/internal/hexailsp/run.go index 25d1929..8b3e840 100644 --- a/internal/hexailsp/run.go +++ b/internal/hexailsp/run.go @@ -3,6 +3,7 @@ package hexailsp import ( + "context" "fmt" "io" "log" @@ -20,7 +21,11 @@ import ( ) // ServerRunner is the minimal interface satisfied by lsp.Server. -type ServerRunner interface{ Run() error } +// Run takes a context so the serve loop is cancelled when the process is +// shutting down (e.g. on SIGINT/SIGTERM from the top-level caller). +type ServerRunner interface { + Run(ctx context.Context) error +} // ConfigurableServerRunner supports runtime option updates. type ConfigurableServerRunner interface { @@ -48,19 +53,21 @@ type ServerFactory func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.S // Run configures logging, loads config, builds the LLM client and runs the LSP server. // It is thin and delegates to RunWithFactory for testability. -func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { - return RunWithConfig(logPath, "", stdin, stdout, stderr) +func Run(ctx context.Context, logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + return RunWithConfig(ctx, logPath, "", stdin, stdout, stderr) } // RunWithConfig is like Run but accepts an explicit config file path. -func RunWithConfig(logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { +// ctx is threaded through config loading and the LSP serve loop so the whole +// run is cancellable from the process entry point. +func RunWithConfig(ctx context.Context, logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if err := llm.RegisterAllProviders(); err != nil { return fmt.Errorf("failed to register LLM providers: %w", err) } - return runWithConfigDependencies(logPath, configPath, stdin, stdout, stderr, defaultRunDependencies()) + return runWithConfigDependencies(ctx, logPath, configPath, stdin, stdout, stderr, defaultRunDependencies()) } -func runWithConfigDependencies(logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer, deps runDependencies) error { +func runWithConfigDependencies(ctx context.Context, logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer, deps runDependencies) error { deps = normalizeRunDependencies(deps) logger := log.New(stderr, "hexai-lsp-server ", log.LstdFlags|log.Lmsgprefix) if strings.TrimSpace(logPath) != "" { @@ -77,23 +84,23 @@ func runWithConfigDependencies(logPath string, configPath string, stdin io.Reade } logging.Bind(logger) loadOpts := appconfig.LoadOptions{ConfigPath: configPath} - cfg := deps.loadConfig(logger, loadOpts) + cfg := deps.loadConfig(ctx, logger, loadOpts) if err := cfg.Validate(); err != nil { return fmt.Errorf("invalid config: %w", err) } if cfg.StatsWindowMinutes > 0 { stats.SetWindow(time.Duration(cfg.StatsWindowMinutes) * time.Minute) } - return runWithDependencies(logPath, configPath, stdin, stdout, logger, cfg, nil, nil, deps) + return runWithDependencies(ctx, logPath, configPath, stdin, stdout, logger, cfg, nil, nil, deps) } // RunWithFactory is the testable entrypoint. When client is nil, it is built from cfg+env. // When factory is nil, lsp.NewServer is used. -func RunWithFactory(logPath string, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error { - return runWithDependencies(logPath, configPath, stdin, stdout, logger, cfg, client, factory, defaultRunDependencies()) +func RunWithFactory(ctx context.Context, logPath string, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error { + return runWithDependencies(ctx, logPath, configPath, stdin, stdout, logger, cfg, client, factory, defaultRunDependencies()) } -func runWithDependencies(logPath string, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory, deps runDependencies) error { +func runWithDependencies(ctx context.Context, logPath string, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory, deps runDependencies) error { deps = normalizeRunDependencies(deps) normalizeLoggingConfig(&cfg) if err := cfg.Validate(); err != nil { @@ -128,7 +135,7 @@ func runWithDependencies(logPath string, configPath string, stdin io.Reader, std configurable.ApplyOptions(opts) }) } - if err := server.Run(); err != nil { + if err := server.Run(ctx); err != nil { return fmt.Errorf("server error: %w", err) } return nil |
