diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index ae3d7ec7689..47fea03e0d7 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -9,6 +9,7 @@ import ( "os/signal" "runtime" "syscall" + "time" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" @@ -45,6 +46,9 @@ func runLSP(args []string) int { defaultLibraryPath := bundled.LibPath() typingsLocation := getGlobalTypingsCacheLocation() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + s := lsp.NewServer(&lsp.ServerOptions{ In: lsp.ToReader(os.Stdin), Out: lsp.ToWriter(os.Stdout), @@ -58,11 +62,11 @@ func runLSP(args []string) int { cmd.Dir = cwd return cmd.Output() }, + SetParentProcessId: func(parentPID int) { + startParentProcessWatchdog(ctx, parentPID) + }, }) - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - if err := s.Run(ctx); err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -136,3 +140,41 @@ func getNonWindowsCacheLocation() string { } return tspath.CombinePaths(homePath, cacheFolder) } + +// startParentProcessWatchdog starts a goroutine that monitors the parent process +// and exits the current process if the parent dies. This prevents orphaned language +// server processes when the editor crashes or is killed. +func startParentProcessWatchdog(ctx context.Context, parentPID int) { + if parentPID <= 0 { + return + } + go func() { + tick := time.Tick(5 * time.Second) + for { + select { + case <-ctx.Done(): + return + case <-tick: + if !isProcessAlive(parentPID) { + fmt.Fprintf(os.Stderr, "Parent process %d has exited, shutting down.\n", parentPID) + os.Exit(1) + } + } + } + }() +} + +// isProcessAlive checks if a process with the given PID is still running. +func isProcessAlive(pid int) bool { + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + // On Unix, FindProcess always succeeds. We need to send signal 0 to check if + // the process exists. On Windows, FindProcess already validates the process exists. + if runtime.GOOS == "windows" { + return true + } + err = proc.Signal(syscall.Signal(0)) + return err == nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 85634eebd98..75386e7af09 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -40,6 +40,7 @@ type ServerOptions struct { TypingsLocation string ParseCache *project.ParseCache NpmInstall func(cwd string, args []string) ([]byte, error) + SetParentProcessId func(parentPID int) } func NewServer(opts *ServerOptions) *Server { @@ -61,6 +62,7 @@ func NewServer(opts *ServerOptions) *Server { typingsLocation: opts.TypingsLocation, parseCache: opts.ParseCache, npmInstall: opts.NpmInstall, + startWatchdog: opts.SetParentProcessId, initComplete: make(chan struct{}), } s.logger = newLogger(s) @@ -175,7 +177,8 @@ type Server struct { npmInstall func(cwd string, args []string) ([]byte, error) - cpuProfiler pprof.CPUProfiler + cpuProfiler pprof.CPUProfiler + startWatchdog func(parentPID int) } func (s *Server) Session() *project.Session { return s.session } @@ -789,6 +792,10 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ s.logger.SetVerbose(true) } + if s.startWatchdog != nil && params.ProcessId.Integer != nil { + s.startWatchdog(int(*params.ProcessId.Integer)) + } + response := &lsproto.InitializeResult{ ServerInfo: &lsproto.ServerInfo{ Name: "typescript-go",