diff --git a/Makefile b/Makefile index 8e395af5c..588eb557f 100644 --- a/Makefile +++ b/Makefile @@ -40,19 +40,27 @@ osqueryi-tables: table.ext extension: .pre-build go run cmd/make/make.go -targets=extension + xp: xp-launcher xp-extension xp-grpc-extension -xp-%: .pre-build +xp-%: + $(MAKE) -j darwin-xp-$* windows-xp-$* linux-xp-$* + +darwin-xp-%: .pre-build go run cmd/make/make.go -targets=$* -linkstamp -os=darwin + +linux-xp-%: .pre-build go run cmd/make/make.go -targets=$* -linkstamp -os=linux + +windows-xp-%: .pre-build go run cmd/make/make.go -targets=$* -linkstamp -os=windows -codesign-darwin: +codesign-darwin: xp codesign --force -s "${CODESIGN_IDENTITY}" -v ./build/darwin/launcher codesign --force -s "${CODESIGN_IDENTITY}" -v ./build/darwin/osquery-extension.ext -codesign: xp codesign-darwin +codesign: codesign-darwin package-builder: .pre-build deps go run cmd/make/make.go -targets=package-builder -linkstamp @@ -100,10 +108,10 @@ lint: \ lint-go-vet \ lint-go-nakedret -lint-go-deadcode: +lint-go-deadcode: deps-go deadcode cmd/ pkg/ -lint-misspell: +lint-misspell: deps-go git ls-files \ | grep -v pkg/simulator/testdata/bad_symlink \ | xargs misspell -error -f 'misspell: {{ .Filename }}:{{ .Line }}:{{ .Column }}:corrected {{ printf "%q" .Original }} to {{ printf "%q" .Corrected }}' @@ -111,7 +119,7 @@ lint-misspell: lint-go-vet: go vet ./cmd/... ./pkg/... -lint-go-nakedret: +lint-go-nakedret: deps-go nakedret ./... diff --git a/cmd/launcher/launcher.go b/cmd/launcher/launcher.go index e06ec5723..6b36ea31e 100644 --- a/cmd/launcher/launcher.go +++ b/cmd/launcher/launcher.go @@ -246,6 +246,24 @@ func runLauncher(ctx context.Context, cancel func(), opts *options, logger log.L // create a rungroup for all the actors we create to allow for easy start/stop var runGroup run.Group + // Create a channel for signals + sigChannel := make(chan os.Signal, 1) + + // Add a rungroup to catch things on the sigChannel + // Attach a notifier for os.Interrupt + runGroup.Add(func() error { + signal.Notify(sigChannel, os.Interrupt) + select { + case <-sigChannel: + level.Info(logger).Log("msg", "beginnning shutdown via signal") + return nil + } + }, func(err error) { + level.Info(logger).Log("msg", "interrupted", "err", err, "stack", fmt.Sprintf("%+v", err)) + cancel() + close(sigChannel) + }) + var client service.KolideService { switch opts.transport { @@ -298,6 +316,7 @@ func runLauncher(ctx context.Context, cancel func(), opts *options, logger log.L NotaryURL: opts.notaryServerURL, MirrorURL: opts.mirrorServerURL, HTTPClient: httpClient, + SigChannel: sigChannel, } // create an updater for osquery @@ -315,7 +334,7 @@ func runLauncher(ctx context.Context, cancel func(), opts *options, logger log.L launcherUpdater, err := createUpdater( ctx, launcherPath, - launcherFinalizer(logger, runnerShutdown), + updateFinalizer(logger, runnerShutdown), logger, config, ) @@ -325,21 +344,6 @@ func runLauncher(ctx context.Context, cancel func(), opts *options, logger log.L runGroup.Add(launcherUpdater.Execute, launcherUpdater.Interrupt) } - // Create the signal notifier and add it to the rungroup - sig := make(chan os.Signal, 1) - runGroup.Add(func() error { - signal.Notify(sig, os.Interrupt) - select { - case <-sig: - level.Info(logger).Log("msg", "beginnning shutdown") - return nil - } - }, func(err error) { - level.Info(logger).Log("msg", "interrupted", "err", err, "stack", fmt.Sprintf("%+v", err)) - cancel() - close(sig) - }) - err = runGroup.Run() return errors.Wrap(err, "run service") } diff --git a/cmd/launcher/svc_windows.go b/cmd/launcher/svc_windows.go index 48982a016..8a92d694a 100644 --- a/cmd/launcher/svc_windows.go +++ b/cmd/launcher/svc_windows.go @@ -84,10 +84,18 @@ func (w *winSvc) Execute(args []string, r <-chan svc.ChangeRequest, changes chan go func() { err := runLauncher(ctx, cancel, w.opts, w.logger) if err != nil { - level.Info(w.logger).Log("err", err, "stack", fmt.Sprintf("%+v", err)) + level.Info(w.logger).Log("msg", "runLauncher exited", "err", err, "stack", fmt.Sprintf("%+v", err)) changes <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted} os.Exit(1) } + + // If we get here, it means runLauncher returned nil. If we do + // nothing, the service is left running, but with no + // functionality. Instead, signal that as a stop to the service + // manager, and exit. We rely on the service manager to restart. + level.Info(w.logger).Log("msg", "runLauncher exited cleanly") + changes <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted} + os.Exit(0) }() for { diff --git a/cmd/launcher/updater-finalizer.go b/cmd/launcher/updater-finalizer.go new file mode 100644 index 000000000..a2c145ca4 --- /dev/null +++ b/cmd/launcher/updater-finalizer.go @@ -0,0 +1,34 @@ +// +build !windows + +package main + +import ( + "fmt" + "os" + "syscall" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/pkg/errors" +) + +// updateFinalizer finalizes a launcher update. It assume the new +// binary has been copied into place, and calls exec, so we start a +// new running launcher in our place. +func updateFinalizer(logger log.Logger, shutdownOsquery func() error) func() error { + return func() error { + if err := shutdownOsquery(); err != nil { + level.Info(logger).Log( + "method", "updateFinalizer", + "err", err, + "stack", fmt.Sprintf("%+v", err), + ) + } + // replace launcher + level.Info(logger).Log("msg", "Exec for updated launcher") + if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil { + return errors.Wrap(err, "restarting launcher") + } + return nil + } +} diff --git a/cmd/launcher/updater-finalizer_windows.go b/cmd/launcher/updater-finalizer_windows.go new file mode 100644 index 000000000..5ce3a75cf --- /dev/null +++ b/cmd/launcher/updater-finalizer_windows.go @@ -0,0 +1,31 @@ +// +build windows + +package main + +import ( + "fmt" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/pkg/errors" +) + +// updateFinalizer finalizes a launcher update. As windows does not +// support an exec, we exit so the service manager will restart +// us. Exit(0) might be more correct, but that's harder to plumb +// through this stack. So, return an error here to trigger an exit +// higher in the stack. +func updateFinalizer(logger log.Logger, shutdownOsquery func() error) func() error { + return func() error { + if err := shutdownOsquery(); err != nil { + level.Info(logger).Log( + "msg", "calling shutdownOsquery", + "method", "updateFinalizer", + "err", err, + "stack", fmt.Sprintf("%+v", err), + ) + } + level.Info(logger).Log("msg", "Exit for updated launcher") + return errors.New("Exiting launcher to allow a service manager restart") + } +} diff --git a/cmd/launcher/updater.go b/cmd/launcher/updater.go index a070c2bc3..b80901c83 100644 --- a/cmd/launcher/updater.go +++ b/cmd/launcher/updater.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "os" - "syscall" "time" "github.com/go-kit/kit/log" @@ -26,6 +25,8 @@ type updaterConfig struct { MirrorURL string HTTPClient *http.Client + + SigChannel chan os.Signal } func createUpdater( @@ -46,6 +47,7 @@ func createUpdater( autoupdate.WithMirrorURL(config.MirrorURL), autoupdate.WithFinalizer(finalizer), autoupdate.WithUpdateChannel(config.UpdateChannel), + autoupdate.WithSigChannel(config.SigChannel), ) if err != nil { return nil, err @@ -59,8 +61,8 @@ func createUpdater( Execute: func() error { level.Info(logger).Log("msg", "updater started") - // run the updater and set the stop function so that the interrupt has aaccess to it - stop, err = updater.Run(tuf.WithFrequency(config.AutoupdateInterval)) + // run the updater and set the stop function so that the interrupt has access to it + stop, err = updater.Run(tuf.WithFrequency(config.AutoupdateInterval), tuf.WithLogger(logger)) if err != nil { return errors.Wrap(err, "running updater") } @@ -79,20 +81,3 @@ func createUpdater( }, }, nil } - -func launcherFinalizer(logger log.Logger, shutdownOsquery func() error) func() error { - return func() error { - if err := shutdownOsquery(); err != nil { - level.Info(logger).Log( - "method", "launcherFinalizer", - "err", err, - "stack", fmt.Sprintf("%+v", err), - ) - } - // replace launcher - if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil { - return errors.Wrap(err, "restarting launcher") - } - return nil - } -} diff --git a/docs/architecture/2018-06-15-request_enrollment_details.md b/docs/architecture/2018-06-15_request_enrollment_details.md similarity index 100% rename from docs/architecture/2018-06-15-request_enrollment_details.md rename to docs/architecture/2018-06-15_request_enrollment_details.md diff --git a/docs/architecture/2018-07-06-control-server-api.md b/docs/architecture/2018-07-06_control-server-api.md similarity index 100% rename from docs/architecture/2018-07-06-control-server-api.md rename to docs/architecture/2018-07-06_control-server-api.md diff --git a/docs/architecture/2018-07-16-reorganization.md b/docs/architecture/2018-07-16_reorganization.md similarity index 100% rename from docs/architecture/2018-07-16-reorganization.md rename to docs/architecture/2018-07-16_reorganization.md diff --git a/docs/architecture/2019-03-11_autoupdate.md b/docs/architecture/2019-03-11_autoupdate.md new file mode 100644 index 000000000..04cc68ea1 --- /dev/null +++ b/docs/architecture/2019-03-11_autoupdate.md @@ -0,0 +1,78 @@ +# Launcher Auto Update Process + +## Authors + +- seph ([@directionless](https://github.com/directionless)) + +## Status + +Accepted (March 11, 2019) + +## Context + +One of the features of Launcher is it's ability to securely update +osquery and itself. The unix implementation is a straightforward +`exec` implementation. However, Windows does not have an `exec`. + +This ADR documents the current implementation, and a solution for +windows. + +## Decision + +New software versions are distributed using [The Update Framework +(TUF)](https://theupdateframework.github.io/). We use a Go client +library that we [built in-house](https://github.com/kolide/updater). + +Launcher periodically checks for new updates. When a new version is +detected, it is downloaded into a staging directory, and then the +running binary is replaced. This code can be found in +[autoupdate.go](/pkg/autoupdate/autoupdate.go) + +On Unix, launcher calls `syscall.Exec` to replace the current +executable without a new process, using the new binary. This code can +be found in [updater.go](/cmd/launcher/updater.go) + +### Windows Variation + +There are two needed changes on Windows. + +First, Windows does not support replacing a running binary on +disk. Attempting will result in a `Permission Denied` error. A +workaround is to rename the old version, and then place them new one +in the correct location. This has the drawback of losing atomicity. + +Second, Windows does not support `exec`. Instead, we will exit +launcher, and assume the service manager will restart. Empirically, it +will start the new binary on the configured path. + +Exiting launcher is hard to navigate. Things inside TUF are buried +deep in routines. Simple returning an error isn't enough. While we +could call `os.Exit` that seems abrupt. So instead, we plumb the +signal channel through, and signal a `os.Interrupt` on it. (Note that +it's not a _signal_ in the posix sense. It's a constant sent to a +channel) + +### Example Code + +There an [example service](/tools/upgrade-exec-service-testing/) +exploring these mechanisms. See code for further comments and +discussion. + +## Consequences + +One consequence of this approach, is that the installed file is +_updated_ in place. While the new binary is verified by Launcher, this +may look like corruption in some packaging systems. + +The update process on Windows is based on the service manager +restarting the service. We don't believe there's a downside here, but +it does increase restart counts. Due to implementation limitations of +WiX, the shortest recovery period is 1 day. + +Due to the nature of this update process, updates that depend on +command line flag changes, require a re-installation of Launcher. They +are handled outside this update process. + +The act of moving a new binary over a running old one (as we do on +unix) results in the running binary no longer being disk. This can +trigger notices in some monitoring software, for example, osquery. diff --git a/go.mod b/go.mod index 739d00b66..41d5c7790 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect github.com/ghodss/yaml v1.0.0 github.com/go-bindata/go-bindata v1.0.0 - github.com/go-kit/kit v0.7.0 + github.com/go-kit/kit v0.8.0 github.com/gogo/protobuf v1.2.0 github.com/golang/protobuf v1.2.0 github.com/google/certificate-transparency-go v1.0.21 // indirect @@ -40,11 +40,11 @@ require ( github.com/jinzhu/gorm v1.9.1 // indirect github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 // indirect - github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect + github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 github.com/knightsc/system_policy v1.1.1-0.20190125011806-04a47ae55cf7 github.com/kolide/kit v0.0.0-20181124013649-bd1a9de64d48 github.com/kolide/osquery-go v0.0.0-20190113061206-be0a8de4cf1d - github.com/kolide/updater v0.0.0-20181127001321-d5bab28ddf67 + github.com/kolide/updater v0.0.0-20190315001611-15bbc19b5b80 github.com/kr/pty v1.1.2 github.com/mattn/go-sqlite3 v1.10.0 github.com/miekg/pkcs11 v0.0.0-20171207072458-57296583125b // indirect diff --git a/go.sum b/go.sum index 393e3039c..b8fe1dc52 100644 --- a/go.sum +++ b/go.sum @@ -65,12 +65,18 @@ github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsd github.com/go-bindata/go-bindata v1.0.0/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-kit/kit v0.7.0 h1:ApufNmWF1H6/wUbAG81hZOHmqwd0zRf8mNfLjYj/064= github.com/go-kit/kit v0.7.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.7.0 h1:S04+lLfST9FvL8dl4R31wVUC/paZp/WQZbLmUgWboGw= github.com/go-stack/stack v1.7.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= @@ -115,8 +121,8 @@ github.com/kolide/kit v0.0.0-20181124013649-bd1a9de64d48 h1:n8FMcks5eBrK5+LGMw5W github.com/kolide/kit v0.0.0-20181124013649-bd1a9de64d48/go.mod h1:wdtviHFxzZ59XeVE7KnsKbj/F+g9J5Cbx1mcXEdLKgY= github.com/kolide/osquery-go v0.0.0-20190113061206-be0a8de4cf1d h1:2/+lLTfZan4IGrD8TGC1ZVkehWLtrRuah47FASAnifE= github.com/kolide/osquery-go v0.0.0-20190113061206-be0a8de4cf1d/go.mod h1:umPEbeG8R8RDJpQ0EMyRdj+6pfnJK4FTzjafwgovDUk= -github.com/kolide/updater v0.0.0-20181127001321-d5bab28ddf67 h1:vq66QzvrIQtUmRDqPl1kHYJT4z25fNNz+Pn9QDiCh9Y= -github.com/kolide/updater v0.0.0-20181127001321-d5bab28ddf67/go.mod h1:arAVA89BRUlXkEhRyXcO6p66LK+HulKv+2WKjiWzwtM= +github.com/kolide/updater v0.0.0-20190315001611-15bbc19b5b80 h1:XFzdAHvTlbQoHZdEgOEiFt93eyfXP6VZCwH5p+lPpBg= +github.com/kolide/updater v0.0.0-20190315001611-15bbc19b5b80/go.mod h1:x3dEGYbZovhD1t8OwEgdyu/4ZCvrn9QvkbPtOZnul8k= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go index ef8fd3e93..9249c234e 100644 --- a/pkg/autoupdate/autoupdate.go +++ b/pkg/autoupdate/autoupdate.go @@ -16,7 +16,6 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" - "github.com/kolide/kit/fs" "github.com/kolide/launcher/pkg/osquery" "github.com/kolide/updater/tuf" "github.com/pkg/errors" @@ -50,6 +49,7 @@ type Updater struct { logger log.Logger bootstrapFn func() error strippedBinaryName string + sigChannel chan os.Signal } // NewUpdater creates a unstarted updater for a specific binary @@ -103,15 +103,17 @@ func NewUpdater(binaryPath, rootDirectory string, logger log.Logger, opts ...Upd return &updater, nil } -// bootstraps local TUF metadata from bindata assets. +// createLocalTufRepo bootstraps local TUF metadata from bindata +// assets. (TUF requires an initial starting repo) func (u *Updater) createLocalTufRepo() error { if err := os.MkdirAll(u.settings.LocalRepoPath, 0755); err != nil { - return err + return errors.Wrapf(err, "mkdir LocalRepoPath (%s)", u.settings.LocalRepoPath) } localRepo := filepath.Base(u.settings.LocalRepoPath) assetPath := path.Join("pkg", "autoupdate", "assets", localRepo) - if err := createTUFRepoDirectory(u.settings.LocalRepoPath, assetPath, AssetDir); err != nil { - return err + + if err := u.createTUFRepoDirectory(u.settings.LocalRepoPath, assetPath, AssetDir); err != nil { + return errors.Wrapf(err, "createTUFRepoDirectory %s", u.settings.LocalRepoPath) } return nil } @@ -120,10 +122,10 @@ type assetDirFunc func(string) ([]string, error) // Creates TUF repo including delegate tree structure on local file system. // assetDir is the bindata AssetDir function. -func createTUFRepoDirectory(localPath string, currentAssetPath string, assetDir assetDirFunc) error { +func (u *Updater) createTUFRepoDirectory(localPath string, currentAssetPath string, assetDir assetDirFunc) error { paths, err := assetDir(currentAssetPath) if err != nil { - return err + return errors.Wrap(err, "assetDir") } for _, assetPath := range paths { @@ -138,7 +140,7 @@ func createTUFRepoDirectory(localPath string, currentAssetPath string, assetDir // files not yet yet there -- Generating an invalid state. Note: // this does not check the validity of the files, they might be // corrupt. - if _, err := os.Stat(fullAssetPath); !os.IsNotExist(err) { + if _, err := os.Stat(fullLocalPath); !os.IsNotExist(err) { continue } @@ -155,9 +157,9 @@ func createTUFRepoDirectory(localPath string, currentAssetPath string, assetDir // if fullAssetPath is not a JSON file, it's a directory. Create the // directory in localPath and recurse into it if err := os.MkdirAll(fullLocalPath, 0755); err != nil { - return err + return errors.Wrapf(err, "mkdir fullLocalPath (%s)", fullLocalPath) } - if err := createTUFRepoDirectory(fullLocalPath, fullAssetPath, assetDir); err != nil { + if err := u.createTUFRepoDirectory(fullLocalPath, fullAssetPath, assetDir); err != nil { return errors.Wrap(err, "could not recurse into createTUFRepoDirectory") } } @@ -175,6 +177,13 @@ func WithHTTPClient(client *http.Client) UpdaterOption { } } +// WithSigChannel configures the channel uses for shutdown signaling +func WithSigChannel(sc chan os.Signal) UpdaterOption { + return func(u *Updater) { + u.sigChannel = sc + } +} + // WithUpdate configures the update channel. // If unspecified, the Updater will use the Stable channel. func WithUpdateChannel(channel UpdateChannel) UpdaterOption { @@ -252,46 +261,6 @@ func (u *Updater) Run(opts ...tuf.Option) (stop func(), err error) { return client.Stop, nil } -// The handler is called by the tuf package when tuf detects a change with -// the remote metadata. -// The handler method will do the following: -// 1) untar the staged staged file, -// 2) replace the existing binary, -// 3) call the Updater's finalizer method, usually a restart function for the running binary. -func (u *Updater) handler() tuf.NotificationHandler { - return func(stagingPath string, err error) { - u.logger.Log("msg", "new staged tuf file", "file", stagingPath, "target", u.target, "binary", u.destination) - - if err != nil { - u.logger.Log("msg", "download failed", "target", u.target, "err", err) - return - } - - if err := fs.UntarBundle(stagingPath, stagingPath); err != nil { - u.logger.Log("msg", "untar downloaded target", "binary", u.target, "err", err) - return - } - - binary := filepath.Join(filepath.Dir(stagingPath), filepath.Base(u.destination)) - if err := os.Rename(binary, u.destination); err != nil { - u.logger.Log("msg", "update binary from staging dir", "binary", u.destination, "err", err) - return - } - - if err := os.Chmod(u.destination, 0755); err != nil { - u.logger.Log("msg", "setting +x permissions on binary", "binary", u.destination, "err", err) - return - } - - if err := u.finalizer(); err != nil { - u.logger.Log("msg", "calling restart function for updated binary", "binary", u.destination, "err", err) - return - } - - u.logger.Log("msg", "completed update for binary", "binary", u.destination) - } -} - // target creates a TUF target for a binary using the Destination. // Ex: darwin/osquery-stable.tar.gz func (u *Updater) setTargetPath() (string, error) { diff --git a/pkg/autoupdate/autoupdate_test.go b/pkg/autoupdate/autoupdate_test.go index ade920b68..1c0881ca7 100644 --- a/pkg/autoupdate/autoupdate_test.go +++ b/pkg/autoupdate/autoupdate_test.go @@ -18,7 +18,8 @@ func TestCreateTUFRepoDirectory(t *testing.T) { localTUFRepoPath, err := ioutil.TempDir("", "") require.NoError(t, err) - require.Nil(t, createTUFRepoDirectory(localTUFRepoPath, "pkg/autoupdate/assets", AssetDir)) + u := &Updater{} + require.NoError(t, u.createTUFRepoDirectory(localTUFRepoPath, "pkg/autoupdate/assets", AssetDir)) knownFilePaths := []string{ "launcher-tuf/root.json", @@ -81,7 +82,7 @@ func TestNewUpdater(t *testing.T) { gun := fmt.Sprintf("kolide/app") tt.opts = append(tt.opts, withoutBootstrap()) u, err := NewUpdater("/tmp/app", "/tmp/tuf", log.NewNopLogger(), tt.opts...) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, tt.target, u.target) @@ -93,7 +94,9 @@ func TestNewUpdater(t *testing.T) { // must have a non-nil finalizer require.NotNil(t, u.finalizer) - assert.Nil(t, u.finalizer()) + + // Running finalizer shouldn't error + require.NoError(t, u.finalizer()) }) } } diff --git a/pkg/autoupdate/handler.go b/pkg/autoupdate/handler.go new file mode 100644 index 000000000..0ff703b23 --- /dev/null +++ b/pkg/autoupdate/handler.go @@ -0,0 +1,52 @@ +// +build !windows + +package autoupdate + +import ( + "os" + "path/filepath" + + "github.com/go-kit/kit/log/level" + "github.com/kolide/kit/fs" + "github.com/kolide/updater/tuf" +) + +// handler is called by the tuf package when tuf detects a change with +// the remote metadata. +// The handler method will do the following: +// 1) untar the staged staged file, +// 2) replace the existing binary, +// 3) call the Updater's finalizer method, usually a restart function for the running binary. +func (u *Updater) handler() tuf.NotificationHandler { + return func(stagingPath string, err error) { + level.Debug(u.logger).Log("msg", "new staged tuf file", "file", stagingPath, "target", u.target, "binary", u.destination) + + if err != nil { + level.Info(u.logger).Log("msg", "download failed", "target", u.target, "err", err) + return + } + + if err := fs.UntarBundle(stagingPath, stagingPath); err != nil { + level.Info(u.logger).Log("msg", "untar downloaded target", "binary", u.target, "err", err) + return + } + + binary := filepath.Join(filepath.Dir(stagingPath), filepath.Base(u.destination)) + if err := os.Rename(binary, u.destination); err != nil { + level.Info(u.logger).Log("msg", "update binary from staging dir", "binary", u.destination, "err", err) + return + } + + if err := os.Chmod(u.destination, 0755); err != nil { + level.Info(u.logger).Log("msg", "setting +x permissions on binary", "binary", u.destination, "err", err) + return + } + + if err := u.finalizer(); err != nil { + level.Info(u.logger).Log("msg", "calling restart function for updated binary", "binary", u.destination, "err", err) + return + } + + level.Debug(u.logger).Log("msg", "completed update for binary", "binary", u.destination) + } +} diff --git a/pkg/autoupdate/handler_windows.go b/pkg/autoupdate/handler_windows.go new file mode 100644 index 000000000..eaf6962a2 --- /dev/null +++ b/pkg/autoupdate/handler_windows.go @@ -0,0 +1,75 @@ +// +build windows +package autoupdate + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/go-kit/kit/log/level" + "github.com/kolide/kit/fs" + "github.com/kolide/updater/tuf" +) + +// handler is called by the tuf package when tuf detects a change with +// the remote metadata. +// The handler method will do the following: +// 1) untar the staged staged file, +// 2) replace the existing binary, +// 3) call the Updater's finalizer method, usually a restart function for the running binary. +// +// The windows variant does a two step copy, as windows does not +// support copying over a running binary. +func (u *Updater) handler() tuf.NotificationHandler { + return func(stagingPath string, err error) { + level.Debug(u.logger).Log( + "msg", "new staged tuf file", + "file", stagingPath, + "target", u.target, + "binary", u.destination, + ) + + if err != nil { + level.Info(u.logger).Log("msg", "download failed", "target", u.target, "err", err) + return + } + + if err := fs.UntarBundle(stagingPath, stagingPath); err != nil { + level.Info(u.logger).Log("msg", "untar downloaded target", "binary", u.target, "err", err) + return + } + + // Set an oldFilePath, and move the current binary to it. Future + // updates will end up overwriting the same oldFilePath. + // TODO: add some cleanup routine to startup. + oldFilePath := filepath.Join( + filepath.Dir(stagingPath), + fmt.Sprintf("old-%s", filepath.Base(u.destination)), + ) + if err := os.Rename(u.destination, oldFilePath); err != nil { + level.Info(u.logger).Log("msg", "Moving binary to oldFilePath", "binary", u.destination, "err", err) + return + } + + binary := filepath.Join(filepath.Dir(stagingPath), filepath.Base(u.destination)) + if err := os.Rename(binary, u.destination); err != nil { + level.Info(u.logger).Log("msg", "update binary from staging dir", "binary", u.destination, "err", err) + return + } + + if err := os.Chmod(u.destination, 0755); err != nil { + level.Info(u.logger).Log("msg", "setting +x permissions on binary", "binary", u.destination, "err", err) + return + } + + // On windows, this is expected to return an error to signal the + // restart. This is a bit confusing, but hard to untangle. + if err := u.finalizer(); err != nil { + level.Info(u.logger).Log("msg", "signaling restart function for updated binary", "binary", u.destination, "err", err) + u.sigChannel <- os.Interrupt + return + } + + level.Debug(u.logger).Log("msg", "completed update for binary", "binary", u.destination) + } +} diff --git a/pkg/packagekit/internal/assets.go b/pkg/packagekit/internal/assets.go index 325047c5a..b18bcbddf 100644 --- a/pkg/packagekit/internal/assets.go +++ b/pkg/packagekit/internal/assets.go @@ -45,7 +45,7 @@ func (fi bindataFileInfo) Sys() interface{} { } var _internalAssetsMainWxs = []byte(` - + @@ -115,7 +115,7 @@ func internalAssetsMainWxs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/main.wxs", size: 1721, mode: os.FileMode(420), modTime: time.Unix(1550456584, 0)} + info := bindataFileInfo{name: "internal/assets/main.wxs", size: 1781, mode: os.FileMode(420), modTime: time.Unix(1552328656, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/pkg/packagekit/internal/assets/main.wxs b/pkg/packagekit/internal/assets/main.wxs index 1dd43811d..b38abca71 100644 --- a/pkg/packagekit/internal/assets/main.wxs +++ b/pkg/packagekit/internal/assets/main.wxs @@ -1,5 +1,5 @@ - + diff --git a/pkg/packagekit/wix/service.go b/pkg/packagekit/wix/service.go index f9dbc2252..8eeba8052 100644 --- a/pkg/packagekit/wix/service.go +++ b/pkg/packagekit/wix/service.go @@ -61,10 +61,12 @@ type ServiceInstall struct { Password string `xml:",attr,omitempty"` Start StartType `xml:",attr,omitempty"` Type string `xml:",attr,omitempty"` - Vital YesNoType `xml:",attr,omitempty"` + Vital YesNoType `xml:",attr,omitempty"` // The overall install should fail if this service fails to install + ServiceConfig *ServiceConfig `xml:",omitempty"` } -// ServiceControl implements http://wixtoolset.org/documentation/manual/v3/xsd/wix/servicecontrol.html +// ServiceControl implements +// http://wixtoolset.org/documentation/manual/v3/xsd/wix/servicecontrol.html type ServiceControl struct { Name string `xml:",attr,omitempty"` Id string `xml:",attr,omitempty"` @@ -74,6 +76,25 @@ type ServiceControl struct { Wait YesNoType `xml:",attr,omitempty"` } +// ServiceConfig implements +// http://wixtoolset.org/documentation/manual/v3/xsd/util/serviceconfig.html +// This is used to set FailureActions. There are some +// limitations. Notably, reset period is in days here, though the +// underlying `sc.exe` command supports seconds. (See +// https://github.com/wixtoolset/issues/issues/5963) +// +// Docs are a bit confusing. This schema is supported, and should +// work. The non-util ServiceConfig generates unsupported CNDL1150 +// errors. +type ServiceConfig struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/wix/UtilExtension ServiceConfig"` + FirstFailureActionType string `xml:",attr,omitempty"` + SecondFailureActionType string `xml:",attr,omitempty"` + ThirdFailureActionType string `xml:",attr,omitempty"` + RestartServiceDelayInSeconds int `xml:",attr,omitempty"` + ResetPeriodInDays int `xml:",attr,omitempty"` +} + // Service represents a wix service. It provides an interface to both // ServiceInstall and ServiceControl. type Service struct { @@ -139,14 +160,25 @@ func NewService(matchString string, opts ...ServiceOpt) *Service { snakeName := r.Replace(strings.TrimSuffix(matchString, ".exe") + "_svc") defaultName := snaker.SnakeToCamel(snakeName) + // Set some defaults. It's not clear we can reset in under a + // day. See https://github.com/wixtoolset/issues/issues/5963 + sconfig := &ServiceConfig{ + FirstFailureActionType: "restart", + SecondFailureActionType: "restart", + ThirdFailureActionType: "restart", + ResetPeriodInDays: 1, + RestartServiceDelayInSeconds: 5, + } + si := &ServiceInstall{ - Name: defaultName, - Id: defaultName, - Account: `NT AUTHORITY\SYSTEM`, - Start: StartAuto, - Type: "ownProcess", - ErrorControl: ErrorControlNormal, - Vital: Yes, + Name: defaultName, + Id: defaultName, + Account: `NT AUTHORITY\SYSTEM`, + Start: StartAuto, + Type: "ownProcess", + ErrorControl: ErrorControlNormal, + Vital: Yes, + ServiceConfig: sconfig, } sc := &ServiceControl{ diff --git a/pkg/packagekit/wix/service_test.go b/pkg/packagekit/wix/service_test.go index f4fce9b86..64c64336e 100644 --- a/pkg/packagekit/wix/service_test.go +++ b/pkg/packagekit/wix/service_test.go @@ -26,7 +26,9 @@ func TestService(t *testing.T) { require.Error(t, err) require.True(t, expectTrue2) - expectedXml := ` + expectedXml := ` + + ` var xmlString bytes.Buffer @@ -34,7 +36,6 @@ func TestService(t *testing.T) { err = service.Xml(&xmlString) require.NoError(t, err) require.Equal(t, expectedXml, strings.TrimSpace(xmlString.String())) - } func TestServiceOptions(t *testing.T) { @@ -42,39 +43,28 @@ func TestServiceOptions(t *testing.T) { var tests = []struct { in *Service - out string + out []string }{ { - in: NewService("my.daemon-is_Great snakeCase.exe"), - out: ` - `, - }, - { - in: NewService("daemon.exe", ServiceName("myDaemon")), - out: ` - `, + in: NewService("daemon.exe", ServiceName("myDaemon")), + out: []string{`Id="myDaemon"`, `Name="myDaemon"`}, }, { - in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first"})), - out: ` - `, + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first"})), + out: []string{`Id="myDaemon"`, `Name="myDaemon"`, `Arguments="first"`}, }, { - in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first with spaces"})), - out: ` - `, + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first with spaces"})), + out: []string{`Id="myDaemon"`, `Name="myDaemon"`, `Arguments=""first with spaces""`}, }, { - in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second"})), - out: ` - `, + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second"})), + out: []string{`Id="myDaemon"`, `Name="myDaemon"`, `Arguments="first second"`}, }, - { - in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second", "third has spaces"})), - out: ` - `, + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second", "third has spaces"})), + out: []string{`Id="myDaemon"`, `Name="myDaemon"`, `Arguments="first second "third has spaces""`}, }, } @@ -82,7 +72,9 @@ func TestServiceOptions(t *testing.T) { var xmlString bytes.Buffer err := tt.in.Xml(&xmlString) require.NoError(t, err) - require.Equal(t, tt.out, strings.TrimSpace(xmlString.String())) + for _, outStr := range tt.out { + require.Contains(t, strings.TrimSpace(xmlString.String()), outStr) + } } } diff --git a/pkg/packagekit/wix/testdata/assets/product.wxs b/pkg/packagekit/wix/testdata/assets/product.wxs index cbac73081..2d34271c1 100644 --- a/pkg/packagekit/wix/testdata/assets/product.wxs +++ b/pkg/packagekit/wix/testdata/assets/product.wxs @@ -1,5 +1,5 @@ - + @@ -12,7 +12,8 @@ Name="Wix Test" Language="1033" Version="1.0.0" - Manufacturer = "Kolide" > + Manufacturer = "Kolide" + UpgradeCode="*" > sc.exe qfailure upgradetest 5000 +[SC] QueryServiceConfig2 SUCCESS + +SERVICE_NAME: upgradetest + RESET_PERIOD (in seconds) : 0 + REBOOT_MESSAGE : + COMMAND_LINE : +``` + +Further Reading: + +* [WiX ServiceConfig](http://wixtoolset.org/documentation/manual/v3/xsd/util/serviceconfig.html) +* [go configs](https://godoc.org/golang.org/x/sys/windows/svc/mgr#Service.RecoveryActions) +* [StackExchange](https://serverfault.com/questions/48600/how-can-i-automatically-restart-a-windows-service-if-it-crashes) + +**Note:** The MSI options to configure this are broken. The +recommendation is to use a Custom Action to call out to +`sc.exe`. Instead, we handle inside the service start. + +### Replace before restart + +Manually testing the idea of moving a binary aside, and dropping in a +new one and then calling `Exit(0)` and letting the service manager +restart... This seems to work + +Test Process: +1. Use my test case of run 5s, exit. +2. svc manager restarts +3. Observe new PIDs. +4. During a 5s loop, move old binary aside and scp new binary in +5. Observe the format of the log messages change on restart + +## Shell Debugging Snippets + +Viewing Event Log: +``` +# Old interface +Get-EventLog -LogName Application -Newest 10 + +# New interface, with full bodies +Get-WinEvent -LogName System -MaxEvents 10 +Get-WinEvent -LogName Application -MaxEvents 10 | Format-Table TimeCreated,Message -wrap +``` + + +Various ways to see service status: +``` shell +Get-Service upgradetest + +sc.exe query upgradetest +sc.exe qc upgradetest +sc.exe qfailure upgradetest 5000 +``` diff --git a/tools/upgrade-exec-service-testing/main.go b/tools/upgrade-exec-service-testing/main.go new file mode 100644 index 000000000..221bb8118 --- /dev/null +++ b/tools/upgrade-exec-service-testing/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "fmt" + "os" + "syscall" + "time" + + "github.com/kardianos/osext" + "github.com/kolide/kit/fs" + "github.com/pkg/errors" +) + +type thingy struct { + binaryName string + stagedFile string +} + +const serviceName = "upgradetest" +const serviceDesc = "Launcher Auto Upgrade Testing" + +type processNotes struct { + Pid int + Path string + Size int64 + ModTime time.Time +} + +var ProcessNotes processNotes + +func main() { + + binaryName, err := osext.Executable() + if err != nil { + panic(errors.Wrap(err, "osext.Executable")) + } + + processStat, err := os.Stat(binaryName) + if err != nil { + panic(errors.Wrap(err, "os.Stat")) + } + + ProcessNotes.Pid = os.Getpid() + ProcessNotes.Path = binaryName + ProcessNotes.Size = processStat.Size() + ProcessNotes.ModTime = processStat.ModTime() + + if len(os.Args) < 2 { + // Let's assume this should be windows services for now + //panic("Need an argument") + _ = runWindowsSvc([]string{}) + } + + var run func([]string) error + + switch os.Args[1] { + case "run": + run = runLoop + case "svc": + run = runWindowsSvc + case "svc-fg": + run = runWindowsSvcForeground + case "install": + run = runInstallService + case "uninstall": + run = runRemoveService + default: + panic("Unknown option") + } + + err = run(os.Args[2:]) + if err != nil { + panic(errors.Wrapf(err, "Running subcommand %s", os.Args[1])) + } + + fmt.Printf("Finished Main (pid %d)\n", ProcessNotes.Pid) + +} + +func oldShit() { + + fmt.Printf("\n\nStarting a new thread. Pid: %d\n", os.Getpid()) + time.Sleep(1 * time.Second) + + binaryName, err := osext.Executable() + if err != nil { + panic(errors.Wrap(err, "osext.Executable")) + } + + // Should this get a random append? + stagedFile := fmt.Sprintf("%s-staged", binaryName) + + // To emulate a new version, just copy the current binary to the staged location + fmt.Println("fs.CopyFile") + if err := fs.CopyFile(binaryName, stagedFile); err != nil { + panic(errors.Wrap(err, "fs.CopyFile")) + } + + b := &thingy{ + binaryName: binaryName, + stagedFile: stagedFile, + } + + if err := b.rename(); err != nil { + panic(err) + } + + fmt.Printf("BAD BAD BAD old thread! (pid %d)\n", os.Getpid()) + +} + +func (b *thingy) rename() error { + tmpCurFile := fmt.Sprintf("%s-old", b.binaryName) + + fmt.Println("os.Rename cur to old") + if err := os.Rename(b.binaryName, tmpCurFile); err != nil { + return errors.Wrap(err, "os.Rename cur top old") + } + + fmt.Println("os.Rename stage to cur") + if err := os.Rename(b.stagedFile, b.binaryName); err != nil { + return errors.Wrap(err, "os.Rename staged to cur") + } + + fmt.Println("os.Chmod") + if err := os.Chmod(b.binaryName, 0755); err != nil { + return errors.Wrap(err, "os.Chmod") + } + + fmt.Println("syscall.Exec") + if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil { + return errors.Wrap(err, "syscall.Exec") + } + + return nil +} diff --git a/tools/upgrade-exec-service-testing/runlauncher_return.go b/tools/upgrade-exec-service-testing/runlauncher_return.go new file mode 100644 index 000000000..e6741c438 --- /dev/null +++ b/tools/upgrade-exec-service-testing/runlauncher_return.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/pkg/errors" +) + +func runLauncher(ctx context.Context, cancel func(), args []string, logger log.Logger) error { + count := 0 + + for { + count = count + 1 + level.Debug(logger).Log( + "msg", "Launcher Loop", + "count", count, + "pid", ProcessNotes.Pid, + "size", ProcessNotes.Size, + "modtime", ProcessNotes.ModTime, + ) + time.Sleep(5 * time.Second) + + if count > 4 { + if err := triggerUpgrade(ctx, cancel, logger); err != nil { + return errors.Wrap(err, "triggerUpgrade") + } + break + } + } + + level.Debug(logger).Log("msg", "I guess we're exiting", "pid", ProcessNotes.Pid) + cancel() + return nil +} diff --git a/tools/upgrade-exec-service-testing/runloop.go b/tools/upgrade-exec-service-testing/runloop.go new file mode 100644 index 000000000..2bf9947fb --- /dev/null +++ b/tools/upgrade-exec-service-testing/runloop.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + + "github.com/kolide/kit/logutil" +) + +func runLoop(args []string) error { + ctx, cancel := context.WithCancel(context.Background()) + logger := logutil.NewServerLogger(true) + + return runLauncher(ctx, cancel, args, logger) + +} diff --git a/tools/upgrade-exec-service-testing/svc-manager_windows.go b/tools/upgrade-exec-service-testing/svc-manager_windows.go new file mode 100644 index 000000000..0f82ed623 --- /dev/null +++ b/tools/upgrade-exec-service-testing/svc-manager_windows.go @@ -0,0 +1,63 @@ +// +build windows + +package main + +import ( + "github.com/kardianos/osext" + "github.com/pkg/errors" + "golang.org/x/sys/windows/svc/mgr" +) + +func runInstallService(args []string) error { + exepath, err := osext.Executable() + if err != nil { + return errors.Wrap(err, "osext.Executable") + } + + m, err := mgr.Connect() + if err != nil { + return errors.Wrap(err, "mgr.Connect") + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err == nil { + s.Close() + return errors.Errorf("service %s already exists", serviceName) + } + + cfg := mgr.Config{DisplayName: serviceDesc, StartType: mgr.StartAutomatic} + + //ra := mgr.RecoveryAction{Type: mgr.ServiceRestart, Delay: 5 * time.Second} + + s, err = m.CreateService(serviceName, exepath, cfg, "svc") + if err != nil { + return errors.Wrap(err, "CreateService") + } + defer s.Close() + + //if err := s.SetRecoveryActions([]mgr.RecoveryAction{ra}, 3); err != nil { + //return errors.Wrap(err, "SetRecoveryActions") + //} + + return nil +} + +func runRemoveService(args []string) error { + m, err := mgr.Connect() + if err != nil { + return errors.Wrap(err, "mgr.Connect") + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err != nil { + s.Close() + return errors.Errorf("service %s is not installed", serviceName) + } + defer s.Close() + + err = s.Delete() + return err + +} diff --git a/tools/upgrade-exec-service-testing/svc.go b/tools/upgrade-exec-service-testing/svc.go new file mode 100644 index 000000000..9755c3a5d --- /dev/null +++ b/tools/upgrade-exec-service-testing/svc.go @@ -0,0 +1,21 @@ +// +build !windows + +package main + +import "errors" + +func runWindowsSvc(args []string) error { + return errors.New("This isn't windows") +} + +func runWindowsSvcForeground(args []string) error { + return errors.New("This isn't windows") +} + +func runInstallService(args []string) error { + return errors.New("This isn't windows") +} + +func runRemoveService(args []string) error { + return errors.New("This isn't windows") +} diff --git a/tools/upgrade-exec-service-testing/svc_windows.go b/tools/upgrade-exec-service-testing/svc_windows.go new file mode 100644 index 000000000..f6b81458d --- /dev/null +++ b/tools/upgrade-exec-service-testing/svc_windows.go @@ -0,0 +1,156 @@ +// +build windows + +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolide/kit/logutil" + "github.com/kolide/launcher/pkg/log/eventlog" + "github.com/pkg/errors" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/mgr" +) + +func runWindowsSvc(args []string) error { + eventLogWriter, err := eventlog.NewWriter(serviceName) + if err != nil { + return errors.Wrap(err, "create eventlog writer") + } + defer eventLogWriter.Close() + + logger := eventlog.New(eventLogWriter) + level.Debug(logger).Log("msg", "service start requested") + + run := svc.Run + return run(serviceName, &winSvc{logger: logger, args: args}) +} + +func runWindowsSvcForeground(args []string) error { + // Foreground mode is inherently a debug mode. So we start the + // logger in debugging mode, instead of looking at opts.debug + logger := logutil.NewCLILogger(true) + level.Debug(logger).Log("msg", "foreground service start requested (debug mode)") + + run := debug.Run + return run(serviceName, &winSvc{logger: logger, args: args}) +} + +type winSvc struct { + logger log.Logger + args []string +} + +func (w *winSvc) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + level.Debug(w.logger).Log("msg", "windows service starting") + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: needs a gofunc probably + if err := fixRecoveryActions(serviceName); err != nil { + level.Info(w.logger).Log("msg", "Failed to fixRecoveryActions", "err", err) + // TODO: Do we actually want to exit here? Not sure. + changes <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted} + os.Exit(1) + } + + go func() { + err := runLauncher(ctx, cancel, w.args, w.logger) + if err != nil { + level.Info(w.logger).Log("err", err, "stack", fmt.Sprintf("%+v", err)) + changes <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted} + os.Exit(1) + } + + level.Info(w.logger).Log("msg", "runLauncher gofunc ended cleanly", "pid", os.Getpid()) + + // Case 1 -- Nothing here + // + // If we do not exit here, we sorta just hang. This doesn't seem + // surprising -- What else would happen. The launcher go routine + // ends, but the signal handling forloop remains. + + // Case 2 -- os.Exit(0) + // + // Logs stop, and the service shows as stopped. Eg: windows + // services saw the clean exit and assumed it was + // intentional. Note that this may depend on some service + // installation parameter. + level.Info(w.logger).Log("msg", "exit(0)") + os.Exit(0) + + // Case 3 -- os.Exit(1) + // + // Same as Case 2. Makes me think something is set oddly in the + // windows service recovery stuff. It really oughgt be + // retrying. Need to figure out how to get to those settings + //level.Info(w.logger).Log("msg", "let's exit(1)") + //os.Exit(1) + }() + + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + level.Info(w.logger).Log("case", "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: + level.Info(w.logger).Log("case", "stop/shutdown") + changes <- svc.Status{State: svc.StopPending} + cancel() + time.Sleep(100 * time.Millisecond) + changes <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted} + return + default: + level.Info(w.logger).Log("err", "unexpected control request", "control_request", c) + } + } + } +} + +// Fix the Recovery Actions. +// +// This is all a hack around MSI's inability to set the recovery actions. +// +// Doing this requires the service name. We ought be able to get it +// inside the service, but I can't see how. So, we'll make some +// assumptions about how the service has been installed. Bummer. +func fixRecoveryActions(name string) error { + m, err := mgr.Connect() + if err != nil { + return errors.Wrap(err, "mgr.Connect") + } + defer m.Disconnect() + + s, err := m.OpenService(name) + if err != nil { + return errors.Errorf("service %s is not installed", name) + } + defer s.Close() + + // Action, and time to wait before performing action + ra := mgr.RecoveryAction{Type: mgr.ServiceRestart, Delay: 1 * time.Second} + + // How many seconds of stable daemon activity resets the RecoveryAction cycle + resetPeriod := uint32(3) + + if err := s.SetRecoveryActions([]mgr.RecoveryAction{ra}, resetPeriod); err != nil { + return errors.Wrap(err, "SetRecoveryActions") + } + + return nil +} diff --git a/tools/upgrade-exec-service-testing/upgrade.go b/tools/upgrade-exec-service-testing/upgrade.go new file mode 100644 index 000000000..8a9f2d651 --- /dev/null +++ b/tools/upgrade-exec-service-testing/upgrade.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "fmt" + "os" + "runtime" + "syscall" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolide/kit/fs" + "github.com/pkg/errors" +) + +func triggerUpgrade(ctx context.Context, cancel func(), logger log.Logger) error { + level.Info(logger).Log( + "msg", "Starting Upgrade", + "origpid", ProcessNotes.Pid, + ) + + // Should this get a random append? + stagedFile := fmt.Sprintf("%s-staged", ProcessNotes.Path) + + // To emulate a new version, just copy the current binary to the staged location + level.Debug(logger).Log("msg", "fs.CopyFile") + if err := fs.CopyFile(ProcessNotes.Path, stagedFile); err != nil { + return (errors.Wrap(err, "fs.CopyFile")) + } + + oldFile := fmt.Sprintf("%s-old", ProcessNotes.Path) + level.Debug(logger).Log("msg", "os.Rename cur to old") + if err := os.Rename(ProcessNotes.Path, oldFile); err != nil { + return errors.Wrap(err, "os.Rename cur top old") + } + + level.Debug(logger).Log("msg", "os.Rename stage to cur") + if err := os.Rename(stagedFile, ProcessNotes.Path); err != nil { + return errors.Wrap(err, "os.Rename staged to cur") + } + + level.Debug(logger).Log("msg", "os.Chmod") + if err := os.Chmod(ProcessNotes.Path, 0755); err != nil { + return errors.Wrap(err, "os.Chmod") + } + + // Our normal process here is to exec the new binary. However, this + // doesn't work on windows -- windows has no exec. So instead, we + // exit, and let the service manager restart us. + if runtime.GOOS == "windows" { + level.Info(logger).Log("msg", "Exiting, so service manager can restart new version") + return nil + } + + // For non-windows machine, exec the new version + level.Debug(logger).Log("msg", "syscall.Exec") + if err := syscall.Exec(ProcessNotes.Path, os.Args, os.Environ()); err != nil { + return errors.Wrap(err, "syscall.Exec") + } + + // Getting here, means the exec call returned + return nil +}