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  }