This commit is contained in:
ge
2025-12-28 20:42:30 +03:00
commit 7a67749f1f
20 changed files with 1130 additions and 0 deletions

8
.editorconfig Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
examples/

76
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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}')
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}