//go:build windows // +build windows package main import ( "errors" "io/fs" "os" "path/filepath" "strings" "syscall" "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) type operation int const ( HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x001A SMTO_ABORTIFHUNG = 0x0002 ERR_BAD_ARGS = 0x000A OPERATION_FAILED = 0x06AC Environment = "Environment" Add operation = iota Remove Open NotSpecified ) func main() { op := NotSpecified if len(os.Args) >= 2 { switch os.Args[1] { case "add": op = Add case "remove": op = Remove case "open": op = Open } } // Stay silent since ran from an installer if op == NotSpecified { alert("Usage: " + filepath.Base(os.Args[0]) + " [add|remove]\n\nThis utility adds or removes the podman directory to the Windows Path.") os.Exit(ERR_BAD_ARGS) } // Hidden operation as a workaround for the installer if op == Open && len(os.Args) > 2 { if err := winOpenFile(os.Args[2]); err != nil { os.Exit(OPERATION_FAILED) } os.Exit(0) } if err := modify(op); err != nil { os.Exit(OPERATION_FAILED) } } func modify(op operation) error { exe, err := os.Executable() if err != nil { return err } exe, err = filepath.EvalSymlinks(exe) if err != nil { return err } target := filepath.Dir(exe) if op == Remove { return removePathFromRegistry(target) } return addPathToRegistry(target) } // Appends a directory to the Windows Path stored in the registry func addPathToRegistry(dir string) error { k, _, err := registry.CreateKey(registry.CURRENT_USER, Environment, registry.WRITE|registry.READ) if err != nil { return err } defer k.Close() existing, typ, err := k.GetStringValue("Path") if err != nil { return err } // Is this directory already on the windows path? for _, element := range strings.Split(existing, ";") { if strings.EqualFold(element, dir) { // Path already added return nil } } // If the existing path is empty we don't want to start with a delimiter if len(existing) > 0 { existing += ";" } existing += dir // It's important to preserve the registry key type so that it will be interpreted correctly // EXPAND = evaluate variables in the expression, e.g. %PATH% should be expanded to the system path // STRING = treat the contents as a string literal if typ == registry.EXPAND_SZ { err = k.SetExpandStringValue("Path", existing) } else { err = k.SetStringValue("Path", existing) } if err == nil { broadcastEnvironmentChange() } return err } // Removes all occurrences of a directory path from the Windows path stored in the registry func removePathFromRegistry(path string) error { k, err := registry.OpenKey(registry.CURRENT_USER, Environment, registry.READ|registry.WRITE) if err != nil { if errors.Is(err, fs.ErrNotExist) { // Nothing to clean up, the Environment registry key does not exist. return nil } return err } defer k.Close() existing, typ, err := k.GetStringValue("Path") if err != nil { return err } var elements []string for _, element := range strings.Split(existing, ";") { if strings.EqualFold(element, path) { continue } elements = append(elements, element) } newPath := strings.Join(elements, ";") // Preserve value type (see corresponding comment above) if typ == registry.EXPAND_SZ { err = k.SetExpandStringValue("Path", newPath) } else { err = k.SetStringValue("Path", newPath) } if err == nil { broadcastEnvironmentChange() } return err } // Sends a notification message to all top level windows informing them the environmental settings have changed. // Applications such as the Windows command prompt and powershell will know to stop caching stale values on // subsequent restarts. Since applications block the sender when receiving a message, we set a 3 second timeout func broadcastEnvironmentChange() { env, _ := syscall.UTF16PtrFromString(Environment) user32 := syscall.NewLazyDLL("user32") proc := user32.NewProc("SendMessageTimeoutW") millis := 3000 _, _, _ = proc.Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(env)), SMTO_ABORTIFHUNG, uintptr(millis), 0) } // Creates an "error" style pop-up window func alert(caption string) int { // Error box style format := 0x10 user32 := syscall.NewLazyDLL("user32.dll") captionPtr, _ := syscall.UTF16PtrFromString(caption) titlePtr, _ := syscall.UTF16PtrFromString("winpath") ret, _, _ := user32.NewProc("MessageBoxW").Call( uintptr(0), uintptr(unsafe.Pointer(captionPtr)), uintptr(unsafe.Pointer(titlePtr)), uintptr(format)) return int(ret) } func winOpenFile(file string) error { verb, _ := syscall.UTF16PtrFromString("open") fileW, _ := syscall.UTF16PtrFromString(file) return windows.ShellExecute(0, verb, fileW, nil, nil, windows.SW_NORMAL) }