Skip to content

Commit 51a1064

Browse files
authored
Exit LSP process if parent process stops existing (#2478)
1 parent 3c6134d commit 51a1064

5 files changed

Lines changed: 96 additions & 3 deletions

File tree

cmd/tsgo/isprocessalive_other.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//go:build !unix && !windows
2+
3+
package main
4+
5+
// isProcessAlive on unsupported platforms always returns true,
6+
// meaning the watchdog will never fire. This is safe: the server
7+
// simply won't detect a dead parent on these platforms.
8+
func isProcessAlive(pid int) bool {
9+
return true
10+
}

cmd/tsgo/isprocessalive_unix.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build unix
2+
3+
package main
4+
5+
import (
6+
"errors"
7+
"os"
8+
"syscall"
9+
)
10+
11+
// isProcessAlive checks if a process with the given PID is still running.
12+
// On Unix, FindProcess always succeeds, so we send signal 0 to probe the
13+
// process. If the signal returns nil or EPERM, the process exists (EPERM
14+
// means it exists but we lack permission to signal it). ESRCH or any
15+
// other error indicates the process is gone.
16+
func isProcessAlive(pid int) bool {
17+
proc, err := os.FindProcess(pid)
18+
if err != nil {
19+
return false
20+
}
21+
err = proc.Signal(syscall.Signal(0))
22+
return err == nil || errors.Is(err, syscall.EPERM)
23+
}

cmd/tsgo/isprocessalive_windows.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//go:build windows
2+
3+
package main
4+
5+
import "syscall"
6+
7+
// isProcessAlive checks if a process with the given PID is still running.
8+
// On Windows, we open the process with SYNCHRONIZE access and use
9+
// WaitForSingleObject with a zero timeout. If the wait times out, the
10+
// process is still running. If the object is signaled, it has exited.
11+
func isProcessAlive(pid int) bool {
12+
const SYNCHRONIZE = 0x00100000
13+
handle, err := syscall.OpenProcess(SYNCHRONIZE, false, uint32(pid))
14+
if err != nil {
15+
return false
16+
}
17+
defer func() { _ = syscall.CloseHandle(handle) }()
18+
ret, err := syscall.WaitForSingleObject(handle, 0)
19+
if err != nil {
20+
return false
21+
}
22+
const WAIT_TIMEOUT = 258
23+
return ret == WAIT_TIMEOUT
24+
}

cmd/tsgo/lsp.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ func runLSP(args []string) int {
4444
defaultLibraryPath := bundled.LibPath()
4545
typingsLocation := osvfs.GetGlobalTypingsCacheLocation()
4646

47+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
48+
defer stop()
49+
4750
s := lsp.NewServer(&lsp.ServerOptions{
4851
In: lsp.ToReader(os.Stdin),
4952
Out: lsp.ToWriter(os.Stdout),
@@ -58,14 +61,39 @@ func runLSP(args []string) int {
5861
return cmd.Output()
5962
},
6063
ProgressDelay: 250 * time.Millisecond,
64+
SetParentProcessID: func(parentPID int) {
65+
startParentProcessWatchdog(ctx, stop, parentPID)
66+
},
6167
})
6268

63-
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
64-
defer stop()
65-
6669
if err := s.Run(ctx); err != nil {
6770
fmt.Fprintln(os.Stderr, err)
6871
return 1
6972
}
7073
return 0
7174
}
75+
76+
// startParentProcessWatchdog starts a goroutine that monitors the parent process
77+
// and cancels the context if the parent dies. This prevents orphaned language
78+
// server processes when the editor crashes or is killed.
79+
func startParentProcessWatchdog(ctx context.Context, stop context.CancelFunc, parentPID int) {
80+
if parentPID <= 0 {
81+
return
82+
}
83+
go func() {
84+
ticker := time.NewTicker(5 * time.Second)
85+
defer ticker.Stop()
86+
for {
87+
select {
88+
case <-ctx.Done():
89+
return
90+
case <-ticker.C:
91+
if !isProcessAlive(parentPID) {
92+
fmt.Fprintf(os.Stderr, "Parent process %d has exited, shutting down.\n", parentPID)
93+
stop()
94+
return
95+
}
96+
}
97+
}
98+
}()
99+
}

internal/lsp/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type ServerOptions struct {
4545
ParseCache *project.ParseCache
4646
NpmInstall func(cwd string, args []string) ([]byte, error)
4747
ProgressDelay time.Duration // delay before showing progress UI; 0 means no delay
48+
SetParentProcessID func(parentPID int)
4849
}
4950

5051
func NewServer(opts *ServerOptions) *Server {
@@ -66,6 +67,7 @@ func NewServer(opts *ServerOptions) *Server {
6667
typingsLocation: opts.TypingsLocation,
6768
parseCache: opts.ParseCache,
6869
npmInstall: opts.NpmInstall,
70+
startWatchdog: opts.SetParentProcessID,
6971
initComplete: make(chan struct{}),
7072
progressDelay: opts.ProgressDelay,
7173
}
@@ -204,6 +206,8 @@ type Server struct {
204206

205207
progressDelay time.Duration
206208
projectProgress *projectLoadingProgress
209+
210+
startWatchdog func(parentPID int)
207211
}
208212

209213
func (s *Server) Session() *project.Session { return s.session }
@@ -1011,6 +1015,10 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ
10111015
s.logger.SetVerbose(true)
10121016
}
10131017

1018+
if s.startWatchdog != nil && params.ProcessId.Integer != nil {
1019+
s.startWatchdog(int(*params.ProcessId.Integer))
1020+
}
1021+
10141022
response := &lsproto.InitializeResult{
10151023
ServerInfo: &lsproto.ServerInfo{
10161024
Name: "typescript-go",

0 commit comments

Comments
 (0)