Files
ss/internal/service/service_windows.go
2026-07-19 21:23:44 +08:00

143 lines
5.0 KiB
Go

//go:build windows
package service
import (
"fmt"
"os"
"os/exec"
"strings"
"golang.org/x/sys/windows"
)
func newPlatformManager(configDir string) (platformManager, error) {
return &windowsServiceManager{configDir: configDir}, nil
}
// windowsServiceManager manages services via sc.exe, with the service
// binPath pointing back at this same ssm executable in
// "service run-windows" mode (see wrapper_windows.go) instead of a separate
// pywin32-based wrapper process. Direct port of WindowsServiceManager in
// service_manager.py, minus the pywin32/JSON-config-file indirection.
type windowsServiceManager struct {
configDir string
}
func isAdmin() bool {
token := windows.GetCurrentProcessToken()
return token.IsElevated()
}
// formatErrorMessage mirrors _format_error_message in service_manager.py.
func formatErrorMessage(operation, serviceName, errText string) string {
lower := strings.ToLower(errText)
switch {
case strings.Contains(lower, "access is denied") || strings.Contains(errText, "5"):
return fmt.Sprintf(
"Failed to %s service '%s': Access denied.\n\n"+
"Administrator privileges are required to %s Windows services.\n\n"+
"Solutions:\n"+
"• Run this program as administrator (right-click → 'Run as administrator')\n"+
"• Open an elevated Command Prompt and run the command manually\n"+
"• Ensure User Account Control (UAC) is enabled and accept the prompt",
operation, serviceName, operation)
case strings.Contains(errText, "1060"):
return fmt.Sprintf(
"Failed to %s service '%s': Service not found.\n\n"+
"The specified service does not exist. Check the service name and try again.",
operation, serviceName)
case strings.Contains(errText, "1062") && (operation == "stop" || operation == "restart"):
return fmt.Sprintf(
"Service '%s' is not currently running.\n\n"+
"This is not an error - the service was already stopped.",
serviceName)
default:
return fmt.Sprintf("Failed to %s service '%s': %s", operation, serviceName, errText)
}
}
// runAsAdmin runs cmd directly if already elevated, otherwise returns an
// error with manual-elevation instructions. Direct port of
// WindowsServiceManager._run_as_admin (service_manager.py:116-134); the
// Python version doesn't self-elevate via UAC either, it only checks and
// instructs.
func runAsAdmin(cmdArgs []string, description string) error {
if !isAdmin() {
commandStr := strings.Join(cmdArgs, " ")
return fmt.Errorf(
"administrator privileges required to %s.\n\n"+
"Command: %s\n\n"+
"Please do one of the following:\n"+
"1. Run this program as administrator (right-click → 'Run as administrator')\n"+
"2. Open an elevated Command Prompt and run: %s\n"+
"3. Accept the UAC prompt when it appears",
strings.ToLower(description), commandStr, commandStr)
}
out, err := exec.Command(cmdArgs[0], cmdArgs[1:]...).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to %s: %s", strings.ToLower(description), string(out))
}
return nil
}
func (w *windowsServiceManager) Install(cfg Config) error {
selfPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine ssm executable path: %w", err)
}
serviceCmd := fmt.Sprintf(`"%s" service run-windows --name "%s" --bin "%s" --args "%s"`,
selfPath, cfg.Name, cfg.ExecutablePath, cfg.Args)
cmdArgs := []string{
"sc", "create", cfg.Name,
"binPath=", serviceCmd,
"start=", "auto",
}
if cfg.Description != "" {
cmdArgs = append(cmdArgs, "DisplayName=", cfg.Description)
}
if err := runAsAdmin(cmdArgs, fmt.Sprintf("install service '%s'", cfg.Name)); err != nil {
return fmt.Errorf("%s", formatErrorMessage("install", cfg.Name, err.Error()))
}
return nil
}
func (w *windowsServiceManager) Uninstall(name string) error {
runAsAdmin([]string{"sc", "stop", name}, fmt.Sprintf("stop service '%s'", name))
if err := runAsAdmin([]string{"sc", "delete", name}, fmt.Sprintf("uninstall service '%s'", name)); err != nil {
return fmt.Errorf("%s", formatErrorMessage("uninstall", name, err.Error()))
}
return nil
}
func (w *windowsServiceManager) Start(name string) error {
if err := runAsAdmin([]string{"sc", "start", name}, fmt.Sprintf("start service '%s'", name)); err != nil {
return fmt.Errorf("%s", formatErrorMessage("start", name, err.Error()))
}
return nil
}
func (w *windowsServiceManager) Stop(name string) error {
if err := runAsAdmin([]string{"sc", "stop", name}, fmt.Sprintf("stop service '%s'", name)); err != nil {
return fmt.Errorf("%s", formatErrorMessage("stop", name, err.Error()))
}
return nil
}
func (w *windowsServiceManager) Restart(name string) error {
if err := runAsAdmin([]string{"sc", "stop", name}, fmt.Sprintf("stop service '%s'", name)); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "1062") {
return fmt.Errorf("%s", formatErrorMessage("restart", name, err.Error()))
}
}
if err := runAsAdmin([]string{"sc", "start", name}, fmt.Sprintf("start service '%s'", name)); err != nil {
return fmt.Errorf("%s", formatErrorMessage("restart", name, err.Error()))
}
return nil
}