diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c98eae885..161159fc7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -429,7 +429,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de net/url from crypto/x509+ os from crypto/rand+ os/exec from github.com/coreos/go-iptables/iptables+ - os/signal from tailscale.com/cmd/tailscaled+ + os/signal from tailscale.com/cmd/tailscaled os/user from github.com/godbus/dbus/v5+ path from github.com/godbus/dbus/v5+ path/filepath from crypto/x509+ diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index 2f3296e40..7d8bce2a4 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -20,6 +20,7 @@ package main // import "tailscale.com/cmd/tailscaled" // to C:\ to run it, like tswin does. import ( + "bufio" "context" "encoding/json" "errors" @@ -27,6 +28,10 @@ import ( "log" "net/netip" "os" + "os/exec" + "os/signal" + "sync" + "syscall" "time" "github.com/dblohm7/wingoes/com" @@ -38,6 +43,7 @@ import ( "tailscale.com/ipn/ipnserver" "tailscale.com/ipn/store" "tailscale.com/logpolicy" + "tailscale.com/logtail/backoff" "tailscale.com/net/dns" "tailscale.com/net/tsdial" "tailscale.com/net/tstun" @@ -129,7 +135,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch // writer that logpolicy already installed as the global // output. logger := log.New(log.Default().Writer(), "", 0) - ipnserver.BabysitProc(ctx, args, logger.Printf) + babysitProc(ctx, args, logger.Printf) }() changes <- svc.Status{State: svc.Running, Accepts: svcAccepts} @@ -427,3 +433,126 @@ func windowsUptime() time.Duration { r, _, _ := getTickCount64Proc.Call() return time.Duration(int64(r)) * time.Millisecond } + +// babysitProc runs the current executable as a child process with the +// provided args, capturing its output, writing it to files, and +// restarting the process on any crashes. +func babysitProc(ctx context.Context, args []string, logf logger.Logf) { + + executable, err := os.Executable() + if err != nil { + panic("cannot determine executable: " + err.Error()) + } + + var proc struct { + mu sync.Mutex + p *os.Process + } + + done := make(chan struct{}) + go func() { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) + var sig os.Signal + select { + case sig = <-interrupt: + logf("babysitProc: got signal: %v", sig) + close(done) + case <-ctx.Done(): + logf("babysitProc: context done") + sig = os.Kill + close(done) + } + + proc.mu.Lock() + proc.p.Signal(sig) + proc.mu.Unlock() + }() + + bo := backoff.NewBackoff("babysitProc", logf, 30*time.Second) + + for { + startTime := time.Now() + log.Printf("exec: %#v %v", executable, args) + cmd := exec.Command(executable, args...) + + // Create a pipe object to use as the subproc's stdin. + // When the writer goes away, the reader gets EOF. + // A subproc can watch its stdin and exit when it gets EOF; + // this is a very reliable way to have a subproc die when + // its parent (us) disappears. + // We never need to actually write to wStdin. + rStdin, wStdin, err := os.Pipe() + if err != nil { + log.Printf("os.Pipe 1: %v", err) + return + } + + // Create a pipe object to use as the subproc's stdout/stderr. + // We'll read from this pipe and send it to logf, line by line. + // We can't use os.exec's io.Writer for this because it + // doesn't care about lines, and thus ends up merging multiple + // log lines into one or splitting one line into multiple + // logf() calls. bufio is more appropriate. + rStdout, wStdout, err := os.Pipe() + if err != nil { + log.Printf("os.Pipe 2: %v", err) + } + go func(r *os.File) { + defer r.Close() + rb := bufio.NewReader(r) + for { + s, err := rb.ReadString('\n') + if s != "" { + logf("%s", s) + } + if err != nil { + break + } + } + }(rStdout) + + cmd.Stdin = rStdin + cmd.Stdout = wStdout + cmd.Stderr = wStdout + err = cmd.Start() + + // Now that the subproc is started, get rid of our copy of the + // pipe reader. Bad things happen on Windows if more than one + // process owns the read side of a pipe. + rStdin.Close() + wStdout.Close() + + if err != nil { + log.Printf("starting subprocess failed: %v", err) + } else { + proc.mu.Lock() + proc.p = cmd.Process + proc.mu.Unlock() + + err = cmd.Wait() + log.Printf("subprocess exited: %v", err) + } + + // If the process finishes, clean up the write side of the + // pipe. We'll make a new one when we restart the subproc. + wStdin.Close() + + if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" { + log.Fatalf("Process ended.") + } + + if time.Since(startTime) < 60*time.Second { + bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err)) + } else { + // Reset the timeout, since the process ran for a while. + bo.BackOff(ctx, nil) + } + + select { + case <-done: + return + default: + } + } +} diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 99cf30b9a..00c89eb7f 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -9,18 +9,14 @@ import ( "context" "fmt" "io" - "log" "net" "net/http" "os" - "os/exec" - "os/signal" "os/user" "path/filepath" "runtime" "strings" "sync" - "syscall" "time" "unicode" @@ -455,131 +451,6 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error { } } -// BabysitProc runs the current executable as a child process with the -// provided args, capturing its output, writing it to files, and -// restarting the process on any crashes. -// -// It's only currently (2020-10-29) used on Windows. -func BabysitProc(ctx context.Context, args []string, logf logger.Logf) { - - executable, err := os.Executable() - if err != nil { - panic("cannot determine executable: " + err.Error()) - } - - var proc struct { - mu sync.Mutex - p *os.Process - } - - done := make(chan struct{}) - go func() { - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) - var sig os.Signal - select { - case sig = <-interrupt: - logf("BabysitProc: got signal: %v", sig) - close(done) - case <-ctx.Done(): - logf("BabysitProc: context done") - sig = os.Kill - close(done) - } - - proc.mu.Lock() - proc.p.Signal(sig) - proc.mu.Unlock() - }() - - bo := backoff.NewBackoff("BabysitProc", logf, 30*time.Second) - - for { - startTime := time.Now() - log.Printf("exec: %#v %v", executable, args) - cmd := exec.Command(executable, args...) - - // Create a pipe object to use as the subproc's stdin. - // When the writer goes away, the reader gets EOF. - // A subproc can watch its stdin and exit when it gets EOF; - // this is a very reliable way to have a subproc die when - // its parent (us) disappears. - // We never need to actually write to wStdin. - rStdin, wStdin, err := os.Pipe() - if err != nil { - log.Printf("os.Pipe 1: %v", err) - return - } - - // Create a pipe object to use as the subproc's stdout/stderr. - // We'll read from this pipe and send it to logf, line by line. - // We can't use os.exec's io.Writer for this because it - // doesn't care about lines, and thus ends up merging multiple - // log lines into one or splitting one line into multiple - // logf() calls. bufio is more appropriate. - rStdout, wStdout, err := os.Pipe() - if err != nil { - log.Printf("os.Pipe 2: %v", err) - } - go func(r *os.File) { - defer r.Close() - rb := bufio.NewReader(r) - for { - s, err := rb.ReadString('\n') - if s != "" { - logf("%s", s) - } - if err != nil { - break - } - } - }(rStdout) - - cmd.Stdin = rStdin - cmd.Stdout = wStdout - cmd.Stderr = wStdout - err = cmd.Start() - - // Now that the subproc is started, get rid of our copy of the - // pipe reader. Bad things happen on Windows if more than one - // process owns the read side of a pipe. - rStdin.Close() - wStdout.Close() - - if err != nil { - log.Printf("starting subprocess failed: %v", err) - } else { - proc.mu.Lock() - proc.p = cmd.Process - proc.mu.Unlock() - - err = cmd.Wait() - log.Printf("subprocess exited: %v", err) - } - - // If the process finishes, clean up the write side of the - // pipe. We'll make a new one when we restart the subproc. - wStdin.Close() - - if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" { - log.Fatalf("Process ended.") - } - - if time.Since(startTime) < 60*time.Second { - bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err)) - } else { - // Reset the timeout, since the process ran for a while. - bo.BackOff(ctx, nil) - } - - select { - case <-done: - return - default: - } - } -} - // getEngineUntilItWorksWrapper returns a getEngine wrapper that does // not call getEngine concurrently and stops calling getEngine once // it's returned a working engine.