...

Source file src/github.com/Microsoft/hcsshim/internal/jobcontainers/process.go

Documentation: github.com/Microsoft/hcsshim/internal/jobcontainers

     1  //go:build windows
     2  
     3  package jobcontainers
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"sync"
    11  
    12  	"github.com/pkg/errors"
    13  	"golang.org/x/sys/windows"
    14  
    15  	"github.com/Microsoft/hcsshim/internal/conpty"
    16  	"github.com/Microsoft/hcsshim/internal/cow"
    17  	"github.com/Microsoft/hcsshim/internal/exec"
    18  	"github.com/Microsoft/hcsshim/internal/hcs"
    19  	"github.com/Microsoft/hcsshim/internal/log"
    20  	"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
    21  	"github.com/Microsoft/hcsshim/internal/winapi"
    22  )
    23  
    24  // JobProcess represents a process run in a job object.
    25  type JobProcess struct {
    26  	cmd            *exec.Exec
    27  	cpty           *conpty.Pty
    28  	procLock       sync.Mutex
    29  	stdioLock      sync.Mutex
    30  	stdin          io.WriteCloser
    31  	stdout         io.ReadCloser
    32  	stderr         io.ReadCloser
    33  	waitBlock      chan struct{}
    34  	closedWaitOnce sync.Once
    35  	waitError      error
    36  }
    37  
    38  var sigMap = map[string]int{
    39  	"CtrlC":        windows.CTRL_C_EVENT,
    40  	"CtrlBreak":    windows.CTRL_BREAK_EVENT,
    41  	"CtrlClose":    windows.CTRL_CLOSE_EVENT,
    42  	"CtrlLogOff":   windows.CTRL_LOGOFF_EVENT,
    43  	"CtrlShutdown": windows.CTRL_SHUTDOWN_EVENT,
    44  }
    45  
    46  var _ cow.Process = &JobProcess{}
    47  
    48  func newProcess(cmd *exec.Exec, cpty *conpty.Pty) *JobProcess {
    49  	return &JobProcess{
    50  		cmd:       cmd,
    51  		cpty:      cpty,
    52  		waitBlock: make(chan struct{}),
    53  	}
    54  }
    55  
    56  func (p *JobProcess) ResizeConsole(ctx context.Context, width, height uint16) error {
    57  	if p.cpty == nil {
    58  		return errors.New("no pseudo console assigned for process")
    59  	}
    60  	if err := p.cpty.Resize(int16(width), int16(height)); err != nil {
    61  		return fmt.Errorf("failed to resize pseudo console for job container: %w", err)
    62  	}
    63  	return nil
    64  }
    65  
    66  // Stdio returns the stdio pipes of the process
    67  func (p *JobProcess) Stdio() (io.Writer, io.Reader, io.Reader) {
    68  	return p.stdin, p.stdout, p.stderr
    69  }
    70  
    71  // Signal sends a signal to the process and returns whether the signal was delivered.
    72  func (p *JobProcess) Signal(ctx context.Context, options interface{}) (bool, error) {
    73  	p.procLock.Lock()
    74  	defer p.procLock.Unlock()
    75  
    76  	if p.exited() {
    77  		return false, fmt.Errorf("signal not sent. process has already exited: %w", hcs.ErrProcessAlreadyStopped)
    78  	}
    79  
    80  	// If options is nil it's assumed we got a sigterm
    81  	if options == nil {
    82  		if err := p.cmd.Kill(); err != nil {
    83  			return false, err
    84  		}
    85  		return true, nil
    86  	}
    87  
    88  	signalOptions, ok := options.(*guestresource.SignalProcessOptionsWCOW)
    89  	if !ok {
    90  		return false, errors.New("unknown signal options")
    91  	}
    92  
    93  	signal, ok := sigMap[string(signalOptions.Signal)]
    94  	if !ok {
    95  		return false, fmt.Errorf("unknown signal %s encountered", signalOptions.Signal)
    96  	}
    97  
    98  	if err := signalProcess(uint32(p.cmd.Pid()), signal); err != nil {
    99  		if errors.Is(err, os.ErrPermission) || hcs.IsAlreadyStopped(err) {
   100  			// The process we are signaling has stopped. Return a proper error that signals this condition.
   101  			return false, fmt.Errorf("failed to send signal: %w", hcs.ErrProcessAlreadyStopped)
   102  		}
   103  		return false, errors.Wrap(err, "failed to send signal")
   104  	}
   105  	return true, nil
   106  }
   107  
   108  // CloseStdin closes the stdin pipe of the process.
   109  func (p *JobProcess) CloseStdin(ctx context.Context) error {
   110  	p.stdioLock.Lock()
   111  	defer p.stdioLock.Unlock()
   112  	if p.stdin != nil {
   113  		if err := p.stdin.Close(); err != nil {
   114  			return errors.Wrap(err, "failed to close job container stdin")
   115  		}
   116  		p.stdin = nil
   117  	}
   118  	return nil
   119  }
   120  
   121  // CloseStdout closes the stdout pipe of the process.
   122  func (p *JobProcess) CloseStdout(ctx context.Context) error {
   123  	p.stdioLock.Lock()
   124  	defer p.stdioLock.Unlock()
   125  	if p.stdout != nil {
   126  		if err := p.stdout.Close(); err != nil {
   127  			return errors.Wrap(err, "failed to close job container stdout")
   128  		}
   129  		p.stdout = nil
   130  	}
   131  	return nil
   132  }
   133  
   134  // CloseStderr closes the stderr pipe of the process.
   135  func (p *JobProcess) CloseStderr(ctx context.Context) error {
   136  	p.stdioLock.Lock()
   137  	defer p.stdioLock.Unlock()
   138  	if p.stderr != nil {
   139  		if err := p.stderr.Close(); err != nil {
   140  			return errors.Wrap(err, "failed to close job container stderr")
   141  		}
   142  		p.stderr = nil
   143  	}
   144  	return nil
   145  }
   146  
   147  // Wait waits for the process to exit. If the process has already exited returns
   148  // the previous error (if any).
   149  func (p *JobProcess) Wait() error {
   150  	<-p.waitBlock
   151  	return p.waitError
   152  }
   153  
   154  // Start starts the job object process
   155  func (p *JobProcess) Start() error {
   156  	return p.cmd.Start()
   157  }
   158  
   159  // This should only be called once.
   160  func (p *JobProcess) waitBackground(ctx context.Context) {
   161  	log.G(ctx).WithField("pid", p.Pid()).Debug("waitBackground for JobProcess")
   162  
   163  	// Wait for process to get signaled/exit/terminate/.
   164  	err := p.cmd.Wait()
   165  
   166  	p.stdioLock.Lock()
   167  	// Close the pseudo console if one was created for this process. Typical scenario is an exec with -it supplied.
   168  	if p.cpty != nil {
   169  		p.cpty.Close()
   170  		p.cpty = nil
   171  	}
   172  	// Wait closes the stdio pipes so theres no need to here.
   173  	p.stdin = nil
   174  	p.stdout = nil
   175  	p.stderr = nil
   176  	p.stdioLock.Unlock()
   177  
   178  	p.closedWaitOnce.Do(func() {
   179  		p.waitError = err
   180  		close(p.waitBlock)
   181  	})
   182  }
   183  
   184  // ExitCode returns the exit code of the process.
   185  func (p *JobProcess) ExitCode() (int, error) {
   186  	p.procLock.Lock()
   187  	defer p.procLock.Unlock()
   188  
   189  	if !p.exited() {
   190  		return -1, errors.New("process has not exited")
   191  	}
   192  	return p.cmd.ExitCode(), nil
   193  }
   194  
   195  // Pid returns the processes PID
   196  func (p *JobProcess) Pid() int {
   197  	return p.cmd.Pid()
   198  }
   199  
   200  // Close cleans up any state associated with the process but does not kill it.
   201  func (p *JobProcess) Close() error {
   202  	p.stdioLock.Lock()
   203  	if p.cpty != nil {
   204  		p.cpty.Close()
   205  		p.cpty = nil
   206  	}
   207  	if p.stdin != nil {
   208  		p.stdin.Close()
   209  		p.stdin = nil
   210  	}
   211  	if p.stdout != nil {
   212  		p.stdout.Close()
   213  		p.stdout = nil
   214  	}
   215  	if p.stderr != nil {
   216  		p.stderr.Close()
   217  		p.stderr = nil
   218  	}
   219  	p.stdioLock.Unlock()
   220  
   221  	p.closedWaitOnce.Do(func() {
   222  		p.waitError = hcs.ErrAlreadyClosed
   223  		close(p.waitBlock)
   224  	})
   225  	return nil
   226  }
   227  
   228  // Kill signals the process to terminate.
   229  // Returns a bool signifying whether the signal was successfully delivered.
   230  func (p *JobProcess) Kill(ctx context.Context) (bool, error) {
   231  	log.G(ctx).WithField("pid", p.Pid()).Debug("killing job process")
   232  
   233  	p.procLock.Lock()
   234  	defer p.procLock.Unlock()
   235  
   236  	if p.exited() {
   237  		return false, errors.New("kill not sent. process already exited")
   238  	}
   239  
   240  	if err := p.cmd.Kill(); err != nil {
   241  		return false, err
   242  	}
   243  	return true, nil
   244  }
   245  
   246  func (p *JobProcess) exited() bool {
   247  	return p.cmd.Exited()
   248  }
   249  
   250  // signalProcess sends the specified signal to a process.
   251  func signalProcess(pid uint32, signal int) error {
   252  	hProc, err := windows.OpenProcess(winapi.PROCESS_ALL_ACCESS, true, pid)
   253  	if err != nil {
   254  		return errors.Wrap(err, "failed to open process")
   255  	}
   256  	defer func() {
   257  		_ = windows.Close(hProc)
   258  	}()
   259  
   260  	// We can't use GenerateConsoleCtrlEvent since that only supports CTRL_C_EVENT and CTRL_BREAK_EVENT.
   261  	// Instead, to handle an arbitrary signal we open a CtrlRoutine thread inside the target process and
   262  	// give it the specified signal to handle. This is safe even with ASLR as even though kernel32.dll's
   263  	// location will be randomized each boot, it will be in the same address for every process. This is why
   264  	// we're able to get the address from a different process and use this as the start address for the routine
   265  	// that the thread will run.
   266  	//
   267  	// Note: This is a hack which is not officially supported.
   268  	k32, err := windows.LoadLibrary("kernel32.dll")
   269  	if err != nil {
   270  		return errors.Wrap(err, "failed to load kernel32 library")
   271  	}
   272  	defer func() {
   273  		_ = windows.FreeLibrary(k32)
   274  	}()
   275  
   276  	proc, err := windows.GetProcAddress(k32, "CtrlRoutine")
   277  	if err != nil {
   278  		return errors.Wrap(err, "failed to load CtrlRoutine")
   279  	}
   280  
   281  	threadHandle, err := winapi.CreateRemoteThread(hProc, nil, 0, proc, uintptr(signal), 0, nil)
   282  	if err != nil {
   283  		return errors.Wrapf(err, "failed to open remote thread in target process %d", pid)
   284  	}
   285  	defer func() {
   286  		_ = windows.Close(threadHandle)
   287  	}()
   288  	return nil
   289  }
   290  

View as plain text