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 }