mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-02 13:49:34 +03:00
init
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.v]
|
||||
indent_style = tab
|
||||
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
*.bat eol=crlf
|
||||
|
||||
*.v linguist-language=V
|
||||
*.vv linguist-language=V
|
||||
*.vsh linguist-language=V
|
||||
v.mod linguist-language=V
|
||||
.vdocignore linguist-language=ignore
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Binaries for programs and plugins
|
||||
main
|
||||
runcmd
|
||||
*.exe
|
||||
*.exe~
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Ignore binary output folders
|
||||
bin/
|
||||
|
||||
# Ignore common editor/system specific metadata
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# ENV
|
||||
.env
|
||||
|
||||
# vweb and database
|
||||
*.db
|
||||
*.js
|
||||
|
||||
TODO
|
||||
1
.vdocignore
Normal file
1
.vdocignore
Normal file
@@ -0,0 +1 @@
|
||||
examples/
|
||||
76
README.md
Normal file
76
README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Run External Commands
|
||||
|
||||
`runcmd` module implements high-level interface for running external commands.
|
||||
|
||||
## Why not vlib `os`?
|
||||
|
||||
The standard V `os` module already contains tools for a similar tasks — `os.Process`,
|
||||
`os.Command`, but I don't like any of them. There is also many functions for executing
|
||||
external commands:
|
||||
|
||||
* `os.execvp()`, `os.execve()` — cross-platform versions of the C functions of the same name.
|
||||
|
||||
* `os.execute()`, `os.execute_opt()`, `os.execute_or_exit()`, `os.execute_or_panic()` — wrap C calls and wait for a command to completed. Under the hood, they perform a dirty hack by calling `sh` with stream redirection `'exec 2>&1;${cmd}'`. Only stdout and exit_code are available in result.
|
||||
|
||||
* `os.system()` — also executes command in the shell, but does not redirect streams. This is fine for running commands that take a long time and write something to the terminal; it's convenient in build scripts.
|
||||
|
||||
* `os.Process` just has an ugly interface with a lot of unnecessary methods. Actually, it's not bad; I copied parts of it.
|
||||
|
||||
* `os.Command` runs `popen()` under the hood and is not suitable for anything other than running a command in the shell (again) with stream processing of the mixed stdout and stderr.
|
||||
|
||||
This `runcmd` module is inspired by os/exec from the Go standard library and provides
|
||||
a fairly flexible interface for starting child processes.
|
||||
|
||||
The obvious downside of this module is that it only works on Linux and likely other
|
||||
POSIX-compliant operating systems. I'm not interested in working on MS Windows, but
|
||||
anyone interested can submit a PR on GitHub to support the worst operating system.
|
||||
|
||||
## Usage
|
||||
|
||||
Basic usage:
|
||||
|
||||
```v
|
||||
import runcmd
|
||||
|
||||
mut cmd := runcmd.new('sh', '-c', 'echo Hello, World!')
|
||||
cmd.run()! // Start and wait for process.
|
||||
// Hello, World!
|
||||
println(cmd.state) // exit status 0
|
||||
```
|
||||
|
||||
You can create a `Command` object directly if that's more convenient. The following
|
||||
example is equivalent to the first:
|
||||
|
||||
```v
|
||||
import runcmd
|
||||
|
||||
mut cmd := runcmd.Command{
|
||||
path: 'sh' // automatically resolves to actual path, e.g. /usr/bin/sh
|
||||
args: ['-c', 'echo Hello, World!']
|
||||
}
|
||||
cmd.run()!
|
||||
println(cmd.state)
|
||||
```
|
||||
|
||||
If you don't want to wait for the child process to complete, call `start()` instead of `run()`:
|
||||
|
||||
```v
|
||||
mut cmd := runcmd.new('sh', '-c', 'sleep 60')
|
||||
pid := cmd.start()!
|
||||
println(pid)
|
||||
```
|
||||
`.state` value is unavailable in this case because we didn't wait for the process to complete.
|
||||
|
||||
If you need to capture standard output and standard error, use the `output()` and
|
||||
`combined_output()`. See examples in its description.
|
||||
|
||||
See also [examples](examples) dir for more examples.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Basic implementation.
|
||||
- [ ] Contexts support for creating cancelable commands, commands with timeouts, etc.
|
||||
- [ ] Process groups support, pgkill().
|
||||
- [ ] Better error handling and more tests...
|
||||
|
||||
Send pull requests for additional features/bugfixes.
|
||||
22
UNLICENSE
Normal file
22
UNLICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
||||
software, either in source code form or as a compiled binary, for any purpose,
|
||||
commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this
|
||||
software dedicate any and all copyright interest in the software to the public
|
||||
domain. We make this dedication for the benefit of the public at large and to
|
||||
the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in
|
||||
perpetuity of all present and future rights to this software under copyright
|
||||
law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
26
bytebuf.v
Normal file
26
bytebuf.v
Normal file
@@ -0,0 +1,26 @@
|
||||
module runcmd
|
||||
|
||||
import io
|
||||
|
||||
// buffer creates simple bytes buffer that can be read through `io.Reader` interface.
|
||||
pub fn buffer(data []u8) ByteBuffer {
|
||||
return ByteBuffer{
|
||||
bytes: data
|
||||
}
|
||||
}
|
||||
|
||||
struct ByteBuffer {
|
||||
bytes []u8
|
||||
mut:
|
||||
pos int
|
||||
}
|
||||
|
||||
// read reads `buf.len` bytes from internal bytes buffer and returns number of bytes read.
|
||||
pub fn (mut b ByteBuffer) read(mut buf []u8) !int {
|
||||
if b.pos >= b.bytes.len {
|
||||
return io.Eof{}
|
||||
}
|
||||
n := copy(mut buf, b.bytes[b.pos..])
|
||||
b.pos += n
|
||||
return n
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
6
dbg.v
Normal file
6
dbg.v
Normal file
@@ -0,0 +1,6 @@
|
||||
module runcmd
|
||||
|
||||
@[if runcmd_trace ?]
|
||||
fn printdbg(s string) {
|
||||
eprintln('runcmd[pid=${v_getpid()}]: ${s}')
|
||||
}
|
||||
44
examples/error_handling.v
Normal file
44
examples/error_handling.v
Normal file
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
import runcmd
|
||||
|
||||
fn main() {
|
||||
// Prepare the command.
|
||||
mut cmd := runcmd.new('sh', '-c', 'echo -n This command always fails! >&2; sleep 30; false')
|
||||
|
||||
// Run this example with `-d runcmd_trace` to see debug logs.
|
||||
// Look for line like this: runcmd[pid=584015]: Process.wait: wait for pid 584016
|
||||
// Try to `kill -9 ${pid_here}` while program runs and see whats happen.
|
||||
|
||||
// Run command with capturing its output.
|
||||
out := cmd.output() or {
|
||||
if err is runcmd.ExitError {
|
||||
// Command exited with non-zero code. Handle it here.
|
||||
eprintln(err)
|
||||
// `err.state` can tell you the failure details.
|
||||
eprintln(err.state)
|
||||
// Let's check if the process was killed by someone...
|
||||
status := runcmd.WaitStatus(err.state.sys())
|
||||
if status.term_signal() == int(os.Signal.kill) {
|
||||
eprintln('Oh, process is killed... ( x__x )')
|
||||
} else {
|
||||
// Not killed.
|
||||
}
|
||||
exit(err.code()) // `err.code()` here contains the command exit status.
|
||||
} else {
|
||||
// Another error occurred. Most likely, something went wrong while executing
|
||||
// the process creation system calls. Check `err.code()` to get the concrete
|
||||
// error, it contains the standard C errno value.
|
||||
// See https://www.man7.org/linux/man-pages/man3/errno.3.html
|
||||
|
||||
// Replace 0 to actual errno value (real errno never be zero).
|
||||
if err.code() == 0 {
|
||||
// Do something here...
|
||||
}
|
||||
|
||||
// Fallback to panic.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
println(out)
|
||||
}
|
||||
59
examples/stream_input_and_output.v
Normal file
59
examples/stream_input_and_output.v
Normal file
@@ -0,0 +1,59 @@
|
||||
import io.string_reader
|
||||
import rand
|
||||
import runcmd
|
||||
import time
|
||||
|
||||
fn main() {
|
||||
// Prepare the command.
|
||||
mut cmd := runcmd.new('cat')
|
||||
|
||||
// Setup I/O redirection.
|
||||
cmd.redirect_stdio = true
|
||||
|
||||
// Start a process.
|
||||
// Note: File descriptors will only become available after the process has started!
|
||||
pid := cmd.start()!
|
||||
println('Child process started with pid ${pid}')
|
||||
|
||||
// Get child file descriptors.
|
||||
mut child_stdin := cmd.stdin()!
|
||||
mut child_stdout := cmd.stdout()!
|
||||
|
||||
// Prepare reader to store command output.
|
||||
mut output := string_reader.StringReader.new(reader: child_stdout)
|
||||
|
||||
// Start stdout reading in a coroutine.
|
||||
//
|
||||
// The reader will be block until the descriptor contains data.
|
||||
// Therefore, to avoid blocking the main thread, we start the reader
|
||||
// in a coroutine.
|
||||
go fn [mut output] () {
|
||||
println('STDOUT reader started!')
|
||||
// Read stdout line by line until EOF.
|
||||
for {
|
||||
line := output.read_line() or { break }
|
||||
println('Recv: ${line}')
|
||||
}
|
||||
}()
|
||||
|
||||
// Start sending data to child in a loop.
|
||||
limit := 5
|
||||
for _ in 0 .. limit {
|
||||
// Generate some data.
|
||||
data := rand.string(10) + '\n'
|
||||
print('Send: ${data}')
|
||||
|
||||
// Write data to child stdin file descriptor.
|
||||
_ := child_stdin.write(data.bytes())!
|
||||
|
||||
// Sleep a bit for demonstration.
|
||||
time.sleep(500 * time.millisecond)
|
||||
}
|
||||
|
||||
// Close stdin by hand so that the child process receives EOF.
|
||||
// Without this child will hang for waiting for input.
|
||||
child_stdin.close()!
|
||||
|
||||
// wait() will close the child stdout file desciptor by itself.
|
||||
cmd.wait()!
|
||||
}
|
||||
28
examples/stream_output.v
Normal file
28
examples/stream_output.v
Normal file
@@ -0,0 +1,28 @@
|
||||
import io.string_reader
|
||||
import runcmd
|
||||
|
||||
fn main() {
|
||||
// Prepare command.
|
||||
mut cmd := runcmd.new('sh', '-c', r'for i in {1..5}; do echo line $i; sleep .5; done; echo finish!')
|
||||
|
||||
// This is required to captute standart I/O streams.
|
||||
cmd.redirect_stdio = true
|
||||
|
||||
// Start child process.
|
||||
pid := cmd.start()!
|
||||
println('Child process started with pid ${pid}')
|
||||
|
||||
// Setup StringReader with stdout input. Note the cmd.stdout()! call, it
|
||||
// returns the io.Reader interface and reads child process stdout file descriptor.
|
||||
mut reader := string_reader.StringReader.new(reader: cmd.stdout()!)
|
||||
|
||||
// Read sdtout line by line until EOF.
|
||||
for {
|
||||
line := reader.read_line() or { break }
|
||||
println('Read: ${line}')
|
||||
}
|
||||
|
||||
cmd.wait()! // Wait to child process completed.
|
||||
|
||||
println('Child state: ${cmd.state}')
|
||||
}
|
||||
38
examples/write_to_child_stdin.v
Normal file
38
examples/write_to_child_stdin.v
Normal file
@@ -0,0 +1,38 @@
|
||||
import io.string_reader
|
||||
import strings
|
||||
import runcmd
|
||||
|
||||
fn main() {
|
||||
input := 'Hello from parent process!'
|
||||
|
||||
// Prepare reader and writer.
|
||||
//
|
||||
// * `reader` reads input from the parent process; it will be copied to the
|
||||
// standard input of the child process.
|
||||
// * `writer` accepts data from the child process; it will be copied from the
|
||||
// standard output of the child process.
|
||||
mut reader := string_reader.StringReader.new(reader: runcmd.buffer(input.bytes()), source: input)
|
||||
mut writer := strings.new_builder(4096)
|
||||
|
||||
// Prepare the command.
|
||||
mut cmd := runcmd.new('cat')
|
||||
|
||||
// Set redirect_stdio to perform I/O copying between parent and child processes.
|
||||
cmd.redirect_stdio = true
|
||||
|
||||
// Setup reader and writer for child I/O streams.
|
||||
cmd.stdin = reader
|
||||
cmd.stdout = writer
|
||||
|
||||
// Start and wait for command.
|
||||
cmd.run()!
|
||||
|
||||
// Get command output as string.
|
||||
output := writer.str()
|
||||
|
||||
// Make sure that `cat` returned the same data that we sent to it as input.
|
||||
assert input == output, 'output data differs from input!'
|
||||
|
||||
println('Child state: ${cmd.state}')
|
||||
println('Child output: ${output}')
|
||||
}
|
||||
73
fdio.v
Normal file
73
fdio.v
Normal file
@@ -0,0 +1,73 @@
|
||||
module runcmd
|
||||
|
||||
import io
|
||||
import os
|
||||
|
||||
struct ReadFd {
|
||||
fd int
|
||||
}
|
||||
|
||||
// read reads the `buf.len` bytes from file descriptor and returns number of
|
||||
// bytes read on success. This function implements the `io.Reader` interface.
|
||||
pub fn (mut f ReadFd) read(mut buf []u8) !int {
|
||||
if buf.len == 0 {
|
||||
return io.Eof{}
|
||||
}
|
||||
nbytes := int(C.read(f.fd, buf.data, buf.len))
|
||||
if nbytes == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
if nbytes == 0 {
|
||||
return io.Eof{}
|
||||
}
|
||||
return nbytes
|
||||
}
|
||||
|
||||
// slurp reads all data from file descriptor (until gets `io.Eof`) and returns
|
||||
// result as byte array.
|
||||
pub fn (mut f ReadFd) slurp() ![]u8 {
|
||||
mut res := []u8{}
|
||||
bufsize := 4096
|
||||
for {
|
||||
mut buf := []u8{len: bufsize, cap: bufsize}
|
||||
nbytes := f.read(mut buf) or {
|
||||
if err is io.Eof {
|
||||
break
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if nbytes == 0 {
|
||||
break
|
||||
}
|
||||
res << buf
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// close closes the underlying file descriptor.
|
||||
pub fn (mut f ReadFd) close() ! {
|
||||
fd_close(f.fd)!
|
||||
}
|
||||
|
||||
struct WriteFd {
|
||||
fd int
|
||||
}
|
||||
|
||||
// write writes the `buf.len` bytes to the file descriptor and returns number
|
||||
// of bytes written on success. This function implements the `io.Writer` interface.
|
||||
pub fn (mut f WriteFd) write(buf []u8) !int {
|
||||
if buf.len == 0 {
|
||||
return 0
|
||||
}
|
||||
nbytes := int(C.write(f.fd, buf.data, buf.len))
|
||||
if nbytes == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
return nbytes
|
||||
}
|
||||
|
||||
// close closes the underlying file descriptor.
|
||||
pub fn (mut f WriteFd) close() ! {
|
||||
fd_close(f.fd)!
|
||||
}
|
||||
17
pipe.c.v
Normal file
17
pipe.c.v
Normal file
@@ -0,0 +1,17 @@
|
||||
module runcmd
|
||||
|
||||
import os
|
||||
|
||||
struct Pipe {
|
||||
pub:
|
||||
r int = -1
|
||||
w int = -1
|
||||
}
|
||||
|
||||
fn pipe() !Pipe {
|
||||
mut fds := [2]int{}
|
||||
if C.pipe(&fds[0]) == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
return Pipe{fds[0], fds[1]}
|
||||
}
|
||||
160
proc.c.v
Normal file
160
proc.c.v
Normal file
@@ -0,0 +1,160 @@
|
||||
module runcmd
|
||||
|
||||
import os
|
||||
|
||||
pub type ProcCallbackFn = fn (mut p Process) !
|
||||
|
||||
pub struct Process {
|
||||
pub:
|
||||
// Absolute path to the executable.
|
||||
path string
|
||||
|
||||
// Arguments that will be passed to the executable.
|
||||
argv []string
|
||||
|
||||
// Environment variables that will be applied to the child process.
|
||||
env map[string]string
|
||||
|
||||
// Working directory for the child process.
|
||||
dir string
|
||||
|
||||
// The *_cb fields stores callback functions that will be executed respectively:
|
||||
// - before calling fork();
|
||||
// - after calling fork() in the parent process, until the function exits;
|
||||
// - after calling fork() in the child process, until the working directory is
|
||||
// changed and execve() is called.
|
||||
pre_fork_cb ProcCallbackFn = fn (mut p Process) ! {}
|
||||
post_fork_parent_cb ProcCallbackFn = fn (mut p Process) ! {}
|
||||
post_fork_child_cb ProcCallbackFn = fn (mut p Process) ! {}
|
||||
mut:
|
||||
pid int = -1
|
||||
}
|
||||
|
||||
struct ProcessState {
|
||||
pid int = -1
|
||||
status WaitStatus = -1
|
||||
}
|
||||
|
||||
// pid returns the child process identifier. If process is not
|
||||
// launched yet -1 wil be returned.
|
||||
pub fn (s ProcessState) pid() int {
|
||||
return s.pid
|
||||
}
|
||||
|
||||
// exited returns true if process is exited.
|
||||
pub fn (s ProcessState) exited() bool {
|
||||
return s.status.exited()
|
||||
}
|
||||
|
||||
// exit_code returns the process exit status code or -1 if process is not exited.
|
||||
pub fn (s ProcessState) exit_code() int {
|
||||
return s.status.exit_code()
|
||||
}
|
||||
|
||||
// success returns true if process if successfuly exited (0 exit status on POSIX).
|
||||
pub fn (s ProcessState) success() bool {
|
||||
return s.status.exit_code() == 0
|
||||
}
|
||||
|
||||
// sys returns the system-specific process state object. For now its always `WaitStatus`.
|
||||
pub fn (s ProcessState) sys() voidptr {
|
||||
// FIXME: Possible V bug: return without explicit voidptr cast corrupts the value...
|
||||
// Reproduces with examples/error_handling.v in SIGKILL check.
|
||||
// return &s.status
|
||||
return unsafe { voidptr(s.status) }
|
||||
}
|
||||
|
||||
// str returns the text representation of process state. For non-started process
|
||||
// it returns 'unknown' state.
|
||||
pub fn (s ProcessState) str() string {
|
||||
mut str := ''
|
||||
match true {
|
||||
s.exited() {
|
||||
str = 'exit status ${s.exit_code()}'
|
||||
}
|
||||
s.status.signaled() {
|
||||
sig := s.status.term_signal()
|
||||
sig_str := os.sigint_to_signal_name(sig)
|
||||
str = 'signal: ${sig} (${sig_str})'
|
||||
}
|
||||
s.status.stopped() {
|
||||
str = 'stop signal: ${s.status.stop_signal()}'
|
||||
}
|
||||
s.status.continued() {
|
||||
str = 'continued'
|
||||
}
|
||||
else {
|
||||
str = 'unknown'
|
||||
}
|
||||
}
|
||||
if s.status.coredump() {
|
||||
str += ' (core dumped)'
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// start starts new child process by performing
|
||||
// [fork(3p)](https://www.man7.org/linux/man-pages/man3/fork.3p.html) and
|
||||
// [execve(3p)](https://man7.org/linux/man-pages/man3/exec.3p.html)
|
||||
// calls. Return value is the child process identifier.
|
||||
pub fn (mut p Process) start() !int {
|
||||
printdbg('${@METHOD}: current pid before fork() = ${v_getpid()}')
|
||||
printdbg('${@METHOD}: executing pre-fork callback')
|
||||
p.pre_fork_cb(mut p)!
|
||||
pid := os.fork()
|
||||
p.pid = pid
|
||||
printdbg('${@METHOD}: pid after fork() = ${pid}')
|
||||
|
||||
if pid != 0 {
|
||||
//
|
||||
// This is the parent process after the fork
|
||||
//
|
||||
printdbg('${@METHOD}: executing post-fork parent callback')
|
||||
p.post_fork_parent_cb(mut p)!
|
||||
return pid
|
||||
}
|
||||
//
|
||||
// This is the child process
|
||||
//
|
||||
printdbg('${@METHOD}: executing post-fork child callback')
|
||||
p.post_fork_child_cb(mut p)!
|
||||
if p.dir != '' {
|
||||
os.chdir(p.dir)!
|
||||
}
|
||||
mut env := []string{}
|
||||
for k, v in p.env {
|
||||
env << k + '=' + v
|
||||
}
|
||||
os.execve(p.path, p.argv, env)!
|
||||
return pid
|
||||
}
|
||||
|
||||
// pid returns the child process identifier. -1 is returned if process is not started.
|
||||
pub fn (p &Process) pid() int {
|
||||
return p.pid
|
||||
}
|
||||
|
||||
// wait waits for process to change state and returns the `ProcessState`.
|
||||
pub fn (p &Process) wait() !ProcessState {
|
||||
printdbg('${@METHOD}: wait for pid ${p.pid}')
|
||||
mut wstatus := 0
|
||||
if C.waitpid(p.pid, &wstatus, 0) == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
return ProcessState{
|
||||
pid: p.pid
|
||||
status: wstatus
|
||||
}
|
||||
}
|
||||
|
||||
// signal sends the `sig` signal to the child process.
|
||||
pub fn (p &Process) signal(sig os.Signal) ! {
|
||||
if C.kill(p.pid, int(sig)) == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
}
|
||||
|
||||
// kill send SIGKILL to the child process.
|
||||
pub fn (p &Process) kill() ! {
|
||||
p.signal(.kill)!
|
||||
}
|
||||
64
runcmd.v
Normal file
64
runcmd.v
Normal file
@@ -0,0 +1,64 @@
|
||||
module runcmd
|
||||
|
||||
import os
|
||||
|
||||
// new creates new Command instance with given command name and arguments.
|
||||
pub fn new(name string, arg ...string) &Command {
|
||||
return &Command{
|
||||
path: name
|
||||
args: arg
|
||||
}
|
||||
}
|
||||
|
||||
// is_present returns true if cmd is present on system. cmd may be a command
|
||||
// name or filepath (relative or absolute).
|
||||
// The result relies on `look_path()` output, see its docs for command search
|
||||
// details.
|
||||
pub fn is_present(cmd string) bool {
|
||||
_ := look_path(cmd) or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// look_path returns the absolute path to executable file. cmd may be a command
|
||||
// name or filepath (relative or absolute). If the name contains a slash, then the
|
||||
// PATH search is not performed, instead the path will be resolved and the file
|
||||
// existence and its permissions will be checked (execution must be allowed).
|
||||
// Note: To use executables located in the current working directory use './file'
|
||||
// instead of just 'file'. Searching for executable files in the current directory
|
||||
// is disabled for security reasons. See https://go.dev/blog/path-security.
|
||||
pub fn look_path(cmd string) !string {
|
||||
if cmd.is_blank() {
|
||||
return os.ExecutableNotFoundError{}
|
||||
}
|
||||
|
||||
// Do not search executable in PATH if its name contains a slashes (as POSIX-shells does),
|
||||
// See PATH in: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03
|
||||
|
||||
if cmd.contains('/') {
|
||||
actual_path := os.abs_path(os.expand_tilde_to_home(os.norm_path(cmd)))
|
||||
if is_executable_file(actual_path) {
|
||||
return actual_path
|
||||
} else {
|
||||
return os.ExecutableNotFoundError{}
|
||||
}
|
||||
}
|
||||
|
||||
paths := os.getenv('PATH').split(os.path_delimiter)
|
||||
|
||||
for path in paths {
|
||||
if path in ['', '.'] {
|
||||
// Prohibit current directory.
|
||||
continue
|
||||
}
|
||||
actual_path := os.abs_path(os.join_path_single(path, cmd))
|
||||
if is_executable_file(actual_path) {
|
||||
return actual_path
|
||||
}
|
||||
}
|
||||
|
||||
return os.ExecutableNotFoundError{}
|
||||
}
|
||||
|
||||
fn is_executable_file(file string) bool {
|
||||
return os.is_file(file) && os.is_executable(file)
|
||||
}
|
||||
24
runcmd_test.v
Normal file
24
runcmd_test.v
Normal file
@@ -0,0 +1,24 @@
|
||||
import runcmd
|
||||
import os
|
||||
import io.util
|
||||
|
||||
fn make_temp_file() !string {
|
||||
_, path := util.temp_file()!
|
||||
os.chmod(path, 0o700)!
|
||||
dump(path)
|
||||
return path
|
||||
}
|
||||
|
||||
fn test_lookup() {
|
||||
path := make_temp_file()!
|
||||
defer { os.rm(path) or {} }
|
||||
assert os.is_abs_path(runcmd.look_path(path)!)
|
||||
assert runcmd.look_path('/nonexistent') or { '' } == ''
|
||||
assert runcmd.look_path('env')! == '/usr/bin/env'
|
||||
}
|
||||
|
||||
fn test_is_present() {
|
||||
path := make_temp_file()!
|
||||
defer { os.rm(path) or {} }
|
||||
assert runcmd.is_present(path)
|
||||
}
|
||||
7
v.mod
Normal file
7
v.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
Module {
|
||||
name: 'runcmd'
|
||||
description: 'Run external commands'
|
||||
version: '0.1.0'
|
||||
license: 'Unlicense'
|
||||
dependencies: []
|
||||
}
|
||||
69
wait.c.v
Normal file
69
wait.c.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module runcmd
|
||||
|
||||
@[trusted]
|
||||
fn C.WIFSTOPPED(int) bool
|
||||
|
||||
@[trusted]
|
||||
fn C.WCOREDUMP(int) bool
|
||||
|
||||
@[trusted]
|
||||
fn C.WIFCONTINUED(int) bool
|
||||
|
||||
@[trusted]
|
||||
fn C.WSTOPSIG(int) int
|
||||
|
||||
// WaitStatus stores the result value of [wait(2)](https://www.man7.org/linux/man-pages/man2/wait.2.html) syscall.
|
||||
pub type WaitStatus = u32
|
||||
|
||||
// exited returns true if process is exited.
|
||||
pub fn (w WaitStatus) exited() bool {
|
||||
return C.WIFEXITED(w)
|
||||
}
|
||||
|
||||
// exit_code returns the process exit status code or -1 if process is not exited.
|
||||
pub fn (w WaitStatus) exit_code() int {
|
||||
if w.exited() {
|
||||
return C.WEXITSTATUS(w)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// signaled returns true if the child process was terminated by a signal.
|
||||
pub fn (w WaitStatus) signaled() bool {
|
||||
return C.WIFSIGNALED(w)
|
||||
}
|
||||
|
||||
// term_signal returns the number of the signal that caused the child process to terminate.
|
||||
pub fn (w WaitStatus) term_signal() int {
|
||||
if w.signaled() {
|
||||
return C.WTERMSIG(w)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// stopped returns true if the child process was stopped by delivery of a signal.
|
||||
pub fn (w WaitStatus) stopped() bool {
|
||||
return C.WIFSTOPPED(w)
|
||||
}
|
||||
|
||||
// stop_signal returns the number of the signal which caused the child to stop.
|
||||
pub fn (w WaitStatus) stop_signal() int {
|
||||
if w.stopped() {
|
||||
return C.WSTOPSIG(w)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// continued returns true if the child process was resumed by delivery of SIGCONT.
|
||||
pub fn (w WaitStatus) continued() bool {
|
||||
return C.WIFCONTINUED(w)
|
||||
}
|
||||
|
||||
// coredump returns true if the child produced a core dump.
|
||||
// See [core(5)](https://man7.org/linux/man-pages/man5/core.5.html).
|
||||
pub fn (w WaitStatus) coredump() bool {
|
||||
if w.signaled() {
|
||||
return C.WCOREDUMP(w)
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user