go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/vpython/application/system_windows.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package application
    16  
    17  import (
    18  	"context"
    19  	"os"
    20  	"os/exec"
    21  	"os/signal"
    22  	"path/filepath"
    23  	"syscall"
    24  	"unicode/utf16"
    25  	"unsafe"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  
    29  	"go.chromium.org/luci/common/logging"
    30  	"go.chromium.org/luci/common/system/environ"
    31  )
    32  
    33  // Copied from https://github.com/golang/go/blob/go1.16.15/src/syscall/exec_windows.go
    34  // createEnvBlock converts an array of environment strings into
    35  // the representation required by CreateProcess: a sequence of NUL
    36  // terminated strings followed by a nil.
    37  // Last bytes are two UCS-2 NULs, or four NUL bytes.
    38  func createEnvBlock(envv []string) *uint16 {
    39  	if len(envv) == 0 {
    40  		return &utf16.Encode([]rune("\x00\x00"))[0]
    41  	}
    42  	length := 0
    43  	for _, s := range envv {
    44  		length += len(s) + 1
    45  	}
    46  	length += 1
    47  
    48  	b := make([]byte, length)
    49  	i := 0
    50  	for _, s := range envv {
    51  		l := len(s)
    52  		copy(b[i:i+l], []byte(s))
    53  		copy(b[i+l:i+l+1], []byte{0})
    54  		i = i + l + 1
    55  	}
    56  	copy(b[i:i+1], []byte{0})
    57  
    58  	return &utf16.Encode([]rune(string(b)))[0]
    59  }
    60  
    61  func execImpl(c context.Context, argv []string, env environ.Env, dir string) error {
    62  	// As of go 1.17, handles to be passed to subprocesses via Cmd.Run must be explicitly
    63  	// specified. To keep the expected behavior of letting Python inherit all inheritable
    64  	// file handles, we instead use syscall directly, based on the Go 1.16 implementation
    65  	// of cmd.Run and syscall.StartProcess.
    66  	// Tracked in https://github.com/golang/go/issues/53652
    67  	resolvedPath, err := exec.LookPath(argv[0])
    68  	if err != nil {
    69  		return errors.Annotate(err, "Could not locate executable for %v", argv[0]).Err()
    70  	}
    71  	resolvedPath, err = filepath.Abs(resolvedPath)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	sys := new(syscall.SysProcAttr)
    77  	procAttr := syscall.ProcAttr{
    78  		Dir:   dir,
    79  		Env:   env.Sorted(),
    80  		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
    81  		Sys:   sys,
    82  	}
    83  
    84  	argv0p, err := syscall.UTF16PtrFromString(resolvedPath)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	var cmdline string
    90  	for _, arg := range argv {
    91  		if len(cmdline) > 0 {
    92  			cmdline += " "
    93  		}
    94  		cmdline += syscall.EscapeArg(arg)
    95  	}
    96  
    97  	var argvp *uint16
    98  	if len(cmdline) != 0 {
    99  		argvp, err = syscall.UTF16PtrFromString(cmdline)
   100  		if err != nil {
   101  			return err
   102  		}
   103  	}
   104  
   105  	var dirp *uint16
   106  	if len(procAttr.Dir) != 0 {
   107  		dirp, err = syscall.UTF16PtrFromString(procAttr.Dir)
   108  		if err != nil {
   109  			return err
   110  		}
   111  	}
   112  
   113  	ch := make(chan os.Signal, 1)
   114  	signal.Notify(ch, os.Interrupt)
   115  	go func() {
   116  		<-ch
   117  		logging.Debugf(c, "os.Interrupt recieved, restoring signal handler.")
   118  		signal.Stop(ch)
   119  		// Due to the nature of os.Interrupt (either CTRL_C_EVENT or
   120  		// CTRL_BREAK_EVENT), they're sent to the entire process group. Since we
   121  		// haven't created a separate group for `cmd`, we don't need to relay the
   122  		// signal (since `cmd` would have gotten it as well).
   123  	}()
   124  
   125  	// Acquire the fork lock so that no other threads
   126  	// create new fds that are not yet close-on-exec
   127  	// before we fork.
   128  	syscall.ForkLock.Lock()
   129  	defer syscall.ForkLock.Unlock()
   130  
   131  	p, _ := syscall.GetCurrentProcess()
   132  	fd := make([]syscall.Handle, len(procAttr.Files))
   133  	for i := range procAttr.Files {
   134  		if procAttr.Files[i] > 0 {
   135  			err := syscall.DuplicateHandle(p, syscall.Handle(procAttr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
   136  			if err != nil {
   137  				panic(err)
   138  			}
   139  			defer syscall.CloseHandle(syscall.Handle(fd[i]))
   140  		}
   141  	}
   142  	si := new(syscall.StartupInfo)
   143  	si.Cb = uint32(unsafe.Sizeof(*si))
   144  	si.Flags = syscall.STARTF_USESTDHANDLES
   145  	if sys.HideWindow {
   146  		si.Flags |= syscall.STARTF_USESHOWWINDOW
   147  		si.ShowWindow = syscall.SW_HIDE
   148  	}
   149  	si.StdInput = fd[0]
   150  	si.StdOutput = fd[1]
   151  	si.StdErr = fd[2]
   152  
   153  	pi := new(syscall.ProcessInformation)
   154  
   155  	flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
   156  	err = syscall.CreateProcess(argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, !sys.NoInheritHandles, flags, createEnvBlock(procAttr.Env), dirp, si, pi)
   157  	if err != nil {
   158  		panic(err)
   159  	}
   160  	defer syscall.CloseHandle(syscall.Handle(pi.Thread))
   161  
   162  	handle := uintptr(pi.Process)
   163  	s, err := syscall.WaitForSingleObject(syscall.Handle(handle), syscall.INFINITE)
   164  	switch s {
   165  	case syscall.WAIT_OBJECT_0:
   166  		break
   167  	case syscall.WAIT_FAILED:
   168  		panic("WaitForSingleObject failed")
   169  	default:
   170  		panic("Unexpected result from WaitForSingleObject")
   171  	}
   172  
   173  	var rc uint32
   174  	if err = syscall.GetExitCodeProcess(syscall.Handle(handle), &rc); err != nil {
   175  		panic(err)
   176  	}
   177  
   178  	// The process had an exit code (includes err==nil, 0).
   179  	logging.Debugf(c, "Python subprocess has terminated: %v", err)
   180  	os.Exit(int(rc))
   181  	panic("must not return")
   182  }