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 }