From 7acd1b1fc877954629cebccfebb8f7dc2b76ad2b Mon Sep 17 00:00:00 2001 From: cloudflare-warp-bot Date: Fri, 18 May 2018 21:07:45 +0000 Subject: [PATCH] Release Argo Tunnel Client 2018.5.5 --- cmd/cloudflared/main.go | 28 ++++---- cmd/cloudflared/signal.go | 1 + cmd/cloudflared/signal_test.go | 16 ++++- cmd/cloudflared/update.go | 31 ++++----- cmd/cloudflared/windows_service.go | 108 +++++++++++++++++++++++++---- tunneldns/metrics.go | 2 +- 6 files changed, 138 insertions(+), 48 deletions(-) diff --git a/cmd/cloudflared/main.go b/cmd/cloudflared/main.go index fb12abfc..7e45b9da 100644 --- a/cmd/cloudflared/main.go +++ b/cmd/cloudflared/main.go @@ -428,14 +428,11 @@ func startServer(c *cli.Context, shutdownC chan struct{}) error { // update needs to be after DNS proxy is up to resolve equinox server address if isAutoupdateEnabled(c) { - if initUpdate(&listeners) { - return nil - } logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq")) wg.Add(1) go func(){ defer wg.Done() - autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC) + errC <- autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC) }() } @@ -451,12 +448,14 @@ func startServer(c *cli.Context, shutdownC chan struct{}) error { errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger) }() + go notifySystemd(connectedSignal) + if c.IsSet("pidFile") { + go writePidFile(connectedSignal, c.String("pidfile")) + } + // Serve DNS proxy stand-alone if no hostname or tag or app is going to run if dnsProxyStandAlone(c) { - if c.IsSet("pidfile") { - go writePidFile(connectedSignal, c.String("pidfile")) - close(connectedSignal) - } + close(connectedSignal) // no grace period, handle SIGINT/SIGTERM immediately return waitToShutdown(&wg, errC, shutdownC, graceShutdownSignal, 0) } @@ -481,10 +480,6 @@ func startServer(c *cli.Context, shutdownC chan struct{}) error { return err } - if c.IsSet("pidFile") { - go writePidFile(connectedSignal, c.String("pidfile")) - } - wg.Add(1) go func() { defer wg.Done() @@ -521,12 +516,13 @@ func waitToShutdown(wg *sync.WaitGroup, return err } -func writePidFile(waitForSignal chan struct{}, pidFile string) { +func notifySystemd(waitForSignal chan struct{}) { <-waitForSignal daemon.SdNotify(false, "READY=1") - if pidFile == "" { - return - } +} + +func writePidFile(waitForSignal chan struct{}, pidFile string) { + <-waitForSignal file, err := os.Create(pidFile) if err != nil { logger.WithError(err).Errorf("Unable to write pid to %s", pidFile) diff --git a/cmd/cloudflared/signal.go b/cmd/cloudflared/signal.go index 0953e7e9..2fb22ff1 100644 --- a/cmd/cloudflared/signal.go +++ b/cmd/cloudflared/signal.go @@ -48,6 +48,7 @@ func waitForSignalWithGraceShutdown(errC chan error, shutdownC, graceShutdownSig } close(shutdownC) case <-shutdownC: + close(graceShutdownSignal) } return nil diff --git a/cmd/cloudflared/signal_test.go b/cmd/cloudflared/signal_test.go index a56edd32..c0e0d546 100644 --- a/cmd/cloudflared/signal_test.go +++ b/cmd/cloudflared/signal_test.go @@ -22,7 +22,7 @@ func testChannelClosed(t *testing.T, c chan struct{}) { case <-c: return default: - t.Fatal("Channel should be readable") + t.Fatal("Channel should be closed") } } @@ -35,6 +35,7 @@ func TestWaitForSignal(t *testing.T) { errC <- serverErr }() + // received error, shutdownC should be closed err := waitForSignal(errC, shutdownC) assert.Equal(t, serverErr, err) testChannelClosed(t, shutdownC) @@ -72,14 +73,25 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) { errC <- serverErr }() + // received error, both shutdownC and graceshutdownC should be closed err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick) assert.Equal(t, serverErr, err) testChannelClosed(t, shutdownC) testChannelClosed(t, graceshutdownC) + // shutdownC closed, graceshutdownC should also be closed and no error + errC = make(chan error) + shutdownC = make(chan struct{}) + graceshutdownC = make(chan struct{}) + close(shutdownC) + err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick) + assert.NoError(t, err) + testChannelClosed(t, shutdownC) + testChannelClosed(t, graceshutdownC) + + // Test handling SIGTERM & SIGINT for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} { - //var wg sync.WaitGroup errC := make(chan error) shutdownC = make(chan struct{}) graceshutdownC = make(chan struct{}) diff --git a/cmd/cloudflared/update.go b/cmd/cloudflared/update.go index 544c23cb..e38a9d9b 100644 --- a/cmd/cloudflared/update.go +++ b/cmd/cloudflared/update.go @@ -61,29 +61,26 @@ func update(_ *cli.Context) error { return nil } -func initUpdate(listeners *gracenet.Net) bool { - if updateApplied() { - os.Args = append(os.Args, "--is-autoupdated=true") - if _, err := listeners.StartProcess(); err != nil { - logger.WithError(err).Error("Unable to restart server automatically") - return false - } - return true - } - return false -} - -func autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) { +func autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error { + tickC := time.Tick(freq) for { if updateApplied() { os.Args = append(os.Args, "--is-autoupdated=true") - if _, err := listeners.StartProcess(); err != nil { + pid, err := listeners.StartProcess() + if err != nil { logger.WithError(err).Error("Unable to restart server automatically") + return err } - close(shutdownC) - return + // stop old process after autoupdate. Otherwise we create a new process + // after each update + logger.Infof("PID of the new process is %d", pid) + return nil + } + select { + case <-tickC: + case <-shutdownC: + return nil } - time.Sleep(freq) } } diff --git a/cmd/cloudflared/windows_service.go b/cmd/cloudflared/windows_service.go index 726bd9f5..be8b2e12 100644 --- a/cmd/cloudflared/windows_service.go +++ b/cmd/cloudflared/windows_service.go @@ -8,9 +8,12 @@ package main import ( "fmt" "os" + "time" + "unsafe" cli "gopkg.in/urfave/cli.v2" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" @@ -19,6 +22,13 @@ import ( const ( windowsServiceName = "Cloudflared" windowsServiceDescription = "Argo Tunnel agent" + + recoverActionDelay = time.Second * 20 + failureCountResetPeriod = time.Hour * 24 + + // not defined in golang.org/x/sys/windows package + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx + serviceConfigFailureActionsFlag = 4 ) func runApp(app *cli.App, shutdownC chan struct{}) { @@ -58,7 +68,8 @@ func runApp(app *cli.App, shutdownC chan struct{}) { elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName)) // Run executes service name by calling windowsService which is a Handler - // interface that implements Execute method + // interface that implements Execute method. + // It will set service status to stop after Execute returns err = svc.Run(windowsServiceName, &windowsService{app: app, elog: elog, shutdownC: shutdownC}) if err != nil { elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err)) @@ -74,34 +85,45 @@ type windowsService struct { } // called by the package code at the start of the service -func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { +func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown - changes <- svc.Status{State: svc.StartPending} - go s.app.Run(args) + statusChan <- svc.Status{State: svc.StartPending} + errC := make(chan error) + go func() { + errC <- s.app.Run(args) + }() + statusChan <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} -loop: for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c)) - changes <- c.CurrentStatus + statusChan <- c.CurrentStatus case svc.Stop: s.elog.Info(1, "received stop control request") - break loop + close(s.shutdownC) + statusChan <- svc.Status{State: svc.StopPending} case svc.Shutdown: s.elog.Info(1, "received shutdown control request") - break loop + close(s.shutdownC) + statusChan <- svc.Status{State: svc.StopPending} default: s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } + case err := <-errC: + ssec = true + if err != nil { + s.elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err)) + errno = 1 + } else { + s.elog.Info(1, "cloudflared terminated without error") + errno = 0 + } + return } } - close(s.shutdownC) - changes <- svc.Status{State: svc.StopPending} - return } func installWindowsService(c *cli.Context) error { @@ -130,12 +152,18 @@ func installWindowsService(c *cli.Context) error { return err } defer s.Close() + logger.Infof("Argo Tunnel agent service is installed") err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { s.Delete() logger.WithError(err).Errorf("Cannot install event logger") return fmt.Errorf("SetupEventLogSource() failed: %s", err) } + err = configRecoveryOption(s.Handle) + if err != nil { + logger.WithError(err).Errorf("Cannot set service recovery actions") + logger.Infof("See %s to manually configure service recovery actions", serviceUrl) + } return nil } @@ -158,6 +186,7 @@ func uninstallWindowsService(c *cli.Context) error { logger.Errorf("Cannot delete service %s", windowsServiceName) return err } + logger.Infof("Argo Tunnel agent service is uninstalled") err = eventlog.Remove(windowsServiceName) if err != nil { logger.Errorf("Cannot remove event logger") @@ -165,3 +194,58 @@ func uninstallWindowsService(c *cli.Context) error { } return nil } + +// defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx +type scAction int + +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx +const ( + scActionNone scAction = iota + scActionRestart + scActionReboot + scActionRunCommand +) + +// defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685939(v=vs.85).aspx +type serviceFailureActions struct { + // time to wait to reset the failure count to zero if there are no failures in seconds + resetPeriod uint32 + rebootMsg *uint16 + command *uint16 + // If failure count is greater than actionCount, the service controller repeats + // the last action in actions + actionCount uint32 + actions uintptr +} + +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms685937(v=vs.85).aspx +// Not supported in Windows Server 2003 and Windows XP +type serviceFailureActionsFlag struct { + // enableActionsForStopsWithErr is of type BOOL, which is declared as + // typedef int BOOL in C + enableActionsForStopsWithErr int +} + +type recoveryAction struct { + recoveryType uint32 + // The time to wait before performing the specified action, in milliseconds + delay uint32 +} + +// until https://github.com/golang/go/issues/23239 is release, we will need to +// configure through ChangeServiceConfig2 +func configRecoveryOption(handle windows.Handle) error { + actions := []recoveryAction{ + {recoveryType: uint32(scActionRestart), delay: uint32(recoverActionDelay / time.Millisecond)}, + } + serviceRecoveryActions := serviceFailureActions{ + resetPeriod: uint32(failureCountResetPeriod / time.Second), + actionCount: uint32(len(actions)), + actions: uintptr(unsafe.Pointer(&actions[0])), + } + if err := windows.ChangeServiceConfig2(handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&serviceRecoveryActions))); err != nil { + return err + } + serviceFailureActionsFlag := serviceFailureActionsFlag{enableActionsForStopsWithErr: 1} + return windows.ChangeServiceConfig2(handle, serviceConfigFailureActionsFlag, (*byte)(unsafe.Pointer(&serviceFailureActionsFlag))) +} diff --git a/tunneldns/metrics.go b/tunneldns/metrics.go index 5f688186..81ea7bce 100644 --- a/tunneldns/metrics.go +++ b/tunneldns/metrics.go @@ -36,7 +36,7 @@ func (p MetricsPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dn status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r) // Update built-in metrics - vars.Report(state, ".", rcode.ToString(rw.Rcode), rw.Len, rw.Start) + vars.Report(ctx, state, ".", rcode.ToString(rw.Rcode), rw.Len, rw.Start) return status, err }