diff --git a/cmd/ts-multi-plug/ts-multi-plug.go b/cmd/ts-multi-plug/ts-multi-plug.go index c3b6ca0..be107ce 100644 --- a/cmd/ts-multi-plug/ts-multi-plug.go +++ b/cmd/ts-multi-plug/ts-multi-plug.go @@ -18,6 +18,7 @@ import ( "os/signal" "strconv" "strings" + "sync/atomic" "syscall" "time" @@ -25,6 +26,9 @@ import ( "tailscale.com/tsnet" ) +// lcPtr is set after LocalClient() so the signal handler can call Logout. +var lcPtr atomic.Pointer[local.Client] + var ( flagHostname string flagDir string @@ -125,21 +129,28 @@ func main() { slog.Info("command started") } - // handle the exit cases either from signal or the upstream command exiting + // cmd.Wait() runs in its own goroutine so it does not block the signal + // handler below. Evaluated inline in a select send case it stalls until + // the child exits, which would prevent the Logout call from ever firing. go func() { - for { - select { - case cmdExitChan <- cmd.Wait(): - // the upstream command has exited - return - case sig := <-signalChan: - slog.Info("signal received, shutting down...", "sig", sig.String()) - - // this will cause the case above with cmd.Wait() to return - // as well ts.Up() to exit early if it hasn't been fully initialized yet - cancelCtx() + cmdExitChan <- cmd.Wait() + }() + + go func() { + sig := <-signalChan + slog.Info("signal received, shutting down...", "sig", sig.String()) + + if lc := lcPtr.Load(); lc != nil { + logoutCtx, cancelLogout := context.WithTimeout(context.Background(), 10*time.Second) + if err := lc.Logout(logoutCtx); err != nil { + slog.Warn("tsnet logout failed", "error", err) + } else { + slog.Info("tsnet logout complete") } + cancelLogout() } + + cancelCtx() }() ts := &tsnet.Server{ @@ -174,6 +185,7 @@ func main() { cancelCtx() os.Exit(1) } + lcPtr.Store(lc) hostname := strings.TrimSuffix(st.Self.DNSName, ".")