Skip to content

Commit

Permalink
build windows msi for arm and amd (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Pickett authored Aug 7, 2024
1 parent edc3e8d commit 1055416
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 27 deletions.
56 changes: 56 additions & 0 deletions cmd/launcher/svc_config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"context"
"log/slog"
"time"

"github.com/kolide/launcher/pkg/launcher"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
22 changes: 18 additions & 4 deletions cmd/package-builder/package-builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions docs/package-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 10 additions & 2 deletions pkg/packagekit/wix/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions pkg/packagekit/wix/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := `<ServiceInstall Account="[SERVICEACCOUNT]" ErrorControl="normal" Id="DaemonSvc" Name="DaemonSvc" Start="auto" Type="ownProcess" Vital="yes">
<ServiceConfig xmlns="http://schemas.microsoft.com/wix/UtilExtension" FirstFailureActionType="restart" SecondFailureActionType="restart" ThirdFailureActionType="restart" RestartServiceDelayInSeconds="5" ResetPeriodInDays="1"></ServiceConfig>
<ServiceConfig xmlns="http://schemas.microsoft.com/wix/2006/wi" DelayedAutoStart="no" OnInstall="yes" OnReinstall="yes"></ServiceConfig>
Expand Down
85 changes: 85 additions & 0 deletions pkg/packagekit/wix/wix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package wix
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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, "</Directory>") {
// 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 "<Directory Id="xxxx"...>"
// so we just check for the first part of the string
if strings.Contains(line, "<Directory") {
if strings.Contains(line, string(amd64)) {
// were in a arch specific bin dir that we want to remove, skip opening tag
// and set current arch specific bin dir so we'll skip closing tag as well
currentArchSpecificBinDir = amd64
continue
}

if strings.Contains(line, string(arm64)) {
// were in a arch specific bin dir that we want to remove, skip opening tag
// and set current arch specific bin dir so we'll skip closing tag as well
currentArchSpecificBinDir = arm64
continue
}
}

heatWrite.WriteString(line)
heatWrite.WriteString("\n")

for _, service := range wo.services {

isMatch, err := service.Match(line)
if err != nil {
return fmt.Errorf("match error: %w", err)
}

if isMatch {
if currentArchSpecificBinDir == none {
return errors.New("service found, but not in a bin directory")
}

// make sure elements are not duplicated in any service
serviceId := fmt.Sprintf("%s%s", baseSvcName, ulid.New())
service.serviceControl.Id = serviceId
service.serviceInstall.Id = serviceId
service.serviceInstall.ServiceConfig.Id = serviceId

// unfortunately, the UtilServiceConfig uses the name of the launcher service as a primary key
// since we have multiple services with the same name, we can't have multiple UtilServiceConfigs
// so we are skipping it for arm64 since it's a much smaller portion of our user base. The correct
// UtilServiceConfig will set when launcher starts up.
if currentArchSpecificBinDir == arm64 {
service.serviceInstall.UtilServiceConfig = nil
}

// create a condition based on architecture
// have to format in the "%P" in "%PROCESSOR_ARCHITECTURE"
heatWrite.WriteString(fmt.Sprintf(`<Condition> %sROCESSOR_ARCHITECTURE="%s" </Condition>`, "%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(`<Condition> %sROCESSOR_ARCHITECTURE="%s" </Condition>`, "%P", strings.ToUpper(string(currentArchSpecificBinDir))))
heatWrite.WriteString("\n")
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/packaging/detectLauncherVersion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/packaging/detectLauncherVersion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions pkg/packaging/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
Loading

0 comments on commit 1055416

Please sign in to comment.