* feat(windows): 增加原生服务安装包 * fix(windows): 收紧服务安装与升级流程 * fix(windows): 固定服务安装路径与语言资源 * fix(windows): 等待安装器进程完成 * fix(windows): 清理首次安装失败残留
207 lines
6.3 KiB
Go
207 lines
6.3 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/svc/mgr"
|
|
)
|
|
|
|
func TestDesiredWindowsServiceConfigUsesLowPrivilegeIsolatedIdentity(t *testing.T) {
|
|
arguments := []string{"service", "run"}
|
|
config := desiredWindowsServiceConfig(`C:\Program Files\GPT-Load\gpt-load.exe`, arguments)
|
|
if config.ServiceStartName != `NT AUTHORITY\LocalService` {
|
|
t.Fatalf("service account = %q", config.ServiceStartName)
|
|
}
|
|
if config.SidType != windows.SERVICE_SID_TYPE_UNRESTRICTED {
|
|
t.Fatalf("service SID type = %d", config.SidType)
|
|
}
|
|
if config.StartType != mgr.StartAutomatic || !config.DelayedAutoStart {
|
|
t.Fatalf("service start policy = %d/%t", config.StartType, config.DelayedAutoStart)
|
|
}
|
|
for _, required := range arguments {
|
|
if !strings.Contains(config.BinaryPathName, required) {
|
|
t.Fatalf("binary path does not contain %q: %s", required, config.BinaryPathName)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{
|
|
"--config-dir",
|
|
"--data-dir",
|
|
"AUTH_KEY",
|
|
"ENCRYPTION_KEY",
|
|
"DATABASE_DSN",
|
|
} {
|
|
if strings.Contains(config.BinaryPathName, forbidden) {
|
|
t.Fatalf("binary path contains secret configuration name %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWindowsServiceRecoveryUsesBoundedBackoff(t *testing.T) {
|
|
actions := windowsServiceRecoveryActions()
|
|
want := []struct {
|
|
actionType int
|
|
delay time.Duration
|
|
}{
|
|
{actionType: mgr.ServiceRestart, delay: 5 * time.Second},
|
|
{actionType: mgr.ServiceRestart, delay: 30 * time.Second},
|
|
{actionType: mgr.ServiceRestart, delay: time.Minute},
|
|
{actionType: mgr.NoAction},
|
|
}
|
|
if len(actions) != len(want) {
|
|
t.Fatalf("recovery action count = %d", len(actions))
|
|
}
|
|
for index, action := range actions {
|
|
if action.Type != want[index].actionType || action.Delay != want[index].delay {
|
|
t.Fatalf("recovery action %d = %#v", index, action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOwnedWindowsServiceConfigAcceptsOnlyOfficialCommandShape(t *testing.T) {
|
|
officialExecutable, err := expectedWindowsServiceExecutable()
|
|
if err != nil {
|
|
t.Fatalf("expectedWindowsServiceExecutable() error = %v", err)
|
|
}
|
|
official := desiredWindowsServiceConfig(
|
|
officialExecutable,
|
|
[]string{"service", "run"},
|
|
)
|
|
if !isOwnedWindowsServiceConfig(official) {
|
|
t.Fatal("official service configuration is not recognized")
|
|
}
|
|
|
|
portable := desiredWindowsServiceConfig(
|
|
`C:\Downloads\gpt-load-windows-amd64.exe`,
|
|
[]string{"service", "run"},
|
|
)
|
|
if isOwnedWindowsServiceConfig(portable) {
|
|
t.Fatal("portable-installed service configuration is recognized")
|
|
}
|
|
|
|
for name, mutate := range map[string]func(*mgr.Config){
|
|
"foreign display name": func(config *mgr.Config) {
|
|
config.DisplayName = "Another Service"
|
|
},
|
|
"foreign description": func(config *mgr.Config) {
|
|
config.Description = "Another service"
|
|
},
|
|
"foreign account": func(config *mgr.Config) {
|
|
config.ServiceStartName = `LocalSystem`
|
|
},
|
|
"foreign SID policy": func(config *mgr.Config) {
|
|
config.SidType = windows.SERVICE_SID_TYPE_NONE
|
|
},
|
|
"foreign command": func(config *mgr.Config) {
|
|
config.BinaryPathName = windowsServiceCommandLine(
|
|
`C:\Program Files\GPT-Load\gpt-load.exe`,
|
|
"other",
|
|
"run",
|
|
)
|
|
},
|
|
"relative executable": func(config *mgr.Config) {
|
|
config.BinaryPathName = windowsServiceCommandLine("gpt-load.exe", "service", "run")
|
|
},
|
|
"custom directories": func(config *mgr.Config) {
|
|
config.BinaryPathName = windowsServiceCommandLine(
|
|
`C:\Program Files\GPT-Load\gpt-load.exe`,
|
|
"service", "run",
|
|
"--config-dir", `C:\`,
|
|
"--data-dir", `C:\data`,
|
|
)
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
config := official
|
|
mutate(&config)
|
|
if isOwnedWindowsServiceConfig(config) {
|
|
t.Fatal("foreign service configuration is recognized as owned")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateWindowsServiceInstallExecutableRequiresProgramFiles(t *testing.T) {
|
|
officialExecutable, err := expectedWindowsServiceExecutable()
|
|
if err != nil {
|
|
t.Fatalf("expectedWindowsServiceExecutable() error = %v", err)
|
|
}
|
|
if err := validateWindowsServiceInstallExecutable(officialExecutable); err != nil {
|
|
t.Fatalf("validateWindowsServiceInstallExecutable(official) error = %v", err)
|
|
}
|
|
if err := validateWindowsServiceInstallExecutable(
|
|
`C:\Downloads\gpt-load-windows-amd64.exe`,
|
|
); !errors.Is(err, errWindowsServiceInstallPath) {
|
|
t.Fatalf("validateWindowsServiceInstallExecutable(portable) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyWindowsServiceUpdateCommitsConfigLast(t *testing.T) {
|
|
desired := mgr.Config{DisplayName: "desired"}
|
|
service := &fakeWindowsServiceUpdater{}
|
|
|
|
if err := applyWindowsServiceUpdate(service, desired); err != nil {
|
|
t.Fatalf("applyWindowsServiceUpdate() error = %v", err)
|
|
}
|
|
if service.config.DisplayName != desired.DisplayName {
|
|
t.Fatalf("service config = %#v, want %#v", service.config, desired)
|
|
}
|
|
if got := service.calls[len(service.calls)-1]; got != "update_config" {
|
|
t.Fatalf("last service call = %q, want update_config", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyWindowsServiceUpdateDoesNotCommitConfigAfterPreparationFailure(t *testing.T) {
|
|
desired := mgr.Config{DisplayName: "desired"}
|
|
service := &fakeWindowsServiceUpdater{recoveryErr: errors.New("recovery failed")}
|
|
|
|
err := applyWindowsServiceUpdate(service, desired)
|
|
if err == nil || !strings.Contains(err.Error(), "recovery failed") {
|
|
t.Fatalf("applyWindowsServiceUpdate() error = %v", err)
|
|
}
|
|
for _, call := range service.calls {
|
|
if call == "update_config" {
|
|
t.Fatal("service config was updated after recovery preparation failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeWindowsServiceUpdater struct {
|
|
config mgr.Config
|
|
recoveryErr error
|
|
updateErr error
|
|
calls []string
|
|
}
|
|
|
|
func (service *fakeWindowsServiceUpdater) Config() (mgr.Config, error) {
|
|
service.calls = append(service.calls, "config")
|
|
return service.config, nil
|
|
}
|
|
|
|
func (service *fakeWindowsServiceUpdater) UpdateConfig(config mgr.Config) error {
|
|
service.calls = append(service.calls, "update_config")
|
|
if service.updateErr != nil {
|
|
return service.updateErr
|
|
}
|
|
service.config = config
|
|
return nil
|
|
}
|
|
|
|
func (service *fakeWindowsServiceUpdater) SetRecoveryActions(
|
|
_ []mgr.RecoveryAction,
|
|
_ uint32,
|
|
) error {
|
|
service.calls = append(service.calls, "set_recovery")
|
|
return service.recoveryErr
|
|
}
|
|
|
|
func (service *fakeWindowsServiceUpdater) SetRecoveryActionsOnNonCrashFailures(_ bool) error {
|
|
service.calls = append(service.calls, "set_non_crash")
|
|
return nil
|
|
}
|