Files
soft_quay/app-modern/platform/windows/platform_windows.go
T

221 lines
5.6 KiB
Go

//go:build windows
package windows
import (
"context"
"errors"
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
"unsafe"
"golang.org/x/sys/windows"
"softbox.local/core/application/launch"
)
const seeMaskNoCloseProcess = 0x00000040
var (
procRtlGetVersion = windows.NewLazySystemDLL("ntdll.dll").NewProc("RtlGetVersion")
procShellExecuteExW = windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteExW")
)
type platform struct{}
func newPlatform() Platform {
return platform{}
}
func (platform) OS() string {
return "windows"
}
func (platform) Edition() Edition {
return EditionModern
}
func (platform) IsCompatible(minOS string) (bool, error) {
version, err := currentVersion()
if err != nil {
return false, err
}
return versionSupports(minOS, version.major, version.minor, version.build, uint32(version.servicePack))
}
func (platform) IsRunning(_ string, entrypoint string) (bool, error) {
return entrypointIsRunning(entrypoint, func() (processSnapshot, error) {
return newToolhelpSnapshot(filepath.Base(entrypoint))
})
}
func (target platform) WaitForExit(ctx context.Context, appID, entrypoint string, timeout time.Duration) error {
return waitForExit(ctx, timeout, func() (bool, error) {
return target.IsRunning(appID, entrypoint)
}, systemExitWaitClock{})
}
func (platform) Start(command launch.Command) (int, error) {
if !filepath.IsAbs(command.Entrypoint) || !filepath.IsAbs(command.WorkingDirectory) {
return 0, fmt.Errorf("launch command must contain absolute paths")
}
if command.RequiresAdmin {
return startElevated(command)
}
cmd := exec.Command(command.Entrypoint)
cmd.Dir = command.WorkingDirectory
if err := cmd.Start(); err != nil {
return 0, err
}
return cmd.Process.Pid, nil
}
type toolhelpSnapshot struct {
handle windows.Handle
targetName string
entry windows.ProcessEntry32
started bool
}
func newToolhelpSnapshot(targetName string) (processSnapshot, error) {
handle, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if err != nil {
return nil, err
}
return &toolhelpSnapshot{handle: handle, targetName: targetName}, nil
}
func (snapshot *toolhelpSnapshot) NextImagePath() (string, bool, error) {
for {
var err error
if !snapshot.started {
snapshot.entry.Size = uint32(unsafe.Sizeof(snapshot.entry))
err = windows.Process32First(snapshot.handle, &snapshot.entry)
snapshot.started = true
} else {
err = windows.Process32Next(snapshot.handle, &snapshot.entry)
}
if err != nil {
if errors.Is(err, windows.ERROR_NO_MORE_FILES) {
return "", false, nil
}
return "", false, err
}
// ExeFile only narrows the expensive query. The identity decision below
// always uses QueryFullProcessImageName's normalized full path.
if !strings.EqualFold(windows.UTF16ToString(snapshot.entry.ExeFile[:]), snapshot.targetName) {
continue
}
path, err := fullProcessImagePath(snapshot.entry.ProcessID)
if err != nil {
return "", false, err
}
return path, true, nil
}
}
func (snapshot *toolhelpSnapshot) Close() error {
return windows.CloseHandle(snapshot.handle)
}
func fullProcessImagePath(pid uint32) (string, error) {
process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", err
}
defer windows.CloseHandle(process)
for size := uint32(260); size <= 32768; size *= 2 {
buffer := make([]uint16, size)
length := size
err = windows.QueryFullProcessImageName(process, 0, &buffer[0], &length)
if err == nil {
return windows.UTF16ToString(buffer[:length]), nil
}
if !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) {
return "", err
}
}
return "", fmt.Errorf("process image path exceeds 32768 UTF-16 code units")
}
type rtlOSVersionInfoEx struct {
size uint32
major uint32
minor uint32
build uint32
platformID uint32
csdVersion [128]uint16
servicePack uint16
servicePackMinor uint16
suiteMask uint16
productType byte
reserved byte
}
func currentVersion() (rtlOSVersionInfoEx, error) {
version := rtlOSVersionInfoEx{size: uint32(unsafe.Sizeof(rtlOSVersionInfoEx{}))}
status, _, _ := procRtlGetVersion.Call(uintptr(unsafe.Pointer(&version)))
if status != 0 {
return rtlOSVersionInfoEx{}, fmt.Errorf("RtlGetVersion failed with status 0x%x", status)
}
return version, nil
}
type shellExecuteInfo struct {
size uint32
mask uint32
hwnd uintptr
verb *uint16
file *uint16
parameters *uint16
directory *uint16
show int32
instance uintptr
idList uintptr
class *uint16
keyClass uintptr
hotKey uint32
icon uintptr
process windows.Handle
}
func startElevated(command launch.Command) (int, error) {
verb, err := windows.UTF16PtrFromString("runas")
if err != nil {
return 0, err
}
file, err := windows.UTF16PtrFromString(command.Entrypoint)
if err != nil {
return 0, err
}
directory, err := windows.UTF16PtrFromString(command.WorkingDirectory)
if err != nil {
return 0, err
}
info := shellExecuteInfo{
size: uint32(unsafe.Sizeof(shellExecuteInfo{})),
mask: seeMaskNoCloseProcess,
verb: verb,
file: file,
directory: directory,
show: 1,
}
result, _, callErr := procShellExecuteExW.Call(uintptr(unsafe.Pointer(&info)))
if result == 0 {
return 0, fmt.Errorf("ShellExecuteExW failed: %w", callErr)
}
if info.process == 0 {
return 0, fmt.Errorf("ShellExecuteExW did not return a process handle")
}
defer windows.CloseHandle(info.process)
pid, err := windows.GetProcessId(info.process)
if err != nil {
return 0, err
}
return int(pid), nil
}