TUN-1451: Make non-interactive, non-service execution possible on Windows

This commit is contained in:
Nick Vollmar 2019-03-04 19:49:57 -06:00
parent abdbc76a46
commit fea3569956
1 changed files with 48 additions and 27 deletions

View File

@ -8,6 +8,7 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"syscall"
"time" "time"
"unsafe" "unsafe"
@ -30,6 +31,10 @@ const (
// not defined in golang.org/x/sys/windows package // not defined in golang.org/x/sys/windows package
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx
serviceConfigFailureActionsFlag = 4 serviceConfigFailureActionsFlag = 4
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
serviceControllerConnectionFailure = 1063
) )
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) { func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
@ -50,16 +55,48 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
}, },
}) })
// `IsAnInteractiveSession()` isn't exactly equivalent to "should the
// process run as a normal EXE?" There are legitimate non-service cases,
// like running cloudflared in a GCP startup script, for which
// `IsAnInteractiveSession()` returns false. For more context, see:
// https://github.com/judwhite/go-svc/issues/6
// It seems that the "correct way" to check "is this a normal EXE?" is:
// 1. attempt to connect to the Service Control Manager
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
// This involves actually trying to start the service.
isIntSess, err := svc.IsAnInteractiveSession() isIntSess, err := svc.IsAnInteractiveSession()
if err != nil { if err != nil {
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err) logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
} }
if isIntSess { if isIntSess {
app.Run(os.Args) app.Run(os.Args)
return return
} }
// Run executes service name by calling windowsService which is a Handler
// interface that implements Execute method.
// It will set service status to stop after Execute returns
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
if err != nil {
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
// Run the app in "interactive" mode anyway.
app.Run(os.Args)
return
}
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
}
}
type windowsService struct {
app *cli.App
shutdownC chan struct{}
graceShutdownC chan struct{}
}
// called by the package code at the start of the service
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
elog, err := eventlog.Open(windowsServiceName) elog, err := eventlog.Open(windowsServiceName)
if err != nil { if err != nil {
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName) logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
@ -68,26 +105,10 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
defer elog.Close() defer elog.Close()
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName)) elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
// Run executes service name by calling windowsService which is a Handler defer func() {
// 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, graceShutdownC: graceShutdownC})
if err != nil {
elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err))
return
}
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName)) elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
} }()
type windowsService struct {
app *cli.App
elog *eventlog.Log
shutdownC chan struct{}
graceShutdownC chan struct{}
}
// called by the package code at the start of the service
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
// the arguments passed here are only meaningful if they were manually // the arguments passed here are only meaningful if they were manually
// specified by the user, e.g. using the Services console or `sc start`. // specified by the user, e.g. using the Services console or `sc start`.
// https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point // https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point
@ -99,7 +120,7 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
// fall back to the arguments from ImagePath (or, as sc calls it, binPath) // fall back to the arguments from ImagePath (or, as sc calls it, binPath)
args = os.Args args = os.Args
} }
s.elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args)) elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
statusChan <- svc.Status{State: svc.StartPending} statusChan <- svc.Status{State: svc.StartPending}
@ -114,26 +135,26 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
case c := <-r: case c := <-r:
switch c.Cmd { switch c.Cmd {
case svc.Interrogate: case svc.Interrogate:
s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c)) elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
statusChan <- c.CurrentStatus statusChan <- c.CurrentStatus
case svc.Stop: case svc.Stop:
s.elog.Info(1, "received stop control request") elog.Info(1, "received stop control request")
close(s.graceShutdownC) close(s.graceShutdownC)
statusChan <- svc.Status{State: svc.StopPending} statusChan <- svc.Status{State: svc.StopPending}
case svc.Shutdown: case svc.Shutdown:
s.elog.Info(1, "received shutdown control request") elog.Info(1, "received shutdown control request")
close(s.shutdownC) close(s.shutdownC)
statusChan <- svc.Status{State: svc.StopPending} statusChan <- svc.Status{State: svc.StopPending}
default: default:
s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
} }
case err := <-errC: case err := <-errC:
ssec = true ssec = true
if err != nil { if err != nil {
s.elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err)) elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
errno = 1 errno = 1
} else { } else {
s.elog.Info(1, "cloudflared terminated without error") elog.Info(1, "cloudflared terminated without error")
errno = 0 errno = 0
} }
return return