143 lines
5.0 KiB
Go
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
|
|
}
|