|
| 1 | +// Copyright The OpenTelemetry Authors |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +//go:build windows |
| 16 | + |
| 17 | +package msi |
| 18 | + |
| 19 | +import ( |
| 20 | + "os" |
| 21 | + "os/exec" |
| 22 | + "path/filepath" |
| 23 | + "strings" |
| 24 | + "syscall" |
| 25 | + "testing" |
| 26 | + "time" |
| 27 | + |
| 28 | + "github.com/stretchr/testify/assert" |
| 29 | + "github.com/stretchr/testify/require" |
| 30 | + "golang.org/x/sys/windows/svc" |
| 31 | + "golang.org/x/sys/windows/svc/mgr" |
| 32 | +) |
| 33 | + |
| 34 | +// Test structure for MSI installation tests |
| 35 | +type msiTest struct { |
| 36 | + name string |
| 37 | + collectorServiceArgs string |
| 38 | + skipSvcStop bool |
| 39 | +} |
| 40 | + |
| 41 | +func TestMSI(t *testing.T) { |
| 42 | + msiInstallerPath := getInstallerPath(t) |
| 43 | + |
| 44 | + tests := []msiTest{ |
| 45 | + { |
| 46 | + name: "default", |
| 47 | + }, |
| 48 | + { |
| 49 | + name: "custom", |
| 50 | + collectorServiceArgs: "--config " + quotedIfRequired(getAlternateConfigFile(t)), |
| 51 | + skipSvcStop: true, |
| 52 | + }, |
| 53 | + } |
| 54 | + |
| 55 | + for _, tt := range tests { |
| 56 | + t.Run(tt.name, func(t *testing.T) { |
| 57 | + runMsiTest(t, tt, msiInstallerPath) |
| 58 | + }) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +func runMsiTest(t *testing.T, test msiTest, msiInstallerPath string) { |
| 63 | + // Build the MSI installation arguments and include the MSI properties map. |
| 64 | + installLogFile := filepath.Join(os.TempDir(), "install.log") |
| 65 | + args := []string{"/i", msiInstallerPath, "/qn", "/l*v", installLogFile} |
| 66 | + |
| 67 | + serviceArgs := quotedIfRequired(test.collectorServiceArgs) |
| 68 | + if test.collectorServiceArgs != "" { |
| 69 | + args = append(args, "COLLECTOR_SVC_ARGS="+serviceArgs) |
| 70 | + } |
| 71 | + |
| 72 | + // Run the MSI installer |
| 73 | + installCmd := exec.Command("msiexec") |
| 74 | + |
| 75 | + // msiexec is one of the noticeable exceptions about how to format the parameters, |
| 76 | + // see https://pkg.go.dev/os/exec#Command, so we need to join the args manually. |
| 77 | + cmdLine := strings.Join(args, " ") |
| 78 | + installCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec " + cmdLine} |
| 79 | + err := installCmd.Run() |
| 80 | + if err != nil { |
| 81 | + logText, _ := os.ReadFile(installLogFile) |
| 82 | + t.Log(string(logText)) |
| 83 | + } |
| 84 | + t.Logf("Install command: %s", installCmd.SysProcAttr.CmdLine) |
| 85 | + require.NoError(t, err, "Failed to install the MSI: %v\nArgs: %v", err, args) |
| 86 | + |
| 87 | + defer func() { |
| 88 | + // Uninstall the MSI |
| 89 | + uninstallCmd := exec.Command("msiexec") |
| 90 | + uninstallCmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "msiexec /x " + msiInstallerPath + " /qn"} |
| 91 | + err := uninstallCmd.Run() |
| 92 | + t.Logf("Uninstall command: %s", uninstallCmd.SysProcAttr.CmdLine) |
| 93 | + require.NoError(t, err, "Failed to uninstall the MSI: %v", err) |
| 94 | + }() |
| 95 | + |
| 96 | + // Verify the service |
| 97 | + scm, err := mgr.Connect() |
| 98 | + require.NoError(t, err) |
| 99 | + defer scm.Disconnect() |
| 100 | + |
| 101 | + collectorSvcName := getServiceName(t) |
| 102 | + service, err := scm.OpenService(collectorSvcName) |
| 103 | + require.NoError(t, err) |
| 104 | + defer service.Close() |
| 105 | + |
| 106 | + // Wait for the service to reach the running state |
| 107 | + require.Eventually(t, func() bool { |
| 108 | + status, err := service.Query() |
| 109 | + require.NoError(t, err) |
| 110 | + return status.State == svc.Running |
| 111 | + }, 10*time.Second, 500*time.Millisecond, "Failed to start the service") |
| 112 | + |
| 113 | + if !test.skipSvcStop { |
| 114 | + defer func() { |
| 115 | + _, err = service.Control(svc.Stop) |
| 116 | + require.NoError(t, err) |
| 117 | + |
| 118 | + require.Eventually(t, func() bool { |
| 119 | + status, err := service.Query() |
| 120 | + require.NoError(t, err) |
| 121 | + return status.State == svc.Stopped |
| 122 | + }, 10*time.Second, 500*time.Millisecond, "Failed to stop the service") |
| 123 | + }() |
| 124 | + } |
| 125 | + |
| 126 | + assertServiceCommand(t, collectorSvcName, serviceArgs) |
| 127 | +} |
| 128 | + |
| 129 | +func assertServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) { |
| 130 | + // Verify the service command |
| 131 | + actualCommand := getServiceCommand(t, serviceName) |
| 132 | + expectedCommand := expectedServiceCommand(t, serviceName, collectorServiceArgs) |
| 133 | + assert.Equal(t, expectedCommand, actualCommand) |
| 134 | +} |
| 135 | + |
| 136 | +func getServiceCommand(t *testing.T, serviceName string) string { |
| 137 | + scm, err := mgr.Connect() |
| 138 | + require.NoError(t, err) |
| 139 | + defer scm.Disconnect() |
| 140 | + |
| 141 | + service, err := scm.OpenService(serviceName) |
| 142 | + require.NoError(t, err) |
| 143 | + defer service.Close() |
| 144 | + |
| 145 | + config, err := service.Config() |
| 146 | + require.NoError(t, err) |
| 147 | + |
| 148 | + return config.BinaryPathName |
| 149 | +} |
| 150 | + |
| 151 | +func expectedServiceCommand(t *testing.T, serviceName, collectorServiceArgs string) string { |
| 152 | + programFilesDir := os.Getenv("PROGRAMFILES") |
| 153 | + require.NotEmpty(t, programFilesDir, "PROGRAMFILES environment variable is not set") |
| 154 | + |
| 155 | + collectorDir := filepath.Join(programFilesDir, "OpenTelemetry Collector") |
| 156 | + collectorExe := filepath.Join(collectorDir, serviceName) + ".exe" |
| 157 | + |
| 158 | + if collectorServiceArgs == "" { |
| 159 | + collectorServiceArgs = "--config " + quotedIfRequired(filepath.Join(collectorDir, "config.yaml")) |
| 160 | + } else { |
| 161 | + // Remove any quotation added for the msiexec command line |
| 162 | + collectorServiceArgs = strings.Trim(collectorServiceArgs, "\"") |
| 163 | + collectorServiceArgs = strings.ReplaceAll(collectorServiceArgs, "\"\"", "\"") |
| 164 | + } |
| 165 | + |
| 166 | + return quotedIfRequired(collectorExe) + " " + collectorServiceArgs |
| 167 | +} |
| 168 | + |
| 169 | +func getServiceName(t *testing.T) string { |
| 170 | + serviceName := os.Getenv("MSI_TEST_COLLECTOR_SERVICE_NAME") |
| 171 | + require.NotEmpty(t, serviceName, "MSI_TEST_COLLECTOR_SERVICE_NAME environment variable is not set") |
| 172 | + return serviceName |
| 173 | +} |
| 174 | + |
| 175 | +func getInstallerPath(t *testing.T) string { |
| 176 | + msiInstallerPath := os.Getenv("MSI_TEST_COLLECTOR_PATH") |
| 177 | + require.NotEmpty(t, msiInstallerPath, "MSI_TEST_COLLECTOR_PATH environment variable is not set") |
| 178 | + _, err := os.Stat(msiInstallerPath) |
| 179 | + require.NoError(t, err) |
| 180 | + return msiInstallerPath |
| 181 | +} |
| 182 | + |
| 183 | +func getAlternateConfigFile(t *testing.T) string { |
| 184 | + alternateConfigFile := os.Getenv("MSI_TEST_ALTERNATE_CONFIG_FILE") |
| 185 | + require.NotEmpty(t, alternateConfigFile, "MSI_TEST_ALTERNATE_CONFIG_FILE environment variable is not set") |
| 186 | + _, err := os.Stat(alternateConfigFile) |
| 187 | + require.NoError(t, err) |
| 188 | + return alternateConfigFile |
| 189 | +} |
| 190 | + |
| 191 | +func quotedIfRequired(s string) string { |
| 192 | + if strings.Contains(s, "\"") || strings.Contains(s, " ") { |
| 193 | + s = strings.ReplaceAll(s, "\"", "\"\"") |
| 194 | + return "\"" + s + "\"" |
| 195 | + } |
| 196 | + return s |
| 197 | +} |
0 commit comments