Use Startup shortcut for Windows autostart
Replace the HKCU Run autostart entry with a per-user Startup folder shortcut. A .lnk stores TargetPath separately, which avoids fragile quoting when the executable path contains spaces. Remove legacy PySentry and GoSentry Run entries when saving autostart settings, and report shortcut status from the actual shortcut target. Add Windows tests that create and read a temporary shortcut with spaces in the path so the PowerShell/COM invocation remains covered.
This commit is contained in:
+116
-29
@@ -1,57 +1,144 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const autostartName = "PySentry"
|
||||
const autostartName = "GoSentry"
|
||||
const legacyAutostartName = "PySentry"
|
||||
const startupShortcutFile = autostartName + ".lnk"
|
||||
|
||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||
if enabled {
|
||||
// Remove any stale entry first. This makes "uncheck, save, check, save"
|
||||
// and even a plain "check, save" repair an old path after the executable
|
||||
// was moved or renamed for a new version.
|
||||
deleteCommand := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/f")
|
||||
configureHiddenWindow(deleteCommand)
|
||||
_ = deleteCommand.Run()
|
||||
|
||||
command := exec.Command("reg.exe", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/t", "REG_SZ", "/d", strconv.Quote(executablePath), "/f")
|
||||
configureHiddenWindow(command)
|
||||
return command.Run()
|
||||
if err := cleanupLegacyRegistryAutostart(); err != nil {
|
||||
return err
|
||||
}
|
||||
command := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/f")
|
||||
configureHiddenWindow(command)
|
||||
_ = command.Run()
|
||||
return nil
|
||||
|
||||
shortcutPath, err := startupShortcutPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enabled {
|
||||
return createStartupShortcut(shortcutPath, executablePath, iconPath)
|
||||
}
|
||||
return removeIfExists(shortcutPath)
|
||||
}
|
||||
|
||||
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
||||
command := exec.Command("reg.exe", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName)
|
||||
configureHiddenWindow(command)
|
||||
output, err := command.Output()
|
||||
shortcutPath, err := startupShortcutPath()
|
||||
if err != nil {
|
||||
return false, "Startup folder cannot be resolved"
|
||||
}
|
||||
_, statErr := os.Stat(shortcutPath)
|
||||
if !expectedEnabled {
|
||||
if err != nil {
|
||||
if os.IsNotExist(statErr) {
|
||||
if legacyRegistryAutostartExists() {
|
||||
return false, "Legacy registry autostart exists; save settings to repair"
|
||||
}
|
||||
return true, "Autostart is off"
|
||||
}
|
||||
return false, "Autostart entry exists while setting is off"
|
||||
}
|
||||
if err != nil {
|
||||
return false, "Autostart entry is missing"
|
||||
if statErr != nil {
|
||||
return false, "Autostart shortcut cannot be checked"
|
||||
}
|
||||
return false, "Autostart shortcut exists while setting is off"
|
||||
}
|
||||
|
||||
actual, ok := parseRegistryRunValue(string(output))
|
||||
if !ok {
|
||||
return false, "Autostart entry cannot be read"
|
||||
if os.IsNotExist(statErr) {
|
||||
if legacyRegistryAutostartExists() {
|
||||
return false, "Legacy registry autostart exists; save settings to repair"
|
||||
}
|
||||
return false, "Autostart shortcut is missing"
|
||||
}
|
||||
if statErr != nil {
|
||||
return false, "Autostart shortcut cannot be checked"
|
||||
}
|
||||
|
||||
actual, err := readShortcutTarget(shortcutPath)
|
||||
if err != nil {
|
||||
return false, "Autostart shortcut cannot be read"
|
||||
}
|
||||
if !sameWindowsPath(actual, executablePath) {
|
||||
return false, "Autostart points to another executable"
|
||||
return false, "Autostart shortcut points to another executable"
|
||||
}
|
||||
return true, "Autostart is configured"
|
||||
}
|
||||
|
||||
func startupShortcutPath() (string, error) {
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData == "" {
|
||||
return "", fmt.Errorf("APPDATA is not set")
|
||||
}
|
||||
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", startupShortcutFile), nil
|
||||
}
|
||||
|
||||
func createStartupShortcut(shortcutPath string, executablePath string, iconPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(shortcutPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workingDirectory := filepath.Dir(executablePath)
|
||||
if iconPath == "" {
|
||||
iconPath = executablePath
|
||||
}
|
||||
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); $shortcut.TargetPath = $env:GOSENTRY_TARGET_PATH; $shortcut.WorkingDirectory = $env:GOSENTRY_WORKING_DIRECTORY; $shortcut.IconLocation = $env:GOSENTRY_ICON_PATH; $shortcut.Save()`
|
||||
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
command.Env = append(os.Environ(),
|
||||
"GOSENTRY_SHORTCUT_PATH="+shortcutPath,
|
||||
"GOSENTRY_TARGET_PATH="+executablePath,
|
||||
"GOSENTRY_WORKING_DIRECTORY="+workingDirectory,
|
||||
"GOSENTRY_ICON_PATH="+iconPath,
|
||||
)
|
||||
configureHiddenWindow(command)
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("create startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readShortcutTarget(shortcutPath string) (string, error) {
|
||||
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath)`
|
||||
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
command.Env = append(os.Environ(), "GOSENTRY_SHORTCUT_PATH="+shortcutPath)
|
||||
configureHiddenWindow(command)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func removeIfExists(path string) error {
|
||||
err := os.Remove(path)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cleanupLegacyRegistryAutostart() error {
|
||||
for _, name := range []string{legacyAutostartName, autostartName} {
|
||||
command := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name, "/f")
|
||||
configureHiddenWindow(command)
|
||||
_ = command.Run()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func legacyRegistryAutostartExists() bool {
|
||||
for _, name := range []string{legacyAutostartName, autostartName} {
|
||||
command := exec.Command("reg.exe", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name)
|
||||
configureHiddenWindow(command)
|
||||
if command.Run() == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseRegistryRunValue(output string) (string, bool) {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
|
||||
Reference in New Issue
Block a user