Skip to content

Commit

Permalink
Switch to a new installer approach using a path manipulation helper
Browse files Browse the repository at this point in the history
Fixes containers#11089 - cleanup PATH on MSI uninstall
Additionally fixes scenarios where the path can be overwritten by setx
Also removes the console flash, since the helper is built as a silent gui
Helper executable can be rerun by user to repair PATHs broken by other tools
Utilizes executable location instead of passed parameters to remove delicate escaping requirements

[NO NEW TESTS NEEDED]

Signed-off-by: Jason T. Greene <[email protected]>
  • Loading branch information
n1hility committed Dec 23, 2021
1 parent 74a58fa commit 3ac5999
Show file tree
Hide file tree
Showing 9 changed files with 958 additions and 5 deletions.
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ podman-remote-windows: ## Build podman-remote for Windows
GOOS=windows \
bin/windows/podman.exe

.PHONY: podman-winpath
podman-winpath: .gopathok $(SOURCES) go.mod go.sum
CGO_ENABLED=0 \
GOOS=windows \
$(GO) build \
$(BUILDFLAGS) \
-ldflags -H=windowsgui \
-o bin/windows/winpath.exe \
./cmd/winpath

.PHONY: podman-remote-darwin
podman-remote-darwin: ## Build podman-remote for macOS
$(MAKE) \
Expand Down Expand Up @@ -685,7 +695,7 @@ podman-remote-release-%.zip: test/version/version ## Build podman-remote for %=$
.PHONY: podman.msi
podman.msi: test/version/version ## Build podman-remote, package for installation on Windows
$(MAKE) podman-v$(RELEASE_NUMBER).msi
podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs
podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath
$(eval DOCFILE := docs/build/remote/windows)
find $(DOCFILE) -print | \
wixl-heat --var var.ManSourceDir --component-group ManFiles \
Expand Down
184 changes: 184 additions & 0 deletions cmd/winpath/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//go:build windows
// +build windows

package main

import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"

"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
NotSpecified
)

func main() {
op := NotSpecified
if len(os.Args) >= 2 {
switch os.Args[1] {
case "add":
op = Add
case "remove":
op = Remove
}
}

// 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)
}

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 occurences 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 cleanup, 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 setings 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)
}
12 changes: 8 additions & 4 deletions contrib/msi/podman.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@
<Component Id="MainExecutable" Guid="73752F94-6589-4C7B-ABED-39D655A19714" Win64="Yes">
<File Id="520C6E17-77A2-4F41-9611-30FA763A0702" Name="podman.exe" Source="bin/windows/podman.exe" KeyPath="yes"/>
</Component>
<Component Id="WinPathExecutable" Guid="00F5B731-D4A6-4B69-87B0-EA4EBAB89F95" Win64="Yes">
<File Id="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Name="winpath.exe" Source="bin/windows/winpath.exe" KeyPath="yes"/>
</Component>
</Directory>
</Directory>
</Directory>
</Directory>

<Property Id="setx" Value="setx.exe"/>
<!-- Directory table entries have a trailing slash, so an extra backslash is needed to prevent escaping the quote -->
<CustomAction Id="ChangePath" ExeCommand="PATH &quot;%PATH%;[INSTALLDIR]\&quot;" Property="setx" Execute="deferred" Impersonate="yes" Return="check"/>
<CustomAction Id="AddPath" ExeCommand="add" FileKey="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Execute="deferred" Impersonate="yes" Return="check"/>
<CustomAction Id="RemovePath" ExeCommand="remove" FileKey="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Execute="deferred" Impersonate="yes" Return="check"/>

<Feature Id="Complete" Level="1">
<ComponentRef Id="INSTALLDIR_Component"/>
<ComponentRef Id="MainExecutable"/>
<ComponentRef Id="WinPathExecutable"/>
<ComponentGroupRef Id="ManFiles"/>
</Feature>

Expand All @@ -46,7 +49,8 @@

<InstallExecuteSequence>
<RemoveExistingProducts Before="InstallInitialize"/>
<Custom Action="ChangePath" After="InstallServices">NOT Installed</Custom>
<Custom Action="AddPath" After="InstallFiles">NOT Installed</Custom>
<Custom Action="RemovePath" Before="RemoveFiles" After="InstallInitiailize">(REMOVE="ALL") AND (NOT UPGRADINGPRODUCTCODE)</Custom>
</InstallExecuteSequence>

</Product>
Expand Down
Loading

0 comments on commit 3ac5999

Please sign in to comment.