Skip to content

Exit LSP process if parent process stops existing#2478

Merged
jakebailey merged 15 commits into
mainfrom
jabaile/watchdog
May 7, 2026
Merged

Exit LSP process if parent process stops existing#2478
jakebailey merged 15 commits into
mainfrom
jabaile/watchdog

Conversation

@jakebailey
Copy link
Copy Markdown
Member

The LSP spec says to exit if the parent process exits.

Add a callback to the server code to set the parent process ID, then periodically check to see if we should exit.

Might help with some of the "tsgo is staying open", but not so much in cases where "restart" is used. But, good to do what the spec says.

Copilot AI review requested due to automatic review settings January 12, 2026 19:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the LSP specification requirement to exit the language server process when the parent process (typically the editor) terminates. This helps prevent orphaned tsgo processes when editors crash or are killed unexpectedly.

Changes:

  • Added a callback mechanism to set the parent process ID during server initialization
  • Implemented a watchdog goroutine that periodically checks if the parent process is still alive
  • Integrated platform-specific process checking logic for Unix and Windows systems

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
internal/lsp/server.go Added SetParentProcessId callback field to ServerOptions and Server struct; invokes the callback during handleInitialize when processId is provided
cmd/tsgo/lsp.go Implemented parent process watchdog with periodic polling and platform-specific process existence checking

Comment thread cmd/tsgo/lsp.go Outdated
Comment thread cmd/tsgo/lsp.go Outdated
@DanielRosenwasser
Copy link
Copy Markdown
Member

If the parent process no longer exists, won't we get an EOF on standard input or something?

@jakebailey
Copy link
Copy Markdown
Member Author

Yeah, I guess that's probably the case...

@DanielRosenwasser
Copy link
Copy Markdown
Member

DanielRosenwasser commented Jan 13, 2026

#2499 might explain why an EOF was not sufficient for shutting down.

Comment thread cmd/tsgo/lsp.go Outdated
return
}
go func() {
tick := time.Tick(5 * time.Second)
Copy link
Copy Markdown
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.

RyanCavanaugh
RyanCavanaugh previously approved these changes Apr 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread cmd/tsgo/lsp.go Outdated
Comment thread cmd/tsgo/lsp.go Outdated
Comment thread internal/lsp/server.go
Comment on lines +1000 to +1002
if s.startWatchdog != nil && params.ProcessId.Integer != nil {
s.startWatchdog(int(*params.ProcessId.Integer))
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This introduces new behavior during initialize (invoking a parent-process callback). There’s existing Go test coverage for the LSP server package, but this path isn’t exercised; please add a small test that handleInitialize calls the callback when processId is provided and does not when it’s null/missing.

Copilot generated this review using guidance from repository custom instructions.
@jakebailey
Copy link
Copy Markdown
Member Author

Going to draft this for the time being again, sorry; I want to double check that these comments are actually correct and would rather not cram it in.

@jakebailey jakebailey marked this pull request as draft April 17, 2026 17:21
@andrewbranch
Copy link
Copy Markdown
Member

I'm not against this if we think we still need it, but it feels like a bug (like #2499 was) if we can't exit just by reading from a STDIN that's no longer connected to anything

@jakebailey
Copy link
Copy Markdown
Member Author

Technically speaking, it's not invalid for a client to (for example) set up some sort of FIFO and then hook that up as the file descriptor for the child process, in which case that FD might never close.

@andrewbranch
Copy link
Copy Markdown
Member

I ended last week with 7 orphaned tsgo processes, so I guess we do still need this.

@DanielRosenwasser
Copy link
Copy Markdown
Member

it feels like a bug

Yeah, it sure does, and I feel iffy about a fix like this without knowing what is actually going wrong. As far as I've seen this behavior isn't present with Strada + the VS Code built-in, and it's not an issue with other language servers.

It certainly could be something with the latest language client, in which case we would want to at least fix it there.


it's not invalid for a client to (for example) set up some sort of FIFO and then hook that up as the file descriptor for the child process, in which case that FD might never close.

Relatedly, we send events periodically about the currently open project, right? Maybe there are no files/projects open, but I think that sending one event to a closed file descriptor would ordinarily be enough to bring the process down.

@jakebailey
Copy link
Copy Markdown
Member Author

Yeah, it sure does, and I feel iffy about a fix like this without knowing what is actually going wrong.

If the client has died without closing the child, I don't think that's our bug. We know at that point that we aren't talking to anyone anymore and should exit.

If the client hasn't died and we have many tsgo processes, then well, that's a client bug and this PR won't affect it.

So, I'm not entirely certain what the concern is.

@jakebailey jakebailey marked this pull request as ready for review May 4, 2026 20:20
@jakebailey jakebailey requested a review from Copilot May 4, 2026 20:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread cmd/tsgo/isprocessalive_other.go Outdated
Comment thread cmd/tsgo/lsp.go Outdated
Comment thread cmd/tsgo/lsp.go Outdated
Comment thread internal/lsp/server.go Outdated
jakebailey added 2 commits May 4, 2026 13:42
- Rename SetParentProcessId to SetParentProcessID (Go initialism convention)
- Use unix build tag instead of !windows for isprocessalive
- Use time.NewTicker with defer Stop() instead of time.Tick
- Call stop() to cancel context for graceful shutdown instead of os.Exit(1)
- Rename file to _unix.go to match build tag
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 7.0 RC milestone May 7, 2026
Comment thread cmd/tsgo/isprocessalive_unix.go
@jakebailey jakebailey requested a review from RyanCavanaugh May 7, 2026 23:15
@jakebailey jakebailey enabled auto-merge May 7, 2026 23:21
@jakebailey jakebailey added this pull request to the merge queue May 7, 2026
Merged via the queue into main with commit 51a1064 May 7, 2026
21 checks passed
@jakebailey jakebailey deleted the jabaile/watchdog branch May 7, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants