From c88dcf22b66937f79b95c7fb594cfb291b425f8e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 16 Jul 2019 01:54:58 -0600 Subject: [PATCH] playing with PATH on Windows --- .gitignore | 2 + winpath/README.md | 63 ++++++++++++ winpath/go.mod | 5 + winpath/go.sum | 2 + winpath/winpath.go | 197 ++++++++++++++++++++++++++++++++++++++ winpath/winpath_unsafe.go | 29 ++++++ 6 files changed, 298 insertions(+) create mode 100644 winpath/README.md create mode 100644 winpath/go.mod create mode 100644 winpath/go.sum create mode 100644 winpath/winpath.go create mode 100644 winpath/winpath_unsafe.go diff --git a/.gitignore b/.gitignore index 9a3a8d8..f5ad4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*~ + # ---> Go # Binaries for programs and plugins *.exe diff --git a/winpath/README.md b/winpath/README.md new file mode 100644 index 0000000..b37d33f --- /dev/null +++ b/winpath/README.md @@ -0,0 +1,63 @@ +# winpath + +An example of getting, setting, and broadcasting PATHs on Windows. + +This requires the `unsafe` package to use a syscall with special message poitners to update `PATH` without a reboot. +It will also build without `unsafe`. + +```bash +go build -tags unsafe -o winpath.exe +``` + +```bash +winpath show + + %USERPROFILE%\AppData\Local\Microsoft\WindowsApps + C:\Users\me\AppData\Local\Programs\Microsoft VS Code\bin + %USERPROFILE%\go\bin + C:\Users\me\AppData\Roaming\npm + C:\Users\me\AppData\Local\Keybase\ +``` + +```bash +winpath append C:\someplace\special + + Run the following for changes to take affect immediately: + PATH %PATH%;C:\someplace\special +``` + +```bash +winpath prepend C:\someplace\special + + Run the following for changes to take affect immediately: + PATH C:\someplace\special;%PATH% +``` + +```bash +winpath remove C:\someplace\special +``` + +# Special Considerations + +Giving away the secret sauce right here: + +* `HWND_BROADCAST` +* `WM_SETTINGCHANGE` + +This is essentially the snippet you need to have the HKCU and HKLM Environment registry keys propagated without rebooting: + +```go + HWND_BROADCAST := uintptr(0xffff) + WM_SETTINGCHANGE := uintptr(0x001A) + _, _, err := syscall. + NewLazyDLL("user32.dll"). + NewProc("SendMessageW"). + Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("ENVIRONMENT")))) + +``` + +* `os.Getenv("COMSPEC")` +* `os.Getenv("SHELL")` + +If you check `SHELL` and it isn't empty, then you're probably in MINGW or some such. +If that's empty but `COMSPEC` isn't, you can be reasonably sure that you're in cmd.exe or Powershell. diff --git a/winpath/go.mod b/winpath/go.mod new file mode 100644 index 0000000..f41c12d --- /dev/null +++ b/winpath/go.mod @@ -0,0 +1,5 @@ +module git.coolaj86.com\coolaj86\go-examples\winpath + +go 1.12 + +require golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 diff --git a/winpath/go.sum b/winpath/go.sum new file mode 100644 index 0000000..c5e9ece --- /dev/null +++ b/winpath/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/winpath/winpath.go b/winpath/winpath.go new file mode 100644 index 0000000..409b23c --- /dev/null +++ b/winpath/winpath.go @@ -0,0 +1,197 @@ +// +build windows + +// We both need to +// * use the registry editor directly to avoid possible PATH truncation +// ( https://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows ) +// ( https://superuser.com/questions/387619/overcoming-the-1024-character-limit-with-setx ) +// * explicitly send WM_SETTINGCHANGE +// ( https://github.com/golang/go/issues/18680#issuecomment-275582179 ) +// * also install as a service +// ( https://github.com/golang/sys/blob/master/windows/svc/example/install.go ) +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/windows/registry" +) + +var sendmsg func() + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: winpath show|append|prepend|remove \n") +} + +func main() { + fmt.Println("PATH:", os.Getenv("PATH")) + //fpath, err := exec.LookPath("reg") + //fmt.Println("LookPath(\"reg\"):", fpath, err) + + shell := os.Getenv("SHELL") + if "" == shell { + if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") { + shell = "cmd" + } + } + fmt.Println("SHELL?", shell) + fmt.Println("os.PathListSeparator:", string(os.PathListSeparator)) + // WM_SETTING_CHANGE + // https://gist.github.com/microo8/c1b9525efab9bb462adf9d123e855c52 + // os.Setenv("PATH") + + // TODO --system to add to the system PATH rather than the user PATH + if len(os.Args) < 2 { + usage() + os.Exit(1) + } + action := os.Args[1] + + paths, err := Paths() + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(2) + } + + if "show" == action { + if len(os.Args) > 2 { + usage() + } + fmt.Println() + for i := range paths { + fmt.Println("\t" + paths[i]) + } + fmt.Println() + return + } + + if len(os.Args) < 3 { + usage() + os.Exit(1) + } + + pathname := os.Args[2] + abspath, err := filepath.Abs(pathname) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(2) + } + if "remove" != action { + stat, err := os.Stat(pathname) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(2) + } + if !stat.IsDir() { + fmt.Fprintf(os.Stderr, "%q is not a directory (folder)\n", pathname) + os.Exit(2) + } + } + + index := -1 + for i := range paths { + if pathname == paths[i] { + index = i + break + } + if abspath == paths[i] { + index = i + break + } + } + + switch action { + default: + usage() + os.Exit(1) + case "append": + if index >= 0 { + fmt.Fprintf(os.Stderr, "%q is already in PATH at position %d. Remove it first to re-order.\n", pathname, index) + os.Exit(3) + } + paths = append(paths, pathname) + fmt.Println("Run this to cause settings to take affect immediately:") + fmt.Println("\tPATH %PATH%;" + pathname) + case "prepend": + if index >= 0 { + fmt.Fprintf(os.Stderr, "%q is already in PATH at position %d. Remove it first to re-order.\n", pathname, index) + os.Exit(3) + } + paths = append([]string{pathname}, paths...) + fmt.Println("Run this to cause settings to take affect immediately:") + fmt.Println("\tPATH " + pathname + ";%PATH%") + case "remove": + if index < 0 { + fmt.Fprintf(os.Stderr, "%q is NOT in PATH.\n", pathname) + os.Exit(3) + } + oldpaths := paths + paths = []string{} + for i := range oldpaths { + if i != index { + paths = append(paths, oldpaths[i]) + } + } + } + + k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.SET_VALUE) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(4) + } + defer k.Close() + // ";" on Windows + err = k.SetStringValue(`Path`, strings.Join(paths, string(os.PathListSeparator))) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(4) + } + err = k.Close() + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(4) + + } + + err = os.Setenv(`PATH`, strings.Join(paths, string(os.PathListSeparator))) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(4) + } + if nil != sendmsg { + fmt.Println("Open a new Terminal for the updated PATH") + sendmsg() + } else { + fmt.Println("You'll need to reboot for setting to take affect") + } +} + +func Paths() ([]string, error) { + k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE) + if err != nil { + return nil, err + } + defer k.Close() + + // This is case insensitive (PATH, Path, path) + s, _, err := k.GetStringValue("Path") + if err != nil { + return nil, err + } + // ";" on Windows + return strings.Split(s, string(os.PathListSeparator)), nil + + /* + shstr := loader() + if "" == shstr { + shstr = "NO_SHELL_DETECTED" + } + if "" == s { + s = "NO_WINREG_PATH" + } + + return fmt.Sprintf("%s\n%s", s, shstr) + */ +} diff --git a/winpath/winpath_unsafe.go b/winpath/winpath_unsafe.go new file mode 100644 index 0000000..8dba646 --- /dev/null +++ b/winpath/winpath_unsafe.go @@ -0,0 +1,29 @@ +// +build windows,unsafe + +package main + +import ( + "os" + "fmt" + "syscall" + "unsafe" +) + +const ( + HWND_BROADCAST = uintptr(0xffff) + WM_SETTINGCHANGE = uintptr(0x001A) +) + +func init() { + sendmsg = func() { + //x, y, err := syscall. + _, _, err := syscall. + NewLazyDLL("user32.dll"). + NewProc("SendMessageW"). + Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("ENVIRONMENT")))) + //fmt.Fprintf(os.Stderr, "%d, %d, %s\n", x, y, err) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + } +}