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