From 83341340b5cb51fc5b6f6a7451bfb81df8389ce5 Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Wed, 19 Jun 2019 13:40:49 -0700 Subject: [PATCH 01/15] replace NSSM with x/sys/windows/svc --- README.md | 18 +- helper_test.go | 3 +- main.go | 139 ++++++------- main_test.go | 3 +- multiuser_windows.go | 189 ++++++++---------- multiuser_windows_service.go | 197 +++++++++++++++++++ simple_docker.go | 4 + usage.go | 6 +- usage_posix.go | 10 +- usage_windows.go | 31 ++- worker_types/ami-test-win2012r2/userdata | 5 +- worker_types/ami-test-win7sp1/userdata | 5 +- worker_types/deepspeech-win-gpu-b/userdata | 5 +- worker_types/deepspeech-win-gpu/userdata | 5 +- worker_types/deepspeech-win/userdata | 5 +- worker_types/gecko-1-b-win2012-beta/userdata | 8 +- worker_types/gecko-1-b-win2012/userdata | 8 +- worker_types/gecko-2-b-win2012/userdata | 8 +- worker_types/gecko-3-b-win2012/userdata | 8 +- worker_types/gecko-t-win10-64-beta/userdata | 8 +- worker_types/gecko-t-win10-64-cu/userdata | 8 +- worker_types/gecko-t-win10-64-gpu-b/userdata | 8 +- worker_types/gecko-t-win10-64-gpu/userdata | 8 +- worker_types/gecko-t-win10-64/userdata | 8 +- worker_types/gecko-t-win7-32-beta/userdata | 8 +- worker_types/gecko-t-win7-32-cu/userdata | 8 +- worker_types/gecko-t-win7-32-gpu-b/userdata | 8 +- worker_types/gecko-t-win7-32-gpu/userdata | 8 +- worker_types/gecko-t-win7-32/userdata | 8 +- worker_types/nss-win2012r2-new/userdata | 5 +- worker_types/nss-win2012r2/userdata | 5 +- worker_types/win2012r2-cu/userdata | 5 +- worker_types/win2012r2/userdata | 5 +- worker_types/win2016/userdata | 5 +- 34 files changed, 434 insertions(+), 328 deletions(-) create mode 100644 multiuser_windows_service.go diff --git a/README.md b/README.md index b0664582..c6633a54 100644 --- a/README.md +++ b/README.md @@ -112,28 +112,25 @@ with automatic quarantining of workers, waiting for custom events, etc). and the public key will be written to standard out. Keep a copy of the public key if you wish to validate artifact signatures. -3. Download NSSM 2.24 from https://nssm.cc/release/nssm-2.24.zip and extract it - under `C:\`. - -4. Install generic-worker as a Windows service running under the `LocalSystem` +3. Install generic-worker as a Windows service running under the `LocalSystem` account, by running the following command as an `Administrator`: * `generic-worker.exe install service` (see `generic-worker.exe --help` to apply non-default configuration settings) -5. Download livelog from https://github.com/taskcluster/livelog/releases and +4. Download livelog from https://github.com/taskcluster/livelog/releases and place it in `C:\generic-worker\livelog.exe`. -6. Download taskcluster proxy from +5. Download taskcluster proxy from https://github.com/taskcluster/taskcluster-proxy/releases and place it in `C:\generic-worker\taskcluster-proxy.exe`. -7. Create `C:\generic-worker\generic-worker.config` with appopriate values. +6. Create `C:\generic-worker\generic-worker.config` with appopriate values. -8. Edit file `C:\generic-worker\generic-worker.config` with appropriate +7. Edit file `C:\generic-worker\generic-worker.config` with appropriate settings (see `generic-worker.exe --help` for information). -9. Reboot the machine, and the worker should be running. Check logs under +8. Reboot the machine, and the worker should be running. Check logs under `C:\generic-worker\generic-worker.log`. @@ -351,6 +348,9 @@ and reports back results to the queue. Usage: generic-worker run [--config CONFIG-FILE] [--configure-for-aws | --configure-for-gcp] + generic-worker install service [--service-name SERVICE-NAME] + [--config CONFIG-FILE] + [--configure-for-aws | --configure-for-gcp] generic-worker show-payload-schema generic-worker new-ed25519-keypair --file ED25519-PRIVATE-KEY-FILE generic-worker --help diff --git a/helper_test.go b/helper_test.go index a3ff4c5e..d3655ba6 100644 --- a/helper_test.go +++ b/helper_test.go @@ -221,7 +221,8 @@ func execute(t *testing.T, expectedExitCode ExitCode) { if err != nil { t.Fatalf("Test setup failure - could not write to tasks-resolved-count.txt file: %v", err) } - exitCode := RunWorker() + interruptChan := make(chan os.Signal, 1) + exitCode := RunWorker(interruptChan) if exitCode != expectedExitCode { t.Fatalf("Something went wrong executing worker - got exit code %v but was expecting exit code %v", exitCode, expectedExitCode) diff --git a/main.go b/main.go index 762c4daf..0a1d5666 100644 --- a/main.go +++ b/main.go @@ -99,79 +99,24 @@ func main() { if revision != "" { versionName += " [ revision: https://github.com/taskcluster/generic-worker/commits/" + revision + " ]" } + arguments, err := docopt.Parse(usage(versionName), nil, true, versionName, false, true) if err != nil { log.Println("Error parsing command line arguments!") panic(err) } + log.Printf("%#v", arguments) + switch { case arguments["show-payload-schema"]: fmt.Println(taskPayloadSchema()) - case arguments["run"]: - configureForAWS = arguments["--configure-for-aws"].(bool) - configureForGCP = arguments["--configure-for-gcp"].(bool) - configFile = arguments["--config"].(string) - config, err = loadConfig(configFile, configureForAWS, configureForGCP) - - // We need to persist the generic-worker config file if we fetched it - // over the network, for example if the config is fetched from the AWS - // Provisioner (--configure-for-aws) or from the Google Cloud service - // (--configure-for-gcp). We delete taskcluster credentials from the - // AWS provisioner as soon as we've fetched them, so unless we persist - // the config on the first run, the worker will not work after reboots. - // - // We persist the config _before_ checking for an error from the - // loadConfig function call, so that if there was an error, we can see - // what the processed config looked like before the error occurred. - // - // Note, we only persist the config file if the file doesn't already - // exist. We don't want to overwrite an existing user-provided config. - // The full config is logged (with secrets obfuscated) in the server - // logs, so this should provide a reliable way to inspect what config - // was in the case of an unexpected failure, including default values - // for config settings not provided in the user-supplied config file. - if _, statError := os.Stat(configFile); os.IsNotExist(statError) && config != nil { - err = config.Persist(configFile) - exitOnError(CANT_SAVE_CONFIG, err, "Not able to persist config file %v", configFile) - } - exitOnError(CANT_LOAD_CONFIG, err, "Error loading configuration file %v", configFile) - - // Config known to be loaded successfully at this point... - - // * If running tasks as dedicated OS users, we should take ownership - // of generic-worker config file, and block access to task users, so - // that tasks can't read from or write to it. - // * If running tasks under the same user account as the generic-worker - // process, then we can't avoid that tasks can read the config file, - // we can just hope that the config file is at least not writable by - // the current user. In this case we won't change file permissions. - secureConfigFile() - - exitCode := RunWorker() + handleConfig(arguments) + interruptChan := make(chan os.Signal, 1) + exitCode := RunWorker(interruptChan) log.Printf("Exiting worker with exit code %v", exitCode) - switch exitCode { - case REBOOT_REQUIRED: - if !config.DisableReboots { - immediateReboot() - } - case IDLE_TIMEOUT: - if config.ShutdownMachineOnIdle { - immediateShutdown("generic-worker idle timeout") - } - case INTERNAL_ERROR: - if config.ShutdownMachineOnInternalError { - immediateShutdown("generic-worker internal error") - } - case NONCURRENT_DEPLOYMENT_ID: - immediateShutdown("generic-worker deploymentId is not latest") - } - os.Exit(int(exitCode)) - case arguments["install"]: - // platform specific... - err := install(arguments) - exitOnError(CANT_INSTALL_GENERIC_WORKER, err, "Error installing generic worker") + handleExitCode(exitCode) case arguments["new-ed25519-keypair"]: err := generateEd25519Keypair(arguments["--file"].(string)) exitOnError(CANT_CREATE_ED25519_KEYPAIR, err, "Error generating ed25519 keypair %v for worker", arguments["--file"].(string)) @@ -181,6 +126,68 @@ func main() { } } +func handleConfig(arguments map[string]interface{}) { + configureForAWS = arguments["--configure-for-aws"].(bool) + configureForGCP = arguments["--configure-for-gcp"].(bool) + configFile = arguments["--config"].(string) + // avoid shadowing + var err error + config, err = loadConfig(configFile, configureForAWS, configureForGCP) + exitOnError(CANT_LOAD_CONFIG, err, "Error loading configuration file %v", configFile) + // We need to persist the generic-worker config file if we fetched it + // over the network, for example if the config is fetched from the AWS + // Provisioner (--configure-for-aws) or from the Google Cloud service + // (--configure-for-gcp). We delete taskcluster credentials from the + // AWS provisioner as soon as we've fetched them, so unless we persist + // the config on the first run, the worker will not work after reboots. + // + // We persist the config _before_ checking for an error from the + // loadConfig function call, so that if there was an error, we can see + // what the processed config looked like before the error occurred. + // + // Note, we only persist the config file if the file doesn't already + // exist. We don't want to overwrite an existing user-provided config. + // The full config is logged (with secrets obfuscated) in the server + // logs, so this should provide a reliable way to inspect what config + // was in the case of an unexpected failure, including default values + // for config settings not provided in the user-supplied config file. + if _, statError := os.Stat(configFile); os.IsNotExist(statError) && config != nil { + err = config.Persist(configFile) + exitOnError(CANT_SAVE_CONFIG, err, "Not able to persist config file %v", configFile) + } + + // Config known to be loaded successfully at this point... + + // * If running tasks as dedicated OS users, we should take ownership + // of generic-worker config file, and block access to task users, so + // that tasks can't read from or write to it. + // * If running tasks under the same user account as the generic-worker + // process, then we can't avoid that tasks can read the config file, + // we can just hope that the config file is at least not writable by + // the current user. In this case we won't change file permissions. + secureConfigFile() +} + +func handleExitCode(exitCode ExitCode) { + switch exitCode { + case REBOOT_REQUIRED: + if !config.DisableReboots { + immediateReboot() + } + case IDLE_TIMEOUT: + if config.ShutdownMachineOnIdle { + immediateShutdown("generic-worker idle timeout") + } + case INTERNAL_ERROR: + if config.ShutdownMachineOnInternalError { + immediateShutdown("generic-worker internal error") + } + case NONCURRENT_DEPLOYMENT_ID: + immediateShutdown("generic-worker deploymentId is not latest") + } + os.Exit(int(exitCode)) +} + func loadConfig(filename string, queryAWSUserData bool, queryGCPMetaData bool) (*gwconfig.Config, error) { // TODO: would be better to have a json schema, and also define defaults in // only one place if possible (defaults also declared in `usage`) @@ -358,7 +365,7 @@ func CwdOrPanic() string { return cwd } -func RunWorker() (exitCode ExitCode) { +func RunWorker(interruptChan chan os.Signal) (exitCode ExitCode) { defer func() { if r := recover(); r != nil { HandleCrash(r) @@ -415,8 +422,7 @@ func RunWorker() (exitCode ExitCode) { // use zero value, to be sure that a check is made before first task runs lastQueriedProvisioner := time.Time{} lastReportedNoTasks := time.Now() - sigInterrupt := make(chan os.Signal, 1) - signal.Notify(sigInterrupt, os.Interrupt) + signal.Notify(interruptChan, os.Interrupt) if RotateTaskEnvironment() { return REBOOT_REQUIRED } @@ -514,7 +520,8 @@ func RunWorker() (exitCode ExitCode) { // since a task could complete in less than that amount of time. select { case <-wait5Seconds.C: - case <-sigInterrupt: + case <-interruptChan: + log.Printf("RunWorker received SIGINT") return WORKER_STOPPED } } diff --git a/main_test.go b/main_test.go index 0d82f72c..df98b4b3 100644 --- a/main_test.go +++ b/main_test.go @@ -31,7 +31,8 @@ func TestIdleWithoutCrash(t *testing.T) { testutil.RequireTaskclusterCredentials(t) start := time.Now() config.IdleTimeoutSecs = 7 - exitCode := RunWorker() + interruptChan := make(chan os.Signal, 1) + exitCode := RunWorker(interruptChan) end := time.Now() if exitCode != IDLE_TIMEOUT { t.Fatalf("Was expecting exit code %v, but got exit code %v", IDLE_TIMEOUT, exitCode) diff --git a/multiuser_windows.go b/multiuser_windows.go index f1e49373..cbda449c 100644 --- a/multiuser_windows.go +++ b/multiuser_windows.go @@ -21,6 +21,7 @@ import ( "github.com/taskcluster/generic-worker/win32" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" + "golang.org/x/sys/windows/svc" ) func (task *TaskRun) formatCommand(index int) string { @@ -235,15 +236,47 @@ func install(arguments map[string]interface{}) (err error) { configFile := convertNilToEmptyString(arguments["--config"]) switch { case arguments["service"]: - nssm := convertNilToEmptyString(arguments["--nssm"]) - serviceName := convertNilToEmptyString(arguments["--service-name"]) + name := convertNilToEmptyString(arguments["--service-name"]) configureForAWS := arguments["--configure-for-aws"].(bool) configureForGCP := arguments["--configure-for-gcp"].(bool) - dir := filepath.Dir(exePath) - return deployService(configFile, nssm, serviceName, exePath, dir, configureForAWS, configureForGCP) + return deployService(configFile, name, exePath, configureForAWS, configureForGCP) } - log.Fatal("Unknown install target - only 'service' is allowed") - return nil + return fmt.Errorf("Unknown install target - only 'service' is allowed") +} + +func remove(arguments map[string]interface{}) error { + switch { + case arguments["service"]: + name := convertNilToEmptyString(arguments["--service-name"]) + return removeService(name) + } + return fmt.Errorf("Unknown remove target - only 'service' is allowed") +} + +func ExePath() (string, error) { + prog := os.Args[0] + p, err := filepath.Abs(prog) + if err != nil { + return "", err + } + fi, err := os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + if filepath.Ext(p) == "" { + p += ".exe" + fi, err = os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + } + return "", err } func makeFileOrDirReadWritableForUser(recurse bool, dir string, user *gwruntime.OSUser) ([]byte, error) { @@ -390,6 +423,37 @@ func rebootBetweenTasks() bool { func platformTargets(arguments map[string]interface{}) ExitCode { switch { + case arguments["install"]: + // platform specific... + err := install(arguments) + if err != nil { + log.Fatalf("failed to install service: %v", err) + return CANT_INSTALL_GENERIC_WORKER + } + case arguments["remove"]: + err := remove(arguments) + if err != nil { + log.Fatalf("failed to remove service: %v", err) + return CANT_REMOVE_GENERIC_WORKER + } + case arguments["run-service"]: + cwd := convertNilToEmptyString(arguments["--working-directory"]) + if cwd != "" { + os.Chdir(cwd) + } + handleConfig(arguments) + // TODO remove! + err := ioutil.WriteFile(`C:\Users\miles\Desktop\generic-worker\handle-config-works`, []byte{}, 0777) + if err != nil { + panic(err) + } + name := convertNilToEmptyString(arguments["--service-name"]) + isIntSess, err := svc.IsAnInteractiveSession() + if err != nil { + log.Fatalf("failed to determine if we are running in an interactive session: %v", err) + } + // debug if interactive session + return runService(name, isIntSess) case arguments["grant-winsta-access"]: sid := arguments["--sid"].(string) err := GrantSIDFullControlOfInteractiveWindowsStationAndDesktop(sid) @@ -398,113 +462,32 @@ func platformTargets(arguments map[string]interface{}) ExitCode { log.Printf("%v", err) return CANT_GRANT_CONTROL_OF_WINSTA_AND_DESKTOP } - return 0 + default: + log.Print("Internal error - no target found to run, yet command line parsing successful") + return INTERNAL_ERROR } - log.Print("Internal error - no target found to run, yet command line parsing successful") - return INTERNAL_ERROR + return 0 } -func CreateRunGenericWorkerBatScript(batScriptFilePath string, configureForAWS bool, configureForGCP bool) error { - runCommand := `.\generic-worker.exe run` - if configureForAWS { - runCommand += ` --configure-for-aws` - } - if configureForGCP { - runCommand += ` --configure-for-gcp` - } - runCommand += ` > .\generic-worker.log 2>&1` - batScriptContents := []byte(strings.Join([]string{ - `:: Run generic-worker`, - ``, - `:: step inside folder containing this script`, - `pushd %~dp0`, - ``, - runCommand, - ``, - `:: Possible exit codes:`, - `:: 0: all tasks completed - only occurs when numberOfTasksToRun > 0`, - `:: 67: rebooting - system reboot has been triggered`, - `:: 68: idle timeout - system shutdown has been triggered if shutdownMachineOnIdle=true`, - `:: 69: internal error - system shutdown has been triggered if shutdownMachineOnInternalError=true`, - `:: 70: deployment ID changed - system shutdown has been triggered`, - ``, - }, "\r\n")) - err := ioutil.WriteFile(batScriptFilePath, batScriptContents, 0755) // note 0755 is mostly ignored on windows +func SetAutoLogin(user *runtime.OSUser) error { + k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`, registry.WRITE) if err != nil { - return fmt.Errorf("Was not able to create file %q due to %s", batScriptFilePath, err) + return fmt.Errorf(`Was not able to create registry key 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' due to %s`, err) } - return nil -} - -// deploys the generic worker as a windows service, running under the windows -// user specified with username/password, such that the generic worker runs -// with the given configuration file configFile. the http://nssm.cc/ executable -// is required to install the service, specified as a file system path. The -// serviceName is the service name given to the newly created service. if the -// service already exists, it is simply updated. -func deployService(configFile, nssm, serviceName, exePath, dir string, configureForAWS bool, configureForGCP bool) error { - targetScript := filepath.Join(filepath.Dir(exePath), "run-generic-worker.bat") - err := CreateRunGenericWorkerBatScript(targetScript, configureForAWS, configureForGCP) + defer k.Close() + err = k.SetDWordValue("AutoAdminLogon", 1) if err != nil { - return err + return fmt.Errorf(`Was not able to set registry entry 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\AutoAdminLogon' to 1 due to %s`, err) } - return runtime.RunCommands( - false, - []string{nssm, "install", serviceName, targetScript}, - []string{nssm, "set", serviceName, "AppDirectory", dir}, - []string{nssm, "set", serviceName, "DisplayName", serviceName}, - []string{nssm, "set", serviceName, "Description", "A taskcluster worker that runs on all mainstream platforms"}, - []string{nssm, "set", serviceName, "Start", "SERVICE_AUTO_START"}, - // By default, NSSM installs as LocalSystem, which we need since we call WTSQueryUserToken. - // So let's not set it. - // []string{nssm, "set", serviceName, "ObjectName", ".\\" + user.Name, user.Password}, - []string{nssm, "set", serviceName, "Type", "SERVICE_WIN32_OWN_PROCESS"}, - []string{nssm, "set", serviceName, "AppPriority", "NORMAL_PRIORITY_CLASS"}, - []string{nssm, "set", serviceName, "AppNoConsole", "1"}, - []string{nssm, "set", serviceName, "AppAffinity", "All"}, - []string{nssm, "set", serviceName, "AppStopMethodSkip", "0"}, - []string{nssm, "set", serviceName, "AppStopMethodConsole", "1500"}, - []string{nssm, "set", serviceName, "AppStopMethodWindow", "1500"}, - []string{nssm, "set", serviceName, "AppStopMethodThreads", "1500"}, - []string{nssm, "set", serviceName, "AppThrottle", "1500"}, - []string{nssm, "set", serviceName, "AppExit", "Default", "Exit"}, - []string{nssm, "set", serviceName, "AppRestartDelay", "0"}, - []string{nssm, "set", serviceName, "AppStdout", filepath.Join(dir, "generic-worker-service.log")}, - []string{nssm, "set", serviceName, "AppStderr", filepath.Join(dir, "generic-worker-service.log")}, - []string{nssm, "set", serviceName, "AppStdoutCreationDisposition", "4"}, - []string{nssm, "set", serviceName, "AppStderrCreationDisposition", "4"}, - []string{nssm, "set", serviceName, "AppRotateFiles", "1"}, - []string{nssm, "set", serviceName, "AppRotateOnline", "1"}, - []string{nssm, "set", serviceName, "AppRotateSeconds", "3600"}, - []string{nssm, "set", serviceName, "AppRotateBytes", "0"}, - ) -} - -func ExePath() (string, error) { - log.Printf("Command args: %#v", os.Args) - prog := os.Args[0] - p, err := filepath.Abs(prog) + err = k.SetStringValue("DefaultUserName", user.Name) if err != nil { - return "", err + return fmt.Errorf(`Was not able to set registry entry 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\DefaultUserName' to %q due to %s`, user.Name, err) } - fi, err := os.Stat(p) - if err == nil { - if !fi.Mode().IsDir() { - return p, nil - } - err = fmt.Errorf("%s is directory", p) - } - if filepath.Ext(p) == "" { - p += ".exe" - fi, err = os.Stat(p) - if err == nil { - if !fi.Mode().IsDir() { - return p, nil - } - err = fmt.Errorf("%s is directory", p) - } + err = k.SetStringValue("DefaultPassword", user.Password) + if err != nil { + return fmt.Errorf(`Was not able to set registry entry 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\DefaultPassword' to %q due to %s`, user.Password, err) } - return "", err + return nil } func GrantSIDFullControlOfInteractiveWindowsStationAndDesktop(sid string) (err error) { diff --git a/multiuser_windows_service.go b/multiuser_windows_service.go new file mode 100644 index 00000000..5d87e6e4 --- /dev/null +++ b/multiuser_windows_service.go @@ -0,0 +1,197 @@ +// +build multiuser + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +var elog debug.Log + +type windowsService struct{} + +// implements Execute for svc.Handler +func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + changes <- svc.Status{State: svc.StartPending} + + // Start worker with interruptChan + interruptChan := make(chan os.Signal, 1) + + go RunWorker(interruptChan) + + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + // Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4 + time.Sleep(100 * time.Millisecond) + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + log.Printf("Shutting down, received %v", c) + interruptChan <- os.Interrupt + break loop + case svc.Pause: + changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} + case svc.Continue: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + default: + log.Printf("Unexpected control request #%d", c) + } + } + } + changes <- svc.Status{State: svc.StopPending} + return false, 0 +} + +func runService(name string, isDebug bool) ExitCode { + var err error + if name == "" { + name = "Generic Worker" + } + + if isDebug { + log.Printf("Debug mode enabled, not using eventlog.") + elog = debug.New(name) + } else { + elog, err = eventlog.Open(name) + if err != nil { + fmt.Printf("Could not open eventlog: %v", err) + return INTERNAL_ERROR + } + } + defer elog.Close() + + elog.Info(1, fmt.Sprintf("Starting service %q", name)) + run := svc.Run + if isDebug { + run = debug.Run + } + err = run(name, &windowsService{}) + if err != nil { + elog.Error(1, fmt.Sprintf("Service %q failed: %v", name, err)) + return INTERNAL_ERROR + } + elog.Info(1, fmt.Sprintf("Stopped service %q", name)) + return 0 +} + +// deploys the generic worker as a windows service named name +// running as the user LocalSystem +// if the service already exists we skip. +func deployService(configFile, name, exePath string, configureForAWS bool, configureForGCP bool) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", name) + } + args := []string{ + "run-service", + } + if configFile != "" { + args = append(args, "--config", configFile) + } + if configureForAWS { + args = append(args, "--configure-for-aws") + } + if configureForGCP { + args = append(args, "--configure-for-gcp") + } + err = installService(name, exePath, args) + if err != nil { + return err + } + log.Printf("Successfully installed service %q.", name) + return nil +} + +func installService(name, exePath string, args []string) error { + config := mgr.Config{ + DisplayName: name, + Description: "A taskcluster worker that runs on all mainstream platforms", + // run as LocalSystem because we call WTSQueryUserToken + ServiceStartName: "LocalSystem", + ServiceType: windows.SERVICE_WIN32_OWN_PROCESS | windows.SERVICE_INTERACTIVE_PROCESS, + StartType: mgr.StartAutomatic, + } + dir := filepath.Dir(exePath) + logPath := filepath.Join(dir, "generic-worker-service.log") + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.CreateService( + name, + exePath, + config, + args..., + ) + if err != nil { + return err + } + defer s.Close() + log.Printf("Created service %q with exePath %q, logPath %q and args %v", + name, exePath, logPath, args) + + // log all events to logfile + err = eventlog.Install( + name, + logPath, + false, + eventlog.Error|eventlog.Warning|eventlog.Info, + ) + if err != nil { + s.Delete() + return fmt.Errorf("SetupEventLogSource() failed: %s", err) + } + + // start service manually in order to fail fast + err = s.Start(args...) + if err != nil { + return fmt.Errorf("Error starting service %q: %v", name, err) + } + return nil +} + +func removeService(name string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("service %s is not installed", name) + } + defer s.Close() + err = s.Delete() + if err != nil { + return err + } + err = eventlog.Remove(name) + if err != nil { + return fmt.Errorf("RemoveEventLogSource() failed: %s", err) + } + log.Printf("Successfully removed service %q.", name) + return nil +} diff --git a/simple_docker.go b/simple_docker.go index 15d1588c..cffd1b14 100644 --- a/simple_docker.go +++ b/simple_docker.go @@ -105,6 +105,10 @@ func install(arguments map[string]interface{}) (err error) { return nil } +func remove(arguments map[string]interface{}) (err error) { + return nil +} + func RenameCrossDevice(oldpath, newpath string) (err error) { // TODO: here we should be able to rename when oldpath and newpath are on // different partitions - for now this will cover 99% of cases. diff --git a/usage.go b/usage.go index c44461d0..1dbf9b20 100644 --- a/usage.go +++ b/usage.go @@ -7,6 +7,7 @@ const ( TASKS_COMPLETE ExitCode = 0 CANT_LOAD_CONFIG ExitCode = 64 CANT_INSTALL_GENERIC_WORKER ExitCode = 65 + CANT_REMOVE_GENERIC_WORKER ExitCode = 66 REBOOT_REQUIRED ExitCode = 67 IDLE_TIMEOUT ExitCode = 68 INTERNAL_ERROR ExitCode = 69 @@ -29,7 +30,8 @@ and reports back results to the queue. Usage: generic-worker run [--config CONFIG-FILE] - [--configure-for-aws | --configure-for-gcp]` + installServiceSummary() + ` + [--configure-for-aws | --configure-for-gcp]` + + installServiceSummary() + removeServiceSummary() + runServiceSummary() + ` generic-worker show-payload-schema generic-worker new-ed25519-keypair --file ED25519-PRIVATE-KEY-FILE` + customTargetsSummary() + ` generic-worker --help @@ -42,7 +44,7 @@ and reports back results to the queue. payload is validated against a json schema baked into the release. This option outputs the json schema used in this version of the generic - worker.` + installService() + ` + worker.` + installServiceDescription() + ` new-ed25519-keypair This will generate a fresh, new ed25519 compliant private/public key pair. The public key will be written to stdout and the private diff --git a/usage_posix.go b/usage_posix.go index b08e7ae3..17afbc8d 100644 --- a/usage_posix.go +++ b/usage_posix.go @@ -6,11 +6,19 @@ func installServiceSummary() string { return "" } +func removeServiceSummary() string { + return "" +} + +func runServiceSummary() string { + return "" +} + func customTargetsSummary() string { return "" } -func installService() string { +func installServiceDescription() string { return "" } diff --git a/usage_windows.go b/usage_windows.go index 831201bf..59d915aa 100644 --- a/usage_windows.go +++ b/usage_windows.go @@ -6,18 +6,30 @@ const ( func installServiceSummary() string { return ` - generic-worker install service [--nssm NSSM-EXE] - [--service-name SERVICE-NAME] + generic-worker install service [--service-name SERVICE-NAME] [--config CONFIG-FILE] [--configure-for-aws | --configure-for-gcp]` } +func removeServiceSummary() string { + return ` + generic-worker remove service [--service-name SERVICE-NAME]` +} + +func runServiceSummary() string { + return ` + generic-worker run-service [--service-name SERVICE-NAME] + [--config CONFIG-FILE] + [--working-directory DIRECTORY] + [--configure-for-aws | --configure-for-gcp]` +} + func customTargetsSummary() string { return ` generic-worker grant-winsta-access --sid SID` } -func installService() string { +func installServiceDescription() string { return ` install service This will install the generic worker as a Windows service running under the Local System @@ -32,6 +44,12 @@ func installService() string { preconditions have been met.` } +func removeServiceDescription() string { + return ` + remove service This will remove the generic worker + Windows service.` +} + func customTargets() string { return ` grant-winsta-access Used internally by generic-worker to grant a @@ -41,11 +59,10 @@ func customTargets() string { func platformCommandLineParameters() string { return ` - --nssm NSSM-EXE The full path to nssm.exe to use for installing - the service. - [default: C:\nssm-2.24\win64\nssm.exe] --service-name SERVICE-NAME The name that the Windows service should be - installed under. [default: Generic Worker]` + installed under. [default: Generic Worker] + --working-directory DIRECTORY The working directory the Generic Worker + service will use. [default: C:\Windows\system32]` } func exitCode74() string { diff --git a/worker_types/ami-test-win2012r2/userdata b/worker_types/ami-test-win2012r2/userdata index 2976ed45..2145f67c 100644 --- a/worker_types/ami-test-win2012r2/userdata +++ b/worker_types/ami-test-win2012r2/userdata @@ -79,9 +79,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # initial clone of mozilla-central # Start-Process "C:\mozilla-build\python\python.exe" -ArgumentList "C:\mozilla-build\python\Scripts\hg clone -u null https://hg.mozilla.org/mozilla-central C:\gecko" -Wait -NoNewWindow -PassThru -RedirectStandardOutput "C:\hg_initial_clone.log" -RedirectStandardError "C:\hg_initial_clone.err" @@ -139,7 +136,7 @@ Copy-Item C:\gopath\bin\livelog.exe C:\generic-worker # install generic-worker Copy-Item C:\gopath\bin\generic-worker.exe C:\generic-worker -# Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +# Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # patch generic worker startup script to rebuild generic worker diff --git a/worker_types/ami-test-win7sp1/userdata b/worker_types/ami-test-win7sp1/userdata index 614e5855..2c6e419f 100644 --- a/worker_types/ami-test-win7sp1/userdata +++ b/worker_types/ami-test-win7sp1/userdata @@ -79,9 +79,6 @@ Start-Process "C:\MozillaBuildSetup.exe" -ArgumentList "/S" -Wait -NoNewWindow - # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -# Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # initial clone of mozilla-central # Start-Process "C:\mozilla-build\python\python.exe" -ArgumentList "C:\mozilla-build\python\Scripts\hg clone -u null https://hg.mozilla.org/mozilla-central C:\gecko" -Wait -NoNewWindow -PassThru -RedirectStandardOutput "C:\hg_initial_clone.log" -RedirectStandardError "C:\hg_initial_clone.err" @@ -139,7 +136,7 @@ Copy-Item C:\gopath\bin\livelog.exe C:\generic-worker # install generic-worker Copy-Item C:\gopath\bin\generic-worker.exe C:\generic-worker -# Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +# Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # patch generic worker startup script to rebuild generic worker diff --git a/worker_types/deepspeech-win-gpu-b/userdata b/worker_types/deepspeech-win-gpu-b/userdata index 584fca30..d5b2b74b 100644 --- a/worker_types/deepspeech-win-gpu-b/userdata +++ b/worker_types/deepspeech-win-gpu-b/userdata @@ -79,9 +79,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -98,7 +95,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central diff --git a/worker_types/deepspeech-win-gpu/userdata b/worker_types/deepspeech-win-gpu/userdata index 855d3c2c..7129d4b8 100644 --- a/worker_types/deepspeech-win-gpu/userdata +++ b/worker_types/deepspeech-win-gpu/userdata @@ -125,9 +125,6 @@ Start-Process "powershell" -ArgumentList "-command `"& {&'Import-Module' Carbon} # Ensure proper PATH setup [Environment]::SetEnvironmentVariable("PATH", $Env:Path + ";C:\tools\msys64\usr\bin;C:\Python36;C:\Program Files\Git\bin", "Machine") -# install nssm, neded for generic-worker -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -144,7 +141,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # download Windows Server 2003 Resource Kit Tools diff --git a/worker_types/deepspeech-win/userdata b/worker_types/deepspeech-win/userdata index 8d4fd228..3e568e87 100644 --- a/worker_types/deepspeech-win/userdata +++ b/worker_types/deepspeech-win/userdata @@ -121,9 +121,6 @@ Start-Process "powershell" -ArgumentList "-command `"& {&'Import-Module' Carbon} # Ensure proper PATH setup [Environment]::SetEnvironmentVariable("PATH", $Env:Path + ";C:\tools\msys64\usr\bin;C:\Python36;C:\Program Files\Git\bin", "Machine") -# install nssm, neded for generic-worker -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -140,7 +137,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # download Windows Server 2003 Resource Kit Tools diff --git a/worker_types/gecko-1-b-win2012-beta/userdata b/worker_types/gecko-1-b-win2012-beta/userdata index a4f2d4fc..f5ad3c83 100644 --- a/worker_types/gecko-1-b-win2012-beta/userdata +++ b/worker_types/gecko-1-b-win2012-beta/userdata @@ -227,14 +227,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # GenericWorkerStateWait $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/run-generic-worker-format-and-reboot.bat", "C:\generic-worker\run-generic-worker.bat") diff --git a/worker_types/gecko-1-b-win2012/userdata b/worker_types/gecko-1-b-win2012/userdata index a4f2d4fc..f5ad3c83 100644 --- a/worker_types/gecko-1-b-win2012/userdata +++ b/worker_types/gecko-1-b-win2012/userdata @@ -227,14 +227,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # GenericWorkerStateWait $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/run-generic-worker-format-and-reboot.bat", "C:\generic-worker\run-generic-worker.bat") diff --git a/worker_types/gecko-2-b-win2012/userdata b/worker_types/gecko-2-b-win2012/userdata index a4f2d4fc..f5ad3c83 100644 --- a/worker_types/gecko-2-b-win2012/userdata +++ b/worker_types/gecko-2-b-win2012/userdata @@ -227,14 +227,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # GenericWorkerStateWait $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/run-generic-worker-format-and-reboot.bat", "C:\generic-worker\run-generic-worker.bat") diff --git a/worker_types/gecko-3-b-win2012/userdata b/worker_types/gecko-3-b-win2012/userdata index 07800ccc..d7e0daf1 100644 --- a/worker_types/gecko-3-b-win2012/userdata +++ b/worker_types/gecko-3-b-win2012/userdata @@ -227,14 +227,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # GenericWorkerStateWait $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/run-generic-worker-format-and-reboot.bat", "C:\generic-worker\run-generic-worker.bat") diff --git a/worker_types/gecko-t-win10-64-beta/userdata b/worker_types/gecko-t-win10-64-beta/userdata index 3f67bd63..272da702 100644 --- a/worker_types/gecko-t-win10-64-beta/userdata +++ b/worker_types/gecko-t-win10-64-beta/userdata @@ -134,14 +134,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win10-64-cu/userdata b/worker_types/gecko-t-win10-64-cu/userdata index 7aabf6da..84f00242 100644 --- a/worker_types/gecko-t-win10-64-cu/userdata +++ b/worker_types/gecko-t-win10-64-cu/userdata @@ -134,14 +134,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win10-64-gpu-b/userdata b/worker_types/gecko-t-win10-64-gpu-b/userdata index fc9a8020..06b3ff11 100644 --- a/worker_types/gecko-t-win10-64-gpu-b/userdata +++ b/worker_types/gecko-t-win10-64-gpu-b/userdata @@ -134,14 +134,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win10-64-gpu/userdata b/worker_types/gecko-t-win10-64-gpu/userdata index 577c9359..107d52fe 100644 --- a/worker_types/gecko-t-win10-64-gpu/userdata +++ b/worker_types/gecko-t-win10-64-gpu/userdata @@ -134,14 +134,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win10-64/userdata b/worker_types/gecko-t-win10-64/userdata index 7aabf6da..84f00242 100644 --- a/worker_types/gecko-t-win10-64/userdata +++ b/worker_types/gecko-t-win10-64/userdata @@ -134,14 +134,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win7-32-beta/userdata b/worker_types/gecko-t-win7-32-beta/userdata index 840feff3..70f841a9 100644 --- a/worker_types/gecko-t-win7-32-beta/userdata +++ b/worker_types/gecko-t-win7-32-beta/userdata @@ -150,14 +150,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win32\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win7-32-cu/userdata b/worker_types/gecko-t-win7-32-cu/userdata index 840feff3..70f841a9 100644 --- a/worker_types/gecko-t-win7-32-cu/userdata +++ b/worker_types/gecko-t-win7-32-cu/userdata @@ -150,14 +150,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win32\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win7-32-gpu-b/userdata b/worker_types/gecko-t-win7-32-gpu-b/userdata index 840feff3..70f841a9 100644 --- a/worker_types/gecko-t-win7-32-gpu-b/userdata +++ b/worker_types/gecko-t-win7-32-gpu-b/userdata @@ -150,14 +150,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win32\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win7-32-gpu/userdata b/worker_types/gecko-t-win7-32-gpu/userdata index 840feff3..70f841a9 100644 --- a/worker_types/gecko-t-win7-32-gpu/userdata +++ b/worker_types/gecko-t-win7-32-gpu/userdata @@ -150,14 +150,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win32\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/gecko-t-win7-32/userdata b/worker_types/gecko-t-win7-32/userdata index cdb1ad2a..de500fa6 100644 --- a/worker_types/gecko-t-win7-32/userdata +++ b/worker_types/gecko-t-win7-32/userdata @@ -150,14 +150,8 @@ New-NetFirewallRule -DisplayName "LiveLog_Get (TCP 60022 Inbound): Allow" -Direc # LiveLog_Put New-NetFirewallRule -DisplayName "LiveLog_Put (TCP 60023 Inbound): Allow" -Direction Inbound -LocalPort 60023 -Protocol TCP -Action Allow -# NSSMDownload -$client.DownloadFile("https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip", "C:\Windows\Temp\NSSMInstall.zip") - -# NSSMInstall: NSSM is required to install Generic Worker as a service. Currently ZipInstall fails, so using 7z instead. -Start-Process "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -oC:\ C:\Windows\Temp\NSSMInstall.zip" -Wait -NoNewWindow - # GenericWorkerInstall -Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --nssm C:\nssm-2.24-103-gdee49fc\win32\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow +Start-Process "C:\generic-worker\generic-worker.exe" -ArgumentList "install service --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow # DisableDesktopInterrupt $client.DownloadFile("https://raw.githubusercontent.com/mozilla-releng/OpenCloudConfig/master/userdata/Configuration/GenericWorker/disable-desktop-interrupt.reg", "C:\generic-worker\disable-desktop-interrupt.reg") diff --git a/worker_types/nss-win2012r2-new/userdata b/worker_types/nss-win2012r2-new/userdata index 37f70abd..a0aa18d6 100644 --- a/worker_types/nss-win2012r2-new/userdata +++ b/worker_types/nss-win2012r2-new/userdata @@ -89,9 +89,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -108,7 +105,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central diff --git a/worker_types/nss-win2012r2/userdata b/worker_types/nss-win2012r2/userdata index 37f70abd..a0aa18d6 100644 --- a/worker_types/nss-win2012r2/userdata +++ b/worker_types/nss-win2012r2/userdata @@ -89,9 +89,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -108,7 +105,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central diff --git a/worker_types/win2012r2-cu/userdata b/worker_types/win2012r2-cu/userdata index 584fca30..d5b2b74b 100644 --- a/worker_types/win2012r2-cu/userdata +++ b/worker_types/win2012r2-cu/userdata @@ -79,9 +79,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -98,7 +95,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central diff --git a/worker_types/win2012r2/userdata b/worker_types/win2012r2/userdata index 584fca30..d5b2b74b 100644 --- a/worker_types/win2012r2/userdata +++ b/worker_types/win2012r2/userdata @@ -79,9 +79,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -98,7 +95,7 @@ $HostsFile_Content = [System.Convert]::FromBase64String($HostsFile_Base64) Set-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value $HostsFile_Content -Encoding Byte # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central diff --git a/worker_types/win2016/userdata b/worker_types/win2016/userdata index cc696bd4..27779702 100644 --- a/worker_types/win2016/userdata +++ b/worker_types/win2016/userdata @@ -79,8 +79,6 @@ Set-Acl "C:\builds" $acl # download tooltool $client.DownloadFile("https://raw.githubusercontent.com/mozilla/release-services/master/src/tooltool/client/tooltool.py", "C:\builds\tooltool.py") -# install nssm -Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm.cc/release/nssm-2.24.zip" # Bizarrely, this sets env TEMP/TMP correctly in default user profile, however, if running a # task as Administrator, including any subprocesses that creates, even if processes under a @@ -111,7 +109,6 @@ Expand-ZIPFile -File "C:\nssm-2.24.zip" -Destination "C:\" -Url "http://www.nssm # # # unmount registry hive # reg unload HKLM\DefaultUser - # download generic-worker md C:\generic-worker $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/download/v15.1.1/generic-worker-multiuser-windows-amd64.exe", "C:\generic-worker\generic-worker.exe") @@ -120,7 +117,7 @@ $client.DownloadFile("https://github.com/taskcluster/generic-worker/releases/dow $client.DownloadFile("https://github.com/taskcluster/livelog/releases/download/v1.1.0/livelog-windows-amd64.exe", "C:\generic-worker\livelog.exe") # install generic-worker -Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --nssm C:\nssm-2.24\win64\nssm.exe --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err +Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install service --configure-for-aws --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # Start-Process C:\generic-worker\generic-worker.exe -ArgumentList "install startup --config C:\generic-worker\generic-worker.config" -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\generic-worker\install.log -RedirectStandardError C:\generic-worker\install.err # initial clone of mozilla-central From 1b5bf2c40fa8023b8b68c2ff70d4b0af362a72c7 Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Fri, 28 Jun 2019 08:10:41 -0700 Subject: [PATCH 02/15] move multiuser_windows_service.go => multiuser_service_windows.go --- multiuser_windows_service.go => multiuser_service_windows.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename multiuser_windows_service.go => multiuser_service_windows.go (97%) diff --git a/multiuser_windows_service.go b/multiuser_service_windows.go similarity index 97% rename from multiuser_windows_service.go rename to multiuser_service_windows.go index 5d87e6e4..8b896322 100644 --- a/multiuser_windows_service.go +++ b/multiuser_service_windows.go @@ -153,7 +153,7 @@ func installService(name, exePath string, args []string) error { log.Printf("Created service %q with exePath %q, logPath %q and args %v", name, exePath, logPath, args) - // log all events to logfile + // log all events to eventlog logfile err = eventlog.Install( name, logPath, @@ -162,7 +162,7 @@ func installService(name, exePath string, args []string) error { ) if err != nil { s.Delete() - return fmt.Errorf("SetupEventLogSource() failed: %s", err) + return fmt.Errorf("Setting up eventlog source failed: %s", err) } // start service manually in order to fail fast From f074efdd011ae464cc684f19ab8e108202f96caa Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Fri, 28 Jun 2019 12:39:55 -0700 Subject: [PATCH 03/15] add windows vendored modules build.cmd: use multiuser build tag keep existing gitattributes directive --- .gitattributes | 1 + Gopkg.lock | 12 +- build.cmd | 2 +- .../integration-cli/fixtures/https/ca.pem | 1 - .../fixtures/https/client-cert.pem | 1 - .../fixtures/https/client-key.pem | 1 - .../fixtures/https/server-cert.pem | 1 - .../fixtures/https/server-key.pem | 1 - .../golang.org/x/sys/windows/svc/debug/log.go | 56 +++ .../x/sys/windows/svc/debug/service.go | 45 +++ vendor/golang.org/x/sys/windows/svc/event.go | 48 +++ .../x/sys/windows/svc/eventlog/install.go | 80 ++++ .../x/sys/windows/svc/eventlog/log.go | 70 ++++ vendor/golang.org/x/sys/windows/svc/go12.c | 24 ++ vendor/golang.org/x/sys/windows/svc/go12.go | 11 + vendor/golang.org/x/sys/windows/svc/go13.go | 31 ++ .../x/sys/windows/svc/mgr/config.go | 145 +++++++ .../golang.org/x/sys/windows/svc/mgr/mgr.go | 162 ++++++++ .../x/sys/windows/svc/mgr/recovery.go | 135 +++++++ .../x/sys/windows/svc/mgr/service.go | 72 ++++ .../golang.org/x/sys/windows/svc/security.go | 62 +++ .../golang.org/x/sys/windows/svc/service.go | 363 ++++++++++++++++++ vendor/golang.org/x/sys/windows/svc/sys_386.s | 69 ++++ .../golang.org/x/sys/windows/svc/sys_amd64.s | 44 +++ vendor/golang.org/x/sys/windows/svc/sys_arm.s | 38 ++ 25 files changed, 1467 insertions(+), 8 deletions(-) delete mode 120000 vendor/github.com/docker/docker/integration-cli/fixtures/https/ca.pem delete mode 120000 vendor/github.com/docker/docker/integration-cli/fixtures/https/client-cert.pem delete mode 120000 vendor/github.com/docker/docker/integration-cli/fixtures/https/client-key.pem delete mode 120000 vendor/github.com/docker/docker/integration-cli/fixtures/https/server-cert.pem delete mode 120000 vendor/github.com/docker/docker/integration-cli/fixtures/https/server-key.pem create mode 100644 vendor/golang.org/x/sys/windows/svc/debug/log.go create mode 100644 vendor/golang.org/x/sys/windows/svc/debug/service.go create mode 100644 vendor/golang.org/x/sys/windows/svc/event.go create mode 100644 vendor/golang.org/x/sys/windows/svc/eventlog/install.go create mode 100644 vendor/golang.org/x/sys/windows/svc/eventlog/log.go create mode 100644 vendor/golang.org/x/sys/windows/svc/go12.c create mode 100644 vendor/golang.org/x/sys/windows/svc/go12.go create mode 100644 vendor/golang.org/x/sys/windows/svc/go13.go create mode 100644 vendor/golang.org/x/sys/windows/svc/mgr/config.go create mode 100644 vendor/golang.org/x/sys/windows/svc/mgr/mgr.go create mode 100644 vendor/golang.org/x/sys/windows/svc/mgr/recovery.go create mode 100644 vendor/golang.org/x/sys/windows/svc/mgr/service.go create mode 100644 vendor/golang.org/x/sys/windows/svc/security.go create mode 100644 vendor/golang.org/x/sys/windows/svc/service.go create mode 100644 vendor/golang.org/x/sys/windows/svc/sys_386.s create mode 100644 vendor/golang.org/x/sys/windows/svc/sys_amd64.s create mode 100644 vendor/golang.org/x/sys/windows/svc/sys_arm.s diff --git a/.gitattributes b/.gitattributes index 2d9e5418..aedcb05a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ /testdata/** eol=lf +* text=auto diff --git a/Gopkg.lock b/Gopkg.lock index 88c7ae56..f1a3643e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -101,7 +101,7 @@ version = "v2.7.1" [[projects]] - digest = "1:e7a14b8ecad894c855d35b978da4284b68174465aea4f3f6dcdcb89dba0df0a1" + digest = "1:9942c9cd3ea91982dcd292853f91093f13cd1d0535a0e7bb3768829d44c08cd2" name = "github.com/docker/docker" packages = [ "api", @@ -517,12 +517,16 @@ [[projects]] branch = "master" - digest = "1:fd2145ecc2ba9d0e8697066c0f60e5d7442942a6be2accfc52d1f3a63de21edc" + digest = "1:c4cb6f2f42141ad29a63d6491a8cc97ced9645e5ace9ef9ed132cd9f6b17e3fd" name = "golang.org/x/sys" packages = [ "unix", "windows", "windows/registry", + "windows/svc", + "windows/svc/debug", + "windows/svc/eventlog", + "windows/svc/mgr", ] pruneopts = "UT" revision = "baf5eb976a8cd65845293cd814ea151018552292" @@ -598,6 +602,10 @@ "golang.org/x/net/context", "golang.org/x/sys/windows", "golang.org/x/sys/windows/registry", + "golang.org/x/sys/windows/svc", + "golang.org/x/sys/windows/svc/debug", + "golang.org/x/sys/windows/svc/eventlog", + "golang.org/x/sys/windows/svc/mgr", "golang.org/x/tools/imports", ] solver-name = "gps-cdcl" diff --git a/build.cmd b/build.cmd index 07064ee5..c8882fa6 100644 --- a/build.cmd +++ b/build.cmd @@ -10,7 +10,7 @@ cd gw-codegen go install -v || exit /b %ERRORLEVEL% cd .. go generate || exit /b %ERRORLEVEL% -go install -v ./... || exit /b %ERRORLEVEL% +go install -v -tags multiuser ./... || exit /b %ERRORLEVEL% :: this counts the number of lines returned by git status :: dump temp file a directory higher, otherwise git status reports the tmp1.txt file! diff --git a/vendor/github.com/docker/docker/integration-cli/fixtures/https/ca.pem b/vendor/github.com/docker/docker/integration-cli/fixtures/https/ca.pem deleted file mode 120000 index 70a3e6ce..00000000 --- a/vendor/github.com/docker/docker/integration-cli/fixtures/https/ca.pem +++ /dev/null @@ -1 +0,0 @@ -../../../integration/testdata/https/ca.pem \ No newline at end of file diff --git a/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-cert.pem b/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-cert.pem deleted file mode 120000 index 45888202..00000000 --- a/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-cert.pem +++ /dev/null @@ -1 +0,0 @@ -../../../integration/testdata/https/client-cert.pem \ No newline at end of file diff --git a/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-key.pem b/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-key.pem deleted file mode 120000 index d5f6bbee..00000000 --- a/vendor/github.com/docker/docker/integration-cli/fixtures/https/client-key.pem +++ /dev/null @@ -1 +0,0 @@ -../../../integration/testdata/https/client-key.pem \ No newline at end of file diff --git a/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-cert.pem b/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-cert.pem deleted file mode 120000 index c1860106..00000000 --- a/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-cert.pem +++ /dev/null @@ -1 +0,0 @@ -../../../integration/testdata/https/server-cert.pem \ No newline at end of file diff --git a/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-key.pem b/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-key.pem deleted file mode 120000 index 48b9c2df..00000000 --- a/vendor/github.com/docker/docker/integration-cli/fixtures/https/server-key.pem +++ /dev/null @@ -1 +0,0 @@ -../../../integration/testdata/https/server-key.pem \ No newline at end of file diff --git a/vendor/golang.org/x/sys/windows/svc/debug/log.go b/vendor/golang.org/x/sys/windows/svc/debug/log.go new file mode 100644 index 00000000..e51ab42a --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/debug/log.go @@ -0,0 +1,56 @@ +// Copyright 2012 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. + +// +build windows + +package debug + +import ( + "os" + "strconv" +) + +// Log interface allows different log implementations to be used. +type Log interface { + Close() error + Info(eid uint32, msg string) error + Warning(eid uint32, msg string) error + Error(eid uint32, msg string) error +} + +// ConsoleLog provides access to the console. +type ConsoleLog struct { + Name string +} + +// New creates new ConsoleLog. +func New(source string) *ConsoleLog { + return &ConsoleLog{Name: source} +} + +// Close closes console log l. +func (l *ConsoleLog) Close() error { + return nil +} + +func (l *ConsoleLog) report(kind string, eid uint32, msg string) error { + s := l.Name + "." + kind + "(" + strconv.Itoa(int(eid)) + "): " + msg + "\n" + _, err := os.Stdout.Write([]byte(s)) + return err +} + +// Info writes an information event msg with event id eid to the console l. +func (l *ConsoleLog) Info(eid uint32, msg string) error { + return l.report("info", eid, msg) +} + +// Warning writes an warning event msg with event id eid to the console l. +func (l *ConsoleLog) Warning(eid uint32, msg string) error { + return l.report("warn", eid, msg) +} + +// Error writes an error event msg with event id eid to the console l. +func (l *ConsoleLog) Error(eid uint32, msg string) error { + return l.report("error", eid, msg) +} diff --git a/vendor/golang.org/x/sys/windows/svc/debug/service.go b/vendor/golang.org/x/sys/windows/svc/debug/service.go new file mode 100644 index 00000000..e621b87a --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/debug/service.go @@ -0,0 +1,45 @@ +// Copyright 2012 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. + +// +build windows + +// Package debug provides facilities to execute svc.Handler on console. +// +package debug + +import ( + "os" + "os/signal" + "syscall" + + "golang.org/x/sys/windows/svc" +) + +// Run executes service name by calling appropriate handler function. +// The process is running on console, unlike real service. Use Ctrl+C to +// send "Stop" command to your service. +func Run(name string, handler svc.Handler) error { + cmds := make(chan svc.ChangeRequest) + changes := make(chan svc.Status) + + sig := make(chan os.Signal) + signal.Notify(sig) + + go func() { + status := svc.Status{State: svc.Stopped} + for { + select { + case <-sig: + cmds <- svc.ChangeRequest{Cmd: svc.Stop, CurrentStatus: status} + case status = <-changes: + } + } + }() + + _, errno := handler.Execute([]string{name}, cmds, changes) + if errno != 0 { + return syscall.Errno(errno) + } + return nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/event.go b/vendor/golang.org/x/sys/windows/svc/event.go new file mode 100644 index 00000000..0508e228 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/event.go @@ -0,0 +1,48 @@ +// Copyright 2012 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. + +// +build windows + +package svc + +import ( + "errors" + + "golang.org/x/sys/windows" +) + +// event represents auto-reset, initially non-signaled Windows event. +// It is used to communicate between go and asm parts of this package. +type event struct { + h windows.Handle +} + +func newEvent() (*event, error) { + h, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return nil, err + } + return &event{h: h}, nil +} + +func (e *event) Close() error { + return windows.CloseHandle(e.h) +} + +func (e *event) Set() error { + return windows.SetEvent(e.h) +} + +func (e *event) Wait() error { + s, err := windows.WaitForSingleObject(e.h, windows.INFINITE) + switch s { + case windows.WAIT_OBJECT_0: + break + case windows.WAIT_FAILED: + return err + default: + return errors.New("unexpected result from WaitForSingleObject") + } + return nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/eventlog/install.go b/vendor/golang.org/x/sys/windows/svc/eventlog/install.go new file mode 100644 index 00000000..c76a3760 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/eventlog/install.go @@ -0,0 +1,80 @@ +// Copyright 2012 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. + +// +build windows + +package eventlog + +import ( + "errors" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +const ( + // Log levels. + Info = windows.EVENTLOG_INFORMATION_TYPE + Warning = windows.EVENTLOG_WARNING_TYPE + Error = windows.EVENTLOG_ERROR_TYPE +) + +const addKeyName = `SYSTEM\CurrentControlSet\Services\EventLog\Application` + +// Install modifies PC registry to allow logging with an event source src. +// It adds all required keys and values to the event log registry key. +// Install uses msgFile as the event message file. If useExpandKey is true, +// the event message file is installed as REG_EXPAND_SZ value, +// otherwise as REG_SZ. Use bitwise of log.Error, log.Warning and +// log.Info to specify events supported by the new event source. +func Install(src, msgFile string, useExpandKey bool, eventsSupported uint32) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.CREATE_SUB_KEY) + if err != nil { + return err + } + defer appkey.Close() + + sk, alreadyExist, err := registry.CreateKey(appkey, src, registry.SET_VALUE) + if err != nil { + return err + } + defer sk.Close() + if alreadyExist { + return errors.New(addKeyName + `\` + src + " registry key already exists") + } + + err = sk.SetDWordValue("CustomSource", 1) + if err != nil { + return err + } + if useExpandKey { + err = sk.SetExpandStringValue("EventMessageFile", msgFile) + } else { + err = sk.SetStringValue("EventMessageFile", msgFile) + } + if err != nil { + return err + } + err = sk.SetDWordValue("TypesSupported", eventsSupported) + if err != nil { + return err + } + return nil +} + +// InstallAsEventCreate is the same as Install, but uses +// %SystemRoot%\System32\EventCreate.exe as the event message file. +func InstallAsEventCreate(src string, eventsSupported uint32) error { + return Install(src, "%SystemRoot%\\System32\\EventCreate.exe", true, eventsSupported) +} + +// Remove deletes all registry elements installed by the correspondent Install. +func Remove(src string) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.SET_VALUE) + if err != nil { + return err + } + defer appkey.Close() + return registry.DeleteKey(appkey, src) +} diff --git a/vendor/golang.org/x/sys/windows/svc/eventlog/log.go b/vendor/golang.org/x/sys/windows/svc/eventlog/log.go new file mode 100644 index 00000000..46e5153d --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/eventlog/log.go @@ -0,0 +1,70 @@ +// Copyright 2012 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. + +// +build windows + +// Package eventlog implements access to Windows event log. +// +package eventlog + +import ( + "errors" + "syscall" + + "golang.org/x/sys/windows" +) + +// Log provides access to the system log. +type Log struct { + Handle windows.Handle +} + +// Open retrieves a handle to the specified event log. +func Open(source string) (*Log, error) { + return OpenRemote("", source) +} + +// OpenRemote does the same as Open, but on different computer host. +func OpenRemote(host, source string) (*Log, error) { + if source == "" { + return nil, errors.New("Specify event log source") + } + var s *uint16 + if host != "" { + s = syscall.StringToUTF16Ptr(host) + } + h, err := windows.RegisterEventSource(s, syscall.StringToUTF16Ptr(source)) + if err != nil { + return nil, err + } + return &Log{Handle: h}, nil +} + +// Close closes event log l. +func (l *Log) Close() error { + return windows.DeregisterEventSource(l.Handle) +} + +func (l *Log) report(etype uint16, eid uint32, msg string) error { + ss := []*uint16{syscall.StringToUTF16Ptr(msg)} + return windows.ReportEvent(l.Handle, etype, 0, eid, 0, 1, 0, &ss[0], nil) +} + +// Info writes an information event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Info(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_INFORMATION_TYPE, eid, msg) +} + +// Warning writes an warning event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Warning(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_WARNING_TYPE, eid, msg) +} + +// Error writes an error event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Error(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_ERROR_TYPE, eid, msg) +} diff --git a/vendor/golang.org/x/sys/windows/svc/go12.c b/vendor/golang.org/x/sys/windows/svc/go12.c new file mode 100644 index 00000000..6f1be1fa --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/go12.c @@ -0,0 +1,24 @@ +// Copyright 2012 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. + +// +build windows +// +build !go1.3 + +// copied from pkg/runtime +typedef unsigned int uint32; +typedef unsigned long long int uint64; +#ifdef _64BIT +typedef uint64 uintptr; +#else +typedef uint32 uintptr; +#endif + +// from sys_386.s or sys_amd64.s +void ·servicemain(void); + +void +·getServiceMain(uintptr *r) +{ + *r = (uintptr)·servicemain; +} diff --git a/vendor/golang.org/x/sys/windows/svc/go12.go b/vendor/golang.org/x/sys/windows/svc/go12.go new file mode 100644 index 00000000..cd8b913c --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/go12.go @@ -0,0 +1,11 @@ +// Copyright 2014 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. + +// +build windows +// +build !go1.3 + +package svc + +// from go12.c +func getServiceMain(r *uintptr) diff --git a/vendor/golang.org/x/sys/windows/svc/go13.go b/vendor/golang.org/x/sys/windows/svc/go13.go new file mode 100644 index 00000000..9d7f3cec --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/go13.go @@ -0,0 +1,31 @@ +// Copyright 2014 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. + +// +build windows +// +build go1.3 + +package svc + +import "unsafe" + +const ptrSize = 4 << (^uintptr(0) >> 63) // unsafe.Sizeof(uintptr(0)) but an ideal const + +// Should be a built-in for unsafe.Pointer? +func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(p) + x) +} + +// funcPC returns the entry PC of the function f. +// It assumes that f is a func value. Otherwise the behavior is undefined. +func funcPC(f interface{}) uintptr { + return **(**uintptr)(add(unsafe.Pointer(&f), ptrSize)) +} + +// from sys_386.s and sys_amd64.s +func servicectlhandler(ctl uint32) uintptr +func servicemain(argc uint32, argv **uint16) + +func getServiceMain(r *uintptr) { + *r = funcPC(servicemain) +} diff --git a/vendor/golang.org/x/sys/windows/svc/mgr/config.go b/vendor/golang.org/x/sys/windows/svc/mgr/config.go new file mode 100644 index 00000000..d804e31f --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/mgr/config.go @@ -0,0 +1,145 @@ +// Copyright 2012 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. + +// +build windows + +package mgr + +import ( + "syscall" + "unicode/utf16" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + // Service start types. + StartManual = windows.SERVICE_DEMAND_START // the service must be started manually + StartAutomatic = windows.SERVICE_AUTO_START // the service will start by itself whenever the computer reboots + StartDisabled = windows.SERVICE_DISABLED // the service cannot be started + + // The severity of the error, and action taken, + // if this service fails to start. + ErrorCritical = windows.SERVICE_ERROR_CRITICAL + ErrorIgnore = windows.SERVICE_ERROR_IGNORE + ErrorNormal = windows.SERVICE_ERROR_NORMAL + ErrorSevere = windows.SERVICE_ERROR_SEVERE +) + +// TODO(brainman): Password is not returned by windows.QueryServiceConfig, not sure how to get it. + +type Config struct { + ServiceType uint32 + StartType uint32 + ErrorControl uint32 + BinaryPathName string // fully qualified path to the service binary file, can also include arguments for an auto-start service + LoadOrderGroup string + TagId uint32 + Dependencies []string + ServiceStartName string // name of the account under which the service should run + DisplayName string + Password string + Description string +} + +func toString(p *uint16) string { + if p == nil { + return "" + } + return syscall.UTF16ToString((*[4096]uint16)(unsafe.Pointer(p))[:]) +} + +func toStringSlice(ps *uint16) []string { + if ps == nil { + return nil + } + r := make([]string, 0) + for from, i, p := 0, 0, (*[1 << 24]uint16)(unsafe.Pointer(ps)); true; i++ { + if p[i] == 0 { + // empty string marks the end + if i <= from { + break + } + r = append(r, string(utf16.Decode(p[from:i]))) + from = i + 1 + } + } + return r +} + +// Config retrieves service s configuration paramteres. +func (s *Service) Config() (Config, error) { + var p *windows.QUERY_SERVICE_CONFIG + n := uint32(1024) + for { + b := make([]byte, n) + p = (*windows.QUERY_SERVICE_CONFIG)(unsafe.Pointer(&b[0])) + err := windows.QueryServiceConfig(s.Handle, p, n, &n) + if err == nil { + break + } + if err.(syscall.Errno) != syscall.ERROR_INSUFFICIENT_BUFFER { + return Config{}, err + } + if n <= uint32(len(b)) { + return Config{}, err + } + } + + b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_DESCRIPTION) + if err != nil { + return Config{}, err + } + p2 := (*windows.SERVICE_DESCRIPTION)(unsafe.Pointer(&b[0])) + + return Config{ + ServiceType: p.ServiceType, + StartType: p.StartType, + ErrorControl: p.ErrorControl, + BinaryPathName: toString(p.BinaryPathName), + LoadOrderGroup: toString(p.LoadOrderGroup), + TagId: p.TagId, + Dependencies: toStringSlice(p.Dependencies), + ServiceStartName: toString(p.ServiceStartName), + DisplayName: toString(p.DisplayName), + Description: toString(p2.Description), + }, nil +} + +func updateDescription(handle windows.Handle, desc string) error { + d := windows.SERVICE_DESCRIPTION{Description: toPtr(desc)} + return windows.ChangeServiceConfig2(handle, + windows.SERVICE_CONFIG_DESCRIPTION, (*byte)(unsafe.Pointer(&d))) +} + +// UpdateConfig updates service s configuration parameters. +func (s *Service) UpdateConfig(c Config) error { + err := windows.ChangeServiceConfig(s.Handle, c.ServiceType, c.StartType, + c.ErrorControl, toPtr(c.BinaryPathName), toPtr(c.LoadOrderGroup), + nil, toStringBlock(c.Dependencies), toPtr(c.ServiceStartName), + toPtr(c.Password), toPtr(c.DisplayName)) + if err != nil { + return err + } + return updateDescription(s.Handle, c.Description) +} + +// queryServiceConfig2 calls Windows QueryServiceConfig2 with infoLevel parameter and returns retrieved service configuration information. +func (s *Service) queryServiceConfig2(infoLevel uint32) ([]byte, error) { + n := uint32(1024) + for { + b := make([]byte, n) + err := windows.QueryServiceConfig2(s.Handle, infoLevel, &b[0], n, &n) + if err == nil { + return b, nil + } + if err.(syscall.Errno) != syscall.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + if n <= uint32(len(b)) { + return nil, err + } + } +} diff --git a/vendor/golang.org/x/sys/windows/svc/mgr/mgr.go b/vendor/golang.org/x/sys/windows/svc/mgr/mgr.go new file mode 100644 index 00000000..76965b56 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/mgr/mgr.go @@ -0,0 +1,162 @@ +// Copyright 2012 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. + +// +build windows + +// Package mgr can be used to manage Windows service programs. +// It can be used to install and remove them. It can also start, +// stop and pause them. The package can query / change current +// service state and config parameters. +// +package mgr + +import ( + "syscall" + "unicode/utf16" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Mgr is used to manage Windows service. +type Mgr struct { + Handle windows.Handle +} + +// Connect establishes a connection to the service control manager. +func Connect() (*Mgr, error) { + return ConnectRemote("") +} + +// ConnectRemote establishes a connection to the +// service control manager on computer named host. +func ConnectRemote(host string) (*Mgr, error) { + var s *uint16 + if host != "" { + s = syscall.StringToUTF16Ptr(host) + } + h, err := windows.OpenSCManager(s, nil, windows.SC_MANAGER_ALL_ACCESS) + if err != nil { + return nil, err + } + return &Mgr{Handle: h}, nil +} + +// Disconnect closes connection to the service control manager m. +func (m *Mgr) Disconnect() error { + return windows.CloseServiceHandle(m.Handle) +} + +func toPtr(s string) *uint16 { + if len(s) == 0 { + return nil + } + return syscall.StringToUTF16Ptr(s) +} + +// toStringBlock terminates strings in ss with 0, and then +// concatenates them together. It also adds extra 0 at the end. +func toStringBlock(ss []string) *uint16 { + if len(ss) == 0 { + return nil + } + t := "" + for _, s := range ss { + if s != "" { + t += s + "\x00" + } + } + if t == "" { + return nil + } + t += "\x00" + return &utf16.Encode([]rune(t))[0] +} + +// CreateService installs new service name on the system. +// The service will be executed by running exepath binary. +// Use config c to specify service parameters. +// Any args will be passed as command-line arguments when +// the service is started; these arguments are distinct from +// the arguments passed to Service.Start or via the "Start +// parameters" field in the service's Properties dialog box. +func (m *Mgr) CreateService(name, exepath string, c Config, args ...string) (*Service, error) { + if c.StartType == 0 { + c.StartType = StartManual + } + if c.ErrorControl == 0 { + c.ErrorControl = ErrorNormal + } + if c.ServiceType == 0 { + c.ServiceType = windows.SERVICE_WIN32_OWN_PROCESS + } + s := syscall.EscapeArg(exepath) + for _, v := range args { + s += " " + syscall.EscapeArg(v) + } + h, err := windows.CreateService(m.Handle, toPtr(name), toPtr(c.DisplayName), + windows.SERVICE_ALL_ACCESS, c.ServiceType, + c.StartType, c.ErrorControl, toPtr(s), toPtr(c.LoadOrderGroup), + nil, toStringBlock(c.Dependencies), toPtr(c.ServiceStartName), toPtr(c.Password)) + if err != nil { + return nil, err + } + if c.Description != "" { + err = updateDescription(h, c.Description) + if err != nil { + return nil, err + } + } + return &Service{Name: name, Handle: h}, nil +} + +// OpenService retrieves access to service name, so it can +// be interrogated and controlled. +func (m *Mgr) OpenService(name string) (*Service, error) { + h, err := windows.OpenService(m.Handle, syscall.StringToUTF16Ptr(name), windows.SERVICE_ALL_ACCESS) + if err != nil { + return nil, err + } + return &Service{Name: name, Handle: h}, nil +} + +// ListServices enumerates services in the specified +// service control manager database m. +// If the caller does not have the SERVICE_QUERY_STATUS +// access right to a service, the service is silently +// omitted from the list of services returned. +func (m *Mgr) ListServices() ([]string, error) { + var err error + var bytesNeeded, servicesReturned uint32 + var buf []byte + for { + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + err = windows.EnumServicesStatusEx(m.Handle, windows.SC_ENUM_PROCESS_INFO, + windows.SERVICE_WIN32, windows.SERVICE_STATE_ALL, + p, uint32(len(buf)), &bytesNeeded, &servicesReturned, nil, nil) + if err == nil { + break + } + if err != syscall.ERROR_MORE_DATA { + return nil, err + } + if bytesNeeded <= uint32(len(buf)) { + return nil, err + } + buf = make([]byte, bytesNeeded) + } + if servicesReturned == 0 { + return nil, nil + } + services := (*[1 << 20]windows.ENUM_SERVICE_STATUS_PROCESS)(unsafe.Pointer(&buf[0]))[:servicesReturned] + var names []string + for _, s := range services { + name := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(s.ServiceName))[:]) + names = append(names, name) + } + return names, nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/mgr/recovery.go b/vendor/golang.org/x/sys/windows/svc/mgr/recovery.go new file mode 100644 index 00000000..71ce2b81 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/mgr/recovery.go @@ -0,0 +1,135 @@ +// Copyright 2018 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. + +// +build windows + +package mgr + +import ( + "errors" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + // Possible recovery actions that the service control manager can perform. + NoAction = windows.SC_ACTION_NONE // no action + ComputerReboot = windows.SC_ACTION_REBOOT // reboot the computer + ServiceRestart = windows.SC_ACTION_RESTART // restart the service + RunCommand = windows.SC_ACTION_RUN_COMMAND // run a command +) + +// RecoveryAction represents an action that the service control manager can perform when service fails. +// A service is considered failed when it terminates without reporting a status of SERVICE_STOPPED to the service controller. +type RecoveryAction struct { + Type int // one of NoAction, ComputerReboot, ServiceRestart or RunCommand + Delay time.Duration // the time to wait before performing the specified action +} + +// SetRecoveryActions sets actions that service controller performs when service fails and +// the time after which to reset the service failure count to zero if there are no failures, in seconds. +// Specify INFINITE to indicate that service failure count should never be reset. +func (s *Service) SetRecoveryActions(recoveryActions []RecoveryAction, resetPeriod uint32) error { + if recoveryActions == nil { + return errors.New("recoveryActions cannot be nil") + } + actions := []windows.SC_ACTION{} + for _, a := range recoveryActions { + action := windows.SC_ACTION{ + Type: uint32(a.Type), + Delay: uint32(a.Delay.Nanoseconds() / 1000000), + } + actions = append(actions, action) + } + rActions := windows.SERVICE_FAILURE_ACTIONS{ + ActionsCount: uint32(len(actions)), + Actions: &actions[0], + ResetPeriod: resetPeriod, + } + return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions))) +} + +// RecoveryActions returns actions that service controller performs when service fails. +// The service control manager counts the number of times service s has failed since the system booted. +// The count is reset to 0 if the service has not failed for ResetPeriod seconds. +// When the service fails for the Nth time, the service controller performs the action specified in element [N-1] of returned slice. +// If N is greater than slice length, the service controller repeats the last action in the slice. +func (s *Service) RecoveryActions() ([]RecoveryAction, error) { + b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS) + if err != nil { + return nil, err + } + p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0])) + if p.Actions == nil { + return nil, err + } + + var recoveryActions []RecoveryAction + actions := (*[1024]windows.SC_ACTION)(unsafe.Pointer(p.Actions))[:p.ActionsCount] + for _, action := range actions { + recoveryActions = append(recoveryActions, RecoveryAction{Type: int(action.Type), Delay: time.Duration(action.Delay) * time.Millisecond}) + } + return recoveryActions, nil +} + +// ResetRecoveryActions deletes both reset period and array of failure actions. +func (s *Service) ResetRecoveryActions() error { + actions := make([]windows.SC_ACTION, 1) + rActions := windows.SERVICE_FAILURE_ACTIONS{ + Actions: &actions[0], + } + return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions))) +} + +// ResetPeriod is the time after which to reset the service failure +// count to zero if there are no failures, in seconds. +func (s *Service) ResetPeriod() (uint32, error) { + b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS) + if err != nil { + return 0, err + } + p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0])) + return p.ResetPeriod, nil +} + +// SetRebootMessage sets service s reboot message. +// If msg is "", the reboot message is deleted and no message is broadcast. +func (s *Service) SetRebootMessage(msg string) error { + rActions := windows.SERVICE_FAILURE_ACTIONS{ + RebootMsg: syscall.StringToUTF16Ptr(msg), + } + return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions))) +} + +// RebootMessage is broadcast to server users before rebooting in response to the ComputerReboot service controller action. +func (s *Service) RebootMessage() (string, error) { + b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS) + if err != nil { + return "", err + } + p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0])) + return toString(p.RebootMsg), nil +} + +// SetRecoveryCommand sets the command line of the process to execute in response to the RunCommand service controller action. +// If cmd is "", the command is deleted and no program is run when the service fails. +func (s *Service) SetRecoveryCommand(cmd string) error { + rActions := windows.SERVICE_FAILURE_ACTIONS{ + Command: syscall.StringToUTF16Ptr(cmd), + } + return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions))) +} + +// RecoveryCommand is the command line of the process to execute in response to the RunCommand service controller action. This process runs under the same account as the service. +func (s *Service) RecoveryCommand() (string, error) { + b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS) + if err != nil { + return "", err + } + p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0])) + return toString(p.Command), nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/mgr/service.go b/vendor/golang.org/x/sys/windows/svc/mgr/service.go new file mode 100644 index 00000000..fdc46af5 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/mgr/service.go @@ -0,0 +1,72 @@ +// Copyright 2012 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. + +// +build windows + +package mgr + +import ( + "syscall" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" +) + +// TODO(brainman): Use EnumDependentServices to enumerate dependent services. + +// Service is used to access Windows service. +type Service struct { + Name string + Handle windows.Handle +} + +// Delete marks service s for deletion from the service control manager database. +func (s *Service) Delete() error { + return windows.DeleteService(s.Handle) +} + +// Close relinquish access to the service s. +func (s *Service) Close() error { + return windows.CloseServiceHandle(s.Handle) +} + +// Start starts service s. +// args will be passed to svc.Handler.Execute. +func (s *Service) Start(args ...string) error { + var p **uint16 + if len(args) > 0 { + vs := make([]*uint16, len(args)) + for i := range vs { + vs[i] = syscall.StringToUTF16Ptr(args[i]) + } + p = &vs[0] + } + return windows.StartService(s.Handle, uint32(len(args)), p) +} + +// Control sends state change request c to the servce s. +func (s *Service) Control(c svc.Cmd) (svc.Status, error) { + var t windows.SERVICE_STATUS + err := windows.ControlService(s.Handle, uint32(c), &t) + if err != nil { + return svc.Status{}, err + } + return svc.Status{ + State: svc.State(t.CurrentState), + Accepts: svc.Accepted(t.ControlsAccepted), + }, nil +} + +// Query returns current status of service s. +func (s *Service) Query() (svc.Status, error) { + var t windows.SERVICE_STATUS + err := windows.QueryServiceStatus(s.Handle, &t) + if err != nil { + return svc.Status{}, err + } + return svc.Status{ + State: svc.State(t.CurrentState), + Accepts: svc.Accepted(t.ControlsAccepted), + }, nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/security.go b/vendor/golang.org/x/sys/windows/svc/security.go new file mode 100644 index 00000000..6fbc9236 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/security.go @@ -0,0 +1,62 @@ +// Copyright 2012 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. + +// +build windows + +package svc + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func allocSid(subAuth0 uint32) (*windows.SID, error) { + var sid *windows.SID + err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, + 1, subAuth0, 0, 0, 0, 0, 0, 0, 0, &sid) + if err != nil { + return nil, err + } + return sid, nil +} + +// IsAnInteractiveSession determines if calling process is running interactively. +// It queries the process token for membership in the Interactive group. +// http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s +func IsAnInteractiveSession() (bool, error) { + interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(interSid) + + serviceSid, err := allocSid(windows.SECURITY_SERVICE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(serviceSid) + + t, err := windows.OpenCurrentProcessToken() + if err != nil { + return false, err + } + defer t.Close() + + gs, err := t.GetTokenGroups() + if err != nil { + return false, err + } + p := unsafe.Pointer(&gs.Groups[0]) + groups := (*[2 << 20]windows.SIDAndAttributes)(p)[:gs.GroupCount] + for _, g := range groups { + if windows.EqualSid(g.Sid, interSid) { + return true, nil + } + if windows.EqualSid(g.Sid, serviceSid) { + return false, nil + } + } + return false, nil +} diff --git a/vendor/golang.org/x/sys/windows/svc/service.go b/vendor/golang.org/x/sys/windows/svc/service.go new file mode 100644 index 00000000..38b147d5 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/service.go @@ -0,0 +1,363 @@ +// Copyright 2012 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. + +// +build windows + +// Package svc provides everything required to build Windows service. +// +package svc + +import ( + "errors" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// State describes service execution state (Stopped, Running and so on). +type State uint32 + +const ( + Stopped = State(windows.SERVICE_STOPPED) + StartPending = State(windows.SERVICE_START_PENDING) + StopPending = State(windows.SERVICE_STOP_PENDING) + Running = State(windows.SERVICE_RUNNING) + ContinuePending = State(windows.SERVICE_CONTINUE_PENDING) + PausePending = State(windows.SERVICE_PAUSE_PENDING) + Paused = State(windows.SERVICE_PAUSED) +) + +// Cmd represents service state change request. It is sent to a service +// by the service manager, and should be actioned upon by the service. +type Cmd uint32 + +const ( + Stop = Cmd(windows.SERVICE_CONTROL_STOP) + Pause = Cmd(windows.SERVICE_CONTROL_PAUSE) + Continue = Cmd(windows.SERVICE_CONTROL_CONTINUE) + Interrogate = Cmd(windows.SERVICE_CONTROL_INTERROGATE) + Shutdown = Cmd(windows.SERVICE_CONTROL_SHUTDOWN) + ParamChange = Cmd(windows.SERVICE_CONTROL_PARAMCHANGE) + NetBindAdd = Cmd(windows.SERVICE_CONTROL_NETBINDADD) + NetBindRemove = Cmd(windows.SERVICE_CONTROL_NETBINDREMOVE) + NetBindEnable = Cmd(windows.SERVICE_CONTROL_NETBINDENABLE) + NetBindDisable = Cmd(windows.SERVICE_CONTROL_NETBINDDISABLE) + DeviceEvent = Cmd(windows.SERVICE_CONTROL_DEVICEEVENT) + HardwareProfileChange = Cmd(windows.SERVICE_CONTROL_HARDWAREPROFILECHANGE) + PowerEvent = Cmd(windows.SERVICE_CONTROL_POWEREVENT) + SessionChange = Cmd(windows.SERVICE_CONTROL_SESSIONCHANGE) +) + +// Accepted is used to describe commands accepted by the service. +// Note that Interrogate is always accepted. +type Accepted uint32 + +const ( + AcceptStop = Accepted(windows.SERVICE_ACCEPT_STOP) + AcceptShutdown = Accepted(windows.SERVICE_ACCEPT_SHUTDOWN) + AcceptPauseAndContinue = Accepted(windows.SERVICE_ACCEPT_PAUSE_CONTINUE) + AcceptParamChange = Accepted(windows.SERVICE_ACCEPT_PARAMCHANGE) + AcceptNetBindChange = Accepted(windows.SERVICE_ACCEPT_NETBINDCHANGE) + AcceptHardwareProfileChange = Accepted(windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE) + AcceptPowerEvent = Accepted(windows.SERVICE_ACCEPT_POWEREVENT) + AcceptSessionChange = Accepted(windows.SERVICE_ACCEPT_SESSIONCHANGE) +) + +// Status combines State and Accepted commands to fully describe running service. +type Status struct { + State State + Accepts Accepted + CheckPoint uint32 // used to report progress during a lengthy operation + WaitHint uint32 // estimated time required for a pending operation, in milliseconds +} + +// ChangeRequest is sent to the service Handler to request service status change. +type ChangeRequest struct { + Cmd Cmd + EventType uint32 + EventData uintptr + CurrentStatus Status + Context uintptr +} + +// Handler is the interface that must be implemented to build Windows service. +type Handler interface { + + // Execute will be called by the package code at the start of + // the service, and the service will exit once Execute completes. + // Inside Execute you must read service change requests from r and + // act accordingly. You must keep service control manager up to date + // about state of your service by writing into s as required. + // args contains service name followed by argument strings passed + // to the service. + // You can provide service exit code in exitCode return parameter, + // with 0 being "no error". You can also indicate if exit code, + // if any, is service specific or not by using svcSpecificEC + // parameter. + Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32) +} + +var ( + // These are used by asm code. + goWaitsH uintptr + cWaitsH uintptr + ssHandle uintptr + sName *uint16 + sArgc uintptr + sArgv **uint16 + ctlHandlerExProc uintptr + cSetEvent uintptr + cWaitForSingleObject uintptr + cRegisterServiceCtrlHandlerExW uintptr +) + +func init() { + k := windows.NewLazySystemDLL("kernel32.dll") + cSetEvent = k.NewProc("SetEvent").Addr() + cWaitForSingleObject = k.NewProc("WaitForSingleObject").Addr() + a := windows.NewLazySystemDLL("advapi32.dll") + cRegisterServiceCtrlHandlerExW = a.NewProc("RegisterServiceCtrlHandlerExW").Addr() +} + +type ctlEvent struct { + cmd Cmd + eventType uint32 + eventData uintptr + context uintptr + errno uint32 +} + +// service provides access to windows service api. +type service struct { + name string + h windows.Handle + cWaits *event + goWaits *event + c chan ctlEvent + handler Handler +} + +func newService(name string, handler Handler) (*service, error) { + var s service + var err error + s.name = name + s.c = make(chan ctlEvent) + s.handler = handler + s.cWaits, err = newEvent() + if err != nil { + return nil, err + } + s.goWaits, err = newEvent() + if err != nil { + s.cWaits.Close() + return nil, err + } + return &s, nil +} + +func (s *service) close() error { + s.cWaits.Close() + s.goWaits.Close() + return nil +} + +type exitCode struct { + isSvcSpecific bool + errno uint32 +} + +func (s *service) updateStatus(status *Status, ec *exitCode) error { + if s.h == 0 { + return errors.New("updateStatus with no service status handle") + } + var t windows.SERVICE_STATUS + t.ServiceType = windows.SERVICE_WIN32_OWN_PROCESS + t.CurrentState = uint32(status.State) + if status.Accepts&AcceptStop != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_STOP + } + if status.Accepts&AcceptShutdown != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_SHUTDOWN + } + if status.Accepts&AcceptPauseAndContinue != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_PAUSE_CONTINUE + } + if status.Accepts&AcceptParamChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_PARAMCHANGE + } + if status.Accepts&AcceptNetBindChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_NETBINDCHANGE + } + if status.Accepts&AcceptHardwareProfileChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE + } + if status.Accepts&AcceptPowerEvent != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_POWEREVENT + } + if status.Accepts&AcceptSessionChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_SESSIONCHANGE + } + if ec.errno == 0 { + t.Win32ExitCode = windows.NO_ERROR + t.ServiceSpecificExitCode = windows.NO_ERROR + } else if ec.isSvcSpecific { + t.Win32ExitCode = uint32(windows.ERROR_SERVICE_SPECIFIC_ERROR) + t.ServiceSpecificExitCode = ec.errno + } else { + t.Win32ExitCode = ec.errno + t.ServiceSpecificExitCode = windows.NO_ERROR + } + t.CheckPoint = status.CheckPoint + t.WaitHint = status.WaitHint + return windows.SetServiceStatus(s.h, &t) +} + +const ( + sysErrSetServiceStatusFailed = uint32(syscall.APPLICATION_ERROR) + iota + sysErrNewThreadInCallback +) + +func (s *service) run() { + s.goWaits.Wait() + s.h = windows.Handle(ssHandle) + argv := (*[100]*int16)(unsafe.Pointer(sArgv))[:sArgc] + args := make([]string, len(argv)) + for i, a := range argv { + args[i] = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(a))[:]) + } + + cmdsToHandler := make(chan ChangeRequest) + changesFromHandler := make(chan Status) + exitFromHandler := make(chan exitCode) + + go func() { + ss, errno := s.handler.Execute(args, cmdsToHandler, changesFromHandler) + exitFromHandler <- exitCode{ss, errno} + }() + + ec := exitCode{isSvcSpecific: true, errno: 0} + outcr := ChangeRequest{ + CurrentStatus: Status{State: Stopped}, + } + var outch chan ChangeRequest + inch := s.c +loop: + for { + select { + case r := <-inch: + if r.errno != 0 { + ec.errno = r.errno + break loop + } + inch = nil + outch = cmdsToHandler + outcr.Cmd = r.cmd + outcr.EventType = r.eventType + outcr.EventData = r.eventData + outcr.Context = r.context + case outch <- outcr: + inch = s.c + outch = nil + case c := <-changesFromHandler: + err := s.updateStatus(&c, &ec) + if err != nil { + // best suitable error number + ec.errno = sysErrSetServiceStatusFailed + if err2, ok := err.(syscall.Errno); ok { + ec.errno = uint32(err2) + } + break loop + } + outcr.CurrentStatus = c + case ec = <-exitFromHandler: + break loop + } + } + + s.updateStatus(&Status{State: Stopped}, &ec) + s.cWaits.Set() +} + +func newCallback(fn interface{}) (cb uintptr, err error) { + defer func() { + r := recover() + if r == nil { + return + } + cb = 0 + switch v := r.(type) { + case string: + err = errors.New(v) + case error: + err = v + default: + err = errors.New("unexpected panic in syscall.NewCallback") + } + }() + return syscall.NewCallback(fn), nil +} + +// BUG(brainman): There is no mechanism to run multiple services +// inside one single executable. Perhaps, it can be overcome by +// using RegisterServiceCtrlHandlerEx Windows api. + +// Run executes service name by calling appropriate handler function. +func Run(name string, handler Handler) error { + runtime.LockOSThread() + + tid := windows.GetCurrentThreadId() + + s, err := newService(name, handler) + if err != nil { + return err + } + + ctlHandler := func(ctl, evtype, evdata, context uintptr) uintptr { + e := ctlEvent{cmd: Cmd(ctl), eventType: uint32(evtype), eventData: evdata, context: context} + // We assume that this callback function is running on + // the same thread as Run. Nowhere in MS documentation + // I could find statement to guarantee that. So putting + // check here to verify, otherwise things will go bad + // quickly, if ignored. + i := windows.GetCurrentThreadId() + if i != tid { + e.errno = sysErrNewThreadInCallback + } + s.c <- e + // Always return NO_ERROR (0) for now. + return 0 + } + + var svcmain uintptr + getServiceMain(&svcmain) + t := []windows.SERVICE_TABLE_ENTRY{ + {ServiceName: syscall.StringToUTF16Ptr(s.name), ServiceProc: svcmain}, + {ServiceName: nil, ServiceProc: 0}, + } + + goWaitsH = uintptr(s.goWaits.h) + cWaitsH = uintptr(s.cWaits.h) + sName = t[0].ServiceName + ctlHandlerExProc, err = newCallback(ctlHandler) + if err != nil { + return err + } + + go s.run() + + err = windows.StartServiceCtrlDispatcher(&t[0]) + if err != nil { + return err + } + return nil +} + +// StatusHandle returns service status handle. It is safe to call this function +// from inside the Handler.Execute because then it is guaranteed to be set. +// This code will have to change once multiple services are possible per process. +func StatusHandle() windows.Handle { + return windows.Handle(ssHandle) +} diff --git a/vendor/golang.org/x/sys/windows/svc/sys_386.s b/vendor/golang.org/x/sys/windows/svc/sys_386.s new file mode 100644 index 00000000..c8a583d7 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/sys_386.s @@ -0,0 +1,69 @@ +// Copyright 2012 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. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL argc+0(FP), AX + MOVL AX, ·sArgc(SB) + MOVL argv+4(FP), AX + MOVL AX, ·sArgv(SB) + + PUSHL BP + PUSHL BX + PUSHL SI + PUSHL DI + + SUBL $12, SP + + MOVL ·sName(SB), AX + MOVL AX, (SP) + MOVL $·servicectlhandler(SB), AX + MOVL AX, 4(SP) + // Set context to 123456 to test issue #25660. + MOVL $123456, 8(SP) + MOVL ·cRegisterServiceCtrlHandlerExW(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + CMPL AX, $0 + JE exit + MOVL AX, ·ssHandle(SB) + + MOVL ·goWaitsH(SB), AX + MOVL AX, (SP) + MOVL ·cSetEvent(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + + MOVL ·cWaitsH(SB), AX + MOVL AX, (SP) + MOVL $-1, AX + MOVL AX, 4(SP) + MOVL ·cWaitForSingleObject(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + +exit: + ADDL $12, SP + + POPL DI + POPL SI + POPL BX + POPL BP + + MOVL 0(SP), CX + ADDL $12, SP + JMP CX + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr { +TEXT ·servicectlhandler(SB),7,$0 + MOVL ·ctlHandlerExProc(SB), CX + JMP CX diff --git a/vendor/golang.org/x/sys/windows/svc/sys_amd64.s b/vendor/golang.org/x/sys/windows/svc/sys_amd64.s new file mode 100644 index 00000000..2f7609c5 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/sys_amd64.s @@ -0,0 +1,44 @@ +// Copyright 2012 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. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL CX, ·sArgc(SB) + MOVQ DX, ·sArgv(SB) + + SUBQ $32, SP // stack for the first 4 syscall params + + MOVQ ·sName(SB), CX + MOVQ $·servicectlhandler(SB), DX + // BUG(pastarmovj): Figure out a way to pass in context in R8. + // Set context to 123456 to test issue #25660. + MOVQ $123456, R8 + MOVQ ·cRegisterServiceCtrlHandlerExW(SB), AX + CALL AX + CMPQ AX, $0 + JE exit + MOVQ AX, ·ssHandle(SB) + + MOVQ ·goWaitsH(SB), CX + MOVQ ·cSetEvent(SB), AX + CALL AX + + MOVQ ·cWaitsH(SB), CX + MOVQ $4294967295, DX + MOVQ ·cWaitForSingleObject(SB), AX + CALL AX + +exit: + ADDQ $32, SP + RET + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func ·servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr { +TEXT ·servicectlhandler(SB),7,$0 + MOVQ ·ctlHandlerExProc(SB), AX + JMP AX diff --git a/vendor/golang.org/x/sys/windows/svc/sys_arm.s b/vendor/golang.org/x/sys/windows/svc/sys_arm.s new file mode 100644 index 00000000..33c692a8 --- /dev/null +++ b/vendor/golang.org/x/sys/windows/svc/sys_arm.s @@ -0,0 +1,38 @@ +// Copyright 2018 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. + +// +build windows + +#include "textflag.h" + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),NOSPLIT|NOFRAME,$0 + MOVM.DB.W [R4, R14], (R13) // push {r4, lr} + MOVW R13, R4 + BIC $0x7, R13 // alignment for ABI + + MOVW R0, ·sArgc(SB) + MOVW R1, ·sArgv(SB) + + MOVW ·sName(SB), R0 + MOVW ·ctlHandlerExProc(SB), R1 + MOVW $0, R2 + MOVW ·cRegisterServiceCtrlHandlerExW(SB), R3 + BL (R3) + CMP $0, R0 + BEQ exit + MOVW R0, ·ssHandle(SB) + + MOVW ·goWaitsH(SB), R0 + MOVW ·cSetEvent(SB), R1 + BL (R1) + + MOVW ·cWaitsH(SB), R0 + MOVW $-1, R1 + MOVW ·cWaitForSingleObject(SB), R2 + BL (R2) + +exit: + MOVW R4, R13 // free extra stack space + MOVM.IA.W (R13), [R4, R15] // pop {r4, pc} From cd9f14a49adcea89dc890b30715ceb01a6ee6f42 Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Sat, 29 Jun 2019 15:05:54 -0700 Subject: [PATCH 04/15] use elogWrapper to wrap eventlog and set it as a log output logging working w/ multiwriter! --- Gopkg.lock | 9 + main.go | 11 +- multiuser_service_windows.go | 106 +++- multiuser_windows.go | 5 - usage.go | 1 + .../natefinch/lumberjack.v2/.gitignore | 23 + .../gopkg.in/natefinch/lumberjack.v2/LICENSE | 21 + .../natefinch/lumberjack.v2/README.md | 174 ++++++ .../gopkg.in/natefinch/lumberjack.v2/chown.go | 11 + .../natefinch/lumberjack.v2/chown_linux.go | 19 + .../natefinch/lumberjack.v2/lumberjack.go | 541 ++++++++++++++++++ 11 files changed, 898 insertions(+), 23 deletions(-) create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/.gitignore create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/LICENSE create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/README.md create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/chown.go create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go create mode 100644 vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go diff --git a/Gopkg.lock b/Gopkg.lock index f1a3643e..c47734ca 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -551,6 +551,14 @@ pruneopts = "UT" revision = "c5ac96b4c419ee8b66a6eb7e805180844f9f176b" +[[projects]] + digest = "1:c805e517269b0ba4c21ded5836019ed7d16953d4026cb7d00041d039c7906be9" + name = "gopkg.in/natefinch/lumberjack.v2" + packages = ["."] + pruneopts = "UT" + revision = "a96e63847dc3c67d17befa69c303767e2f84e54f" + version = "v2.1" + [[projects]] digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" name = "gopkg.in/yaml.v2" @@ -607,6 +615,7 @@ "golang.org/x/sys/windows/svc/eventlog", "golang.org/x/sys/windows/svc/mgr", "golang.org/x/tools/imports", + "gopkg.in/natefinch/lumberjack.v2", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/main.go b/main.go index 0a1d5666..0e148d69 100644 --- a/main.go +++ b/main.go @@ -106,8 +106,6 @@ func main() { panic(err) } - log.Printf("%#v", arguments) - switch { case arguments["show-payload-schema"]: fmt.Println(taskPayloadSchema()) @@ -1195,6 +1193,15 @@ func exitOnError(exitCode ExitCode, err error, logMessage string, args ...interf if err == nil { return } + // useful for debugging broken logging + filename := filepath.Join( + filepath.Dir(os.Args[0]), + fmt.Sprintf("generic-worker-crash-%d.log", time.Now().Unix()), + ) + err = ioutil.WriteFile(filename, []byte(fmt.Sprintf(logMessage, args...)+"\n"+err.Error()), 0666) + if err != nil { + log.Printf("Could not open crash file %q: %v", filename, err) + } log.Printf(logMessage, args...) log.Printf("%v", err) os.Exit(int(exitCode)) diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 8b896322..e9658855 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -4,8 +4,11 @@ package main import ( "fmt" + "io" + "io/ioutil" "log" "os" + "path" "path/filepath" "time" @@ -14,26 +17,75 @@ import ( "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" + + "gopkg.in/natefinch/lumberjack.v2" ) -var elog debug.Log +// stderr handle is invalid when run as service +var logWriter = io.MultiWriter(ioutil.Discard) + +func init() { + log.SetOutput(logWriter) + manageLogFile() +} + +func manageLogFile() { + dir := filepath.Dir(os.Args[0]) + logPath := filepath.Join(dir, "generic-worker.log") + logWriter = io.MultiWriter( + logWriter, + &lumberjack.Logger{ + Filename: logPath, + MaxBackups: 10, + MaxSize: 20, // megabytes + MaxAge: 7, //days + Compress: true, // disabled by default + }, + ) + log.SetOutput(logWriter) + // lumberjack opens logfile on first write + // multiwriter will fail if write to any writer fails + // so we aggressively handle that scenario + err := log.Output(2, fmt.Sprintf("Opened logfile %q", logPath)) + if err != nil { + exitOnError(CANT_LOG_PROPERLY, err, "Unable to log to logfile %q with writer: %v", logPath, logWriter) + } +} + +// elogWrapper is used to allow eventlog +// to be written to by go's log package +// it eats severity in the process +type elogWrapper struct { + debug.Log +} + +func (e elogWrapper) Write(p []byte) (n int, err error) { + return len(p), e.Info(1, string(p)) +} type windowsService struct{} // implements Execute for svc.Handler -func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { +func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue changes <- svc.Status{State: svc.StartPending} // Start worker with interruptChan interruptChan := make(chan os.Signal, 1) - go RunWorker(interruptChan) + go func() { + exitCode = uint32(RunWorker(interruptChan)) + // kill the service + interruptChan <- os.Interrupt + }() changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} loop: for { select { + case <-interruptChan: + // we send this when RunWorker exits + break loop case c := <-r: switch c.Cmd { case svc.Interrogate: @@ -55,7 +107,7 @@ loop: } } changes <- svc.Status{State: svc.StopPending} - return false, 0 + return true, exitCode } func runService(name string, isDebug bool) ExitCode { @@ -64,29 +116,49 @@ func runService(name string, isDebug bool) ExitCode { name = "Generic Worker" } + var elog debug.Log if isDebug { log.Printf("Debug mode enabled, not using eventlog.") elog = debug.New(name) } else { elog, err = eventlog.Open(name) if err != nil { - fmt.Printf("Could not open eventlog: %v", err) - return INTERNAL_ERROR + exitOnError(INTERNAL_ERROR, err, "Could not open eventlog %q", name) } } + logWriter = io.MultiWriter(logWriter, elogWrapper{elog}) + log.SetOutput(logWriter) + // multiwriter will fail if write to any writer fails + // so we aggressively handle that scenario + err = log.Output(2, fmt.Sprintf("Wrote to eventlog %q successfully", name)) + if err != nil { + exitOnError(CANT_LOG_PROPERLY, err, "Unable to log to eventlog %q with writer: %v", name, logWriter) + } defer elog.Close() - elog.Info(1, fmt.Sprintf("Starting service %q", name)) + dir := path.Dir(os.Args[0]) + err = os.Chdir(dir) + if err != nil { + exitOnError(INTERNAL_ERROR, err, "Unable to chdir to %q", dir) + } + + log.Printf("Starting service %q", name) run := svc.Run if isDebug { run = debug.Run } err = run(name, &windowsService{}) if err != nil { - elog.Error(1, fmt.Sprintf("Service %q failed: %v", name, err)) - return INTERNAL_ERROR + exitOnError(INTERNAL_ERROR, err, "Service %q failed", name) + } + // use Output to use all configured loggers and handle err + // io.MultiWriter fails for _all_ writers if any fail + // this helps us catch that, as normal log.Print* calls + // silently eat errors + err = log.Output(2, fmt.Sprintf("Stopped service %q", name)) + if err != nil { + exitOnError(CANT_LOG_PROPERLY, err, "Unable to log to one or more log outputs, configured writer: %v", logWriter) } - elog.Info(1, fmt.Sprintf("Stopped service %q", name)) return 0 } @@ -134,7 +206,6 @@ func installService(name, exePath string, args []string) error { StartType: mgr.StartAutomatic, } dir := filepath.Dir(exePath) - logPath := filepath.Join(dir, "generic-worker-service.log") m, err := mgr.Connect() if err != nil { return err @@ -150,13 +221,16 @@ func installService(name, exePath string, args []string) error { return err } defer s.Close() - log.Printf("Created service %q with exePath %q, logPath %q and args %v", - name, exePath, logPath, args) + log.Printf("Created service %q with exePath %q, and args %v", + name, exePath, args) + + // TODO configure an eventlog message file + // https://docs.microsoft.com/en-us/windows/desktop/eventlog/message-files - // log all events to eventlog logfile + // configure eventlog source err = eventlog.Install( name, - logPath, + filepath.Join(dir, "eventlog-message-file.txt"), false, eventlog.Error|eventlog.Warning|eventlog.Info, ) @@ -190,7 +264,7 @@ func removeService(name string) error { } err = eventlog.Remove(name) if err != nil { - return fmt.Errorf("RemoveEventLogSource() failed: %s", err) + return fmt.Errorf("Removing eventlog source failed: %s", err) } log.Printf("Successfully removed service %q.", name) return nil diff --git a/multiuser_windows.go b/multiuser_windows.go index cbda449c..ef0ad005 100644 --- a/multiuser_windows.go +++ b/multiuser_windows.go @@ -442,11 +442,6 @@ func platformTargets(arguments map[string]interface{}) ExitCode { os.Chdir(cwd) } handleConfig(arguments) - // TODO remove! - err := ioutil.WriteFile(`C:\Users\miles\Desktop\generic-worker\handle-config-works`, []byte{}, 0777) - if err != nil { - panic(err) - } name := convertNilToEmptyString(arguments["--service-name"]) isIntSess, err := svc.IsAnInteractiveSession() if err != nil { diff --git a/usage.go b/usage.go index 1dbf9b20..14971b98 100644 --- a/usage.go +++ b/usage.go @@ -18,6 +18,7 @@ const ( CANT_CREATE_ED25519_KEYPAIR ExitCode = 75 CANT_SAVE_CONFIG ExitCode = 76 CANT_SECURE_CONFIG ExitCode = 77 + CANT_LOG_PROPERLY ExitCode = 78 ) func usage(versionName string) string { diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/.gitignore b/vendor/gopkg.in/natefinch/lumberjack.v2/.gitignore new file mode 100644 index 00000000..83656241 --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/LICENSE b/vendor/gopkg.in/natefinch/lumberjack.v2/LICENSE new file mode 100644 index 00000000..c3d4cc30 --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Nate Finch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/README.md b/vendor/gopkg.in/natefinch/lumberjack.v2/README.md new file mode 100644 index 00000000..9e971545 --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/README.md @@ -0,0 +1,174 @@ +# lumberjack [![GoDoc](https://godoc.org/gopkg.in/natefinch/lumberjack.v2?status.png)](https://godoc.org/gopkg.in/natefinch/lumberjack.v2) [![Build Status](https://drone.io/github.com/natefinch/lumberjack/status.png)](https://drone.io/github.com/natefinch/lumberjack/latest) [![Build status](https://ci.appveyor.com/api/projects/status/00gchpxtg4gkrt5d)](https://ci.appveyor.com/project/natefinch/lumberjack) [![Coverage Status](https://coveralls.io/repos/natefinch/lumberjack/badge.svg?branch=v2.0)](https://coveralls.io/r/natefinch/lumberjack?branch=v2.0) + +### Lumberjack is a Go package for writing logs to rolling files. + +Package lumberjack provides a rolling logger. + +Note that this is v2.0 of lumberjack, and should be imported using gopkg.in +thusly: + + import "gopkg.in/natefinch/lumberjack.v2" + +The package name remains simply lumberjack, and the code resides at +https://github.com/natefinch/lumberjack under the v2.0 branch. + +Lumberjack is intended to be one part of a logging infrastructure. +It is not an all-in-one solution, but instead is a pluggable +component at the bottom of the logging stack that simply controls the files +to which logs are written. + +Lumberjack plays well with any logging package that can write to an +io.Writer, including the standard library's log package. + +Lumberjack assumes that only one process is writing to the output files. +Using the same lumberjack configuration from multiple processes on the same +machine will result in improper behavior. + + +**Example** + +To use lumberjack with the standard library's log package, just pass it into the SetOutput function when your application starts. + +Code: + +```go +log.SetOutput(&lumberjack.Logger{ + Filename: "/var/log/myapp/foo.log", + MaxSize: 500, // megabytes + MaxBackups: 3, + MaxAge: 28, //days +}) +``` + + + +## type Logger +``` go +type Logger struct { + // Filename is the file to write logs to. Backup log files will be retained + // in the same directory. It uses -lumberjack.log in + // os.TempDir() if empty. + Filename string `json:"filename" yaml:"filename"` + + // MaxSize is the maximum size in megabytes of the log file before it gets + // rotated. It defaults to 100 megabytes. + MaxSize int `json:"maxsize" yaml:"maxsize"` + + // MaxAge is the maximum number of days to retain old log files based on the + // timestamp encoded in their filename. Note that a day is defined as 24 + // hours and may not exactly correspond to calendar days due to daylight + // savings, leap seconds, etc. The default is not to remove old log files + // based on age. + MaxAge int `json:"maxage" yaml:"maxage"` + + // MaxBackups is the maximum number of old log files to retain. The default + // is to retain all old log files (though MaxAge may still cause them to get + // deleted.) + MaxBackups int `json:"maxbackups" yaml:"maxbackups"` + + // LocalTime determines if the time used for formatting the timestamps in + // backup files is the computer's local time. The default is to use UTC + // time. + LocalTime bool `json:"localtime" yaml:"localtime"` + // contains filtered or unexported fields +} +``` +Logger is an io.WriteCloser that writes to the specified filename. + +Logger opens or creates the logfile on first Write. If the file exists and +is less than MaxSize megabytes, lumberjack will open and append to that file. +If the file exists and its size is >= MaxSize megabytes, the file is renamed +by putting the current time in a timestamp in the name immediately before the +file's extension (or the end of the filename if there's no extension). A new +log file is then created using original filename. + +Whenever a write would cause the current log file exceed MaxSize megabytes, +the current file is closed, renamed, and a new log file created with the +original name. Thus, the filename you give Logger is always the "current" log +file. + +Backups use the log file name given to Logger, in the form `name-timestamp.ext` +where name is the filename without the extension, timestamp is the time at which +the log was rotated formatted with the time.Time format of +`2006-01-02T15-04-05.000` and the extension is the original extension. For +example, if your Logger.Filename is `/var/log/foo/server.log`, a backup created +at 6:30pm on Nov 11 2016 would use the filename +`/var/log/foo/server-2016-11-04T18-30-00.000.log` + +### Cleaning Up Old Log Files +Whenever a new logfile gets created, old log files may be deleted. The most +recent files according to the encoded timestamp will be retained, up to a +number equal to MaxBackups (or all of them if MaxBackups is 0). Any files +with an encoded timestamp older than MaxAge days are deleted, regardless of +MaxBackups. Note that the time encoded in the timestamp is the rotation +time, which may differ from the last time that file was written to. + +If MaxBackups and MaxAge are both 0, no old log files will be deleted. + + + + + + + + + + + +### func (\*Logger) Close +``` go +func (l *Logger) Close() error +``` +Close implements io.Closer, and closes the current logfile. + + + +### func (\*Logger) Rotate +``` go +func (l *Logger) Rotate() error +``` +Rotate causes Logger to close the existing log file and immediately create a +new one. This is a helper function for applications that want to initiate +rotations outside of the normal rotation rules, such as in response to +SIGHUP. After rotating, this initiates a cleanup of old log files according +to the normal rules. + +**Example** + +Example of how to rotate in response to SIGHUP. + +Code: + +```go +l := &lumberjack.Logger{} +log.SetOutput(l) +c := make(chan os.Signal, 1) +signal.Notify(c, syscall.SIGHUP) + +go func() { + for { + <-c + l.Rotate() + } +}() +``` + +### func (\*Logger) Write +``` go +func (l *Logger) Write(p []byte) (n int, err error) +``` +Write implements io.Writer. If a write would cause the log file to be larger +than MaxSize, the file is closed, renamed to include a timestamp of the +current time, and a new log file is created using the original log file name. +If the length of the write is greater than MaxSize, an error is returned. + + + + + + + + + +- - - +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/chown.go b/vendor/gopkg.in/natefinch/lumberjack.v2/chown.go new file mode 100644 index 00000000..11d06697 --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/chown.go @@ -0,0 +1,11 @@ +// +build !linux + +package lumberjack + +import ( + "os" +) + +func chown(_ string, _ os.FileInfo) error { + return nil +} diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go b/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go new file mode 100644 index 00000000..2758ec9c --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/chown_linux.go @@ -0,0 +1,19 @@ +package lumberjack + +import ( + "os" + "syscall" +) + +// os_Chown is a var so we can mock it out during tests. +var os_Chown = os.Chown + +func chown(name string, info os.FileInfo) error { + f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + f.Close() + stat := info.Sys().(*syscall.Stat_t) + return os_Chown(name, int(stat.Uid), int(stat.Gid)) +} diff --git a/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go b/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go new file mode 100644 index 00000000..ca19da44 --- /dev/null +++ b/vendor/gopkg.in/natefinch/lumberjack.v2/lumberjack.go @@ -0,0 +1,541 @@ +// Package lumberjack provides a rolling logger. +// +// Note that this is v2.0 of lumberjack, and should be imported using gopkg.in +// thusly: +// +// import "gopkg.in/natefinch/lumberjack.v2" +// +// The package name remains simply lumberjack, and the code resides at +// https://github.com/natefinch/lumberjack under the v2.0 branch. +// +// Lumberjack is intended to be one part of a logging infrastructure. +// It is not an all-in-one solution, but instead is a pluggable +// component at the bottom of the logging stack that simply controls the files +// to which logs are written. +// +// Lumberjack plays well with any logging package that can write to an +// io.Writer, including the standard library's log package. +// +// Lumberjack assumes that only one process is writing to the output files. +// Using the same lumberjack configuration from multiple processes on the same +// machine will result in improper behavior. +package lumberjack + +import ( + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" +) + +const ( + backupTimeFormat = "2006-01-02T15-04-05.000" + compressSuffix = ".gz" + defaultMaxSize = 100 +) + +// ensure we always implement io.WriteCloser +var _ io.WriteCloser = (*Logger)(nil) + +// Logger is an io.WriteCloser that writes to the specified filename. +// +// Logger opens or creates the logfile on first Write. If the file exists and +// is less than MaxSize megabytes, lumberjack will open and append to that file. +// If the file exists and its size is >= MaxSize megabytes, the file is renamed +// by putting the current time in a timestamp in the name immediately before the +// file's extension (or the end of the filename if there's no extension). A new +// log file is then created using original filename. +// +// Whenever a write would cause the current log file exceed MaxSize megabytes, +// the current file is closed, renamed, and a new log file created with the +// original name. Thus, the filename you give Logger is always the "current" log +// file. +// +// Backups use the log file name given to Logger, in the form +// `name-timestamp.ext` where name is the filename without the extension, +// timestamp is the time at which the log was rotated formatted with the +// time.Time format of `2006-01-02T15-04-05.000` and the extension is the +// original extension. For example, if your Logger.Filename is +// `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would +// use the filename `/var/log/foo/server-2016-11-04T18-30-00.000.log` +// +// Cleaning Up Old Log Files +// +// Whenever a new logfile gets created, old log files may be deleted. The most +// recent files according to the encoded timestamp will be retained, up to a +// number equal to MaxBackups (or all of them if MaxBackups is 0). Any files +// with an encoded timestamp older than MaxAge days are deleted, regardless of +// MaxBackups. Note that the time encoded in the timestamp is the rotation +// time, which may differ from the last time that file was written to. +// +// If MaxBackups and MaxAge are both 0, no old log files will be deleted. +type Logger struct { + // Filename is the file to write logs to. Backup log files will be retained + // in the same directory. It uses -lumberjack.log in + // os.TempDir() if empty. + Filename string `json:"filename" yaml:"filename"` + + // MaxSize is the maximum size in megabytes of the log file before it gets + // rotated. It defaults to 100 megabytes. + MaxSize int `json:"maxsize" yaml:"maxsize"` + + // MaxAge is the maximum number of days to retain old log files based on the + // timestamp encoded in their filename. Note that a day is defined as 24 + // hours and may not exactly correspond to calendar days due to daylight + // savings, leap seconds, etc. The default is not to remove old log files + // based on age. + MaxAge int `json:"maxage" yaml:"maxage"` + + // MaxBackups is the maximum number of old log files to retain. The default + // is to retain all old log files (though MaxAge may still cause them to get + // deleted.) + MaxBackups int `json:"maxbackups" yaml:"maxbackups"` + + // LocalTime determines if the time used for formatting the timestamps in + // backup files is the computer's local time. The default is to use UTC + // time. + LocalTime bool `json:"localtime" yaml:"localtime"` + + // Compress determines if the rotated log files should be compressed + // using gzip. + Compress bool `json:"compress" yaml:"compress"` + + size int64 + file *os.File + mu sync.Mutex + + millCh chan bool + startMill sync.Once +} + +var ( + // currentTime exists so it can be mocked out by tests. + currentTime = time.Now + + // os_Stat exists so it can be mocked out by tests. + os_Stat = os.Stat + + // megabyte is the conversion factor between MaxSize and bytes. It is a + // variable so tests can mock it out and not need to write megabytes of data + // to disk. + megabyte = 1024 * 1024 +) + +// Write implements io.Writer. If a write would cause the log file to be larger +// than MaxSize, the file is closed, renamed to include a timestamp of the +// current time, and a new log file is created using the original log file name. +// If the length of the write is greater than MaxSize, an error is returned. +func (l *Logger) Write(p []byte) (n int, err error) { + l.mu.Lock() + defer l.mu.Unlock() + + writeLen := int64(len(p)) + if writeLen > l.max() { + return 0, fmt.Errorf( + "write length %d exceeds maximum file size %d", writeLen, l.max(), + ) + } + + if l.file == nil { + if err = l.openExistingOrNew(len(p)); err != nil { + return 0, err + } + } + + if l.size+writeLen > l.max() { + if err := l.rotate(); err != nil { + return 0, err + } + } + + n, err = l.file.Write(p) + l.size += int64(n) + + return n, err +} + +// Close implements io.Closer, and closes the current logfile. +func (l *Logger) Close() error { + l.mu.Lock() + defer l.mu.Unlock() + return l.close() +} + +// close closes the file if it is open. +func (l *Logger) close() error { + if l.file == nil { + return nil + } + err := l.file.Close() + l.file = nil + return err +} + +// Rotate causes Logger to close the existing log file and immediately create a +// new one. This is a helper function for applications that want to initiate +// rotations outside of the normal rotation rules, such as in response to +// SIGHUP. After rotating, this initiates compression and removal of old log +// files according to the configuration. +func (l *Logger) Rotate() error { + l.mu.Lock() + defer l.mu.Unlock() + return l.rotate() +} + +// rotate closes the current file, moves it aside with a timestamp in the name, +// (if it exists), opens a new file with the original filename, and then runs +// post-rotation processing and removal. +func (l *Logger) rotate() error { + if err := l.close(); err != nil { + return err + } + if err := l.openNew(); err != nil { + return err + } + l.mill() + return nil +} + +// openNew opens a new log file for writing, moving any old log file out of the +// way. This methods assumes the file has already been closed. +func (l *Logger) openNew() error { + err := os.MkdirAll(l.dir(), 0744) + if err != nil { + return fmt.Errorf("can't make directories for new logfile: %s", err) + } + + name := l.filename() + mode := os.FileMode(0644) + info, err := os_Stat(name) + if err == nil { + // Copy the mode off the old logfile. + mode = info.Mode() + // move the existing file + newname := backupName(name, l.LocalTime) + if err := os.Rename(name, newname); err != nil { + return fmt.Errorf("can't rename log file: %s", err) + } + + // this is a no-op anywhere but linux + if err := chown(name, info); err != nil { + return err + } + } + + // we use truncate here because this should only get called when we've moved + // the file ourselves. if someone else creates the file in the meantime, + // just wipe out the contents. + f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return fmt.Errorf("can't open new logfile: %s", err) + } + l.file = f + l.size = 0 + return nil +} + +// backupName creates a new filename from the given name, inserting a timestamp +// between the filename and the extension, using the local time if requested +// (otherwise UTC). +func backupName(name string, local bool) string { + dir := filepath.Dir(name) + filename := filepath.Base(name) + ext := filepath.Ext(filename) + prefix := filename[:len(filename)-len(ext)] + t := currentTime() + if !local { + t = t.UTC() + } + + timestamp := t.Format(backupTimeFormat) + return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)) +} + +// openExistingOrNew opens the logfile if it exists and if the current write +// would not put it over MaxSize. If there is no such file or the write would +// put it over the MaxSize, a new file is created. +func (l *Logger) openExistingOrNew(writeLen int) error { + l.mill() + + filename := l.filename() + info, err := os_Stat(filename) + if os.IsNotExist(err) { + return l.openNew() + } + if err != nil { + return fmt.Errorf("error getting log file info: %s", err) + } + + if info.Size()+int64(writeLen) >= l.max() { + return l.rotate() + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + // if we fail to open the old log file for some reason, just ignore + // it and open a new log file. + return l.openNew() + } + l.file = file + l.size = info.Size() + return nil +} + +// genFilename generates the name of the logfile from the current time. +func (l *Logger) filename() string { + if l.Filename != "" { + return l.Filename + } + name := filepath.Base(os.Args[0]) + "-lumberjack.log" + return filepath.Join(os.TempDir(), name) +} + +// millRunOnce performs compression and removal of stale log files. +// Log files are compressed if enabled via configuration and old log +// files are removed, keeping at most l.MaxBackups files, as long as +// none of them are older than MaxAge. +func (l *Logger) millRunOnce() error { + if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress { + return nil + } + + files, err := l.oldLogFiles() + if err != nil { + return err + } + + var compress, remove []logInfo + + if l.MaxBackups > 0 && l.MaxBackups < len(files) { + preserved := make(map[string]bool) + var remaining []logInfo + for _, f := range files { + // Only count the uncompressed log file or the + // compressed log file, not both. + fn := f.Name() + if strings.HasSuffix(fn, compressSuffix) { + fn = fn[:len(fn)-len(compressSuffix)] + } + preserved[fn] = true + + if len(preserved) > l.MaxBackups { + remove = append(remove, f) + } else { + remaining = append(remaining, f) + } + } + files = remaining + } + if l.MaxAge > 0 { + diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge)) + cutoff := currentTime().Add(-1 * diff) + + var remaining []logInfo + for _, f := range files { + if f.timestamp.Before(cutoff) { + remove = append(remove, f) + } else { + remaining = append(remaining, f) + } + } + files = remaining + } + + if l.Compress { + for _, f := range files { + if !strings.HasSuffix(f.Name(), compressSuffix) { + compress = append(compress, f) + } + } + } + + for _, f := range remove { + errRemove := os.Remove(filepath.Join(l.dir(), f.Name())) + if err == nil && errRemove != nil { + err = errRemove + } + } + for _, f := range compress { + fn := filepath.Join(l.dir(), f.Name()) + errCompress := compressLogFile(fn, fn+compressSuffix) + if err == nil && errCompress != nil { + err = errCompress + } + } + + return err +} + +// millRun runs in a goroutine to manage post-rotation compression and removal +// of old log files. +func (l *Logger) millRun() { + for _ = range l.millCh { + // what am I going to do, log this? + _ = l.millRunOnce() + } +} + +// mill performs post-rotation compression and removal of stale log files, +// starting the mill goroutine if necessary. +func (l *Logger) mill() { + l.startMill.Do(func() { + l.millCh = make(chan bool, 1) + go l.millRun() + }) + select { + case l.millCh <- true: + default: + } +} + +// oldLogFiles returns the list of backup log files stored in the same +// directory as the current log file, sorted by ModTime +func (l *Logger) oldLogFiles() ([]logInfo, error) { + files, err := ioutil.ReadDir(l.dir()) + if err != nil { + return nil, fmt.Errorf("can't read log file directory: %s", err) + } + logFiles := []logInfo{} + + prefix, ext := l.prefixAndExt() + + for _, f := range files { + if f.IsDir() { + continue + } + if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil { + logFiles = append(logFiles, logInfo{t, f}) + continue + } + if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil { + logFiles = append(logFiles, logInfo{t, f}) + continue + } + // error parsing means that the suffix at the end was not generated + // by lumberjack, and therefore it's not a backup file. + } + + sort.Sort(byFormatTime(logFiles)) + + return logFiles, nil +} + +// timeFromName extracts the formatted time from the filename by stripping off +// the filename's prefix and extension. This prevents someone's filename from +// confusing time.parse. +func (l *Logger) timeFromName(filename, prefix, ext string) (time.Time, error) { + if !strings.HasPrefix(filename, prefix) { + return time.Time{}, errors.New("mismatched prefix") + } + if !strings.HasSuffix(filename, ext) { + return time.Time{}, errors.New("mismatched extension") + } + ts := filename[len(prefix) : len(filename)-len(ext)] + return time.Parse(backupTimeFormat, ts) +} + +// max returns the maximum size in bytes of log files before rolling. +func (l *Logger) max() int64 { + if l.MaxSize == 0 { + return int64(defaultMaxSize * megabyte) + } + return int64(l.MaxSize) * int64(megabyte) +} + +// dir returns the directory for the current filename. +func (l *Logger) dir() string { + return filepath.Dir(l.filename()) +} + +// prefixAndExt returns the filename part and extension part from the Logger's +// filename. +func (l *Logger) prefixAndExt() (prefix, ext string) { + filename := filepath.Base(l.filename()) + ext = filepath.Ext(filename) + prefix = filename[:len(filename)-len(ext)] + "-" + return prefix, ext +} + +// compressLogFile compresses the given log file, removing the +// uncompressed log file if successful. +func compressLogFile(src, dst string) (err error) { + f, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open log file: %v", err) + } + defer f.Close() + + fi, err := os_Stat(src) + if err != nil { + return fmt.Errorf("failed to stat log file: %v", err) + } + + if err := chown(dst, fi); err != nil { + return fmt.Errorf("failed to chown compressed log file: %v", err) + } + + // If this file already exists, we presume it was created by + // a previous attempt to compress the log file. + gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) + if err != nil { + return fmt.Errorf("failed to open compressed log file: %v", err) + } + defer gzf.Close() + + gz := gzip.NewWriter(gzf) + + defer func() { + if err != nil { + os.Remove(dst) + err = fmt.Errorf("failed to compress log file: %v", err) + } + }() + + if _, err := io.Copy(gz, f); err != nil { + return err + } + if err := gz.Close(); err != nil { + return err + } + if err := gzf.Close(); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + if err := os.Remove(src); err != nil { + return err + } + + return nil +} + +// logInfo is a convenience struct to return the filename and its embedded +// timestamp. +type logInfo struct { + timestamp time.Time + os.FileInfo +} + +// byFormatTime sorts by newest time formatted in the name. +type byFormatTime []logInfo + +func (b byFormatTime) Less(i, j int) bool { + return b[i].timestamp.After(b[j].timestamp) +} + +func (b byFormatTime) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +func (b byFormatTime) Len() int { + return len(b) +} From b438d47bce5537f2a96ccbb25a380ea56f06a3f5 Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Mon, 1 Jul 2019 18:51:54 -0700 Subject: [PATCH 05/15] use CANT_LOG_PROPERLY for eventlog open fail --- multiuser_service_windows.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index e9658855..0bd25451 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -83,9 +83,10 @@ func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, change loop: for { select { + // we send this when RunWorker exits case <-interruptChan: - // we send this when RunWorker exits break loop + // received change request case c := <-r: switch c.Cmd { case svc.Interrogate: @@ -123,7 +124,7 @@ func runService(name string, isDebug bool) ExitCode { } else { elog, err = eventlog.Open(name) if err != nil { - exitOnError(INTERNAL_ERROR, err, "Could not open eventlog %q", name) + exitOnError(CANT_LOG_PROPERLY, err, "Could not open eventlog %q", name) } } logWriter = io.MultiWriter(logWriter, elogWrapper{elog}) @@ -149,7 +150,7 @@ func runService(name string, isDebug bool) ExitCode { } err = run(name, &windowsService{}) if err != nil { - exitOnError(INTERNAL_ERROR, err, "Service %q failed", name) + exitOnError(INTERNAL_ERROR, err, "Failed to start service %q", name) } // use Output to use all configured loggers and handle err // io.MultiWriter fails for _all_ writers if any fail @@ -174,7 +175,7 @@ func deployService(configFile, name, exePath string, configureForAWS bool, confi s, err := m.OpenService(name) if err == nil { s.Close() - return fmt.Errorf("service %s already exists", name) + return fmt.Errorf("Service %s already exists", name) } args := []string{ "run-service", From ecc18fcb2a790b2e07883d72d63384a451ba017c Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 2 Jul 2019 08:55:15 -0700 Subject: [PATCH 06/15] windows: run go test with -tags multiuser --- build.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cmd b/build.cmd index c8882fa6..b4b05041 100644 --- a/build.cmd +++ b/build.cmd @@ -23,5 +23,5 @@ git rev-parse HEAD > revision.txt set /p REVISION=< revision.txt del revision.txt set GORACE=history_size=7 -go test -ldflags "-X github.com/taskcluster/generic-worker.revision=%REVISION%" ./... || exit /b %ERRORLEVEL% +go test -tags multiuser -ldflags "-X github.com/taskcluster/generic-worker.revision=%REVISION%" ./... || exit /b %ERRORLEVEL% ineffassign . || exit /b %ERRORLEVEL% From 1cbeffac4f70b339257ebe6afc14d9e64aa4a79c Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 2 Jul 2019 17:06:32 -0700 Subject: [PATCH 07/15] change CANT_REMOVE_GENERIC_WORKER code to a fresh one --- usage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usage.go b/usage.go index 14971b98..d8d9ee43 100644 --- a/usage.go +++ b/usage.go @@ -7,7 +7,6 @@ const ( TASKS_COMPLETE ExitCode = 0 CANT_LOAD_CONFIG ExitCode = 64 CANT_INSTALL_GENERIC_WORKER ExitCode = 65 - CANT_REMOVE_GENERIC_WORKER ExitCode = 66 REBOOT_REQUIRED ExitCode = 67 IDLE_TIMEOUT ExitCode = 68 INTERNAL_ERROR ExitCode = 69 @@ -19,6 +18,7 @@ const ( CANT_SAVE_CONFIG ExitCode = 76 CANT_SECURE_CONFIG ExitCode = 77 CANT_LOG_PROPERLY ExitCode = 78 + CANT_REMOVE_GENERIC_WORKER ExitCode = 79 ) func usage(versionName string) string { From 99d9d7be392218d9b0817e53fce672e8740934bb Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 2 Jul 2019 17:07:03 -0700 Subject: [PATCH 08/15] add a test for install/remove service add TestRunServiceWithoutEventlogSource, TestRunServiceWithBrokenWriter, use SKIP_ADMINISTRATOR_TESTS env var to skip service install/remove tests --- helper_windows_test.go | 5 + multiuser_service_windows.go | 41 ++++-- multiuser_service_windows_test.go | 221 ++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 multiuser_service_windows_test.go diff --git a/helper_windows_test.go b/helper_windows_test.go index 0e0de049..01eca4c6 100644 --- a/helper_windows_test.go +++ b/helper_windows_test.go @@ -116,3 +116,8 @@ func copyTestdataFileTo(src, dest string) []string { func singleCommandNoArgs(command string) []string { return []string{command} } + +func shouldRunAdminTests() bool { + _, ok := os.LookupEnv("SKIP_ADMINISTRATOR_TESTS") + return !ok +} diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 0bd25451..4734ef6c 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -67,7 +67,7 @@ type windowsService struct{} // implements Execute for svc.Handler func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { - const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} // Start worker with interruptChan @@ -95,19 +95,16 @@ loop: time.Sleep(100 * time.Millisecond) changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: + changes <- svc.Status{State: svc.StopPending} log.Printf("Shutting down, received %v", c) interruptChan <- os.Interrupt break loop - case svc.Pause: - changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} - case svc.Continue: - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} default: log.Printf("Unexpected control request #%d", c) } } } - changes <- svc.Status{State: svc.StopPending} + changes <- svc.Status{State: svc.Stopped} return true, exitCode } @@ -194,6 +191,12 @@ func deployService(configFile, name, exePath string, configureForAWS bool, confi return err } log.Printf("Successfully installed service %q.", name) + + // start service manually in order to fail fast + err = s.Start(args...) + if err != nil { + return fmt.Errorf("Error starting service %q: %v", name, err) + } return nil } @@ -225,26 +228,34 @@ func installService(name, exePath string, args []string) error { log.Printf("Created service %q with exePath %q, and args %v", name, exePath, args) - // TODO configure an eventlog message file + // minimal eventlog message file // https://docs.microsoft.com/en-us/windows/desktop/eventlog/message-files + messageFileText := `MessageIdTypedef=DWORD +SeverityNames=(Informational=0x1:STATUS_SEVERITY_INFORMATIONAL) +FacilityNames=(System=0x0:FACILITY_SYSTEM) + +MessageId=0x1 +Severity=Informational +Facility=System +Language=English` + + eventlogMessageFilepath := filepath.Join(dir, "eventlog-message-file.txt") + err = ioutil.WriteFile(eventlogMessageFilepath, []byte(messageFileText), 0644) + if err != nil { + return fmt.Errorf("Could not write eventlog message file: %s", err) + } // configure eventlog source err = eventlog.Install( name, - filepath.Join(dir, "eventlog-message-file.txt"), - false, + eventlogMessageFilepath, + true, eventlog.Error|eventlog.Warning|eventlog.Info, ) if err != nil { s.Delete() return fmt.Errorf("Setting up eventlog source failed: %s", err) } - - // start service manually in order to fail fast - err = s.Start(args...) - if err != nil { - return fmt.Errorf("Error starting service %q: %v", name, err) - } return nil } diff --git a/multiuser_service_windows_test.go b/multiuser_service_windows_test.go new file mode 100644 index 00000000..3ed9bc9f --- /dev/null +++ b/multiuser_service_windows_test.go @@ -0,0 +1,221 @@ +package main + +import ( + "fmt" + "io" + "os" + "testing" + "time" + + "golang.org/x/sys/windows/svc" + + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" + + "github.com/taskcluster/slugid-go/slugid" +) + +func unprivilegedRecovery(t *testing.T) { + if r := recover(); r != nil { + t.Log("A panic can occur if tests are run as an unprivileged user.") + t.Log("Disable tests that require administrator privileges by setting `SKIP_ADMINISTRATOR_TESTS` in your environment") + t.Logf("Caught panic: %v", r) + } +} + +func setupService(t *testing.T, name string) { + // doesn't have to be real + path := os.Args[0] + args := []string{} + err := installService(name, path, args) + if err != nil { + t.Fatal(err) + } +} + +func cleanupService(t *testing.T, name string) { + // remove service + err := removeService(name) + if err != nil { + t.Fatal(err) + } +} + +// requires elevated privileges +func TestInstallAndRemoveService(t *testing.T) { + defer unprivilegedRecovery(t) + + if !shouldRunAdminTests() { + t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) + } + + name := "generic-worker-" + slugid.Nice() + setupService(t, name) + + // service manager + m, err := mgr.Connect() + if err != nil { + t.Fatal(err) + } + defer m.Disconnect() + + // check for service + _, err = m.OpenService(name) + if err != nil { + t.Fatalf("Did not find expected service %q: %v", name, err) + } + + // check for eventlog source + elog, err := eventlog.Open(name) + if err != nil { + t.Fatalf("Did not find expected eventlog source %q: %v", name, err) + } + elog.Close() + + cleanupService(t, name) + + // this is unrealistic, usually the service is marked for deletion + // but not actually removed until reboot + + // // hopefully service gets removed + // <-time.After(2 * time.Second) + + // // verify service is removed + // _, err = m.OpenService(name) + // if err == nil { + // t.Fatalf("Found service %q after it should have been removed", name) + // } + + // // verify eventlog is removed + // _, err = eventlog.Open(name) + // if err == nil { + // t.Fatalf("Found eventlog source %q after it should have been removed", name) + // } +} + +func TestRunServiceWithoutEventlogSource(t *testing.T) { + defer unprivilegedRecovery(t) + defer setup(t)() + + if !shouldRunAdminTests() { + t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) + } + + name := "generic-worker-" + slugid.Nice() + setupService(t, name) + + // remove eventlog source + err := eventlog.Remove(name) + if err != nil { + t.Fatal(err) + } + + // now we try runService in non-interactive mode + // which should attempt to use eventlog and fail + // with CANT_LOG_PROPERLY + + exitCode := runService(name, false) + + if exitCode != CANT_LOG_PROPERLY { + t.Fatalf("Expected runService() to exit with CANT_LOG_PROPERLY, got %q", exitCode) + } + + cleanupService(t, name) +} + +type brokenWriter struct{} + +func (w brokenWriter) Write(bs []byte) (int, error) { + return -1, fmt.Errorf("broken writer is broken") +} + +func TestRunServiceWithBrokenWriter(t *testing.T) { + defer unprivilegedRecovery(t) + defer setup(t)() + + if !shouldRunAdminTests() { + t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) + } + + name := "generic-worker-" + slugid.Nice() + setupService(t, name) + + // add brokenWriter + logWriter = io.MultiWriter(logWriter, brokenWriter{}) + + // now we try runService in non-interactive mode + // which should attempt to use brokenWriter and fail + // with CANT_LOG_PROPERLY + + exitCode := runService(name, false) + + if exitCode != CANT_LOG_PROPERLY { + t.Fatalf("Expected runService() to exit with CANT_LOG_PROPERLY, got %q", exitCode) + } + + cleanupService(t, name) +} + +func TestWindowsServiceInteraction(t *testing.T) { + defer setup(t)() + + // Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) + r := make(chan svc.ChangeRequest, 1) + c := make(chan svc.Status, 1) + + s := windowsService{} + + var exitCode uint32 + go func() { + _, exitCode = s.Execute([]string{}, r, c) + t.Logf("Got exit code %v from Execute()", exitCode) + }() + + var status svc.Status + select { + case <-time.After(time.Second * 5): + t.Fatalf("Timeout waiting for status svc.StartPending") + case status = <-c: + if status.State != svc.StartPending { + t.Fatalf("Expected state svc.StartPending, got status %v", status) + } + } + + select { + case <-time.After(time.Second * 5): + t.Fatalf("Timeout waiting for status svc.Running") + case status = <-c: + if status.State != svc.Running { + t.Fatalf("Expected state svc.Running, got status %v", status) + } + } + + <-time.After(1 * time.Second) + + // send Stop + r <- svc.ChangeRequest{Cmd: svc.Stop} + + fmt.Printf("Sent Stop change request") + + select { + case <-time.After(time.Second * 5): + t.Fatalf("Timeout waiting for status svc.StopPending") + case status = <-c: + if status.State != svc.StopPending { + t.Fatalf("Expected state svc.StopPending, got status %v", status) + } + } + + select { + case <-time.After(time.Second * 5): + t.Fatalf("Timeout waiting for status svc.Stopped") + case status = <-c: + if status.State != svc.Stopped { + t.Fatalf("Expected state svc.Stopped, got status %v", status) + } + } + + if ExitCode(exitCode) != WORKER_STOPPED { + t.Fatalf("Expected exit code %v, got: %v", WORKER_STOPPED, exitCode) + } +} From 115afd33e97b267906439ed6789994f670875dac Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Mon, 8 Jul 2019 16:02:48 -0700 Subject: [PATCH 09/15] split out eventlog configuration from service configuration, fix tests locally rewrite runservice to return ExitCodes properly --- .gitattributes | 2 +- main.go | 16 ++-- multiuser_service_windows.go | 52 ++++++++----- multiuser_service_windows_test.go | 123 +++++++++++++++++++----------- 4 files changed, 119 insertions(+), 74 deletions(-) diff --git a/.gitattributes b/.gitattributes index aedcb05a..ba500954 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ /testdata/** eol=lf -* text=auto +# * text=auto diff --git a/main.go b/main.go index 0e148d69..cf9261c8 100644 --- a/main.go +++ b/main.go @@ -1189,19 +1189,23 @@ func RotateTaskEnvironment() (reboot bool) { return false } -func exitOnError(exitCode ExitCode, err error, logMessage string, args ...interface{}) { - if err == nil { - return - } +func writeCrashFile(exitCode ExitCode, err error, logMessage string, args ...interface{}) { // useful for debugging broken logging filename := filepath.Join( filepath.Dir(os.Args[0]), - fmt.Sprintf("generic-worker-crash-%d.log", time.Now().Unix()), + fmt.Sprintf("generic-worker-crash-exit-%d-%d.log", exitCode, time.Now().Unix()), ) - err = ioutil.WriteFile(filename, []byte(fmt.Sprintf(logMessage, args...)+"\n"+err.Error()), 0666) + err = ioutil.WriteFile(filename, []byte(fmt.Sprintf("exited with %d, %q, %#v", exitCode, logMessage, args)+"\n"+err.Error()), 0666) if err != nil { log.Printf("Could not open crash file %q: %v", filename, err) } +} + +func exitOnError(exitCode ExitCode, err error, logMessage string, args ...interface{}) { + if err == nil { + return + } + writeCrashFile(exitCode, err, logMessage, args...) log.Printf(logMessage, args...) log.Printf("%v", err) os.Exit(int(exitCode)) diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 4734ef6c..12bf166f 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -25,13 +25,17 @@ import ( var logWriter = io.MultiWriter(ioutil.Discard) func init() { + // default for a service is C:\Windows\system32 + dir := path.Dir(os.Args[0]) + err := os.Chdir(dir) + if err != nil { + exitOnError(INTERNAL_ERROR, err, "Unable to chdir to %q", dir) + } log.SetOutput(logWriter) - manageLogFile() + manageLogFile(filepath.Join(path.Dir(os.Args[0]), "generic-worker.log")) } -func manageLogFile() { - dir := filepath.Dir(os.Args[0]) - logPath := filepath.Join(dir, "generic-worker.log") +func manageLogFile(logPath string) { logWriter = io.MultiWriter( logWriter, &lumberjack.Logger{ @@ -120,8 +124,9 @@ func runService(name string, isDebug bool) ExitCode { elog = debug.New(name) } else { elog, err = eventlog.Open(name) - if err != nil { - exitOnError(CANT_LOG_PROPERLY, err, "Could not open eventlog %q", name) + if err != nil || elog == nil { + writeCrashFile(CANT_LOG_PROPERLY, err, "Could not open eventlog %q", name) + return CANT_LOG_PROPERLY } } logWriter = io.MultiWriter(logWriter, elogWrapper{elog}) @@ -130,16 +135,11 @@ func runService(name string, isDebug bool) ExitCode { // so we aggressively handle that scenario err = log.Output(2, fmt.Sprintf("Wrote to eventlog %q successfully", name)) if err != nil { - exitOnError(CANT_LOG_PROPERLY, err, "Unable to log to eventlog %q with writer: %v", name, logWriter) + writeCrashFile(CANT_LOG_PROPERLY, err, "Unable to log to eventlog %q with writer: %v", name, logWriter) + return CANT_LOG_PROPERLY } defer elog.Close() - dir := path.Dir(os.Args[0]) - err = os.Chdir(dir) - if err != nil { - exitOnError(INTERNAL_ERROR, err, "Unable to chdir to %q", dir) - } - log.Printf("Starting service %q", name) run := svc.Run if isDebug { @@ -147,7 +147,8 @@ func runService(name string, isDebug bool) ExitCode { } err = run(name, &windowsService{}) if err != nil { - exitOnError(INTERNAL_ERROR, err, "Failed to start service %q", name) + writeCrashFile(INTERNAL_ERROR, err, "Failed to start service %q", name) + return CANT_LOG_PROPERLY } // use Output to use all configured loggers and handle err // io.MultiWriter fails for _all_ writers if any fail @@ -155,7 +156,8 @@ func runService(name string, isDebug bool) ExitCode { // silently eat errors err = log.Output(2, fmt.Sprintf("Stopped service %q", name)) if err != nil { - exitOnError(CANT_LOG_PROPERLY, err, "Unable to log to one or more log outputs, configured writer: %v", logWriter) + writeCrashFile(CANT_LOG_PROPERLY, err, "Unable to log to one or more log outputs, configured writer: %v", logWriter) + return CANT_LOG_PROPERLY } return 0 } @@ -186,11 +188,16 @@ func deployService(configFile, name, exePath string, configureForAWS bool, confi if configureForGCP { args = append(args, "--configure-for-gcp") } - err = installService(name, exePath, args) + err = configureService(name, exePath, args) if err != nil { return err } - log.Printf("Successfully installed service %q.", name) + log.Printf("Successfully configured service %q.", name) + err = configureEventlogSource(name, exePath) + if err != nil { + return err + } + log.Printf("Successfully configured eventlog source %q.", name) // start service manually in order to fail fast err = s.Start(args...) @@ -200,7 +207,7 @@ func deployService(configFile, name, exePath string, configureForAWS bool, confi return nil } -func installService(name, exePath string, args []string) error { +func configureService(name, exePath string, args []string) error { config := mgr.Config{ DisplayName: name, Description: "A taskcluster worker that runs on all mainstream platforms", @@ -209,7 +216,6 @@ func installService(name, exePath string, args []string) error { ServiceType: windows.SERVICE_WIN32_OWN_PROCESS | windows.SERVICE_INTERACTIVE_PROCESS, StartType: mgr.StartAutomatic, } - dir := filepath.Dir(exePath) m, err := mgr.Connect() if err != nil { return err @@ -227,6 +233,11 @@ func installService(name, exePath string, args []string) error { defer s.Close() log.Printf("Created service %q with exePath %q, and args %v", name, exePath, args) + return nil +} + +func configureEventlogSource(name, exePath string) error { + dir := filepath.Dir(exePath) // minimal eventlog message file // https://docs.microsoft.com/en-us/windows/desktop/eventlog/message-files @@ -240,7 +251,7 @@ Facility=System Language=English` eventlogMessageFilepath := filepath.Join(dir, "eventlog-message-file.txt") - err = ioutil.WriteFile(eventlogMessageFilepath, []byte(messageFileText), 0644) + err := ioutil.WriteFile(eventlogMessageFilepath, []byte(messageFileText), 0644) if err != nil { return fmt.Errorf("Could not write eventlog message file: %s", err) } @@ -253,7 +264,6 @@ Language=English` eventlog.Error|eventlog.Warning|eventlog.Info, ) if err != nil { - s.Delete() return fmt.Errorf("Setting up eventlog source failed: %s", err) } return nil diff --git a/multiuser_service_windows_test.go b/multiuser_service_windows_test.go index 3ed9bc9f..4c895b7a 100644 --- a/multiuser_service_windows_test.go +++ b/multiuser_service_windows_test.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io" "os" "testing" "time" @@ -23,14 +22,20 @@ func unprivilegedRecovery(t *testing.T) { } } -func setupService(t *testing.T, name string) { +func setupService(t *testing.T, name string, configureEventlog bool) { // doesn't have to be real path := os.Args[0] args := []string{} - err := installService(name, path, args) + err := configureService(name, path, args) if err != nil { t.Fatal(err) } + if configureEventlog { + err = configureEventlogSource(name, path) + if err != nil { + t.Fatal(err) + } + } } func cleanupService(t *testing.T, name string) { @@ -42,7 +47,7 @@ func cleanupService(t *testing.T, name string) { } // requires elevated privileges -func TestInstallAndRemoveService(t *testing.T) { +func TestConfigureAndRemoveService(t *testing.T) { defer unprivilegedRecovery(t) if !shouldRunAdminTests() { @@ -50,7 +55,8 @@ func TestInstallAndRemoveService(t *testing.T) { } name := "generic-worker-" + slugid.Nice() - setupService(t, name) + setupService(t, name, true) + defer cleanupService(t, name) // service manager m, err := mgr.Connect() @@ -72,8 +78,6 @@ func TestInstallAndRemoveService(t *testing.T) { } elog.Close() - cleanupService(t, name) - // this is unrealistic, usually the service is marked for deletion // but not actually removed until reboot @@ -93,67 +97,94 @@ func TestInstallAndRemoveService(t *testing.T) { // } } -func TestRunServiceWithoutEventlogSource(t *testing.T) { - defer unprivilegedRecovery(t) - defer setup(t)() - - if !shouldRunAdminTests() { - t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) - } - - name := "generic-worker-" + slugid.Nice() - setupService(t, name) - - // remove eventlog source - err := eventlog.Remove(name) - if err != nil { - t.Fatal(err) - } - - // now we try runService in non-interactive mode - // which should attempt to use eventlog and fail - // with CANT_LOG_PROPERLY - - exitCode := runService(name, false) - - if exitCode != CANT_LOG_PROPERLY { - t.Fatalf("Expected runService() to exit with CANT_LOG_PROPERLY, got %q", exitCode) - } - - cleanupService(t, name) -} - type brokenWriter struct{} func (w brokenWriter) Write(bs []byte) (int, error) { return -1, fmt.Errorf("broken writer is broken") } +// func wrapTest(t *testing.T, name string, errorFunc func(*exec.ExitError)) { +// if we are running the test +// as opposed to wrapping the test +// if os.Getenv(t.Name()) == "1" { +// name := "generic-worker-" + slugid.Nice() +// setupService(t, name, true) +// defer cleanupService(t, name) +// // use brokenWriter +// logWriter = brokenWriter{} +// // now we try runService in non-interactive mode +// // which should attempt to use brokenWriter and fail +// // with CANT_LOG_PROPERLY +// runService(name, false) +// return +// } + +// // wrapping the test + +// // exitOnError will be called, so os.Exit() will be called +// // so we need to run this test in a wrapper + +// cmd := exec.Command(os.Args[0], fmt.Sprintf("-test.run=%s", t.Name())) +// cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", t.Name())) +// stderrPipe, err := cmd.StderrPipe() +// if err != nil { +// t.Fatal(err) +// } +// stdoutPipe, err := cmd.StdoutPipe() +// if err != nil { +// t.Fatalf("Error getting StdoutPipe: %v", err) +// } +// if err := cmd.Start(); err != nil { +// t.Fatal(err) +// } +// stderr, err := ioutil.ReadAll(stderrPipe) +// if err != nil { +// t.Fatal(err) +// } +// stdout, err := ioutil.ReadAll(stdoutPipe) +// if err != nil { +// t.Fatal(err) +// } +// err = cmd.Wait() +// // if nonzero exit code, castable to ExitError +// if exitError, ok := err.(*exec.ExitError); ok && err != nil { +// // as expected, there was an error +// exitCode := exitError.ExitCode() +// if exitCode != int(CANT_LOG_PROPERLY) { +// t.Logf("Expected runService() to exit with CANT_LOG_PROPERLY, got: %s", exitError.Error()) +// t.Logf("stderr: %s", stderr) +// t.Logf("stdout: %s", stdout) +// t.Fail() +// } +// } else { +// t.Fatalf("Expected an error from %q: %v", strings.Join(cmd.Args, " "), err) +// } +// } + func TestRunServiceWithBrokenWriter(t *testing.T) { defer unprivilegedRecovery(t) - defer setup(t)() if !shouldRunAdminTests() { t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) } + // if we are running the test + // as opposed to wrapping the test name := "generic-worker-" + slugid.Nice() - setupService(t, name) + setupService(t, name, true) + defer cleanupService(t, name) - // add brokenWriter - logWriter = io.MultiWriter(logWriter, brokenWriter{}) + // use brokenWriter + logWriter = brokenWriter{} // now we try runService in non-interactive mode // which should attempt to use brokenWriter and fail // with CANT_LOG_PROPERLY - exitCode := runService(name, false) - + // as expected, there was an error if exitCode != CANT_LOG_PROPERLY { - t.Fatalf("Expected runService() to exit with CANT_LOG_PROPERLY, got %q", exitCode) + t.Fatalf("Expected runService() to exit with CANT_LOG_PROPERLY, got: %v", exitCode) } - - cleanupService(t, name) } func TestWindowsServiceInteraction(t *testing.T) { From 2a46fe770a56c370a00ee5c3b6ce225cc92b15be Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 9 Jul 2019 10:10:18 -0700 Subject: [PATCH 10/15] fix race in Execute() / TestWindowsServiceInteraction --- multiuser_service_windows.go | 3 +++ multiuser_service_windows_test.go | 20 ++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 12bf166f..8302c14a 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -76,11 +76,13 @@ func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, change // Start worker with interruptChan interruptChan := make(chan os.Signal, 1) + exitChan := make(chan uint32, 1) go func() { exitCode = uint32(RunWorker(interruptChan)) // kill the service interruptChan <- os.Interrupt + exitChan <- exitCode }() changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} @@ -109,6 +111,7 @@ loop: } } changes <- svc.Status{State: svc.Stopped} + exitCode = <-exitChan return true, exitCode } diff --git a/multiuser_service_windows_test.go b/multiuser_service_windows_test.go index 4c895b7a..2b68e614 100644 --- a/multiuser_service_windows_test.go +++ b/multiuser_service_windows_test.go @@ -196,10 +196,12 @@ func TestWindowsServiceInteraction(t *testing.T) { s := windowsService{} - var exitCode uint32 go func() { - _, exitCode = s.Execute([]string{}, r, c) + _, exitCode := s.Execute([]string{}, r, c) t.Logf("Got exit code %v from Execute()", exitCode) + if ExitCode(exitCode) != WORKER_STOPPED { + t.Fatalf("Expected exit code %v, got: %v", WORKER_STOPPED, exitCode) + } }() var status svc.Status @@ -208,7 +210,7 @@ func TestWindowsServiceInteraction(t *testing.T) { t.Fatalf("Timeout waiting for status svc.StartPending") case status = <-c: if status.State != svc.StartPending { - t.Fatalf("Expected state svc.StartPending, got status %v", status) + t.Fatalf("Expected state svc.StartPending, got status %#v", status) } } @@ -217,7 +219,7 @@ func TestWindowsServiceInteraction(t *testing.T) { t.Fatalf("Timeout waiting for status svc.Running") case status = <-c: if status.State != svc.Running { - t.Fatalf("Expected state svc.Running, got status %v", status) + t.Fatalf("Expected state svc.Running, got status %#v", status) } } @@ -226,14 +228,12 @@ func TestWindowsServiceInteraction(t *testing.T) { // send Stop r <- svc.ChangeRequest{Cmd: svc.Stop} - fmt.Printf("Sent Stop change request") - select { case <-time.After(time.Second * 5): t.Fatalf("Timeout waiting for status svc.StopPending") case status = <-c: if status.State != svc.StopPending { - t.Fatalf("Expected state svc.StopPending, got status %v", status) + t.Fatalf("Expected state svc.StopPending, got status %#v", status) } } @@ -242,11 +242,7 @@ func TestWindowsServiceInteraction(t *testing.T) { t.Fatalf("Timeout waiting for status svc.Stopped") case status = <-c: if status.State != svc.Stopped { - t.Fatalf("Expected state svc.Stopped, got status %v", status) + t.Fatalf("Expected state svc.Stopped, got status %#v", status) } } - - if ExitCode(exitCode) != WORKER_STOPPED { - t.Fatalf("Expected exit code %v, got: %v", WORKER_STOPPED, exitCode) - } } From 9c85674d9422e513314dd0a7509963d88a664b09 Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 9 Jul 2019 15:09:05 -0700 Subject: [PATCH 11/15] cleanup service interaction test, more dodging race conditions, split out some logic --- .gitattributes | 1 - multiuser_service_windows.go | 3 +- multiuser_service_windows_test.go | 140 ++++++++++-------------------- 3 files changed, 45 insertions(+), 99 deletions(-) diff --git a/.gitattributes b/.gitattributes index ba500954..2d9e5418 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ /testdata/** eol=lf -# * text=auto diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 8302c14a..2740a2f5 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -79,10 +79,9 @@ func (*windowsService) Execute(args []string, r <-chan svc.ChangeRequest, change exitChan := make(chan uint32, 1) go func() { - exitCode = uint32(RunWorker(interruptChan)) + exitChan <- uint32(RunWorker(interruptChan)) // kill the service interruptChan <- os.Interrupt - exitChan <- exitCode }() changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} diff --git a/multiuser_service_windows_test.go b/multiuser_service_windows_test.go index 2b68e614..52d01844 100644 --- a/multiuser_service_windows_test.go +++ b/multiuser_service_windows_test.go @@ -81,8 +81,9 @@ func TestConfigureAndRemoveService(t *testing.T) { // this is unrealistic, usually the service is marked for deletion // but not actually removed until reboot - // // hopefully service gets removed - // <-time.After(2 * time.Second) + // TODO + // if we dip into the registry to remove the service + // we can actually verify it right after removal // // verify service is removed // _, err = m.OpenService(name) @@ -103,64 +104,6 @@ func (w brokenWriter) Write(bs []byte) (int, error) { return -1, fmt.Errorf("broken writer is broken") } -// func wrapTest(t *testing.T, name string, errorFunc func(*exec.ExitError)) { -// if we are running the test -// as opposed to wrapping the test -// if os.Getenv(t.Name()) == "1" { -// name := "generic-worker-" + slugid.Nice() -// setupService(t, name, true) -// defer cleanupService(t, name) -// // use brokenWriter -// logWriter = brokenWriter{} -// // now we try runService in non-interactive mode -// // which should attempt to use brokenWriter and fail -// // with CANT_LOG_PROPERLY -// runService(name, false) -// return -// } - -// // wrapping the test - -// // exitOnError will be called, so os.Exit() will be called -// // so we need to run this test in a wrapper - -// cmd := exec.Command(os.Args[0], fmt.Sprintf("-test.run=%s", t.Name())) -// cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", t.Name())) -// stderrPipe, err := cmd.StderrPipe() -// if err != nil { -// t.Fatal(err) -// } -// stdoutPipe, err := cmd.StdoutPipe() -// if err != nil { -// t.Fatalf("Error getting StdoutPipe: %v", err) -// } -// if err := cmd.Start(); err != nil { -// t.Fatal(err) -// } -// stderr, err := ioutil.ReadAll(stderrPipe) -// if err != nil { -// t.Fatal(err) -// } -// stdout, err := ioutil.ReadAll(stdoutPipe) -// if err != nil { -// t.Fatal(err) -// } -// err = cmd.Wait() -// // if nonzero exit code, castable to ExitError -// if exitError, ok := err.(*exec.ExitError); ok && err != nil { -// // as expected, there was an error -// exitCode := exitError.ExitCode() -// if exitCode != int(CANT_LOG_PROPERLY) { -// t.Logf("Expected runService() to exit with CANT_LOG_PROPERLY, got: %s", exitError.Error()) -// t.Logf("stderr: %s", stderr) -// t.Logf("stdout: %s", stdout) -// t.Fail() -// } -// } else { -// t.Fatalf("Expected an error from %q: %v", strings.Join(cmd.Args, " "), err) -// } -// } - func TestRunServiceWithBrokenWriter(t *testing.T) { defer unprivilegedRecovery(t) @@ -187,6 +130,26 @@ func TestRunServiceWithBrokenWriter(t *testing.T) { } } +// https://godoc.org/golang.org/x/sys/windows#SERVICE_STOPPED +// SERVICE_STOPPED = 1 +// SERVICE_START_PENDING = 2 +// SERVICE_STOP_PENDING = 3 +// SERVICE_RUNNING = 4 +// SERVICE_CONTINUE_PENDING = 5 +// SERVICE_PAUSE_PENDING = 6 +// SERVICE_PAUSED = 7 +// SERVICE_NO_CHANGE = 0xffffffff +func receiveStateOrTimeout(t *testing.T, c <-chan svc.Status, expected svc.State) { + select { + case <-time.After(time.Second * 5): + t.Fatalf("Timeout waiting for status %#v", expected) + case state := <-c: + if state.State != expected { + t.Fatalf("Expected state %#v, got Status %#v", expected, state) + } + } +} + func TestWindowsServiceInteraction(t *testing.T) { defer setup(t)() @@ -196,53 +159,38 @@ func TestWindowsServiceInteraction(t *testing.T) { s := windowsService{} + exitChan := make(chan ExitCode, 1) go func() { - _, exitCode := s.Execute([]string{}, r, c) - t.Logf("Got exit code %v from Execute()", exitCode) - if ExitCode(exitCode) != WORKER_STOPPED { - t.Fatalf("Expected exit code %v, got: %v", WORKER_STOPPED, exitCode) - } + _, e := s.Execute([]string{}, r, c) + exitChan <- ExitCode(e) }() - var status svc.Status - select { - case <-time.After(time.Second * 5): - t.Fatalf("Timeout waiting for status svc.StartPending") - case status = <-c: - if status.State != svc.StartPending { - t.Fatalf("Expected state svc.StartPending, got status %#v", status) - } - } + receiveStateOrTimeout(t, c, svc.StartPending) + receiveStateOrTimeout(t, c, svc.Running) - select { - case <-time.After(time.Second * 5): - t.Fatalf("Timeout waiting for status svc.Running") - case status = <-c: - if status.State != svc.Running { - t.Fatalf("Expected state svc.Running, got status %#v", status) - } - } + // send Interrogate + r <- svc.ChangeRequest{Cmd: svc.Interrogate} + t.Log("Sent Interrogate ChangeRequest to service") - <-time.After(1 * time.Second) + // 0 value, not a real State + receiveStateOrTimeout(t, c, svc.State(0)) + receiveStateOrTimeout(t, c, svc.State(0)) // send Stop r <- svc.ChangeRequest{Cmd: svc.Stop} + t.Log("Sent Stop ChangeRequest to service") - select { - case <-time.After(time.Second * 5): - t.Fatalf("Timeout waiting for status svc.StopPending") - case status = <-c: - if status.State != svc.StopPending { - t.Fatalf("Expected state svc.StopPending, got status %#v", status) - } - } + receiveStateOrTimeout(t, c, svc.StopPending) + receiveStateOrTimeout(t, c, svc.Stopped) + t.Log("Waiting for exit code from Execute()") select { - case <-time.After(time.Second * 5): - t.Fatalf("Timeout waiting for status svc.Stopped") - case status = <-c: - if status.State != svc.Stopped { - t.Fatalf("Expected state svc.Stopped, got status %#v", status) + case <-time.After(time.Second * 60): + t.Fatalf("Timeout waiting for exit code from Execute()") + case exitCode := <-exitChan: + t.Logf("Got exit code %v from Execute()", exitCode) + if ExitCode(exitCode) != WORKER_STOPPED { + t.Fatalf("Expected exit code %v, got: %v", WORKER_STOPPED, exitCode) } } } From 518722b7d7ac156a5642763cbadcb2d483d184bb Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Thu, 11 Jul 2019 15:21:15 -0700 Subject: [PATCH 12/15] address review feedback use io.MultiWriter() instead of ioutil.Discard --- README.md | 2 +- helper_windows_test.go | 1 + multiuser_service_windows.go | 14 ++++++++++---- multiuser_service_windows_test.go | 11 +++++------ multiuser_windows.go | 19 +++++++++++++------ simple_docker.go | 8 -------- usage.go | 3 ++- usage_windows.go | 8 ++++---- 8 files changed, 36 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index c6633a54..1361c64d 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,7 @@ and reports back results to the queue. Usage: generic-worker run [--config CONFIG-FILE] [--configure-for-aws | --configure-for-gcp] - generic-worker install service [--service-name SERVICE-NAME] + generic-worker install service [--service-name SERVICE-NAME] [--config CONFIG-FILE] [--configure-for-aws | --configure-for-gcp] generic-worker show-payload-schema diff --git a/helper_windows_test.go b/helper_windows_test.go index 01eca4c6..61831c95 100644 --- a/helper_windows_test.go +++ b/helper_windows_test.go @@ -117,6 +117,7 @@ func singleCommandNoArgs(command string) []string { return []string{command} } +// this is opt-out so that we don't skip tests by default func shouldRunAdminTests() bool { _, ok := os.LookupEnv("SKIP_ADMINISTRATOR_TESTS") return !ok diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 2740a2f5..55e34637 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -22,17 +22,18 @@ import ( ) // stderr handle is invalid when run as service -var logWriter = io.MultiWriter(ioutil.Discard) +var logWriter = io.MultiWriter() func init() { - // default for a service is C:\Windows\system32 + // TODO make this work with --working-directory + // default to generic-worker executable parent dir dir := path.Dir(os.Args[0]) err := os.Chdir(dir) if err != nil { exitOnError(INTERNAL_ERROR, err, "Unable to chdir to %q", dir) } log.SetOutput(logWriter) - manageLogFile(filepath.Join(path.Dir(os.Args[0]), "generic-worker.log")) + manageLogFile(filepath.Join(filepath.Dir(os.Args[0]), "generic-worker.log")) } func manageLogFile(logPath string) { @@ -216,7 +217,8 @@ func configureService(name, exePath string, args []string) error { // run as LocalSystem because we call WTSQueryUserToken ServiceStartName: "LocalSystem", ServiceType: windows.SERVICE_WIN32_OWN_PROCESS | windows.SERVICE_INTERACTIVE_PROCESS, - StartType: mgr.StartAutomatic, + // the service will start by itself whenever the computer reboots + StartType: mgr.StartAutomatic, } m, err := mgr.Connect() if err != nil { @@ -282,6 +284,10 @@ func removeService(name string) error { return fmt.Errorf("service %s is not installed", name) } defer s.Close() + + // ignore error, we're trying to delete + _, _ = s.Control(svc.Stop) + err = s.Delete() if err != nil { return err diff --git a/multiuser_service_windows_test.go b/multiuser_service_windows_test.go index 52d01844..6c4f370e 100644 --- a/multiuser_service_windows_test.go +++ b/multiuser_service_windows_test.go @@ -18,7 +18,7 @@ func unprivilegedRecovery(t *testing.T) { if r := recover(); r != nil { t.Log("A panic can occur if tests are run as an unprivileged user.") t.Log("Disable tests that require administrator privileges by setting `SKIP_ADMINISTRATOR_TESTS` in your environment") - t.Logf("Caught panic: %v", r) + t.Fatalf("Caught panic: %v", r) } } @@ -56,7 +56,6 @@ func TestConfigureAndRemoveService(t *testing.T) { name := "generic-worker-" + slugid.Nice() setupService(t, name, true) - defer cleanupService(t, name) // service manager m, err := mgr.Connect() @@ -78,10 +77,12 @@ func TestConfigureAndRemoveService(t *testing.T) { } elog.Close() - // this is unrealistic, usually the service is marked for deletion + // usually the service is marked for deletion // but not actually removed until reboot - // TODO + cleanupService(t, name) + + // TODO these don't work // if we dip into the registry to remove the service // we can actually verify it right after removal @@ -111,8 +112,6 @@ func TestRunServiceWithBrokenWriter(t *testing.T) { t.Skipf("SKIP_ADMINISTRATOR_TESTS set, skipping %q", t.Name()) } - // if we are running the test - // as opposed to wrapping the test name := "generic-worker-" + slugid.Nice() setupService(t, name, true) defer cleanupService(t, name) diff --git a/multiuser_windows.go b/multiuser_windows.go index ef0ad005..45316073 100644 --- a/multiuser_windows.go +++ b/multiuser_windows.go @@ -427,25 +427,32 @@ func platformTargets(arguments map[string]interface{}) ExitCode { // platform specific... err := install(arguments) if err != nil { - log.Fatalf("failed to install service: %v", err) + log.Printf("failed to install service: %v", err) return CANT_INSTALL_GENERIC_WORKER } case arguments["remove"]: err := remove(arguments) if err != nil { - log.Fatalf("failed to remove service: %v", err) + log.Printf("failed to remove service: %v", err) return CANT_REMOVE_GENERIC_WORKER } case arguments["run-service"]: - cwd := convertNilToEmptyString(arguments["--working-directory"]) - if cwd != "" { - os.Chdir(cwd) + dir := convertNilToEmptyString(arguments["--working-directory"]) + // default to generic-worker executable parent dir + if dir == "" { + dir = filepath.Dir(os.Args[0]) + } + err := os.Chdir(dir) + if err != nil { + log.Printf("Unable to chdir to %q: %v", dir, err) + return INTERNAL_ERROR } handleConfig(arguments) name := convertNilToEmptyString(arguments["--service-name"]) isIntSess, err := svc.IsAnInteractiveSession() if err != nil { - log.Fatalf("failed to determine if we are running in an interactive session: %v", err) + log.Printf("failed to determine if we are running in an interactive session: %v", err) + return INTERNAL_ERROR } // debug if interactive session return runService(name, isIntSess) diff --git a/simple_docker.go b/simple_docker.go index cffd1b14..3f948759 100644 --- a/simple_docker.go +++ b/simple_docker.go @@ -101,14 +101,6 @@ func purgeOldTasks() error { return nil } -func install(arguments map[string]interface{}) (err error) { - return nil -} - -func remove(arguments map[string]interface{}) (err error) { - return nil -} - func RenameCrossDevice(oldpath, newpath string) (err error) { // TODO: here we should be able to rename when oldpath and newpath are on // different partitions - for now this will cover 99% of cases. diff --git a/usage.go b/usage.go index d8d9ee43..fd8cf1e8 100644 --- a/usage.go +++ b/usage.go @@ -307,5 +307,6 @@ and reports back results to the queue. 75 Not able to create an ed25519 key pair. 76 Not able to save generic-worker config file after fetching it from AWS provisioner or Google Cloud metadata.` + exitCode77() + ` -` + 78 Not able to log properly. + 79 Not able to remove generic-worker from the system.` } diff --git a/usage_windows.go b/usage_windows.go index 59d915aa..fc8e8d35 100644 --- a/usage_windows.go +++ b/usage_windows.go @@ -18,8 +18,8 @@ func removeServiceSummary() string { func runServiceSummary() string { return ` - generic-worker run-service [--service-name SERVICE-NAME] - [--config CONFIG-FILE] + generic-worker run-service [--service-name SERVICE-NAME] + [--config CONFIG-FILE] [--working-directory DIRECTORY] [--configure-for-aws | --configure-for-gcp]` } @@ -46,7 +46,7 @@ func installServiceDescription() string { func removeServiceDescription() string { return ` - remove service This will remove the generic worker + remove service This will remove the generic worker Windows service.` } @@ -62,7 +62,7 @@ func platformCommandLineParameters() string { --service-name SERVICE-NAME The name that the Windows service should be installed under. [default: Generic Worker] --working-directory DIRECTORY The working directory the Generic Worker - service will use. [default: C:\Windows\system32]` + service will use.` } func exitCode74() string { From 77f8dcb31ed67c1315a19cd91ae257122b7de58e Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Mon, 15 Jul 2019 09:50:17 -0700 Subject: [PATCH 13/15] flesh out throwaway error comment --- multiuser_service_windows.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/multiuser_service_windows.go b/multiuser_service_windows.go index 55e34637..f8cc1250 100644 --- a/multiuser_service_windows.go +++ b/multiuser_service_windows.go @@ -285,7 +285,10 @@ func removeService(name string) error { } defer s.Close() - // ignore error, we're trying to delete + // If this fails it's essentially because something went wrong + // interacting with the service, i.e. it's not running, + // it didn't respond, etc. + // We don't care about an error, we're just trying to delete it. _, _ = s.Control(svc.Stop) err = s.Delete() From 6787581b4dd472fe602c796d5d4860b4fd4f2fea Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 16 Jul 2019 09:09:24 -0700 Subject: [PATCH 14/15] rename env var to skip admin required tests to GW_SKIP_ADMIN_REQUIRED_TESTS --- helper_windows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper_windows_test.go b/helper_windows_test.go index 61831c95..c62fd512 100644 --- a/helper_windows_test.go +++ b/helper_windows_test.go @@ -119,6 +119,6 @@ func singleCommandNoArgs(command string) []string { // this is opt-out so that we don't skip tests by default func shouldRunAdminTests() bool { - _, ok := os.LookupEnv("SKIP_ADMINISTRATOR_TESTS") + _, ok := os.LookupEnv("GW_SKIP_SERVICE_INSTALLATION_TESTS") return !ok } From 5dff72c7b64777621910d3c0efc2cea0114f37fc Mon Sep 17 00:00:00 2001 From: Miles Crabill Date: Tue, 16 Jul 2019 09:13:20 -0700 Subject: [PATCH 15/15] add GW_SKIP_ADMIN_REQUIRED_TESTS doc to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1361c64d..978db72a 100644 --- a/README.md +++ b/README.md @@ -693,6 +693,12 @@ Only used in a single __Windows-specific__ test - if you don't have a Z: drive setup on your computer, or you do but you also run tests from the Z: drive, you can set this env var to a non-empty string to skip this test. +### `GW_SKIP_ADMIN_REQUIRED_TESTS` + +This environment variable applies to Windows tests that rely on administrative +privileges to pass. This includes tests that install and remove Generic Worker +as a service. + ### `GW_TESTS_RUN_AS_CURRENT_USER` This environment variable applies only to the __multiuser__ engine.