Files
run/runtime/process_window_windows.go

771 lines
27 KiB
Go

//go:build windows
package runtime
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
const (
managedProcessHelperFlag = "--run-managed-process-helper"
managedProcessDetachedFlags = windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP | windows.CREATE_BREAKAWAY_FROM_JOB
)
func configureManagedProcessCommand(cmd *exec.Cmd) {
// Run may itself be launched by a service or task scheduler job that
// terminates its process tree on shutdown. The helper is the durable
// owner of the game process, so ask Windows to keep it outside that job.
// The helper itself does not need a console: its stdout/stderr are already
// durable files. Detaching also prevents an operator Ctrl+C used to restart
// Run from propagating into the helper, cmd wrapper, or game process whose
// logs the next Run instance must resume tailing.
// CREATE_NEW_CONSOLE creates a second hidden conhost in a non-interactive
// Task Scheduler session and prevents the child pseudo console from
// initializing on Windows Server.
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: managedProcessDetachedFlags, HideWindow: true}
}
// Windows console output is collected by a child helper that owns the
// pseudo-console. The helper inherits the durable stdout/stderr files, so it
// remains attached to the supervised process when Run itself is updated or
// restarted. The new Run instance resumes tailing those files by offset.
type windowsFileManagedProcess struct {
cmd *exec.Cmd
helperExecutable string
targetPID int
}
type managedProcessHelperSpec struct {
Command managedProcessHelperCommand `json:"command"`
StopEventName string `json:"stopEventName,omitempty"`
StdoutPath string `json:"stdoutPath,omitempty"`
StderrPath string `json:"stderrPath,omitempty"`
PIDPath string `json:"pidPath,omitempty"`
}
type managedProcessHelperCommand struct {
WorkDir string `json:"workDir,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
OutputMode string `json:"outputMode,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
JobID string `json:"jobId,omitempty"`
Capability string `json:"capability,omitempty"`
Action string `json:"action,omitempty"`
}
func managedProcessHelperCommandFromProcess(command ProcessCommand) managedProcessHelperCommand {
return managedProcessHelperCommand{WorkDir: command.WorkDir, Args: append([]string(nil), command.Args...), Env: copyStringMap(command.Env), OutputMode: command.OutputMode, Timeout: command.Timeout, JobID: command.JobID, Capability: command.Capability, Action: command.Action}
}
func (command managedProcessHelperCommand) processCommand() ProcessCommand {
return ProcessCommand{WorkDir: command.WorkDir, Args: append([]string(nil), command.Args...), Env: copyStringMap(command.Env), OutputMode: command.OutputMode, Timeout: command.Timeout, JobID: command.JobID, Capability: command.Capability, Action: command.Action}
}
func startManagedProcess(command ProcessCommand, files managedProcessFiles, stopEventName string) (managedProcess, error) {
pidPath := files.stdout.Name() + ".pid"
_ = os.Remove(pidPath)
body, err := json.Marshal(managedProcessHelperSpec{Command: managedProcessHelperCommandFromProcess(command), StopEventName: stopEventName, StdoutPath: files.stdout.Name(), StderrPath: files.stderr.Name(), PIDPath: pidPath})
if err != nil {
return nil, fmt.Errorf("encode managed process helper spec: %w", err)
}
payload := base64.RawURLEncoding.EncodeToString(body)
executable, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("resolve Run executable for managed process helper: %w", err)
}
helperExecutable, err := prepareManagedProcessHelperExecutable(executable, files.stdout.Name())
if err != nil {
return nil, fmt.Errorf("prepare managed process helper executable: %w", err)
}
cmd := exec.Command(helperExecutable, managedProcessHelperFlag, payload)
cmd.Stdout = files.stdout
cmd.Stderr = files.stderr
configureManagedProcessCommand(cmd)
if err := cmd.Start(); err != nil {
_ = os.Remove(helperExecutable)
return nil, err
}
targetPID := cmd.Process.Pid
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if body, readErr := os.ReadFile(pidPath); readErr == nil {
if parsed, parseErr := strconv.Atoi(strings.TrimSpace(string(body))); parseErr == nil && parsed > 0 {
targetPID = parsed
break
}
}
time.Sleep(10 * time.Millisecond)
}
return &windowsFileManagedProcess{cmd: cmd, helperExecutable: helperExecutable, targetPID: targetPID}, nil
}
func prepareManagedProcessHelperExecutable(executable, outputPath string) (string, error) {
directory := filepath.Dir(outputPath)
if err := os.MkdirAll(directory, 0o700); err != nil {
return "", err
}
temporary, err := os.CreateTemp(directory, ".run-managed-helper-*.exe")
if err != nil {
return "", err
}
helper := temporary.Name()
if err := temporary.Close(); err != nil {
_ = os.Remove(helper)
return "", err
}
if err := os.Remove(helper); err != nil {
return "", err
}
if err := copyExecutable(executable, helper); err != nil {
_ = os.Remove(helper)
return "", err
}
return helper, nil
}
func (process *windowsFileManagedProcess) PID() int {
return process.cmd.Process.Pid
}
func (process *windowsFileManagedProcess) TargetPID() int {
if process.targetPID > 0 {
return process.targetPID
}
return process.PID()
}
func (process *windowsFileManagedProcess) Wait() (int, error) {
err := process.cmd.Wait()
if process.helperExecutable != "" {
_ = os.Remove(process.helperExecutable)
}
if process.cmd.ProcessState == nil {
return -1, err
}
return process.cmd.ProcessState.ExitCode(), err
}
func (process *windowsFileManagedProcess) Kill() error {
return process.cmd.Process.Kill()
}
// RunManagedProcessHelper is invoked by the same executable in a detached
// child process. It is deliberately not a worker mode and does not register
// with Platform.
func RunManagedProcessHelper(args []string) (bool, int) {
if len(args) < 3 || args[1] != managedProcessHelperFlag {
return false, 0
}
body, err := base64.RawURLEncoding.DecodeString(args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "invalid managed process helper payload: %v\n", err)
return true, 2
}
var spec managedProcessHelperSpec
if err := json.Unmarshal(body, &spec); err != nil || len(spec.Command.Args) == 0 {
if err == nil {
err = fmt.Errorf("managed process command is empty")
}
fmt.Fprintf(os.Stderr, "invalid managed process helper spec: %v\n", err)
return true, 2
}
code, err := runManagedProcessHelper(spec)
if err != nil {
fmt.Fprintf(os.Stderr, "managed process helper failed: %v\n", err)
if code == 0 {
code = 1
}
}
return true, code
}
func runManagedProcessHelper(spec managedProcessHelperSpec) (int, error) {
defer scheduleManagedProcessHelperCleanup()
stdout, closeStdout, err := openManagedProcessOutput(spec.StdoutPath, os.Stdout)
if err != nil {
return 1, fmt.Errorf("open managed process stdout: %w", err)
}
defer closeStdout()
stderr, closeStderr, err := openManagedProcessOutput(spec.StderrPath, os.Stderr)
if err != nil {
return 1, fmt.Errorf("open managed process stderr: %w", err)
}
defer closeStderr()
command := spec.Command.processCommand()
stopEventName := spec.StopEventName
// The plugin-declared pipes mode uses ordinary inherited handles. The
// durable output files are attached directly to the child process, so this
// path remains valid when Run itself is updated or restarted. Console mode
// is deliberately kept separate for applications that switch from
// redirected handles to a Windows console after startup.
if command.OutputMode != "console" {
return runManagedProcessWithPipes(command, stopEventName, spec.PIDPath, stdout, stderr)
}
return runManagedProcessWithPseudoConsole(command, stopEventName, spec.PIDPath, stdout, stderr)
}
func scheduleManagedProcessHelperCleanup() {
executable, err := os.Executable()
if err != nil || strings.TrimSpace(executable) == "" {
return
}
commandLine := "ping 127.0.0.1 -n 2 >nul & del /f /q " + quoteWindowsCommandArg(executable)
cleanup := exec.Command("cmd.exe", "/d", "/c", commandLine)
cleanup.Stdout = io.Discard
cleanup.Stderr = io.Discard
configureManagedProcessCommand(cleanup)
_ = cleanup.Start()
}
func openManagedProcessOutput(path string, fallback *os.File) (io.Writer, func(), error) {
if strings.TrimSpace(path) == "" {
return fallback, func() {}, nil
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return nil, nil, err
}
return file, func() { _ = file.Close() }, nil
}
func runManagedProcessWithPipes(command ProcessCommand, stopEventName string, pidPath string, stdout io.Writer, stderr io.Writer) (int, error) {
if len(command.Args) == 0 {
return 1, fmt.Errorf("managed process command is empty")
}
application := command.Args[0]
if strings.EqualFold(application, "cmd.exe") {
if comspec := os.Getenv("ComSpec"); comspec != "" {
application = comspec
}
}
cmd := exec.Command(application, command.Args[1:]...)
cmd.Dir = command.WorkDir
cmd.Env = os.Environ()
for key, value := range command.Env {
cmd.Env = append(cmd.Env, key+"="+value)
}
cmd.Stdout = stdout
cmd.Stderr = stderr
configureManagedProcessCommand(cmd)
if err := cmd.Start(); err != nil {
return 1, fmt.Errorf("start managed process with pipes: %w", err)
}
if err := writeManagedProcessPID(pidPath, cmd.Process.Pid); err != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return 1, fmt.Errorf("persist managed process pid: %w", err)
}
processHandle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE|windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(cmd.Process.Pid))
if err != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return 1, fmt.Errorf("open managed process handle: %w", err)
}
job, err := createManagedProcessJob(processHandle)
_ = windows.CloseHandle(processHandle)
if err != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return 1, err
}
defer windows.CloseHandle(job)
stopCleanup, err := watchManagedProcessStop(job, stopEventName)
if err != nil {
_ = windows.TerminateJobObject(job, 1)
_ = cmd.Wait()
return 1, err
}
defer stopCleanup()
waitErr := cmd.Wait()
if cmd.ProcessState == nil {
if waitErr != nil {
return 1, waitErr
}
return 1, fmt.Errorf("managed process has no exit state")
}
exitCode := cmd.ProcessState.ExitCode()
if waitErr != nil {
// exec.Cmd returns *exec.ExitError for a normal non-zero exit. Return
// the actual code without treating it as an internal supervisor error;
// the outer helper will propagate the code to the durable supervisor.
if _, ok := waitErr.(*exec.ExitError); !ok {
return 1, waitErr
}
}
if exitCode > 255 {
return 1, fmt.Errorf("managed process exited with code %d", exitCode)
}
return exitCode, nil
}
func writeManagedProcessPID(path string, pid int) error {
if strings.TrimSpace(path) == "" || pid <= 0 {
return nil
}
temporary := path + ".tmp"
if err := os.WriteFile(temporary, []byte(strconv.Itoa(pid)+"\n"), 0o600); err != nil {
return err
}
if err := os.Rename(temporary, path); err != nil {
_ = os.Remove(temporary)
return err
}
return nil
}
type synchronizedOutputWriter struct {
mu sync.Mutex
w io.Writer
}
func (writer *synchronizedOutputWriter) Write(body []byte) (int, error) {
writer.mu.Lock()
defer writer.mu.Unlock()
return writer.w.Write(body)
}
func runManagedProcessWithPseudoConsole(command ProcessCommand, stopEventName string, pidPath string, stdout io.Writer, stderr io.Writer) (int, error) {
// Direct console executables use a pseudo-console so programs that require
// a console handle still have a bounded, hidden console surface. This is
// also used for plugin-declared console capture around a Windows shell.
inputRead, inputWrite, err := createPseudoConsolePipe()
if err != nil {
return 1, fmt.Errorf("create pseudo-console input pipe: %w", err)
}
outputRead, outputWrite, err := createPseudoConsolePipe()
if err != nil {
closePseudoConsoleHandles(inputRead, inputWrite)
return 1, fmt.Errorf("create pseudo-console output pipe: %w", err)
}
var console windows.Handle
if err := windows.CreatePseudoConsole(windows.Coord{X: 160, Y: 50}, inputRead, outputWrite, 0, &console); err != nil {
closePseudoConsoleHandles(inputRead, inputWrite, outputRead, outputWrite)
return 1, fmt.Errorf("create pseudo-console: %w", err)
}
_ = windows.CloseHandle(inputRead)
_ = windows.CloseHandle(outputWrite)
attributes, err := windows.NewProcThreadAttributeList(1)
if err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("create pseudo-console process attributes: %w", err)
}
defer attributes.Delete()
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE expects the HPCON handle value as
// lpValue, not the address of the local variable that stores the handle.
if err := attributes.Update(windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, unsafe.Pointer(uintptr(console)), unsafe.Sizeof(console)); err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("configure pseudo-console process attributes: %w", err)
}
applicationPath := command.Args[0]
commandIsCmd := strings.EqualFold(applicationPath, "cmd.exe")
if commandIsCmd {
applicationPath = os.Getenv("ComSpec")
if applicationPath == "" {
applicationPath = `C:\Windows\System32\cmd.exe`
}
}
// CreateProcess receives both an application name and a command line.
// Keep the command-line program name identical to the resolved application
// name; cmd.exe can fail with STATUS_DLL_INIT_FAILED when the former is
// left as the short name while the latter is an absolute path.
commandArgs := append([]string(nil), command.Args...)
commandArgs[0] = applicationPath
commandLine, err := windows.UTF16FromString(windows.ComposeCommandLine(commandArgs))
if err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("encode managed process command: %w", err)
}
var applicationName *uint16
if !commandIsCmd && strings.ContainsAny(applicationPath, `:\`) {
applicationName, err = windows.UTF16PtrFromString(applicationPath)
if err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("encode managed process executable: %w", err)
}
}
environment, err := managedProcessEnvironment(command.Env)
if err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("encode managed process environment: %w", err)
}
var workDir *uint16
if command.WorkDir != "" {
workDir, err = windows.UTF16PtrFromString(command.WorkDir)
if err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("encode managed process working directory: %w", err)
}
}
// A pseudo-console supplies the child's console surface itself; do not
// combine it with STARTF_USESHOWWINDOW, which makes cmd.exe fail during
// initialization on some non-interactive Windows Server sessions.
startup := &windows.StartupInfoEx{StartupInfo: windows.StartupInfo{Cb: uint32(unsafe.Sizeof(windows.StartupInfoEx{}))}, ProcThreadAttributeList: attributes.List()}
var processInfo windows.ProcessInformation
// The helper has already detached from any parent job. A second
// CREATE_BREAKAWAY_FROM_JOB on the pseudo-console client is rejected by
// some Windows Server builds during console initialization and surfaces as
// STATUS_DLL_INIT_FAILED from the otherwise valid child process.
if err := windows.CreateProcess(applicationName, &commandLine[0], nil, nil, false, windows.CREATE_UNICODE_ENVIRONMENT|windows.EXTENDED_STARTUPINFO_PRESENT, environment, workDir, &startup.StartupInfo, &processInfo); err != nil {
windows.ClosePseudoConsole(console)
closePseudoConsoleHandles(inputWrite, outputRead)
return 1, fmt.Errorf("start managed process in pseudo-console: %w", err)
}
_ = windows.CloseHandle(processInfo.Thread)
_ = windows.CloseHandle(inputWrite)
if err := writeManagedProcessPID(pidPath, int(processInfo.ProcessId)); err != nil {
_ = windows.TerminateProcess(processInfo.Process, 1)
_ = windows.CloseHandle(processInfo.Process)
windows.ClosePseudoConsole(console)
_ = windows.CloseHandle(outputRead)
return 1, fmt.Errorf("persist managed process pid: %w", err)
}
job, err := createManagedProcessJob(processInfo.Process)
if err != nil {
_ = windows.TerminateProcess(processInfo.Process, 1)
_ = windows.CloseHandle(processInfo.Process)
windows.ClosePseudoConsole(console)
_ = windows.CloseHandle(outputRead)
return 1, err
}
defer windows.CloseHandle(job)
stopCleanup, err := watchManagedProcessStop(job, stopEventName)
if err != nil {
_ = windows.TerminateJobObject(job, 1)
windows.ClosePseudoConsole(console)
_ = windows.CloseHandle(processInfo.Process)
return 1, err
}
defer stopCleanup()
outputFile := os.NewFile(uintptr(outputRead), "run-pseudo-console-output")
if outputFile == nil {
_ = windows.TerminateProcess(processInfo.Process, 1)
_ = windows.CloseHandle(processInfo.Process)
windows.ClosePseudoConsole(console)
return 1, fmt.Errorf("open pseudo-console output")
}
outputDone := make(chan struct{})
outputWriter := &synchronizedOutputWriter{w: stdout}
go func() {
_, _ = io.Copy(outputWriter, outputFile)
close(outputDone)
}()
_, waitErr := windows.WaitForSingleObject(processInfo.Process, windows.INFINITE)
var exitCode uint32
if waitErr == nil {
waitErr = windows.GetExitCodeProcess(processInfo.Process, &exitCode)
}
windows.ClosePseudoConsole(console)
<-outputDone
_ = outputFile.Close()
_ = windows.CloseHandle(processInfo.Process)
if waitErr != nil {
return 1, waitErr
}
if exitCode > 255 {
return 1, fmt.Errorf("managed process exited with code %d", exitCode)
}
return int(exitCode), nil
}
func runManagedShellWithPipes(command ProcessCommand, stopEventName string, stdout io.Writer, stderr io.Writer) (int, error) {
inputRead, inputWrite, err := createPseudoConsolePipe()
if err != nil {
return 1, fmt.Errorf("create managed shell input pipe: %w", err)
}
stdoutRead, stdoutWrite, err := createPseudoConsolePipe()
if err != nil {
closePseudoConsoleHandles(inputRead, inputWrite)
return 1, fmt.Errorf("create managed shell stdout pipe: %w", err)
}
stderrRead, stderrWrite, err := createPseudoConsolePipe()
if err != nil {
closePseudoConsoleHandles(inputRead, inputWrite, stdoutRead, stdoutWrite)
return 1, fmt.Errorf("create managed shell stderr pipe: %w", err)
}
closeOnError := func() {
closePseudoConsoleHandles(inputRead, inputWrite, stdoutRead, stdoutWrite, stderrRead, stderrWrite)
}
if err := windows.SetHandleInformation(stdoutRead, windows.HANDLE_FLAG_INHERIT, 0); err != nil {
closeOnError()
return 1, fmt.Errorf("make managed shell stdout pipe private: %w", err)
}
if err := windows.SetHandleInformation(stderrRead, windows.HANDLE_FLAG_INHERIT, 0); err != nil {
closeOnError()
return 1, fmt.Errorf("make managed shell stderr pipe private: %w", err)
}
applicationPath := os.Getenv("ComSpec")
if applicationPath == "" {
applicationPath = `C:\Windows\System32\cmd.exe`
}
commandArgs := append([]string(nil), command.Args...)
commandArgs[0] = applicationPath
commandLine, err := windows.UTF16FromString(windows.ComposeCommandLine(commandArgs))
if err != nil {
closeOnError()
return 1, fmt.Errorf("encode managed shell command: %w", err)
}
applicationName, err := windows.UTF16PtrFromString(applicationPath)
if err != nil {
closeOnError()
return 1, fmt.Errorf("encode managed shell executable: %w", err)
}
environment, err := managedProcessEnvironment(command.Env)
if err != nil {
closeOnError()
return 1, fmt.Errorf("encode managed shell environment: %w", err)
}
var workDir *uint16
if command.WorkDir != "" {
workDir, err = windows.UTF16PtrFromString(command.WorkDir)
if err != nil {
closeOnError()
return 1, fmt.Errorf("encode managed shell working directory: %w", err)
}
}
startup := &windows.StartupInfo{Cb: uint32(unsafe.Sizeof(windows.StartupInfo{})), Flags: windows.STARTF_USESTDHANDLES | windows.STARTF_USESHOWWINDOW, ShowWindow: windows.SW_HIDE, StdInput: inputRead, StdOutput: stdoutWrite, StdErr: stderrWrite}
var processInfo windows.ProcessInformation
if err := windows.CreateProcess(applicationName, &commandLine[0], nil, nil, true, managedProcessDetachedFlags|windows.CREATE_UNICODE_ENVIRONMENT, environment, workDir, startup, &processInfo); err != nil {
closeOnError()
return 1, fmt.Errorf("start managed shell: %w", err)
}
_ = windows.CloseHandle(processInfo.Thread)
closePseudoConsoleHandles(inputRead, inputWrite, stdoutWrite, stderrWrite)
job, err := createManagedProcessJob(processInfo.Process)
if err != nil {
_ = windows.TerminateProcess(processInfo.Process, 1)
_ = windows.CloseHandle(processInfo.Process)
closePseudoConsoleHandles(stdoutRead, stderrRead)
return 1, err
}
defer windows.CloseHandle(job)
stopCleanup, err := watchManagedProcessStop(job, stopEventName)
if err != nil {
_ = windows.TerminateJobObject(job, 1)
_ = windows.CloseHandle(processInfo.Process)
return 1, err
}
defer stopCleanup()
stdoutFile := os.NewFile(uintptr(stdoutRead), "run-managed-shell-stdout")
stderrFile := os.NewFile(uintptr(stderrRead), "run-managed-shell-stderr")
if stdoutFile == nil || stderrFile == nil {
if stdoutFile != nil {
_ = stdoutFile.Close()
}
if stderrFile != nil {
_ = stderrFile.Close()
}
_ = windows.TerminateProcess(processInfo.Process, 1)
_ = windows.CloseHandle(processInfo.Process)
return 1, fmt.Errorf("open managed shell output pipes")
}
outputDone := make(chan struct{})
outputWriter := &synchronizedOutputWriter{w: stdout}
go func() {
_, _ = io.Copy(outputWriter, stdoutFile)
_ = stdoutFile.Close()
close(outputDone)
}()
errorDone := make(chan struct{})
errorWriter := &synchronizedOutputWriter{w: stderr}
go func() {
_, _ = io.Copy(errorWriter, stderrFile)
_ = stderrFile.Close()
close(errorDone)
}()
_, waitErr := windows.WaitForSingleObject(processInfo.Process, windows.INFINITE)
var exitCode uint32
if waitErr == nil {
waitErr = windows.GetExitCodeProcess(processInfo.Process, &exitCode)
}
<-outputDone
<-errorDone
_ = windows.CloseHandle(processInfo.Process)
if waitErr != nil {
return 1, waitErr
}
if exitCode > 255 {
return 1, fmt.Errorf("managed shell exited with code %d", exitCode)
}
return int(exitCode), nil
}
func createManagedProcessJob(process windows.Handle) (windows.Handle, error) {
job, err := windows.CreateJobObject(nil, nil)
if err != nil {
return 0, fmt.Errorf("create managed process job: %w", err)
}
if err := windows.AssignProcessToJobObject(job, process); err != nil {
_ = windows.CloseHandle(job)
return 0, fmt.Errorf("assign managed process to job: %w", err)
}
return job, nil
}
func watchManagedProcessStop(job windows.Handle, name string) (func(), error) {
if strings.TrimSpace(name) == "" {
return func() {}, nil
}
eventName, err := windows.UTF16PtrFromString(name)
if err != nil {
return nil, fmt.Errorf("encode managed process stop event: %w", err)
}
event, eventErr := windows.CreateEvent(nil, 1, 0, eventName)
if eventErr != nil && eventErr != windows.ERROR_ALREADY_EXISTS {
if event != 0 {
_ = windows.CloseHandle(event)
}
return nil, fmt.Errorf("create managed process stop event: %w", eventErr)
}
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-stop:
return
default:
}
result, waitErr := windows.WaitForSingleObject(event, 100)
if waitErr != nil {
return
}
if result == windows.WAIT_OBJECT_0 {
_ = windows.TerminateJobObject(job, 1)
return
}
}
}()
return func() {
close(stop)
<-done
_ = windows.CloseHandle(event)
}, nil
}
func requestManagedProcessStop(identity ProcessIdentity) error {
if strings.TrimSpace(identity.StopEventName) != "" {
name, err := windows.UTF16PtrFromString(identity.StopEventName)
if err != nil {
return err
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
event, openErr := windows.OpenEvent(windows.EVENT_MODIFY_STATE|windows.SYNCHRONIZE, false, name)
if openErr == nil {
setErr := windows.SetEvent(event)
_ = windows.CloseHandle(event)
return setErr
}
time.Sleep(25 * time.Millisecond)
}
}
return fmt.Errorf("managed process stop event is unavailable")
}
func forceManagedProcessStop(identity ProcessIdentity) error {
if err := requestManagedProcessStop(identity); err == nil {
return nil
}
if identity.PID <= 0 {
return fmt.Errorf("managed process pid is invalid")
}
// A recovered process can outlive its helper. In that case no stop-event
// watcher remains. Target only the persisted process PID here: /T would
// recursively terminate descendants and can take down a game process that
// was deliberately kept alive while Run is being restarted or updated.
command := exec.Command("taskkill.exe", "/PID", strconv.Itoa(identity.PID), "/F")
command.Stdout = io.Discard
command.Stderr = io.Discard
return command.Run()
}
func createPseudoConsolePipe() (windows.Handle, windows.Handle, error) {
var readHandle, writeHandle windows.Handle
// ConPTY owns these handles through its internal duplication. Keeping the
// pipe ends non-inheritable matches the Windows ConPTY contract and avoids
// leaking the pseudo-console handles into the client process.
if err := windows.CreatePipe(&readHandle, &writeHandle, nil, 0); err != nil {
return 0, 0, err
}
return readHandle, writeHandle, nil
}
func closePseudoConsoleHandles(handles ...windows.Handle) {
for _, handle := range handles {
if handle != 0 && handle != windows.InvalidHandle {
_ = windows.CloseHandle(handle)
}
}
}
func managedProcessEnvironment(values map[string]string) (*uint16, error) {
if len(values) == 0 {
// A nil environment tells CreateProcess to inherit the helper's
// environment. This preserves Windows' special drive-current-directory
// entries and avoids rebuilding a potentially incomplete environment
// block for the common case.
return nil, nil
}
environment := make(map[string]string, len(values))
for _, entry := range os.Environ() {
keyEnd := strings.IndexByte(entry, '=')
if strings.HasPrefix(entry, "=") {
// Windows stores drive current directories as =C:=C:\\...;
// the first equals sign is part of that variable's name.
if next := strings.IndexByte(entry[1:], '='); next >= 0 {
keyEnd = next + 1
}
}
if keyEnd > 0 {
key := entry[:keyEnd]
environment[strings.ToUpper(key)] = entry
}
}
for key, value := range values {
environment[strings.ToUpper(key)] = key + "=" + value
}
entries := make([]string, 0, len(environment))
for _, entry := range environment {
entries = append(entries, entry)
}
sort.Slice(entries, func(i, j int) bool { return strings.ToUpper(entries[i]) < strings.ToUpper(entries[j]) })
encoded := utf16.Encode([]rune(strings.Join(entries, "\x00") + "\x00\x00"))
return &encoded[0], nil
}