all repos

clerk @ 0ff087c

missing tooling for ledger/hledger

clerk/internal/lsp/lifecycle.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: comply with lsp3.18 lifecycle, 29 days ago
1
package lsp
2
3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"runtime"
8
	"syscall"
9
	"time"
10
11
	"go.lsp.dev/jsonrpc2"
12
	"go.lsp.dev/protocol"
13
)
14
15
var (
16
	errServerNotInitialized = jsonrpc2.NewError(jsonrpc2.ServerNotInitialized, "server not initialized")
17
	errInitializeOnce       = jsonrpc2.NewError(jsonrpc2.InvalidRequest, "initialize may only be sent once")
18
)
19
20
type ExitError struct{ Code int }
21
22
func (e *ExitError) Error() string {
23
	return fmt.Sprintf("lsp exit code %d", e.Code)
24
}
25
26
type serverState int
27
28
const (
29
	stateNew                 serverState = iota // before the initialize request has been answered
30
	stateInitialized                            // an InitializeResult was sent
31
	stateShutdownRequested                      // shutdown was received
32
	stateExited                                 // exit arrived without a prior shutdown
33
	stateExitedAfterShutdown                    // exit arrived after shutdown
34
)
35
36
func (st serverState) ExitCode() int {
37
	switch st {
38
	case stateExitedAfterShutdown:
39
		return 0
40
	default:
41
		return 1
42
	}
43
}
44
45
// lifecycle gates the connection on the initialize request: before it, only
46
// initialize and exit are served; everything else answers ServerNotInitialized
47
// and notifications are dropped. Every later message is served — like gopls,
48
// the server trusts its client, so shutdown and exit leave subsequent requests
49
// to fail naturally. The handlers themselves enforce initialize-once and the
50
// exit code.
51
func (s *server) lifecycle(next jsonrpc2.Handler) jsonrpc2.Handler {
52
	return func(ctx context.Context, req *jsonrpc2.Request) (any, error) {
53
		s.stateMu.Lock()
54
		st := s.state
55
		s.stateMu.Unlock()
56
		if st != stateNew || req.Method() == protocol.MethodInitialize || req.Method() == protocol.MethodExit {
57
			return next(ctx, req)
58
		}
59
		return rejectCall(errServerNotInitialized, req)
60
	}
61
}
62
63
// rejectCall answers a call with err and drops a notification.
64
func rejectCall(err error, req *jsonrpc2.Request) (any, error) {
65
	if req.IsCall() {
66
		return nil, err
67
	}
68
	return nil, nil // notification dropped
69
}
70
71
// parentPollInterval is how often watchParent probes the parent process.
72
const parentPollInterval = 2 * time.Second
73
74
// watchParent exits the server when the parent process named in the initialize
75
// params dies, as the spec's processId semantics require. Liveness is probed
76
// with signal 0, which is unix-only; elsewhere the stream EOF that follows a
77
// parent death covers it. The first probe runs immediately, so a parent that
78
// is already dead is detected at once. Exit runs the same lifecycle path as
79
// the exit notification, so the exit code follows the shutdown phase.
80
func (s *server) watchParent(pid int32) {
81
	if pid <= 0 || runtime.GOOS == "windows" || runtime.GOOS == "plan9" {
82
		return
83
	}
84
	go func() {
85
		t := time.NewTicker(parentPollInterval)
86
		defer t.Stop()
87
		for {
88
			// Only ESRCH proves death; nil (alive) and other errors leave it open.
89
			if err := syscall.Kill(int(pid), 0); err == nil || !errors.Is(err, syscall.ESRCH) {
90
				<-t.C
91
				continue
92
			}
93
			s.Exit(context.Background())
94
			return
95
		}
96
	}()
97
}