From 105541692a49014a1b35603070dbb220e647fd93 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Wed, 7 Aug 2024 15:46:02 -0700 Subject: [PATCH] build windows msi for arm and amd (#1796) --- cmd/launcher/svc_config_windows.go | 56 ++++++++++++++ cmd/package-builder/package-builder.go | 22 +++++- docs/package-builder.md | 24 ++++++ pkg/packagekit/wix/service.go | 12 ++- pkg/packagekit/wix/service_test.go | 10 ++- pkg/packagekit/wix/wix.go | 85 +++++++++++++++++++++ pkg/packaging/detectLauncherVersion.go | 8 +- pkg/packaging/detectLauncherVersion_test.go | 2 +- pkg/packaging/fetch.go | 8 ++ pkg/packaging/packaging.go | 48 ++++++++---- 10 files changed, 248 insertions(+), 27 deletions(-) diff --git a/cmd/launcher/svc_config_windows.go b/cmd/launcher/svc_config_windows.go index b45c708c3..05ebf7cbf 100644 --- a/cmd/launcher/svc_config_windows.go +++ b/cmd/launcher/svc_config_windows.go @@ -6,6 +6,7 @@ package main import ( "context" "log/slog" + "time" "github.com/kolide/launcher/pkg/launcher" @@ -64,6 +65,8 @@ func checkServiceConfiguration(logger *slog.Logger, opts *launcher.Options) { checkDependOnService(launcherServiceKey, logger) checkRestartActions(logger) + + setRecoveryActions(context.TODO(), logger) } // checkDelayedAutostart checks the current value of `DelayedAutostart` (whether to wait ~2 minutes @@ -184,3 +187,56 @@ func checkRestartActions(logger *slog.Logger) { logger.Log(context.TODO(), slog.LevelInfo, "successfully set RecoveryActionsOnNonCrashFailures flag") } + +// setRecoveryActions sets the recovery actions for the launcher service. +// previously defined via wix ServicConfig Element (Util Extension) https://wixtoolset.org/docs/v3/xsd/util/serviceconfig/ +func setRecoveryActions(ctx context.Context, logger *slog.Logger) { + sman, err := mgr.Connect() + if err != nil { + logger.Log(ctx, slog.LevelError, + "connecting to service control manager", + "err", err, + ) + + return + } + + defer sman.Disconnect() + + launcherService, err := sman.OpenService(launcherServiceName) + if err != nil { + logger.Log(ctx, slog.LevelError, + "opening the launcher service from control manager", + "err", err, + ) + + return + } + + defer launcherService.Close() + + recoveryActions := []mgr.RecoveryAction{ + { + // first failure + Type: mgr.ServiceRestart, + Delay: 5 * time.Second, + }, + { + // second failure + Type: mgr.ServiceRestart, + Delay: 5 * time.Second, + }, + { + // subsequent failures + Type: mgr.ServiceRestart, + Delay: 5 * time.Second, + }, + } + + if err := launcherService.SetRecoveryActions(recoveryActions, 24*60*60); err != nil { // 24 hours + logger.Log(ctx, slog.LevelError, + "setting RecoveryActions", + "err", err, + ) + } +} diff --git a/cmd/package-builder/package-builder.go b/cmd/package-builder/package-builder.go index fd79b3fb5..6063a8f2a 100644 --- a/cmd/package-builder/package-builder.go +++ b/cmd/package-builder/package-builder.go @@ -79,6 +79,16 @@ func runMake(args []string) error { env.String("LAUNCHER_VERSION", "stable"), "What TUF channel to download launcher from. Supports filesystem paths", ) + flLauncherPath = flagset.String( + "launcher_path", + "", + "Path of local launcher binary to use in packaging", + ) + flLauncherArmPath = flagset.String( + "launcher_arm_path", + "", + "Path of local launcher arm64 binary to use in packaging", + ) flExtensionVersion = flagset.String( "extension_version", env.String("EXTENSION_VERSION", "stable"), @@ -230,10 +240,14 @@ func runMake(args []string) error { } packageOptions := packaging.PackageOptions{ - PackageVersion: *flPackageVersion, - OsqueryVersion: *flOsqueryVersion, - OsqueryFlags: flOsqueryFlags, - LauncherVersion: *flLauncherVersion, + PackageVersion: *flPackageVersion, + OsqueryVersion: *flOsqueryVersion, + OsqueryFlags: flOsqueryFlags, + LauncherVersion: *flLauncherVersion, + LauncherPath: *flLauncherPath, + // LauncherArmPath can be used for windows arm64 packages when you want + // to specify a local path to the launcher binary + LauncherArmPath: *flLauncherArmPath, ExtensionVersion: *flExtensionVersion, Hostname: *flHostname, Secret: *flEnrollSecret, diff --git a/docs/package-builder.md b/docs/package-builder.md index 40ba01f02..b56392321 100644 --- a/docs/package-builder.md +++ b/docs/package-builder.md @@ -193,3 +193,27 @@ Note that the windows package will only install as `ALLUSERS`. You may need to use elevated privileges to install it. This will likely be confusing. `msiexec.exe` will either silently fail, or be inscrutable. But `start` will work. + +###### Windows Multi-Archecture MSI + +Package builder creates an MSI containing both amd64 & arm64 binaries. This is done putting both of binaries and their service installation in to the MSI with mutually exclusive conditions based on processor architecture. + +To accomplish we are using heat to harvest files set up like ... + +``` +└───pkg_root + └───Launcher-kolide-k2 + ├───bin + │ ├───amd64 + │ │ launcher.exe + │ │ osqueryd.exe + │ │ + │ └───arm64 + │ launcher.exe + │ osqueryd.exe + │ + └───conf + ... +``` + +then post processing the generated xml to remove the amd64 and arm64 directory tags so that all the exe files are in the same directory in the xml, then add the mutually exclusive tags to the binary elements diff --git a/pkg/packagekit/wix/service.go b/pkg/packagekit/wix/service.go index 131fa399e..45f8e4900 100644 --- a/pkg/packagekit/wix/service.go +++ b/pkg/packagekit/wix/service.go @@ -90,6 +90,12 @@ type ServiceControl struct { // This is used needed to set DelayedAutoStart type ServiceConfig struct { // TODO: this should need a namespace, and yet. See https://github.com/golang/go/issues/36813 + + // Id will be automaticlly set to the parent ServiceName attribute if not set. + // This will result in an error from wix if there are multiple services with the same name + // that occurs when we are creating an MSI with both arm64 and amd64 binaries. + // So we set Id in the heat post processing step. + Id string `xml:",attr,omitempty"` XMLName xml.Name `xml:"http://schemas.microsoft.com/wix/2006/wi ServiceConfig"` DelayedAutoStart YesNoType `xml:",attr,omitempty"` OnInstall YesNoType `xml:",attr,omitempty"` @@ -215,6 +221,7 @@ func NewService(matchString string, opts ...ServiceOpt) *Service { // probably better to specific a ServiceName, but this might be an // okay default. defaultName := cleanServiceName(strings.TrimSuffix(matchString, ".exe") + ".svc") + si := &ServiceInstall{ Name: defaultName, Id: defaultName, @@ -237,8 +244,9 @@ func NewService(matchString string, opts ...ServiceOpt) *Service { } s := &Service{ - matchString: matchString, - expectedCount: 1, + matchString: matchString, + // one count for arm64, one for amd64 + expectedCount: 2, count: 0, serviceInstall: si, serviceControl: sc, diff --git a/pkg/packagekit/wix/service_test.go b/pkg/packagekit/wix/service_test.go index 340b440d9..44bb7a8b0 100644 --- a/pkg/packagekit/wix/service_test.go +++ b/pkg/packagekit/wix/service_test.go @@ -17,15 +17,21 @@ func TestService(t *testing.T) { require.NoError(t, err) require.False(t, expectFalse) + // first match expectTrue, err := service.Match("daemon.exe") require.NoError(t, err) require.True(t, expectTrue) - // Should error. count now exceeds expectedCount. + // second match expectTrue2, err := service.Match("daemon.exe") - require.Error(t, err) + require.NoError(t, err) require.True(t, expectTrue2) + // third match, should error + expectTrue3, err := service.Match("daemon.exe") + require.Error(t, err) + require.True(t, expectTrue3) + expectedXml := ` diff --git a/pkg/packagekit/wix/wix.go b/pkg/packagekit/wix/wix.go index 4528e78b1..ec2e66eea 100644 --- a/pkg/packagekit/wix/wix.go +++ b/pkg/packagekit/wix/wix.go @@ -3,6 +3,7 @@ package wix import ( "bytes" "context" + "errors" "fmt" "os" "os/exec" @@ -12,6 +13,7 @@ import ( "github.com/go-kit/kit/log/level" "github.com/kolide/kit/fsutil" + "github.com/kolide/kit/ulid" "github.com/kolide/launcher/pkg/contexts/ctxlog" ) @@ -161,6 +163,15 @@ func New(packageRoot string, identifier string, mainWxsContent []byte, wixOpts . // Cleanup removes temp directories. Meant to be called in a defer. func (wo *wixTool) Cleanup() { if wo.skipCleanup { + // if the wix_skip_cleanup flag is set, we don't want to clean up the temp directories + // this is useful when debugging wix generation + // print the directories that would be cleaned up so they can be easily found + // and inspected + fmt.Print("skipping cleanup of temp directories\n") + for _, d := range wo.cleanDirs { + fmt.Printf("skipping cleanup of %s\n", d) + } + return } @@ -223,19 +234,93 @@ func (wo *wixTool) addServices(ctx context.Context) error { } defer heatWrite.Close() + type archSpecificBinDir string + + const ( + none archSpecificBinDir = "" + amd64 archSpecificBinDir = "amd64" + arm64 archSpecificBinDir = "arm64" + ) + currentArchSpecificBinDir := none + + baseSvcName := wo.services[0].serviceInstall.Id + lines := strings.Split(string(heatContent), "\n") for _, line := range lines { + + if currentArchSpecificBinDir != none && strings.Contains(line, "") { + // were in a arch specific bin dir that we want to remove, don't write closing tag + currentArchSpecificBinDir = none + continue + } + + // the directory tag will look like "" + // so we just check for the first part of the string + if strings.Contains(line, " %sROCESSOR_ARCHITECTURE="%s" `, "%P", strings.ToUpper(string(currentArchSpecificBinDir)))) + heatWrite.WriteString("\n") + if err := service.Xml(heatWrite); err != nil { return fmt.Errorf("adding service: %w", err) } + + continue + } + + if strings.Contains(line, "osqueryd.exe") { + if currentArchSpecificBinDir == none { + return fmt.Errorf("osqueryd.exe found, but not in a bin directory") + } + + // create a condition based on architecture + heatWrite.WriteString(fmt.Sprintf(` %sROCESSOR_ARCHITECTURE="%s" `, "%P", strings.ToUpper(string(currentArchSpecificBinDir)))) + heatWrite.WriteString("\n") } } } diff --git a/pkg/packaging/detectLauncherVersion.go b/pkg/packaging/detectLauncherVersion.go index f16c9a85b..169e83f5c 100644 --- a/pkg/packaging/detectLauncherVersion.go +++ b/pkg/packaging/detectLauncherVersion.go @@ -19,7 +19,8 @@ func (p *PackageOptions) detectLauncherVersion(ctx context.Context) error { logger := log.With(ctxlog.FromContext(ctx), "library", "detectLauncherVersion") level.Debug(logger).Log("msg", "Attempting launcher autodetection") - launcherPath := p.launcherLocation(filepath.Join(p.packageRoot, p.binDir)) + launcherPath := p.launcherLocation(filepath.Join(p.packageRoot, p.binDir, string(p.target.Arch))) + stdout, err := p.execOut(ctx, launcherPath, "-version") if err != nil { return fmt.Errorf("failed to exec -- possibly can't autodetect while cross compiling: out `%s`: %w", stdout, err) @@ -50,8 +51,9 @@ func (p *PackageOptions) detectLauncherVersion(ctx context.Context) error { // fall back to the common location if it doesn't. func (p *PackageOptions) launcherLocation(binDir string) string { if p.target.Platform == Darwin { - // We want /usr/local/Kolide.app, not /usr/local/bin/Kolide.app, so we use Dir to strip out `bin` - appBundleBinaryPath := filepath.Join(filepath.Dir(binDir), "Kolide.app", "Contents", "MacOS", "launcher") + // We want /usr/local/Kolide.app, not /usr/local/bin/universal/Kolide.app, so we use Dir to strip out `bin` + // and universal + appBundleBinaryPath := filepath.Join(filepath.Dir(filepath.Dir(binDir)), "Kolide.app", "Contents", "MacOS", "launcher") if info, err := os.Stat(appBundleBinaryPath); err == nil && !info.IsDir() { return appBundleBinaryPath } diff --git a/pkg/packaging/detectLauncherVersion_test.go b/pkg/packaging/detectLauncherVersion_test.go index 5eebfd0a5..3f7e6f4a9 100644 --- a/pkg/packaging/detectLauncherVersion_test.go +++ b/pkg/packaging/detectLauncherVersion_test.go @@ -95,7 +95,7 @@ func TestLauncherLocation(t *testing.T) { // Create a temp directory with an app bundle in it tmpDir := t.TempDir() - binDir := filepath.Join(tmpDir, "bin") + binDir := filepath.Join(tmpDir, "bin", "universal") require.NoError(t, os.MkdirAll(binDir, 0755)) baseDir := filepath.Join(tmpDir, "Kolide.app", "Contents", "MacOS") require.NoError(t, os.MkdirAll(baseDir, 0755)) diff --git a/pkg/packaging/fetch.go b/pkg/packaging/fetch.go index 0c7263152..7688cd6bb 100644 --- a/pkg/packaging/fetch.go +++ b/pkg/packaging/fetch.go @@ -38,6 +38,14 @@ func FetchBinary(ctx context.Context, localCacheDir, name, binaryName, channelOr return "", errors.New("empty cache dir argument") } + // put binaries in arch specific directory to avoid naming collisions in wix msi building + // where a single destination will have multiple, mutally exclusive sources + localCacheDir = filepath.Join(localCacheDir, string(target.Arch)) + + if err := os.MkdirAll(localCacheDir, fsutil.DirMode); err != nil { + return "", fmt.Errorf("could not create cache directory: %w", err) + } + localBinaryPath := filepath.Join(localCacheDir, fmt.Sprintf("%s-%s-%s", name, target.Platform, channelOrVersion), binaryName) localPackagePath := filepath.Join(localCacheDir, fmt.Sprintf("%s-%s-%s.tar.gz", name, target.Platform, channelOrVersion)) diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index 0bba3ce61..b7a7982fd 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -36,6 +36,8 @@ type PackageOptions struct { OsqueryVersion string OsqueryFlags []string // Additional flags to pass to the runtime osquery instance LauncherVersion string + LauncherPath string + LauncherArmPath string ExtensionVersion string Hostname string Secret string @@ -58,12 +60,6 @@ type PackageOptions struct { WixSkipCleanup bool DisableService bool - // Normally we'd download the same version we bake into the - // autoupdate. But occasionally, it's handy to make a package - // with a different version. - LauncherDownloadVersionOverride string - OsqueryDownloadVersionOverride string - AppleNotarizeAccountId string // The 10 character apple account id AppleNotarizeAppPassword string // app password for notarization service AppleNotarizeUserId string // User id to authenticate to the notarization service with @@ -215,22 +211,39 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar // Install binaries into packageRoot // TODO parallization // TODO windows file extensions - - if p.OsqueryDownloadVersionOverride == "" { - p.OsqueryDownloadVersionOverride = p.OsqueryVersion - } - if err := p.getBinary(ctx, "osqueryd", p.target.PlatformBinaryName("osqueryd"), p.OsqueryDownloadVersionOverride); err != nil { + if err := p.getBinary(ctx, "osqueryd", p.target.PlatformBinaryName("osqueryd"), p.OsqueryVersion); err != nil { return fmt.Errorf("fetching binary osqueryd: %w", err) } - if p.LauncherDownloadVersionOverride == "" { - p.LauncherDownloadVersionOverride = p.LauncherVersion + launcherVersion := p.LauncherVersion + if p.LauncherPath != "" { + launcherVersion = p.LauncherPath } - if err := p.getBinary(ctx, "launcher", p.target.PlatformBinaryName("launcher"), p.LauncherDownloadVersionOverride); err != nil { + if err := p.getBinary(ctx, "launcher", p.target.PlatformBinaryName("launcher"), launcherVersion); err != nil { return fmt.Errorf("fetching binary launcher: %w", err) } + // for windows, make a separate target for arm64 + if p.target.Platform == Windows { + // make a copy of P + packageOptsCopy := *p + packageOptsCopy.target.Arch = Arm64 + + if err := packageOptsCopy.getBinary(ctx, "osqueryd", packageOptsCopy.target.PlatformBinaryName("osqueryd"), packageOptsCopy.OsqueryVersion); err != nil { + return fmt.Errorf("fetching binary osqueryd: %w", err) + } + + launcherVersion := packageOptsCopy.LauncherVersion + if packageOptsCopy.LauncherArmPath != "" { + launcherVersion = packageOptsCopy.LauncherArmPath + } + + if err := packageOptsCopy.getBinary(ctx, "launcher", packageOptsCopy.target.PlatformBinaryName("launcher"), launcherVersion); err != nil { + return fmt.Errorf("fetching binary launcher: %w", err) + } + } + // Some darwin specific bits if p.target.Platform == Darwin { if err := p.renderNewSyslogConfig(ctx); err != nil { @@ -366,10 +379,15 @@ func (p *PackageOptions) getBinary(ctx context.Context, symbolicName, binaryName return nil } + binPath := filepath.Join(p.packageRoot, p.binDir, string(p.target.Arch), binaryName) + if err := os.MkdirAll(filepath.Dir(binPath), fsutil.DirMode); err != nil { + return fmt.Errorf("could not create directory for binary %s: %w", binaryName, err) + } + // Not an app bundle -- just copy the binary. if err := fsutil.CopyFile( localPath, - filepath.Join(p.packageRoot, p.binDir, binaryName), + binPath, ); err != nil { return fmt.Errorf("could not copy binary %s: %w", binaryName, err) }