mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-27 12:05:40 +00:00
db39a43f06
This PR is all about adding functionality that will enable the installer's upgrade sequence to terminate processes belonging to the previous version, and then subsequently restart instances belonging to the new version within the session(s) corresponding to the processes that were killed. There are multiple parts to this: * We add support for the Restart Manager APIs, which allow us to query the OS for a list of processes locking specific files; * We add the RestartableProcess and RestartableProcesses types that query additional information about the running processes that will allow us to correctly restart them in the future. These types also provide the ability to terminate the processes. * We add the StartProcessInSession family of APIs that permit us to create new processes within specific sessions. This is needed in order to properly attach a new GUI process to the same RDP session and desktop that its previously-terminated counterpart would have been running in. * I tweaked the winutil token APIs again. * A lot of this stuff is pretty hard to test without a very elaborate harness, but I added a unit test for the most complicated part (though it requires LocalSystem to run). Updates https://github.com/tailscale/corp/issues/13998 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package winutil
|
|
|
|
import (
|
|
"fmt"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const oldFashionedCleanupExitCode = 7778
|
|
|
|
// oldFashionedCleanup cleans up any outstanding binaries using older APIs.
|
|
// This would be necessary if the restart manager were to fail during the test.
|
|
func oldFashionedCleanup(t *testing.T, binary string) {
|
|
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
|
if err != nil {
|
|
t.Logf("CreateToolhelp32Snapshot failed: %v", err)
|
|
}
|
|
defer windows.CloseHandle(snap)
|
|
|
|
binary = filepath.Clean(binary)
|
|
binbase := filepath.Base(binary)
|
|
pe := windows.ProcessEntry32{
|
|
Size: uint32(unsafe.Sizeof(windows.ProcessEntry32{})),
|
|
}
|
|
for perr := windows.Process32First(snap, &pe); perr == nil; perr = windows.Process32Next(snap, &pe) {
|
|
curBin := windows.UTF16ToString(pe.ExeFile[:])
|
|
// Coarse check against the leaf name of the binary
|
|
if !strings.EqualFold(binbase, curBin) {
|
|
continue
|
|
}
|
|
|
|
proc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_TERMINATE, false, pe.ProcessID)
|
|
if err != nil {
|
|
t.Logf("OpenProcess failed: %v", err)
|
|
continue
|
|
}
|
|
defer windows.CloseHandle(proc)
|
|
|
|
img, err := ProcessImageName(proc)
|
|
if err != nil {
|
|
t.Logf("ProcessImageName failed: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Now check that their fully-qualified paths match.
|
|
if !strings.EqualFold(binary, filepath.Clean(img)) {
|
|
continue
|
|
}
|
|
|
|
t.Logf("Found leftover pid %d, terminating...", pe.ProcessID)
|
|
if err := windows.TerminateProcess(proc, oldFashionedCleanupExitCode); err != nil && err != windows.ERROR_ACCESS_DENIED {
|
|
t.Logf("TerminateProcess failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRestartableProcessesImpl(N int, t *testing.T) {
|
|
const binary = "testrestartableprocesses"
|
|
fq := pathToTestProg(t, binary)
|
|
|
|
for i := 0; i < N; i++ {
|
|
startTestProg(t, binary, "RestartableProcess")
|
|
}
|
|
t.Cleanup(func() {
|
|
oldFashionedCleanup(t, fq)
|
|
})
|
|
|
|
logf := func(format string, args ...any) {
|
|
t.Logf(format, args...)
|
|
}
|
|
rms, err := NewRestartManagerSession(logf)
|
|
if err != nil {
|
|
t.Fatalf("NewRestartManagerSession: %v", err)
|
|
}
|
|
defer rms.Close()
|
|
|
|
if err := rms.AddPaths([]string{fq}); err != nil {
|
|
t.Fatalf("AddPaths: %v", err)
|
|
}
|
|
|
|
ups, err := rms.AffectedProcesses()
|
|
if err != nil {
|
|
t.Fatalf("AffectedProcesses: %v", err)
|
|
}
|
|
|
|
rps := NewRestartableProcesses()
|
|
defer rps.Close()
|
|
|
|
for _, up := range ups {
|
|
rp, err := up.AsRestartableProcess()
|
|
if err != nil {
|
|
t.Errorf("AsRestartableProcess: %v", err)
|
|
continue
|
|
}
|
|
rps.Add(rp)
|
|
}
|
|
|
|
const terminateWithExitCode = 7777
|
|
if err := rps.Terminate(logf, terminateWithExitCode, time.Duration(15)*time.Second); err != nil {
|
|
t.Errorf("Terminate: %v", err)
|
|
}
|
|
|
|
for k, v := range rps {
|
|
if v.hasExitCode {
|
|
if v.exitCode != terminateWithExitCode {
|
|
// Not strictly an error, but worth noting.
|
|
logf("Subprocess %d terminated with unexpected exit code %d", k, v.exitCode)
|
|
}
|
|
} else {
|
|
t.Errorf("Subprocess %d did not produce an exit code", k)
|
|
}
|
|
if v.handle != 0 {
|
|
t.Errorf("Subprocess %d is unexpectedly still open", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRestartableProcesses(t *testing.T) {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
t.Fatalf("Could not obtain current user")
|
|
}
|
|
if u.Uid != localSystemSID {
|
|
t.Skipf("This test must be run as SYSTEM")
|
|
}
|
|
|
|
forN := func(fn func(int, *testing.T)) func([]int) {
|
|
return func(ns []int) {
|
|
for _, n := range ns {
|
|
t.Run(fmt.Sprintf("N=%d", n), func(tt *testing.T) { fn(n, tt) })
|
|
}
|
|
}
|
|
}(testRestartableProcessesImpl)
|
|
|
|
// Testing indicates that the restart manager cannot handle more than 127 processes (on Windows 10, at least), so we use that as our highest value.
|
|
ns := []int{0, 1, _MAXIMUM_WAIT_OBJECTS - 1, _MAXIMUM_WAIT_OBJECTS, _MAXIMUM_WAIT_OBJECTS + 1, _MAXIMUM_WAIT_OBJECTS*2 - 1}
|
|
forN(ns)
|
|
}
|