blob: 35ddf8064a4e4ae138b409aac5d8ea2dfeee1888 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vpython
import (
"context"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"unicode/utf16"
"unsafe"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/vpython/python"
"go.chromium.org/luci/vpython/venv"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/system/environ"
)
// systemSpecificLaunch launches the process described by "cmd" while ensuring
// that the VirtualEnv lock is held throughout its duration (best effort).
//
// On Windows, we don't forward signals. Forwarding signals on Windows is
// nuanced. For now, we won't, since sending them via Python is similarly
// nuanced and not commonly done.
//
// For more discussion, see:
// https://github.com/golang/go/issues/6720
//
// On Windows, we launch it as a child process and interpret any signal that we
// receive as terminal, cancelling the child.
func systemSpecificLaunch(c context.Context, ve *venv.Env, cl *python.CommandLine, env environ.Env, dir string) error {
return Exec(c, ve.Interpreter(), cl, env, dir, nil)
}
// Copied from https://github.com/golang/go/blob/go1.16.15/src/syscall/exec_windows.go
// createEnvBlock converts an array of environment strings into
// the representation required by CreateProcess: a sequence of NUL
// terminated strings followed by a nil.
// Last bytes are two UCS-2 NULs, or four NUL bytes.
func createEnvBlock(envv []string) *uint16 {
if len(envv) == 0 {
return &utf16.Encode([]rune("\x00\x00"))[0]
}
length := 0
for _, s := range envv {
length += len(s) + 1
}
length += 1
b := make([]byte, length)
i := 0
for _, s := range envv {
l := len(s)
copy(b[i:i+l], []byte(s))
copy(b[i+l:i+l+1], []byte{0})
i = i + l + 1
}
copy(b[i:i+1], []byte{0})
return &utf16.Encode([]rune(string(b)))[0]
}
func execImpl(c context.Context, argv []string, env environ.Env, dir string, setupFn func() error) error {
// As of go 1.17, handles to be passed to subprocesses via Cmd.Run must be explicitly
// specified. To keep the expected behavior of letting Python inherit all inheritable
// file handles, we instead use syscall directly, based on the Go 1.16 implementation
// of cmd.Run and syscall.StartProcess.
// Tracked in https://github.com/golang/go/issues/53652
resolvedPath, err := exec.LookPath(argv[0])
if err != nil {
return errors.Annotate(err, "Could not locate executable for %v", argv[0]).Err()
}
resolvedPath, err = filepath.Abs(resolvedPath)
if err != nil {
return err
}
sys := new(syscall.SysProcAttr)
procAttr := syscall.ProcAttr{
Dir: dir,
Env: env.Sorted(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: sys,
}
argv0p, err := syscall.UTF16PtrFromString(resolvedPath)
if err != nil {
return err
}
var cmdline string
for _, arg := range argv {
if len(cmdline) > 0 {
cmdline += " "
}
cmdline += syscall.EscapeArg(arg)
}
var argvp *uint16
if len(cmdline) != 0 {
argvp, err = syscall.UTF16PtrFromString(cmdline)
if err != nil {
return err
}
}
var dirp *uint16
if len(procAttr.Dir) != 0 {
dirp, err = syscall.UTF16PtrFromString(procAttr.Dir)
if err != nil {
return err
}
}
// At this point, ANY ERROR will be fatal (panic). We assume that each
// operation may permanently alter our runtime environment.
if setupFn != nil {
if err := setupFn(); err != nil {
panic(err)
}
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
<-ch
logging.Debugf(c, "os.Interrupt recieved, restoring signal handler.")
signal.Stop(ch)
// Due to the nature of os.Interrupt (either CTRL_C_EVENT or
// CTRL_BREAK_EVENT), they're sent to the entire process group. Since we
// haven't created a separate group for `cmd`, we don't need to relay the
// signal (since `cmd` would have gotten it as well).
}()
// Acquire the fork lock so that no other threads
// create new fds that are not yet close-on-exec
// before we fork.
syscall.ForkLock.Lock()
defer syscall.ForkLock.Unlock()
p, _ := syscall.GetCurrentProcess()
fd := make([]syscall.Handle, len(procAttr.Files))
for i := range procAttr.Files {
if procAttr.Files[i] > 0 {
err := syscall.DuplicateHandle(p, syscall.Handle(procAttr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
if err != nil {
panic(err)
}
defer syscall.CloseHandle(syscall.Handle(fd[i]))
}
}
si := new(syscall.StartupInfo)
si.Cb = uint32(unsafe.Sizeof(*si))
si.Flags = syscall.STARTF_USESTDHANDLES
if sys.HideWindow {
si.Flags |= syscall.STARTF_USESHOWWINDOW
si.ShowWindow = syscall.SW_HIDE
}
si.StdInput = fd[0]
si.StdOutput = fd[1]
si.StdErr = fd[2]
pi := new(syscall.ProcessInformation)
flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
err = syscall.CreateProcess(argv0p, argvp, sys.ProcessAttributes, sys.ThreadAttributes, !sys.NoInheritHandles, flags, createEnvBlock(procAttr.Env), dirp, si, pi)
if err != nil {
panic(err)
}
defer syscall.CloseHandle(syscall.Handle(pi.Thread))
handle := uintptr(pi.Process)
s, err := syscall.WaitForSingleObject(syscall.Handle(handle), syscall.INFINITE)
switch s {
case syscall.WAIT_OBJECT_0:
break
case syscall.WAIT_FAILED:
panic("WaitForSingleObject failed")
default:
panic("Unexpected result from WaitForSingleObject")
}
var rc uint32
if err = syscall.GetExitCodeProcess(syscall.Handle(handle), &rc); err != nil {
panic(err)
}
// The process had an exit code (includes err==nil, 0).
logging.Debugf(c, "Python subprocess has terminated: %v", err)
os.Exit(int(rc))
panic("must not return")
}