blob: 4356ab18e4e15bfdec81462e0af1ce7b862f40fa [file] [log] [blame]
// Copyright 2019 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 exec2
import (
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"go.chromium.org/luci/common/errors"
)
type attr struct {
jobMu sync.Mutex
job windows.Handle
}
func iterateChildThreads(pid uint32, f func(uint32) error) error {
handle, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, pid)
if err != nil {
return errors.Annotate(err, "failed to get snapshot").Err()
}
defer windows.CloseHandle(handle)
var threadEntry windows.ThreadEntry32
threadEntry.Size = uint32(unsafe.Sizeof(threadEntry))
if err := windows.Thread32First(handle, &threadEntry); err != nil {
if serr, ok := err.(syscall.Errno); !ok || serr != windows.ERROR_NO_MORE_FILES {
return errors.Annotate(err, "failed to call Thread32First").Err()
}
return nil
}
for {
if pid == threadEntry.OwnerProcessID {
if err := f(threadEntry.ThreadID); err != nil {
return err
}
}
if err := windows.Thread32Next(handle, &threadEntry); err != nil {
if serr, ok := err.(syscall.Errno); !ok || serr != windows.ERROR_NO_MORE_FILES {
return errors.Annotate(err, "failed to call Thread32Next").Err()
}
return nil
}
}
}
func (c *Cmd) setupCmd() {
c.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.CREATE_SUSPENDED | windows.CREATE_NEW_PROCESS_GROUP,
}
}
func createJobObject() (windows.Handle, error) {
job, err := windows.CreateJobObject(nil, nil)
if err != nil {
return 0, errors.Annotate(err, "failed to create job object").Err()
}
// TODO(tikuta): use SetInformationJobObject
return job, nil
}
func (c *Cmd) start() error {
if err := c.Cmd.Start(); err != nil {
return errors.Annotate(err, "failed to start process").Err()
}
pid := uint32(c.Process.Pid)
success := false
defer func() {
if !success {
c.Process.Kill()
c.wait()
}
}()
job, err := createJobObject()
if err != nil {
return errors.Annotate(err, "failed to create job object").Err()
}
defer func() {
if !success {
windows.CloseHandle(job)
}
}()
c.attr.jobMu.Lock()
c.attr.job = job
c.attr.jobMu.Unlock()
// TODO: potential performance improvement https://crbug.com/974202
process, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, pid)
if err != nil {
return errors.Annotate(err, "failed to open process handle").Err()
}
defer windows.CloseHandle(process)
if err := windows.AssignProcessToJobObject(job, process); err != nil {
return errors.Annotate(err, "failed to assign process to job object").Err()
}
// TODO: potential performance improvement https://crbug.com/974202
err = iterateChildThreads(pid, func(tid uint32) error {
thread, err := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, tid)
if err != nil {
return errors.Annotate(err, "failed to call OpenThread").Err()
}
defer windows.CloseHandle(thread)
if _, err := windows.ResumeThread(thread); err != nil {
return errors.Annotate(err, "failed to call ResumeThread").Err()
}
return nil
})
if err != nil {
return err
}
success = true
return nil
}
func (c *Cmd) terminate() error {
// Child process is created with CREATE_NEW_PROCESS_GROUP flag.
// And we use CTRL_BREAK_EVENT here to send signal to all child process group instead of a child process.
return windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(c.Process.Pid))
}
func (c *Cmd) wait() error {
if err := c.Cmd.Wait(); err != nil {
return err
}
c.attr.jobMu.Lock()
if c.attr.job != windows.InvalidHandle {
if err := windows.CloseHandle(c.attr.job); err != nil {
return errors.Annotate(err, "failed to close job object handle").Err()
}
c.attr.job = windows.InvalidHandle
}
c.attr.jobMu.Unlock()
return nil
}
func (c *Cmd) kill() error {
c.attr.jobMu.Lock()
defer c.attr.jobMu.Unlock()
if err := windows.TerminateJobObject(c.attr.job, 1); err != nil {
return errors.Annotate(err, "failed to terminate job object").Err()
}
if err := windows.CloseHandle(c.attr.job); err != nil {
return errors.Annotate(err, "failed to close job object handle").Err()
}
c.attr.job = windows.InvalidHandle
return nil
}