Files
runcmd/cmd.v
2025-12-28 20:42:30 +03:00

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()
}
}