T1.6: Create src/platform/autostart; move autostart logic
Move autostart_*.go and tests from src/core to src/platform/autostart. Update src/gui/app.go to call autostart.SetAutostart / autostart.AutostartStatus. Add quoteDesktopExec to desktop_linux.go (was co-located in autostart_linux.go). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
//go:build linux
|
||||
|
||||
package autostart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
)
|
||||
|
||||
const autostartDesktopFileName = "gosentry.desktop"
|
||||
const legacyAutostartDesktopFileName = "pysentry.desktop"
|
||||
|
||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||
desktopPath, err := autostartDesktopPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// A desktop scheduler with a tray icon belongs to the graphical session, so
|
||||
// Linux autostart is implemented through XDG Autostart instead of a systemd
|
||||
// user service. systemd is tempting because it is explicit and scriptable,
|
||||
// but it is the wrong owner for a windowed app that should inherit the
|
||||
// desktop session environment and appear in the tray predictably.
|
||||
if err := cleanupLegacySystemdAutostart(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupLegacyDesktopAutostart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GoSentry
|
||||
Comment=GoSentry desktop scheduler
|
||||
Exec=%s %s
|
||||
%s
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
`, quoteDesktopExec(executablePath), domain.StartInTrayArgument, desktopIconLine(iconPath))
|
||||
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
||||
}
|
||||
|
||||
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
||||
desktopPath, err := autostartDesktopPath()
|
||||
if err != nil {
|
||||
return false, "Cannot resolve XDG autostart directory"
|
||||
}
|
||||
if legacySystemdAutostartExists() {
|
||||
return false, "Legacy systemd autostart entry still exists"
|
||||
}
|
||||
if legacyDesktopAutostartExists() {
|
||||
return false, "Legacy desktop autostart entry still exists"
|
||||
}
|
||||
data, readErr := os.ReadFile(desktopPath)
|
||||
|
||||
if !expectedEnabled {
|
||||
if os.IsNotExist(readErr) {
|
||||
return true, "Autostart is off"
|
||||
}
|
||||
return false, "Autostart desktop entry exists while setting is off"
|
||||
}
|
||||
if readErr != nil {
|
||||
return false, "Autostart desktop entry is missing"
|
||||
}
|
||||
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + domain.StartInTrayArgument
|
||||
if !strings.Contains(string(data), expectedExec) {
|
||||
return false, "Autostart desktop entry points to another executable"
|
||||
}
|
||||
return true, "Autostart is configured"
|
||||
}
|
||||
|
||||
func autostartDesktopPath() (string, error) {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configHome = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
|
||||
}
|
||||
|
||||
func legacyAutostartDesktopPath() (string, error) {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configHome = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "autostart", legacyAutostartDesktopFileName), nil
|
||||
}
|
||||
|
||||
func quoteDesktopExec(path string) string {
|
||||
return strconv.Quote(path)
|
||||
}
|
||||
|
||||
func desktopIconLine(iconPath string) string {
|
||||
if strings.TrimSpace(iconPath) == "" {
|
||||
return ""
|
||||
}
|
||||
return "Icon=" + iconPath
|
||||
}
|
||||
|
||||
func cleanupLegacySystemdAutostart() error {
|
||||
unitPath, err := legacySystemdUnitPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(unitPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Older PySentry builds used a systemd user unit for autostart. The current
|
||||
// GoSentry implementation uses XDG Autostart because it is a GUI/tray
|
||||
// application and should be launched by the desktop session. Disable and
|
||||
// remove the old unit so the two mechanisms do not fight or start duplicates.
|
||||
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
|
||||
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
_ = exec.Command("systemctl", "--user", "daemon-reload").Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupLegacyDesktopAutostart() error {
|
||||
desktopPath, err := legacyAutostartDesktopPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The old PySentry desktop file is removed proactively instead of tolerated
|
||||
// alongside the new one. Leaving both files in place would risk duplicate
|
||||
// launches or confusing status diagnostics after the rename.
|
||||
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func legacyDesktopAutostartExists() bool {
|
||||
desktopPath, err := legacyAutostartDesktopPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(desktopPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func legacySystemdAutostartExists() bool {
|
||||
unitPath, err := legacySystemdUnitPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(unitPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func legacySystemdUnitPath() (string, error) {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configHome = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "systemd", "user", "pysentry.service"), nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//go:build linux
|
||||
|
||||
package autostart
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
)
|
||||
|
||||
func TestLinuxAutostartStartsInTray(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
|
||||
executablePath := "/opt/Go Sentry/gosentry"
|
||||
if err := SetAutostart(true, executablePath, "/opt/Go Sentry/gosentry.png"); err != nil {
|
||||
t.Fatalf("enable autostart: %v", err)
|
||||
}
|
||||
|
||||
desktopPath, err := autostartDesktopPath()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve desktop path: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(desktopPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read desktop entry: %v", err)
|
||||
}
|
||||
|
||||
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + domain.StartInTrayArgument
|
||||
if !strings.Contains(string(data), expectedExec) {
|
||||
t.Fatalf("desktop entry does not start in tray: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxAutostartRemovesLegacyDesktopEntry(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
|
||||
legacyPath, err := legacyAutostartDesktopPath()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve legacy desktop path: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(legacyPath), 0o755); err != nil {
|
||||
t.Fatalf("create legacy desktop directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, []byte("[Desktop Entry]\nName=PySentry\n"), 0o644); err != nil {
|
||||
t.Fatalf("write legacy desktop entry: %v", err)
|
||||
}
|
||||
|
||||
if err := SetAutostart(true, "/opt/gosentry/gosentry", ""); err != nil {
|
||||
t.Fatalf("enable autostart: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("legacy desktop entry still exists or cannot be checked: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !windows && !linux
|
||||
|
||||
package autostart
|
||||
|
||||
import "fmt"
|
||||
|
||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("autostart is not implemented for this platform")
|
||||
}
|
||||
|
||||
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
||||
if !expectedEnabled {
|
||||
return true, "Autostart is off"
|
||||
}
|
||||
return false, "Autostart is not implemented for this platform"
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package autostart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/platform/winproc"
|
||||
)
|
||||
|
||||
const autostartName = "GoSentry"
|
||||
const legacyAutostartName = "PySentry"
|
||||
const startupShortcutFile = autostartName + ".lnk"
|
||||
|
||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||
// Windows autostart used to write HKCU\Run values, but that approach became
|
||||
// brittle once paths with spaces and the "--start-in-tray" argument entered
|
||||
// the picture. A Startup-folder shortcut stores target path and arguments as
|
||||
// separate structured fields, so it avoids quoting bugs and more closely
|
||||
// matches how a user would configure a GUI app by hand.
|
||||
if err := cleanupLegacyRegistryAutostart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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) {
|
||||
shortcutPath, err := startupShortcutPath()
|
||||
if err != nil {
|
||||
return false, "Startup folder cannot be resolved"
|
||||
}
|
||||
_, statErr := os.Stat(shortcutPath)
|
||||
if !expectedEnabled {
|
||||
if os.IsNotExist(statErr) {
|
||||
if legacyRegistryAutostartExists() {
|
||||
return false, "Legacy registry autostart exists; save settings to repair"
|
||||
}
|
||||
return true, "Autostart is off"
|
||||
}
|
||||
if statErr != nil {
|
||||
return false, "Autostart shortcut cannot be checked"
|
||||
}
|
||||
return false, "Autostart shortcut exists while setting is off"
|
||||
}
|
||||
|
||||
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, arguments, err := readShortcut(shortcutPath)
|
||||
if err != nil {
|
||||
return false, "Autostart shortcut cannot be read"
|
||||
}
|
||||
if !sameWindowsPath(actual, executablePath) {
|
||||
return false, "Autostart shortcut points to another executable"
|
||||
}
|
||||
if strings.TrimSpace(arguments) != domain.StartInTrayArgument {
|
||||
return false, "Autostart shortcut does not start in tray"
|
||||
}
|
||||
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
|
||||
}
|
||||
// WScript.Shell is used here deliberately instead of a third-party Go COM
|
||||
// wrapper. The PowerShell bridge is not glamorous, but it is already present
|
||||
// on supported Windows systems and keeps the dependency surface much smaller
|
||||
// for a project that otherwise aims to stay light.
|
||||
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); $shortcut.TargetPath = $env:GOSENTRY_TARGET_PATH; $shortcut.Arguments = $env:GOSENTRY_ARGUMENTS; $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_ARGUMENTS="+domain.StartInTrayArgument,
|
||||
"GOSENTRY_WORKING_DIRECTORY="+workingDirectory,
|
||||
"GOSENTRY_ICON_PATH="+iconPath,
|
||||
)
|
||||
winproc.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 readShortcut(shortcutPath string) (string, string, error) {
|
||||
// Force UTF-8 before writing the path. PowerShell defaults to the system
|
||||
// OEM code page (e.g. CP866 on Russian Windows). Without this override,
|
||||
// [Console]::Out.Write encodes Cyrillic and other non-ASCII characters as
|
||||
// OEM bytes; Go then reads them as UTF-8 and gets a different string from
|
||||
// os.Executable, causing AutostartStatus to report "shortcut points to
|
||||
// another executable" for any install path that contains non-ASCII chars.
|
||||
// New-Object System.Text.UTF8Encoding($false) is UTF-8 without BOM.
|
||||
script := `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); $shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath + [Environment]::NewLine + $shortcut.Arguments)`
|
||||
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
command.Env = append(os.Environ(), "GOSENTRY_SHORTCUT_PATH="+shortcutPath)
|
||||
winproc.ConfigureHiddenWindow(command)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
lines := strings.SplitN(string(output), "\n", 2)
|
||||
target := strings.TrimSpace(lines[0])
|
||||
arguments := ""
|
||||
if len(lines) > 1 {
|
||||
arguments = strings.TrimSpace(lines[1])
|
||||
}
|
||||
return target, arguments, nil
|
||||
}
|
||||
|
||||
func readShortcutTarget(shortcutPath string) (string, error) {
|
||||
target, _, err := readShortcut(shortcutPath)
|
||||
return target, err
|
||||
}
|
||||
|
||||
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")
|
||||
winproc.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)
|
||||
winproc.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))
|
||||
for index, field := range fields {
|
||||
if field == "REG_SZ" && index+1 < len(fields) {
|
||||
value := strings.Join(fields[index+1:], " ")
|
||||
value = strings.Trim(value, `"`)
|
||||
return value, value != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func sameWindowsPath(left string, right string) bool {
|
||||
left = normalizeWindowsPath(left)
|
||||
right = normalizeWindowsPath(right)
|
||||
if strings.EqualFold(left, right) {
|
||||
return true
|
||||
}
|
||||
// If the string comparison fails, compare by filesystem object identity.
|
||||
// os.SameFile uses the volume serial number and file index on Windows, so
|
||||
// it correctly handles cases where one path uses an NTFS 8.3 short name
|
||||
// while the other uses the long name. Windows generates 8.3 names for
|
||||
// directory entries that contain spaces; when the process is launched via
|
||||
// a Startup-folder shortcut the OS may resolve the PIDL to the short-name
|
||||
// form, so os.Executable can return a different string than WScript reads
|
||||
// back from TargetPath even though both point to the same file. The same
|
||||
// fallback also covers directory junction points.
|
||||
leftInfo, leftErr := os.Lstat(left)
|
||||
rightInfo, rightErr := os.Lstat(right)
|
||||
if leftErr == nil && rightErr == nil {
|
||||
return os.SameFile(leftInfo, rightInfo)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeWindowsPath(p string) string {
|
||||
p = strings.Trim(p, `"`)
|
||||
// filepath.Clean preserves the \\?\ extended-length device path prefix that
|
||||
// Windows adds for paths exceeding MAX_PATH. Strip it so the cleaned result
|
||||
// compares equal to the same path without the prefix.
|
||||
p = strings.TrimPrefix(p, `\\?\`)
|
||||
return filepath.Clean(p)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//go:build windows
|
||||
|
||||
package autostart
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
)
|
||||
|
||||
func TestParseRegistryRunValue(t *testing.T) {
|
||||
output := `
|
||||
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
|
||||
GoSentry REG_SZ "D:\Apps\GoSentry\gosentry.exe"
|
||||
`
|
||||
value, ok := parseRegistryRunValue(output)
|
||||
if !ok {
|
||||
t.Fatal("expected registry value to parse")
|
||||
}
|
||||
if value != `D:\Apps\GoSentry\gosentry.exe` {
|
||||
t.Fatalf("unexpected value: %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
|
||||
if !sameWindowsPath(`"D:\Apps\GoSentry\gosentry.exe"`, `d:\apps\gosentry\gosentry.exe`) {
|
||||
t.Fatal("expected paths to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWindowsPathHandlesSpaces(t *testing.T) {
|
||||
if !sameWindowsPath(`"D:\Local Git\GoSentry\gosentry.exe"`, `d:\local git\gosentry\gosentry.exe`) {
|
||||
t.Fatal("expected paths with spaces to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWindowsPathStripsExtendedLengthPrefix(t *testing.T) {
|
||||
if !sameWindowsPath(`\\?\D:\Apps\GoSentry\gosentry.exe`, `D:\Apps\GoSentry\gosentry.exe`) {
|
||||
t.Fatal("expected \\\\?\\-prefixed path to match plain path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWindowsPathMatchesShortNameViaFilesystem(t *testing.T) {
|
||||
// Create a file inside a directory whose name contains a space. On NTFS
|
||||
// systems that have 8.3 name generation enabled, Windows also assigns a
|
||||
// short name to the directory (e.g. "Local~1"). WScript.Shell may return
|
||||
// the long form while os.Executable returns the short form (or vice versa).
|
||||
// Verify that sameWindowsPath treats both representations as equal.
|
||||
tempDir := t.TempDir()
|
||||
dirWithSpace := filepath.Join(tempDir, "Local Git")
|
||||
if err := os.MkdirAll(dirWithSpace, 0755); err != nil {
|
||||
t.Fatalf("create dir: %v", err)
|
||||
}
|
||||
longPath := filepath.Join(dirWithSpace, "gosentry.exe")
|
||||
if err := os.WriteFile(longPath, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("create file: %v", err)
|
||||
}
|
||||
|
||||
// GetShortPathName converts the long path to its 8.3 equivalent when 8.3
|
||||
// names are available; it returns the unchanged path otherwise.
|
||||
p16, err := syscall.UTF16PtrFromString(longPath)
|
||||
if err != nil {
|
||||
t.Fatalf("UTF16PtrFromString: %v", err)
|
||||
}
|
||||
buf := make([]uint16, syscall.MAX_PATH)
|
||||
n, err := syscall.GetShortPathName(p16, &buf[0], uint32(len(buf)))
|
||||
if err != nil {
|
||||
t.Skipf("GetShortPathName: %v", err)
|
||||
}
|
||||
shortPath := syscall.UTF16ToString(buf[:n])
|
||||
|
||||
if !sameWindowsPath(longPath, shortPath) {
|
||||
t.Fatalf("sameWindowsPath(%q, %q) = false; want true", longPath, shortPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartupShortcutPathUsesUserStartupFolder(t *testing.T) {
|
||||
t.Setenv("APPDATA", `C:\Users\mixem\AppData\Roaming`)
|
||||
|
||||
path, err := startupShortcutPath()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := `C:\Users\mixem\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\GoSentry.lnk`
|
||||
if path != expected {
|
||||
t.Fatalf("unexpected shortcut path: %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStartupShortcutHandlesCyrillicPath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
shortcutPath := filepath.Join(tempDir, "GoSentry.lnk")
|
||||
targetPath := filepath.Join(tempDir, "Программы и драйвера", "GoSentry", "gosentry.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
t.Fatalf("create target directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("create target file: %v", err)
|
||||
}
|
||||
|
||||
if err := createStartupShortcut(shortcutPath, targetPath, ""); err != nil {
|
||||
t.Fatalf("create shortcut: %v", err)
|
||||
}
|
||||
|
||||
actual, arguments, err := readShortcut(shortcutPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read shortcut: %v", err)
|
||||
}
|
||||
if !sameWindowsPath(actual, targetPath) {
|
||||
t.Fatalf("shortcut target mismatch: got %q want %q", actual, targetPath)
|
||||
}
|
||||
if arguments != domain.StartInTrayArgument {
|
||||
t.Fatalf("shortcut arguments mismatch: got %q want %q", arguments, domain.StartInTrayArgument)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStartupShortcutHandlesSpaces(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
shortcutPath := filepath.Join(tempDir, "GoSentry test.lnk")
|
||||
targetPath := filepath.Join(tempDir, "Program Files", "GoSentry", "gosentry.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
t.Fatalf("create target directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("create target file: %v", err)
|
||||
}
|
||||
|
||||
if err := createStartupShortcut(shortcutPath, targetPath, ""); err != nil {
|
||||
t.Fatalf("create shortcut: %v", err)
|
||||
}
|
||||
|
||||
actual, arguments, err := readShortcut(shortcutPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read shortcut: %v", err)
|
||||
}
|
||||
if !sameWindowsPath(actual, targetPath) {
|
||||
t.Fatalf("shortcut target mismatch: got %q want %q", actual, targetPath)
|
||||
}
|
||||
if arguments != domain.StartInTrayArgument {
|
||||
t.Fatalf("shortcut arguments mismatch: got %q want %q", arguments, domain.StartInTrayArgument)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user