Relaunch through UAC when (un)installing the service unprivileged
--install-service/--remove-service on Windows no longer fail with 'Access is denied' from a normal shell: the process re-runs itself via ShellExecuteEx 'runas', waits for the elevated child and mirrors its exit code. The child gets --elevated-child and pauses for a keypress so its console output stays readable. Declining the prompt reports 'UAC prompt declined'.
This commit is contained in:
@@ -9,14 +9,18 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
|
||||
@@ -242,6 +246,83 @@ func Remove() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var procShellExecuteExW = windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteExW")
|
||||
|
||||
const seeMaskNoCloseProcess = 0x40
|
||||
|
||||
// shellExecuteInfo mirrors SHELLEXECUTEINFOW (64-bit layout).
|
||||
type shellExecuteInfo struct {
|
||||
cbSize uint32
|
||||
fMask uint32
|
||||
hwnd uintptr
|
||||
lpVerb *uint16
|
||||
lpFile *uint16
|
||||
lpParameters *uint16
|
||||
lpDirectory *uint16
|
||||
nShow int32
|
||||
_ int32
|
||||
hInstApp uintptr
|
||||
lpIDList unsafe.Pointer
|
||||
lpClass *uint16
|
||||
hkeyClass uintptr
|
||||
dwHotKey uint32
|
||||
_ uint32
|
||||
hIcon uintptr
|
||||
hProcess windows.Handle
|
||||
}
|
||||
|
||||
// Elevated reports whether the current process token is UAC-elevated.
|
||||
func Elevated() bool {
|
||||
var token windows.Token
|
||||
if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); err != nil {
|
||||
return false
|
||||
}
|
||||
defer token.Close()
|
||||
return token.IsElevated()
|
||||
}
|
||||
|
||||
// RelaunchElevated re-runs the current executable elevated via the UAC
|
||||
// "runas" verb with the given arguments, waits for the child, and returns
|
||||
// its exit code. The child gets a fresh console window for its output.
|
||||
func RelaunchElevated(args []string) (int, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
quoted := make([]string, len(args))
|
||||
for i, a := range args {
|
||||
quoted[i] = syscall.EscapeArg(a)
|
||||
}
|
||||
cwd, _ := os.Getwd()
|
||||
verb, _ := windows.UTF16PtrFromString("runas")
|
||||
exeP, _ := windows.UTF16PtrFromString(exe)
|
||||
params, _ := windows.UTF16PtrFromString(strings.Join(quoted, " "))
|
||||
dir, _ := windows.UTF16PtrFromString(cwd)
|
||||
info := shellExecuteInfo{
|
||||
fMask: seeMaskNoCloseProcess,
|
||||
lpVerb: verb,
|
||||
lpFile: exeP,
|
||||
lpParameters: params,
|
||||
lpDirectory: dir,
|
||||
nShow: windows.SW_NORMAL,
|
||||
}
|
||||
info.cbSize = uint32(unsafe.Sizeof(info))
|
||||
r, _, callErr := procShellExecuteExW.Call(uintptr(unsafe.Pointer(&info)))
|
||||
if r == 0 {
|
||||
if errors.Is(callErr, syscall.Errno(1223)) { // ERROR_CANCELLED
|
||||
return 0, ErrUserCancelled
|
||||
}
|
||||
return 0, fmt.Errorf("ShellExecuteEx: %w", callErr)
|
||||
}
|
||||
defer windows.CloseHandle(windows.Handle(info.hProcess))
|
||||
windows.WaitForSingleObject(windows.Handle(info.hProcess), windows.INFINITE)
|
||||
var code uint32
|
||||
if err := windows.GetExitCodeProcess(windows.Handle(info.hProcess), &code); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(code), nil
|
||||
}
|
||||
|
||||
// grantAccess gives the virtual account the icacls permission set (e.g.
|
||||
// "(OI)(CI)(M)") on path.
|
||||
func grantAccess(path, perms string) error {
|
||||
|
||||
Reference in New Issue
Block a user