mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-02 13:49:34 +03:00
init
This commit is contained in:
374
cmd.v
Normal file
374
cmd.v
Normal file
@@ -0,0 +1,374 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user