Skip to content

Commit 980829d

Browse files
committed
internal/lsp/lsprpc: add an AutoDialer abstraction
Refactor the lsprpc package to move the logic for 'automatic' server discovery into an AutoDialer abstraction, which both implements the v2 jsonrpc2 Dialer interface, and provides a dialNet method that can be used for the existing v1 APIs. Along the way, simplify the evaluation of remote arguments to eliminate the overly abstract RemoteOption. Change-Id: Ic3def17ccc237007a7eb2cc41a12cf058fca9be3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/332490 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]>
1 parent cb1acef commit 980829d

File tree

7 files changed

+187
-202
lines changed

7 files changed

+187
-202
lines changed

internal/lsp/cmd/serve.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ gopls server flags are:
5656
f.PrintDefaults()
5757
}
5858

59+
func (s *Serve) remoteArgs(network, address string) []string {
60+
args := []string{"serve",
61+
"-listen", fmt.Sprintf(`%s;%s`, network, address),
62+
}
63+
if s.RemoteDebug != "" {
64+
args = append(args, "-debug", s.RemoteDebug)
65+
}
66+
if s.RemoteListenTimeout != 0 {
67+
args = append(args, "-listen.timeout", s.RemoteListenTimeout.String())
68+
}
69+
if s.RemoteLogfile != "" {
70+
args = append(args, "-logfile", s.RemoteLogfile)
71+
}
72+
return args
73+
}
74+
5975
// Run configures a server based on the flags, and then runs it.
6076
// It blocks until the server shuts down.
6177
func (s *Serve) Run(ctx context.Context, args ...string) error {
@@ -77,12 +93,11 @@ func (s *Serve) Run(ctx context.Context, args ...string) error {
7793
}
7894
var ss jsonrpc2.StreamServer
7995
if s.app.Remote != "" {
80-
network, addr := lsprpc.ParseAddr(s.app.Remote)
81-
ss = lsprpc.NewForwarder(network, addr,
82-
lsprpc.RemoteDebugAddress(s.RemoteDebug),
83-
lsprpc.RemoteListenTimeout(s.RemoteListenTimeout),
84-
lsprpc.RemoteLogfile(s.RemoteLogfile),
85-
)
96+
var err error
97+
ss, err = lsprpc.NewForwarder(s.app.Remote, s.remoteArgs)
98+
if err != nil {
99+
return errors.Errorf("creating forwarder: %w", err)
100+
}
86101
} else {
87102
ss = lsprpc.NewStreamServer(cache.New(s.app.options), isDaemon)
88103
}

internal/lsp/lsprpc/autostart_default.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import (
1111
)
1212

1313
var (
14-
startRemote = startRemoteDefault
14+
daemonize = func(*exec.Cmd) {}
1515
autoNetworkAddress = autoNetworkAddressDefault
1616
verifyRemoteOwnership = verifyRemoteOwnershipDefault
1717
)
1818

19-
func startRemoteDefault(goplsPath string, args ...string) error {
20-
cmd := exec.Command(goplsPath, args...)
19+
func runRemote(cmd *exec.Cmd) error {
20+
daemonize(cmd)
2121
if err := cmd.Start(); err != nil {
2222
return errors.Errorf("starting remote gopls: %w", err)
2323
}

internal/lsp/lsprpc/autostart_posix.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,28 @@ import (
1111
"crypto/sha256"
1212
"errors"
1313
"fmt"
14-
exec "golang.org/x/sys/execabs"
1514
"log"
1615
"os"
1716
"os/user"
1817
"path/filepath"
1918
"strconv"
2019
"syscall"
2120

21+
exec "golang.org/x/sys/execabs"
22+
2223
"golang.org/x/xerrors"
2324
)
2425

2526
func init() {
26-
startRemote = startRemotePosix
27+
daemonize = daemonizePosix
2728
autoNetworkAddress = autoNetworkAddressPosix
2829
verifyRemoteOwnership = verifyRemoteOwnershipPosix
2930
}
3031

31-
func startRemotePosix(goplsPath string, args ...string) error {
32-
cmd := exec.Command(goplsPath, args...)
32+
func daemonizePosix(cmd *exec.Cmd) {
3333
cmd.SysProcAttr = &syscall.SysProcAttr{
3434
Setsid: true,
3535
}
36-
if err := cmd.Start(); err != nil {
37-
return xerrors.Errorf("starting remote gopls: %w", err)
38-
}
39-
return nil
4036
}
4137

4238
// autoNetworkAddress resolves an id on the 'auto' pseduo-network to a

internal/lsp/lsprpc/dialer.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package lsprpc
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"io"
11+
"net"
12+
"os"
13+
"time"
14+
15+
exec "golang.org/x/sys/execabs"
16+
"golang.org/x/tools/internal/event"
17+
errors "golang.org/x/xerrors"
18+
)
19+
20+
// AutoNetwork is the pseudo network type used to signal that gopls should use
21+
// automatic discovery to resolve a remote address.
22+
const AutoNetwork = "auto"
23+
24+
// An AutoDialer is a jsonrpc2 dialer that understands the 'auto' network.
25+
type AutoDialer struct {
26+
network, addr string // the 'real' network and address
27+
isAuto bool // whether the server is on the 'auto' network
28+
29+
executable string
30+
argFunc func(network, addr string) []string
31+
}
32+
33+
func NewAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*AutoDialer, error) {
34+
d := AutoDialer{
35+
argFunc: argFunc,
36+
}
37+
d.network, d.addr = ParseAddr(rawAddr)
38+
if d.network == AutoNetwork {
39+
d.isAuto = true
40+
bin, err := os.Executable()
41+
if err != nil {
42+
return nil, errors.Errorf("getting executable: %w", err)
43+
}
44+
d.executable = bin
45+
d.network, d.addr = autoNetworkAddress(bin, d.addr)
46+
}
47+
return &d, nil
48+
}
49+
50+
// Dial implements the jsonrpc2.Dialer interface.
51+
func (d *AutoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
52+
conn, err := d.dialNet(ctx)
53+
return conn, err
54+
}
55+
56+
// TODO(rFindley): remove this once we no longer need to integrate with v1 of
57+
// the jsonrpc2 package.
58+
func (d *AutoDialer) dialNet(ctx context.Context) (net.Conn, error) {
59+
// Attempt to verify that we own the remote. This is imperfect, but if we can
60+
// determine that the remote is owned by a different user, we should fail.
61+
ok, err := verifyRemoteOwnership(d.network, d.addr)
62+
if err != nil {
63+
// If the ownership check itself failed, we fail open but log an error to
64+
// the user.
65+
event.Error(ctx, "unable to check daemon socket owner, failing open", err)
66+
} else if !ok {
67+
// We successfully checked that the socket is not owned by us, we fail
68+
// closed.
69+
return nil, fmt.Errorf("socket %q is owned by a different user", d.addr)
70+
}
71+
const dialTimeout = 1 * time.Second
72+
// Try dialing our remote once, in case it is already running.
73+
netConn, err := net.DialTimeout(d.network, d.addr, dialTimeout)
74+
if err == nil {
75+
return netConn, nil
76+
}
77+
if d.isAuto && d.argFunc != nil {
78+
if d.network == "unix" {
79+
// Sometimes the socketfile isn't properly cleaned up when the server
80+
// shuts down. Since we have already tried and failed to dial this
81+
// address, it should *usually* be safe to remove the socket before
82+
// binding to the address.
83+
// TODO(rfindley): there is probably a race here if multiple server
84+
// instances are simultaneously starting up.
85+
if _, err := os.Stat(d.addr); err == nil {
86+
if err := os.Remove(d.addr); err != nil {
87+
return nil, errors.Errorf("removing remote socket file: %w", err)
88+
}
89+
}
90+
}
91+
args := d.argFunc(d.network, d.addr)
92+
cmd := exec.Command(d.executable, args...)
93+
if err := runRemote(cmd); err != nil {
94+
return nil, err
95+
}
96+
}
97+
98+
const retries = 5
99+
// It can take some time for the newly started server to bind to our address,
100+
// so we retry for a bit.
101+
for retry := 0; retry < retries; retry++ {
102+
startDial := time.Now()
103+
netConn, err = net.DialTimeout(d.network, d.addr, dialTimeout)
104+
if err == nil {
105+
return netConn, nil
106+
}
107+
event.Log(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err))
108+
// In case our failure was a fast-failure, ensure we wait at least
109+
// f.dialTimeout before trying again.
110+
if retry != retries-1 {
111+
time.Sleep(dialTimeout - time.Since(startDial))
112+
}
113+
}
114+
return nil, errors.Errorf("dialing remote: %w", err)
115+
}

0 commit comments

Comments
 (0)