init
This commit is contained in:
@@ -0,0 +1,745 @@
|
||||
//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"
|
||||
|
||||
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. 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: windows.CREATE_NO_WINDOW | windows.CREATE_BREAKAWAY_FROM_JOB, 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 ProcessCommand `json:"command"`
|
||||
StopEventName string `json:"stopEventName,omitempty"`
|
||||
StdoutPath string `json:"stdoutPath,omitempty"`
|
||||
StderrPath string `json:"stderrPath,omitempty"`
|
||||
PIDPath string `json:"pidPath,omitempty"`
|
||||
}
|
||||
|
||||
func startManagedProcess(command ProcessCommand, files managedProcessFiles, stopEventName string) (managedProcess, error) {
|
||||
pidPath := files.stdout.Name() + ".pid"
|
||||
_ = os.Remove(pidPath)
|
||||
body, err := json.Marshal(managedProcessHelperSpec{Command: 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
|
||||
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, windows.CREATE_BREAKAWAY_FROM_JOB|windows.CREATE_NO_WINDOW|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
|
||||
}
|
||||
Reference in New Issue
Block a user