Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions cmd/tsgo/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/signal"
"runtime"
"syscall"
"time"

"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/core"
Expand Down Expand Up @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a random amount of time. I don't know what the right number is.

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
}
9 changes: 8 additions & 1 deletion internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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",
Expand Down