//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 EditionLegacy } 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 }