mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-02 13:49:34 +03:00
375 lines
10 KiB
V
375 lines
10 KiB
V
module runcmd
|
|
|
|
import io
|
|
import os
|
|
import strings
|
|
|
|
type IOCopyFn = fn () !
|
|
|
|
@[heap]
|
|
pub struct Command {
|
|
pub mut:
|
|
// path may be a command name or absolute path to executable.
|
|
// If the specified path is not absolute, it will be obtained
|
|
// using the look_path() function before starting the process.
|
|
path string
|
|
|
|
// args holds command line arguments passed to the executable.
|
|
args []string
|
|
|
|
// env contains key-value pairs of environment variables that
|
|
// will be passed to the process. If not specified the current
|
|
// os.environ() will be used. If you want to update current
|
|
// environ instead of overriding it, use merge() function from
|
|
// `maps` module: `maps.merge(os.environ(), {'MYENV': 'value'})`
|
|
env map[string]string
|
|
|
|
// dir specifies the current working directory for the child
|
|
// process. If not specified, the current working directory will
|
|
// be used.
|
|
dir string
|
|
|
|
// If true create pipes for standart in/out/err streams and
|
|
// duplicate child streams to created file descriptors.
|
|
// redirect_stdio is required for work with child I/O.
|
|
redirect_stdio bool
|
|
|
|
// If the field is filled (any of stdin, stdout, stderr), then
|
|
// after the process is started, a coroutine will be launched
|
|
// in the background, which will copy the corresponding data
|
|
// stream between the child and parent processes.
|
|
//
|
|
// This fields must be set BEFORE calling the start() function.
|
|
//
|
|
// This is useful if we want to feed data to the child process
|
|
// via standard input or read stdout and/or stderr entirely.
|
|
// Since the coroutines copies the stream continuously in a loop,
|
|
// the data can only be processed once it has been completely
|
|
// read. To process data in chunks, do not set these fields, but
|
|
// use the stdin(), stdout() and stderr() functions to obtain
|
|
// the child process's file descriptors after calling start().
|
|
stdin ?io.Reader
|
|
stdout ?io.Writer
|
|
stderr ?io.Writer
|
|
|
|
// state holds an information about underlying process.
|
|
// This is set only if process if finished. Call `run()`
|
|
// or `wait()` to get actual state value.
|
|
// This value MUST NOT be changed by API user.
|
|
state ProcessState
|
|
mut:
|
|
// process holds the underlying Process.
|
|
process &Process = unsafe { nil }
|
|
|
|
// stdio holds a file descriptors for I/O processing.
|
|
// There is:
|
|
// * 0 — child process stdin, we must write into it.
|
|
// * 1 — child process stdout, we must read from it.
|
|
// * 2 — child process stderr, we must read from it.
|
|
stdio [3]int = [-1, -1, -1]!
|
|
|
|
// stdio_copy_fns is array of closures to copy data between
|
|
// parent and child processes. For standard I/O streams
|
|
// it does (if some of .stdin, .stdout and .stderr fields is set):
|
|
// * read from .stdin reader and write data into child stdin fd.
|
|
// * read from child stdout fd and write into .stdout writer.
|
|
// * read from child stderr fd and write into .stderr writer.
|
|
stdio_copy_fns []IOCopyFn
|
|
}
|
|
|
|
// run starts a specified command and waits for it. After call see the .state
|
|
// value to get finished process identifier, exit status and other attributes.
|
|
// `run()` is shorthand for:
|
|
// ```v
|
|
// cmd.start()!
|
|
// cmd.wait()!
|
|
// ```
|
|
pub fn (mut c Command) run() ! {
|
|
c.start()!
|
|
c.wait()!
|
|
}
|
|
|
|
// output runs the command and returns its stdout on success. If command exit
|
|
// status is non-zero `ExitError` error is returned.
|
|
// Example:
|
|
// ```v
|
|
// mut okcmd := runcmd.new('sh', '-c', 'echo Hello, World!')
|
|
// output := okcmd.output()!
|
|
// // Hello, World!
|
|
//
|
|
// mut badcmd := runcmd.new('sh', '-c', 'echo -n Error! >&2; false')
|
|
// output := badcmd.output() or {
|
|
// if err is runcmd.ExitError {
|
|
// eprintln(err)
|
|
// exit(err.code())
|
|
// } else {
|
|
// // error starting process or handling I/O, see errno in err.code().
|
|
// panic(err)
|
|
// }
|
|
// }
|
|
// // &runcmd.ExitError{
|
|
// // state: exit status 1
|
|
// // stderr: 'Error!'
|
|
// // }
|
|
// ```
|
|
pub fn (mut c Command) output() !string {
|
|
mut out := strings.new_builder(4096)
|
|
mut err := strings.new_builder(4096)
|
|
c.redirect_stdio = true
|
|
c.stdout = out
|
|
c.stderr = err
|
|
c.start()!
|
|
c.wait()!
|
|
if !c.state.success() {
|
|
return ExitError{
|
|
state: c.state
|
|
stderr: err.str()
|
|
}
|
|
}
|
|
return out.str()
|
|
}
|
|
|
|
// combined_output runs the command and returns its combined stdout and stderr.
|
|
// Unlike `output()`, this function does not return `ExitError` on command failure.
|
|
// Note: The order of lines from stdout and stderr is not guaranteed, since
|
|
// reading from the corresponding file descriptors is done concurrently.
|
|
// Example:
|
|
// ```v
|
|
// mut cmd := runcmd.new('sh', '-c', 'echo Hello, STDOUT!; echo Hello, STDERR! >&2')
|
|
// output := cmd.combined_output()!
|
|
// // Hello, STDOUT!
|
|
// // Hello, STDERR!
|
|
// ```
|
|
pub fn (mut c Command) combined_output() !string {
|
|
mut out := strings.new_builder(4096)
|
|
c.redirect_stdio = true
|
|
c.stdout = out
|
|
c.stderr = out
|
|
c.start()!
|
|
c.wait()!
|
|
return out.str()
|
|
}
|
|
|
|
// start starts a specified command and does not wait for it to complete. Call
|
|
// `wait()` after `start()` has successfully completed to wait for the command
|
|
// to complete and release associated resources.
|
|
// Note: `.state` field is not set after `start()` call.
|
|
pub fn (mut c Command) start() !int {
|
|
if !isnil(c.process) {
|
|
return error('runcmd: process already started')
|
|
}
|
|
|
|
mut pipes := [3]Pipe{}
|
|
if c.redirect_stdio {
|
|
pipes[0] = pipe()! // stdin
|
|
pipes[1] = pipe()! // stdout
|
|
pipes[2] = pipe()! // stderr
|
|
}
|
|
|
|
post_fork_parent_cb := fn [mut c, pipes] (mut p Process) ! {
|
|
if !c.redirect_stdio {
|
|
return
|
|
}
|
|
c.stdio[0] = pipes[0].w
|
|
c.stdio[1] = pipes[1].r
|
|
c.stdio[2] = pipes[2].r
|
|
fd_close(pipes[0].r)!
|
|
fd_close(pipes[1].w)!
|
|
fd_close(pipes[2].w)!
|
|
}
|
|
|
|
post_fork_child_cb := fn [mut c, pipes] (mut p Process) ! {
|
|
if !c.redirect_stdio {
|
|
return
|
|
}
|
|
fd_close(pipes[0].w)!
|
|
fd_close(pipes[1].r)!
|
|
fd_close(pipes[2].r)!
|
|
fd_dup2(pipes[0].r, 0)!
|
|
fd_dup2(pipes[1].w, 1)!
|
|
fd_dup2(pipes[2].w, 2)!
|
|
fd_close(pipes[0].r)!
|
|
fd_close(pipes[1].w)!
|
|
fd_close(pipes[2].w)!
|
|
}
|
|
|
|
if c.redirect_stdio {
|
|
if c.stdin != none {
|
|
c.stdio_copy_fns << fn [mut c] () ! {
|
|
printdbg('Command.start: stdin copy callback called')
|
|
mut fd := c.stdin()!
|
|
printdbg('Command.start: stdin copy callback: child stdin fd=${fd.fd}')
|
|
if c.stdin != none {
|
|
// FIXME: V bug?: without `if` guard acessing
|
|
// to c.stdin causes SIGSEGV.
|
|
io_copy(mut c.stdin, mut fd, 'copy stdin')!
|
|
printdbg('Command.start: stdin copy callback: close child stdin fd after copy')
|
|
fd_close(fd.fd)!
|
|
}
|
|
}
|
|
}
|
|
if c.stdout != none {
|
|
c.stdio_copy_fns << fn [mut c] () ! {
|
|
printdbg('Command.start: stdout copy callback called')
|
|
mut fd := c.stdout()!
|
|
if c.stdout != none {
|
|
io_copy(mut fd, mut c.stdout, 'copy stdout')!
|
|
}
|
|
}
|
|
}
|
|
if c.stderr != none {
|
|
c.stdio_copy_fns << fn [mut c] () ! {
|
|
printdbg('Command.start: stderr copy callback called')
|
|
mut fd := c.stderr()!
|
|
if c.stderr != none {
|
|
io_copy(mut fd, mut c.stderr, 'copy stderr')!
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare and start child process.
|
|
path := look_path(c.path)!
|
|
c.path = path
|
|
c.process = &Process{
|
|
path: path
|
|
argv: c.args
|
|
env: if c.env.len == 0 { os.environ() } else { c.env }
|
|
dir: if c.dir == '' { os.getwd() } else { c.dir }
|
|
post_fork_parent_cb: post_fork_parent_cb
|
|
post_fork_child_cb: post_fork_child_cb
|
|
}
|
|
|
|
pid := c.process.start()!
|
|
|
|
// Start I/O copy callbacks.
|
|
if c.stdio_copy_fns.len > 0 {
|
|
for f in c.stdio_copy_fns {
|
|
go fn (func IOCopyFn) {
|
|
printdbg('Command.start: starting I/O copy closure in coroutine')
|
|
func() or { eprintln('error in I/O copy coroutine: ${err}') }
|
|
}(f)
|
|
}
|
|
}
|
|
|
|
// TODO: Handle context here...
|
|
return pid
|
|
}
|
|
|
|
// wait waits to previously started command is finished. After call see the `.state`
|
|
// field value to get finished process identifier, exit status and other attributes.
|
|
// `wait()` will return an error if the process has not been started or wait has
|
|
// already been called.
|
|
pub fn (mut c Command) wait() ! {
|
|
if isnil(c.process) {
|
|
return error('runcmd: wait for non-started process')
|
|
} else if c.state != ProcessState{} {
|
|
return error('runcmd: wait already called')
|
|
}
|
|
c.state = c.process.wait()!
|
|
unsafe { c.release()! }
|
|
}
|
|
|
|
// release releases all resources assocuated with process.
|
|
@[unsafe]
|
|
pub fn (mut c Command) release() ! {
|
|
for fd in c.stdio {
|
|
if fd == -1 {
|
|
continue
|
|
}
|
|
fd_close(fd) or {
|
|
if err.code() == 9 {
|
|
// Ignore EBADF error, fd is already closed.
|
|
continue
|
|
}
|
|
printdbg('${@METHOD}: cannot close fd: ${err}')
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// stdin returns an open file descriptor associated with the standard
|
|
// input stream of the child process. This descriptor is writable only
|
|
// by the parent process.
|
|
pub fn (c Command) stdin() !WriteFd {
|
|
return if c.stdio[0] != -1 {
|
|
WriteFd{c.stdio[0]}
|
|
} else {
|
|
printdbg('${@METHOD}: invalid fd -1')
|
|
error_with_code('Bad file descriptor', 9)
|
|
}
|
|
}
|
|
|
|
// stdout returns an open file descriptor associated with the standard
|
|
// output stream of the child process. This descriptor is read-only for
|
|
// the parent process.
|
|
pub fn (c Command) stdout() !ReadFd {
|
|
return if c.stdio[1] != -1 {
|
|
ReadFd{c.stdio[1]}
|
|
} else {
|
|
printdbg('${@METHOD}: invalid fd -1')
|
|
error_with_code('Bad file descriptor', 9)
|
|
}
|
|
}
|
|
|
|
// stderr returns an open file descriptor associated with the standard
|
|
// error stream of the child process. This descriptor is read-only for
|
|
// the parent process.
|
|
pub fn (c Command) stderr() !ReadFd {
|
|
return if c.stdio[2] != -1 {
|
|
ReadFd{c.stdio[2]}
|
|
} else {
|
|
printdbg('${@METHOD}: invalid fd -1')
|
|
error_with_code('Bad file descriptor', 9)
|
|
}
|
|
}
|
|
|
|
pub struct ExitError {
|
|
pub:
|
|
state ProcessState
|
|
stderr string
|
|
}
|
|
|
|
// code returns an exit status code of a failed process.
|
|
pub fn (e ExitError) code() int {
|
|
return e.state.exit_code()
|
|
}
|
|
|
|
// msg returns message about command failure.
|
|
pub fn (e ExitError) msg() string {
|
|
return 'command exited with non-zero code'
|
|
}
|
|
|
|
// io_copy is copypasta from io.cp() with some debug logs.
|
|
fn io_copy(mut src io.Reader, mut dst io.Writer, msg string) ! {
|
|
mut buf := []u8{len: 4096}
|
|
defer {
|
|
unsafe {
|
|
buf.free()
|
|
}
|
|
}
|
|
for {
|
|
nr := src.read(mut buf) or {
|
|
printdbg('${@FN}: (${msg}) got error from reader, breaking loop: ${err}')
|
|
break
|
|
}
|
|
printdbg('${@FN}: (${msg}) ${nr} bytes read from src to buf')
|
|
nw := dst.write(buf[..nr]) or { return err }
|
|
printdbg('${@FN}: (${msg}) ${nw} bytes written to dst')
|
|
}
|
|
}
|
|
|
|
// Wrap os.fd_* functions for errors handling...
|
|
|
|
fn fd_close(fd int) ! {
|
|
if os.fd_close(fd) == -1 {
|
|
return os.last_error()
|
|
}
|
|
}
|
|
|
|
fn fd_dup2(fd1 int, fd2 int) ! {
|
|
if os.fd_dup2(fd1, fd2) == -1 {
|
|
return os.last_error()
|
|
}
|
|
}
|