go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/system/exec2/exec2_windows.go (about) 1 // Copyright 2019 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 exec2 16 17 import ( 18 "sync" 19 "syscall" 20 "unsafe" 21 22 "golang.org/x/sys/windows" 23 24 "go.chromium.org/luci/common/errors" 25 ) 26 27 type attr struct { 28 jobMu sync.Mutex 29 job windows.Handle 30 } 31 32 func iterateChildThreads(pid uint32, f func(uint32) error) error { 33 handle, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, pid) 34 if err != nil { 35 return errors.Annotate(err, "failed to get snapshot").Err() 36 } 37 defer windows.CloseHandle(handle) 38 39 var threadEntry windows.ThreadEntry32 40 threadEntry.Size = uint32(unsafe.Sizeof(threadEntry)) 41 42 if err := windows.Thread32First(handle, &threadEntry); err != nil { 43 if serr, ok := err.(syscall.Errno); !ok || serr != windows.ERROR_NO_MORE_FILES { 44 return errors.Annotate(err, "failed to call Thread32First").Err() 45 } 46 return nil 47 } 48 49 for { 50 if pid == threadEntry.OwnerProcessID { 51 if err := f(threadEntry.ThreadID); err != nil { 52 return err 53 } 54 } 55 56 if err := windows.Thread32Next(handle, &threadEntry); err != nil { 57 if serr, ok := err.(syscall.Errno); !ok || serr != windows.ERROR_NO_MORE_FILES { 58 return errors.Annotate(err, "failed to call Thread32Next").Err() 59 } 60 return nil 61 } 62 } 63 } 64 65 func (c *Cmd) setupCmd() { 66 c.SysProcAttr = &syscall.SysProcAttr{ 67 CreationFlags: windows.CREATE_SUSPENDED | windows.CREATE_NEW_PROCESS_GROUP, 68 } 69 } 70 71 func createJobObject() (windows.Handle, error) { 72 job, err := windows.CreateJobObject(nil, nil) 73 if err != nil { 74 return 0, errors.Annotate(err, "failed to create job object").Err() 75 } 76 77 // TODO(tikuta): use SetInformationJobObject 78 79 return job, nil 80 } 81 82 func (c *Cmd) start() error { 83 if err := c.Cmd.Start(); err != nil { 84 return errors.Annotate(err, "failed to start process").Err() 85 } 86 87 pid := uint32(c.Process.Pid) 88 89 success := false 90 defer func() { 91 if !success { 92 c.Process.Kill() 93 c.wait() 94 } 95 }() 96 97 job, err := createJobObject() 98 if err != nil { 99 return errors.Annotate(err, "failed to create job object").Err() 100 } 101 102 defer func() { 103 if !success { 104 windows.CloseHandle(job) 105 } 106 }() 107 108 c.attr.jobMu.Lock() 109 c.attr.job = job 110 c.attr.jobMu.Unlock() 111 112 // TODO: potential performance improvement https://crbug.com/974202 113 process, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, pid) 114 if err != nil { 115 return errors.Annotate(err, "failed to open process handle").Err() 116 } 117 defer windows.CloseHandle(process) 118 119 if err := windows.AssignProcessToJobObject(job, process); err != nil { 120 return errors.Annotate(err, "failed to assign process to job object").Err() 121 } 122 123 // TODO: potential performance improvement https://crbug.com/974202 124 err = iterateChildThreads(pid, func(tid uint32) error { 125 thread, err := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, tid) 126 if err != nil { 127 return errors.Annotate(err, "failed to call OpenThread").Err() 128 } 129 defer windows.CloseHandle(thread) 130 131 if _, err := windows.ResumeThread(thread); err != nil { 132 return errors.Annotate(err, "failed to call ResumeThread").Err() 133 } 134 return nil 135 }) 136 if err != nil { 137 return err 138 } 139 140 success = true 141 return nil 142 } 143 144 func (c *Cmd) terminate() error { 145 // Child process is created with CREATE_NEW_PROCESS_GROUP flag. 146 // And we use CTRL_BREAK_EVENT here to send signal to all child process group instead of a child process. 147 return windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(c.Process.Pid)) 148 } 149 150 func (c *Cmd) wait() error { 151 if err := c.Cmd.Wait(); err != nil { 152 return err 153 } 154 155 c.attr.jobMu.Lock() 156 if c.attr.job != windows.InvalidHandle { 157 if err := windows.CloseHandle(c.attr.job); err != nil { 158 return errors.Annotate(err, "failed to close job object handle").Err() 159 } 160 c.attr.job = windows.InvalidHandle 161 } 162 c.attr.jobMu.Unlock() 163 164 return nil 165 } 166 167 func (c *Cmd) kill() error { 168 c.attr.jobMu.Lock() 169 defer c.attr.jobMu.Unlock() 170 171 if err := windows.TerminateJobObject(c.attr.job, 1); err != nil { 172 return errors.Annotate(err, "failed to terminate job object").Err() 173 } 174 175 if err := windows.CloseHandle(c.attr.job); err != nil { 176 return errors.Annotate(err, "failed to close job object handle").Err() 177 } 178 c.attr.job = windows.InvalidHandle 179 180 return nil 181 }