github.com/iDigitalFlame/xmt@v0.5.4/cmd/exec.go (about)

     1  // Copyright (C) 2020 - 2023 iDigitalFlame
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU General Public License as published by
     5  // the Free Software Foundation, either version 3 of the License, or
     6  // any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  //
    16  
    17  // Package cmd contains functions that can be used to execute external processes.
    18  // Some OS versions have more advanced featuresets that are avaliable.
    19  package cmd
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"io"
    25  	"sync/atomic"
    26  	"time"
    27  
    28  	"github.com/iDigitalFlame/xmt/cmd/filter"
    29  	"github.com/iDigitalFlame/xmt/util/xerr"
    30  )
    31  
    32  const (
    33  	exitStopped uint32 = 0x1337
    34  
    35  	cookieStopped uint32 = 0x1
    36  	cookieFinal   uint32 = 0x2
    37  	cookieRelease uint32 = 0x4
    38  )
    39  
    40  // Process is a struct that represents an executable command and allows for setting
    41  // options in order change the operating functions.
    42  type Process struct {
    43  	ctx            context.Context
    44  	Stdout, Stderr io.Writer
    45  	err            error
    46  
    47  	Stdin  io.Reader
    48  	ch     chan struct{}
    49  	cancel context.CancelFunc
    50  
    51  	Dir       string
    52  	Args, Env []string
    53  	x         executable
    54  
    55  	Timeout             time.Duration
    56  	flags, exit, cookie uint32
    57  	split               bool
    58  }
    59  
    60  // Run will start the process and wait until it completes.
    61  //
    62  // This function will return the same errors as the 'Start' function if they
    63  // occur or the 'Wait' function if any errors occur during Process runtime.
    64  func (p *Process) Run() error {
    65  	if err := p.Start(); err != nil {
    66  		return err
    67  	}
    68  	return p.Wait()
    69  }
    70  
    71  // Pid returns the current process PID. This function returns zero if the
    72  // process has not been started.
    73  func (p *Process) Pid() uint32 {
    74  	if !p.x.isStarted() {
    75  		return 0
    76  	}
    77  	return p.x.Pid()
    78  }
    79  
    80  // Wait will block until the Process completes or is terminated by a call to
    81  // Stop.
    82  //
    83  // This will start the process if not already started.
    84  func (p *Process) Wait() error {
    85  	if !p.x.isStarted() {
    86  		if err := p.Start(); err != nil {
    87  			return err
    88  		}
    89  	} else if !p.Running() {
    90  		return p.err
    91  	}
    92  	<-p.ch
    93  	return p.err
    94  }
    95  
    96  // Stop will attempt to terminate the currently running Process instance.
    97  //
    98  // Stopping a Process may prevent the ability to read the Stdout/Stderr and any
    99  // proper exit codes.
   100  func (p *Process) Stop() error {
   101  	if !p.x.isStarted() || !p.Running() {
   102  		return nil
   103  	}
   104  	return p.stopWith(exitStopped, p.x.kill(exitStopped, p))
   105  }
   106  
   107  // Start will attempt to start the Process and will return an errors that occur
   108  // while starting the Process.
   109  //
   110  // This function will return 'ErrEmptyCommand' if the 'Args' parameter is empty
   111  // and 'ErrAlreadyStarted' if attempting to start a Process that already has
   112  // been started previously.
   113  func (p *Process) Start() error {
   114  	if p.Running() || atomic.LoadUint32(&p.cookie) > 0 {
   115  		return ErrAlreadyStarted
   116  	}
   117  	if len(p.Args) == 0 {
   118  		return ErrEmptyCommand
   119  	}
   120  	if p.ctx == nil {
   121  		p.ctx = context.Background()
   122  	}
   123  	if p.Timeout > 0 {
   124  		p.ctx, p.cancel = context.WithTimeout(p.ctx, p.Timeout)
   125  	} else {
   126  		p.cancel = func() {}
   127  	}
   128  	p.ch = make(chan struct{})
   129  	atomic.StoreUint32(&p.cookie, 0)
   130  	if err := p.x.start(p.ctx, p, false); err != nil {
   131  		return p.stopWith(exitStopped, err)
   132  	}
   133  	return nil
   134  }
   135  
   136  // Flags returns the current set flags value based on the configured options.
   137  func (p *Process) Flags() uint32 {
   138  	return p.flags
   139  }
   140  
   141  // Running returns true if the current Process is running, false otherwise.
   142  func (p *Process) Running() bool {
   143  	if !p.x.isStarted() || !p.x.isRunning() {
   144  		return false
   145  	}
   146  	return p.running()
   147  }
   148  func (p *Process) running() bool {
   149  	select {
   150  	case <-p.ch:
   151  		return false
   152  	default:
   153  	}
   154  	return true
   155  }
   156  
   157  // Release will attempt to release the resources for this Process, including
   158  // handles.
   159  //
   160  // After the first call to this function, all other function calls will fail
   161  // with errors. Repeated calls to this function return nil and are a NOP.
   162  func (p *Process) Release() error {
   163  	if !p.x.isStarted() {
   164  		return ErrNotStarted
   165  	}
   166  	p.x.close()
   167  	return nil
   168  }
   169  
   170  // Resume will attempt to resume this process. This will attempt to resume
   171  // the process using an OS-dependent syscall.
   172  //
   173  // This will not affect already running processes.
   174  func (p *Process) Resume() error {
   175  	if !p.x.isStarted() {
   176  		return ErrNotStarted
   177  	}
   178  	if !p.Running() {
   179  		return nil
   180  	}
   181  	return p.x.Resume()
   182  }
   183  
   184  // Suspend will attempt to suspend this process. This will attempt to suspend
   185  // the process using an OS-dependent syscall.
   186  //
   187  // This will not affect already suspended processes.
   188  func (p *Process) Suspend() error {
   189  	if !p.x.isStarted() {
   190  		return ErrNotStarted
   191  	}
   192  	if !p.Running() {
   193  		return nil
   194  	}
   195  	return p.x.Suspend()
   196  }
   197  
   198  // SetUID will set the process UID at runtime. This function takes the numerical
   199  // UID value. Use '-1' to disable this setting. The UID value is validated at
   200  // runtime.
   201  //
   202  // This function has no effect on Windows devices.
   203  func (p *Process) SetUID(u int32) {
   204  	p.x.SetUID(u, p)
   205  }
   206  
   207  // SetGID will set the process GID at runtime. This function takes the numerical
   208  // GID value. Use '-1' to disable this setting. The GID value is validated at runtime.
   209  //
   210  // This function has no effect on Windows devices.
   211  func (p *Process) SetGID(g int32) {
   212  	p.x.SetGID(g, p)
   213  }
   214  
   215  // SetFlags will set the startup Flag values used for Windows programs. This
   216  // function overrides many of the 'Set*' functions.
   217  func (p *Process) SetFlags(f uint32) {
   218  	p.flags = f
   219  }
   220  
   221  // NewProcess creates a new process instance that uses the supplied string
   222  // vardict as the command line arguments. Similar to '&Process{Args: s}'.
   223  func NewProcess(s ...string) *Process {
   224  	return &Process{Args: s}
   225  }
   226  
   227  // SetToken will set the User or Process Token handle that this Process will
   228  // run under.
   229  //
   230  // WARNING: This may cause issues when running with a parent process.
   231  //
   232  // This function has no effect on commands that do not generate windows or
   233  // if the device is not running Windows.
   234  func (p *Process) SetToken(t uintptr) {
   235  	p.x.SetToken(t)
   236  }
   237  
   238  // SetNoWindow will hide or show the window of the newly spawned process.
   239  //
   240  // This function has no effect on commands that do not generate windows or
   241  // if the device is not running Windows.
   242  func (p *Process) SetNoWindow(h bool) {
   243  	p.x.SetNoWindow(h, p)
   244  }
   245  
   246  // SetDetached will detach or detach the console of the newly spawned process
   247  // from the parent. This function has no effect on non-console commands. Setting
   248  // this to true disables SetNewConsole.
   249  //
   250  // This function has no effect if the device is not running Windows.
   251  func (p *Process) SetDetached(d bool) {
   252  	p.x.SetDetached(d, p)
   253  }
   254  
   255  // SetSuspended will delay the execution of this Process and will put the
   256  // process in a suspended state until it is resumed using a Resume call.
   257  //
   258  // This function has no effect if the device is not running Windows.
   259  func (p *Process) SetSuspended(s bool) {
   260  	p.x.SetSuspended(s, p)
   261  }
   262  
   263  // SetInheritEnv will change the behavior of the Environment variable
   264  // inheritance on startup. If true (the default), the current Environment
   265  // variables will be filled in, even if 'Env' is not empty.
   266  //
   267  // If set to false, the current Environment variables will not be added into
   268  // the Process's starting Environment.
   269  func (p *Process) SetInheritEnv(i bool) {
   270  	p.split = !i
   271  }
   272  
   273  // SetNewConsole will allocate a new console for the newly spawned process.
   274  // This console output will be independent of the parent process.
   275  //
   276  // This function has no effect if the device is not running Windows.
   277  func (p *Process) SetNewConsole(c bool) {
   278  	p.x.SetNewConsole(c, p)
   279  }
   280  
   281  // SetFullscreen will set the window fullscreen state of the newly spawned process.
   282  // This function has no effect on commands that do not generate windows.
   283  //
   284  // This function has no effect if the device is not running Windows.
   285  func (p *Process) SetFullscreen(f bool) {
   286  	p.x.SetFullscreen(f)
   287  }
   288  
   289  // SetWindowDisplay will set the window display mode of the newly spawned process.
   290  // This function has no effect on commands that do not generate windows.
   291  //
   292  // See the 'SW_*' values in winuser.h or the Golang windows package documentation for more details.
   293  //
   294  // This function has no effect if the device is not running Windows.
   295  func (p *Process) SetWindowDisplay(m int) {
   296  	p.x.SetWindowDisplay(m)
   297  }
   298  
   299  // SetWindowTitle will set the title of the new spawned window to the
   300  // specified string. This function has no effect on commands that do not
   301  // generate windows. Setting the value to an empty string will unset this
   302  // setting.
   303  //
   304  // This function has no effect if the device is not running Windows.
   305  func (p *Process) SetWindowTitle(s string) {
   306  	p.x.SetWindowTitle(s)
   307  }
   308  
   309  // Output runs the Process and returns its standard output. Any returned error
   310  // will usually be of type *ExitError.
   311  func (p *Process) Output() ([]byte, error) {
   312  	if p.Stdout != nil {
   313  		return nil, xerr.Sub("stdout already set", 0x37)
   314  	}
   315  	var b bytes.Buffer
   316  	p.Stdout = &b
   317  	err := p.Run()
   318  	return b.Bytes(), err
   319  }
   320  
   321  // Handle returns the handle of the current running Process. The return is an
   322  // uintptr that can converted into a Handle.
   323  //
   324  // This function returns an error if the Process was not started. The handle
   325  // is not expected to be valid after the Process exits or is terminated.
   326  //
   327  // This function always returns 'ErrNoWindows' on non-Windows devices.
   328  func (p *Process) Handle() (uintptr, error) {
   329  	if !p.x.isStarted() {
   330  		return 0, ErrNotStarted
   331  	}
   332  	return p.x.Handle(), nil
   333  }
   334  
   335  // ExitCode returns the Exit Code of the process.
   336  //
   337  // If the Process is still running or has not been started, this function returns
   338  // an 'ErrStillRunning' error.
   339  func (p *Process) ExitCode() (int32, error) {
   340  	if p.x.isStarted() && p.Running() {
   341  		return 0, ErrStillRunning
   342  	}
   343  	return int32(p.exit), nil
   344  }
   345  
   346  // SetLogin will set the User credentials that this Process will run under.
   347  //
   348  // WARNING: This may cause issues when running with a parent process.
   349  //
   350  // Currently only supported on Windows devices.
   351  func (p *Process) SetLogin(u, d, pw string) {
   352  	p.x.SetLogin(u, d, pw)
   353  }
   354  
   355  // SetWindowSize will set the window display size of the newly spawned process.
   356  // This function has no effect on commands that do not generate windows.
   357  //
   358  // This function has no effect if the device is not running Windows.
   359  func (p *Process) SetWindowSize(w, h uint32) {
   360  	p.x.SetWindowSize(w, h)
   361  }
   362  
   363  // SetParent will instruct the Process to choose a parent with the supplied
   364  // process Filter. If the Filter is nil this will use the current process (default).
   365  // Setting the Parent process will automatically set 'SetNewConsole' to true
   366  //
   367  // This function has no effect if the device is not running Windows.
   368  func (p *Process) SetParent(f *filter.Filter) {
   369  	p.x.SetParent(f, p)
   370  }
   371  
   372  // SetWindowPosition will set the window position of the newly spawned process.
   373  // This function has no effect on commands that do not generate windows.
   374  //
   375  // This function has no effect if the device is not running Windows.
   376  func (p *Process) SetWindowPosition(x, y uint32) {
   377  	p.x.SetWindowPosition(x, y)
   378  }
   379  
   380  // CombinedOutput runs the Process and returns its combined standard output
   381  // and standard error. Any returned error will usually be of type *ExitError.
   382  func (p *Process) CombinedOutput() ([]byte, error) {
   383  	if p.Stdout != nil {
   384  		return nil, xerr.Sub("stdout already set", 0x37)
   385  	}
   386  	if p.Stderr != nil {
   387  		return nil, xerr.Sub("stderr already set", 0x38)
   388  	}
   389  	var b bytes.Buffer
   390  	p.Stdout = &b
   391  	p.Stderr = &b
   392  	err := p.Run()
   393  	return b.Bytes(), err
   394  }
   395  func (p *Process) stopWith(c uint32, e error) error {
   396  	if !p.running() {
   397  		return e
   398  	}
   399  	if atomic.LoadUint32(&p.cookie)&cookieFinal == 0 {
   400  		if atomic.SwapUint32(&p.cookie, p.cookie|cookieStopped|cookieFinal)&cookieStopped == 0 {
   401  			p.x.kill(exitStopped, p)
   402  		}
   403  		if err := p.ctx.Err(); err != nil && p.exit == 0 {
   404  			p.err, p.exit = err, c
   405  		}
   406  		p.x.close()
   407  		close(p.ch)
   408  	}
   409  	if p.cancel(); p.err == nil && p.ctx.Err() != nil {
   410  		if e != nil {
   411  			p.err = e
   412  			return e
   413  		}
   414  		return nil
   415  	}
   416  	if p.err == nil && e != nil {
   417  		p.err = e
   418  	}
   419  	return p.err
   420  }
   421  
   422  // StdinPipe returns a pipe that will be connected to the Process's standard
   423  // input when the Process starts. The pipe will be closed automatically after
   424  // the Processes starts. A caller need only call Close to force the pipe to
   425  // close sooner.
   426  func (p *Process) StdinPipe() (io.WriteCloser, error) {
   427  	if p.x.isStarted() {
   428  		return nil, ErrAlreadyStarted
   429  	}
   430  	if p.Stdin != nil {
   431  		return nil, xerr.Sub("stdin already set", 0x39)
   432  	}
   433  	return p.x.StdinPipe(p)
   434  }
   435  
   436  // StdoutPipe returns a pipe that will be connected to the Process's
   437  // standard output when the Processes starts.
   438  //
   439  // The pipe will be closed after the Process exits, so most callers need not
   440  // close the pipe themselves. It is thus incorrect to call Wait before all
   441  // reads from the pipe have completed. For the same reason, it is incorrect
   442  // to use Run when using StderrPipe.
   443  //
   444  // See the stdlib StdoutPipe example for idiomatic usage.
   445  func (p *Process) StdoutPipe() (io.ReadCloser, error) {
   446  	if p.x.isStarted() {
   447  		return nil, ErrAlreadyStarted
   448  	}
   449  	if p.Stdout != nil {
   450  		return nil, xerr.Sub("stdout already set", 0x37)
   451  	}
   452  	return p.x.StdoutPipe(p)
   453  }
   454  
   455  // StderrPipe returns a pipe that will be connected to the Process's
   456  // standard error when the Processes starts.
   457  //
   458  // The pipe will be closed after the Process exits, so most callers need
   459  // not close the pipe themselves. It is thus incorrect to call Wait before all
   460  // reads from the pipe have completed. For the same reason, it is incorrect
   461  // to use Run when using StderrPipe.
   462  //
   463  // See the stdlib StdoutPipe example for idiomatic usage.
   464  func (p *Process) StderrPipe() (io.ReadCloser, error) {
   465  	if p.x.isStarted() {
   466  		return nil, ErrAlreadyStarted
   467  	}
   468  	if p.Stdout != nil {
   469  		return nil, xerr.Sub("stderr already set", 0x38)
   470  	}
   471  	return p.x.StderrPipe(p)
   472  }
   473  
   474  // NewProcessContext creates a new process instance that uses the supplied
   475  // string vardict as the command line arguments.
   476  //
   477  // This function accepts a context that can be used to control the cancellation
   478  // of this process.
   479  func NewProcessContext(x context.Context, s ...string) *Process {
   480  	return &Process{Args: s, ctx: x}
   481  }