arblant
firefox-vpn-client / core / internal / update / process_windows.go
process_windows.go161 lines · 4.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
package update

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
)

const platformSupported = true

// mutexName names the lock that an update of the app in appDir holds while
// it runs (Apply), so a core started meanwhile leaves its files alone.
func mutexName(appDir string) string {
	sum := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(appDir))))
	return `Local\FirefoxVPNUpdate-` + hex.EncodeToString(sum[:8])
}

// updating reports whether an update of the app in appDir runs now.
func updating(appDir string) bool {
	name, err := windows.UTF16PtrFromString(mutexName(appDir))
	if err != nil {
		return false
	}
	h, err := windows.OpenMutex(windows.SYNCHRONIZE, false, name)
	if err != nil {
		return false
	}
	windows.CloseHandle(h)
	return true
}

// lockApp takes the lock of updating(appDir) until the returned function
// is called; it fails while another update of the same app runs.
func lockApp(appDir string) (func(), error) {
	name, err := windows.UTF16PtrFromString(mutexName(appDir))
	if err != nil {
		return nil, err
	}
	h, err := windows.CreateMutex(nil, false, name)
	if err != nil {
		if h != 0 {
			windows.CloseHandle(h)
		}
		if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
			return nil, errors.New("another update of this app is running")
		}
		return nil, err
	}
	return func() { windows.CloseHandle(h) }, nil
}

// waitForExit waits until the processes have exited, for at most timeout
// in all. A process that is gone already counts as exited. The processes
// are opened before any wait, so a finished one's id cannot be taken by
// another program in between.
func waitForExit(pids []int, timeout time.Duration) error {
	type proc struct {
		pid int
		h   windows.Handle
	}
	var procs []proc
	for _, pid := range pids {
		h, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid))
		if err != nil {
			continue
		}
		procs = append(procs, proc{pid, h})
	}
	defer func() {
		for _, p := range procs {
			windows.CloseHandle(p.h)
		}
	}()
	deadline := time.Now().Add(timeout)
	for _, p := range procs {
		left := max(time.Until(deadline), 0)
		ev, err := windows.WaitForSingleObject(p.h, uint32(left/time.Millisecond))
		if err != nil {
			return fmt.Errorf("waiting for process %d: %w", p.pid, err)
		}
		if ev != windows.WAIT_OBJECT_0 {
			return fmt.Errorf("the app did not close (process %d still runs)", p.pid)
		}
	}
	return nil
}

// uninstallKey is Setup's entry in Windows' list of installed apps, for the
// current user: its AppId (app/windows/installer/firefox-vpn.iss) and "_is1".
// Tests use a key of their own.
var uninstallKey = `Software\Microsoft\Windows\CurrentVersion\Uninstall\{6F3A2C1E-9B4D-4E7A-8C21-5D0F7B9E4A63}_is1`

// setInstalledVersion sets version v in Setup's entry when Setup installed
// the app in appDir; nothing for a portable copy (no entry, or one for
// another folder).
func setInstalledVersion(appDir, v string) error {
	k, err := registry.OpenKey(registry.CURRENT_USER, uninstallKey, registry.QUERY_VALUE|registry.SET_VALUE)
	if errors.Is(err, registry.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	defer k.Close()
	location, _, err := k.GetStringValue("InstallLocation")
	if err != nil || !strings.EqualFold(filepath.Clean(location), filepath.Clean(appDir)) {
		return nil
	}
	if err := k.SetStringValue("DisplayVersion", v); err != nil {
		return err
	}
	parts := strings.Split(v, ".")
	part := func(i int) uint32 {
		if i >= len(parts) {
			return 0
		}
		n, _ := strconv.ParseUint(parts[i], 10, 32)
		return uint32(n)
	}
	for name, value := range map[string]uint32{"MajorVersion": part(0), "VersionMajor": part(0), "MinorVersion": part(1), "VersionMinor": part(1)} {
		if err := k.SetDWordValue(name, value); err != nil {
			return err
		}
	}
	return nil
}

// startUpdater starts the copy of the core at exe with -update, apart from
// this process: no console, its own process group, its working folder
// outside the app's (an open working folder could not be renamed).
func startUpdater(exe, dataDir string) error {
	cmd := exec.Command(exe, "-update", "-data-dir", dataDir)
	cmd.Dir = filepath.Dir(exe)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
	}
	if err := cmd.Start(); err != nil {
		return err
	}
	return cmd.Process.Release()
}

// startApp starts the app in appDir with args.
func startApp(appDir string, args []string) error {
	cmd := exec.Command(filepath.Join(appDir, AppExe), args...)
	cmd.Dir = appDir
	if err := cmd.Start(); err != nil {
		return err
	}
	return cmd.Process.Release()
}