mirror of https://github.com/golang/go.git
115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
// Copyright 2021 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package lsprpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
exec "golang.org/x/sys/execabs"
|
|
"golang.org/x/tools/internal/event"
|
|
)
|
|
|
|
// AutoNetwork is the pseudo network type used to signal that gopls should use
|
|
// automatic discovery to resolve a remote address.
|
|
const AutoNetwork = "auto"
|
|
|
|
// An AutoDialer is a jsonrpc2 dialer that understands the 'auto' network.
|
|
type AutoDialer struct {
|
|
network, addr string // the 'real' network and address
|
|
isAuto bool // whether the server is on the 'auto' network
|
|
|
|
executable string
|
|
argFunc func(network, addr string) []string
|
|
}
|
|
|
|
func NewAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*AutoDialer, error) {
|
|
d := AutoDialer{
|
|
argFunc: argFunc,
|
|
}
|
|
d.network, d.addr = ParseAddr(rawAddr)
|
|
if d.network == AutoNetwork {
|
|
d.isAuto = true
|
|
bin, err := os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting executable: %w", err)
|
|
}
|
|
d.executable = bin
|
|
d.network, d.addr = autoNetworkAddress(bin, d.addr)
|
|
}
|
|
return &d, nil
|
|
}
|
|
|
|
// Dial implements the jsonrpc2.Dialer interface.
|
|
func (d *AutoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
|
|
conn, err := d.dialNet(ctx)
|
|
return conn, err
|
|
}
|
|
|
|
// TODO(rFindley): remove this once we no longer need to integrate with v1 of
|
|
// the jsonrpc2 package.
|
|
func (d *AutoDialer) dialNet(ctx context.Context) (net.Conn, error) {
|
|
// Attempt to verify that we own the remote. This is imperfect, but if we can
|
|
// determine that the remote is owned by a different user, we should fail.
|
|
ok, err := verifyRemoteOwnership(d.network, d.addr)
|
|
if err != nil {
|
|
// If the ownership check itself failed, we fail open but log an error to
|
|
// the user.
|
|
event.Error(ctx, "unable to check daemon socket owner, failing open", err)
|
|
} else if !ok {
|
|
// We successfully checked that the socket is not owned by us, we fail
|
|
// closed.
|
|
return nil, fmt.Errorf("socket %q is owned by a different user", d.addr)
|
|
}
|
|
const dialTimeout = 1 * time.Second
|
|
// Try dialing our remote once, in case it is already running.
|
|
netConn, err := net.DialTimeout(d.network, d.addr, dialTimeout)
|
|
if err == nil {
|
|
return netConn, nil
|
|
}
|
|
if d.isAuto && d.argFunc != nil {
|
|
if d.network == "unix" {
|
|
// Sometimes the socketfile isn't properly cleaned up when the server
|
|
// shuts down. Since we have already tried and failed to dial this
|
|
// address, it should *usually* be safe to remove the socket before
|
|
// binding to the address.
|
|
// TODO(rfindley): there is probably a race here if multiple server
|
|
// instances are simultaneously starting up.
|
|
if _, err := os.Stat(d.addr); err == nil {
|
|
if err := os.Remove(d.addr); err != nil {
|
|
return nil, fmt.Errorf("removing remote socket file: %w", err)
|
|
}
|
|
}
|
|
}
|
|
args := d.argFunc(d.network, d.addr)
|
|
cmd := exec.Command(d.executable, args...)
|
|
if err := runRemote(cmd); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
const retries = 5
|
|
// It can take some time for the newly started server to bind to our address,
|
|
// so we retry for a bit.
|
|
for retry := 0; retry < retries; retry++ {
|
|
startDial := time.Now()
|
|
netConn, err = net.DialTimeout(d.network, d.addr, dialTimeout)
|
|
if err == nil {
|
|
return netConn, nil
|
|
}
|
|
event.Log(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err))
|
|
// In case our failure was a fast-failure, ensure we wait at least
|
|
// f.dialTimeout before trying again.
|
|
if retry != retries-1 {
|
|
time.Sleep(dialTimeout - time.Since(startDial))
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("dialing remote: %w", err)
|
|
}
|