13 Commits

Author SHA1 Message Date
ge
a43070aa95 mod: bump version 2026-01-08 09:58:26 +03:00
ge
fdc57e846a examples: add command_with_chroot.c.v 2026-01-08 09:57:02 +03:00
ge
e4c4c9ef87 feat,breaking: add pre_exec_hooks support, rework Process hooks 2026-01-08 09:56:47 +03:00
ge
a3b5d4def5 readme: upd 2026-01-08 09:55:32 +03:00
ge
4ccc80f4a5 doc: fix docs for look_path/1 2026-01-08 09:55:04 +03:00
ge
21ebb3dbfa fix: improve error handling in Process 2026-01-08 05:39:19 +03:00
ge
04faf33542 examples: update command_with_timeout.v, remove io.string_reader from write_to_child_string.v 2026-01-08 05:12:41 +03:00
ge
acd7a0cfc6 mod: bump version 2026-01-04 16:19:14 +03:00
ge
4df4fcec47 fix: allow start ctx_watch without Command.cancel fn 2026-01-04 16:16:46 +03:00
ge
8a4262dc1a mod: bump version, add repo_url 2026-01-03 19:18:32 +03:00
ge
3cd5e40ac8 readme: update 2026-01-03 19:18:15 +03:00
ge
055dab663e feat: contexts support 2026-01-03 19:17:50 +03:00
ge
b790cfef0a ci: Add CI 2025-12-28 20:53:35 +03:00
12 changed files with 424 additions and 90 deletions

48
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Docs
on:
push:
branches: [ "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup V
run: |
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
unzip -q /tmp/v.zip -d /tmp
echo /tmp/v >> "$GITHUB_PATH"
- name: Build docs
run: |
v doc -f html -m .
pushd _docs
ln -vs ${{ github.event.repository.name }}.html index.html
ls -alFh
popd
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: _docs/
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
permissions:
contents: read
pages: write
id-token: write

27
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Lint and test
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup V
run: |
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
unzip -q /tmp/v.zip -d /tmp
echo /tmp/v >> "$GITHUB_PATH"
- name: Run tests
run: |
v fmt -verify .
v vet -v -W -I -F -r .
v missdoc -r --verify .
v -stats test .

View File

@@ -1,25 +1,23 @@
# Run External Commands # Run External Commands
`runcmd` module implements high-level interface for running external commands. `runcmd` module implements a high-level interface for running external commands.
## Why not vlib `os`? ## Why not vlib `os`?
The standard V `os` module already contains tools for a similar tasks — `os.Process`, The standard V `os` module already contains many tools for running external commands,
`os.Command`, but I don't like any of them. There is also many functions for executing but I don't like any of them. So let's overview.
external commands:
* `os.execvp()`, `os.execve()` — cross-platform versions of the C functions of the same name. * `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.execute()`, `os.execute_opt()`, `os.execute_or_exit()`, `os.execute_or_panic()`starts and waits for a command to completed. Under the hood, they perform a dirty hack by calling shell with stream redirection `'exec 2>&1;${cmd}'`.
* `util.execute_with_timeout()` (from `os.util`) — just an `os.execute()` wrapper.
* `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.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.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. * `os.Command` calls `C.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 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 POSIX-compliant operating systems. I'm not interested in working on MS Windows, but
@@ -38,20 +36,6 @@ cmd.run()! // Start and wait for process.
println(cmd.state) // exit status 0 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()`: If you don't want to wait for the child process to complete, call `start()` instead of `run()`:
```v ```v
@@ -64,12 +48,13 @@ println(pid)
If you need to capture standard output and standard error, use the `output()` and If you need to capture standard output and standard error, use the `output()` and
`combined_output()`. See examples in its description. `combined_output()`. See examples in its description.
See also [examples](examples) dir for more examples. To learn more about the `runcmd`'s capabilities and usage, see the [examples](examples)
directory. **Examples are very important**.
## Roadmap ## Roadmap
- [x] Basic implementation. - [x] Basic implementation.
- [ ] Contexts support for creating cancelable commands, commands with timeouts, etc. - [x] Contexts support for creating cancelable commands, commands with timeouts, etc.
- [ ] Process groups support, pgkill(). - [ ] Process groups support, pgkill().
- [ ] Better error handling and more tests... - [ ] Better error handling and more tests...

114
cmd.v
View File

@@ -1,11 +1,14 @@
module runcmd module runcmd
import context
import io import io
import os import os
import strings import strings
type IOCopyFn = fn () ! type IOCopyFn = fn () !
pub type CommandCancelFn = fn () !
@[heap] @[heap]
pub struct Command { pub struct Command {
pub mut: pub mut:
@@ -24,9 +27,9 @@ pub mut:
// `maps` module: `maps.merge(os.environ(), {'MYENV': 'value'})` // `maps` module: `maps.merge(os.environ(), {'MYENV': 'value'})`
env map[string]string env map[string]string
// dir specifies the current working directory for the child // dir specifies the working directory for the child process.
// process. If not specified, the current working directory will // If not specified, the current working directory of parent
// be used. // will be used.
dir string dir string
// If true create pipes for standart in/out/err streams and // If true create pipes for standart in/out/err streams and
@@ -52,15 +55,34 @@ pub mut:
stdout ?io.Writer stdout ?io.Writer
stderr ?io.Writer stderr ?io.Writer
// ctx holds a command context. It may be used to make a command
// cancelable or set timeout/deadline for it.
ctx ?context.Context
// cancel function is used to terminate the child process. Do
// not confuse with the context's cancel function.
//
// The default command cancel function created in with_context()
// terminates the command by sending SIGTERM signal to child. You
// can override it by setting your own command cancel function.
// If cancel is none the child process won't be terminated even
// if context is timed out or canceled.
cancel ?CommandCancelFn
// pre_exec_hooks will be called before starting the command in
// the child process. Hooks can be used to modify a child's envi-
// ronment, for example to perform a chroot.
pre_exec_hooks []ProcessHookFn
// process holds the underlying Process once started.
process ?&Process
// state holds an information about underlying process. // state holds an information about underlying process.
// This is set only if process if finished. Call `run()` // This is set only if process if finished. Call `run()`
// or `wait()` to get actual state value. // or `wait()` to get actual state value.
// This value MUST NOT be changed by API user. // This value MUST NOT be changed by API user.
state ProcessState state ProcessState
mut: mut:
// process holds the underlying Process.
process &Process = unsafe { nil }
// stdio holds a file descriptors for I/O processing. // stdio holds a file descriptors for I/O processing.
// There is: // There is:
// * 0 — child process stdin, we must write into it. // * 0 — child process stdin, we must write into it.
@@ -77,7 +99,7 @@ mut:
stdio_copy_fns []IOCopyFn stdio_copy_fns []IOCopyFn
} }
// run starts a specified command and waits for it. After call see the .state // 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. // value to get finished process identifier, exit status and other attributes.
// `run()` is shorthand for: // `run()` is shorthand for:
// ```v // ```v
@@ -93,12 +115,15 @@ pub fn (mut c Command) run() ! {
// status is non-zero `ExitError` error is returned. // status is non-zero `ExitError` error is returned.
// Example: // Example:
// ```v // ```v
// import runcmd
//
// mut okcmd := runcmd.new('sh', '-c', 'echo Hello, World!') // mut okcmd := runcmd.new('sh', '-c', 'echo Hello, World!')
// output := okcmd.output()! // ok_out := okcmd.output()!
// println(ok_out)
// // Hello, World! // // Hello, World!
// //
// mut badcmd := runcmd.new('sh', '-c', 'echo -n Error! >&2; false') // mut badcmd := runcmd.new('sh', '-c', 'echo -n Error! >&2; false')
// output := badcmd.output() or { // bad_out := badcmd.output() or {
// if err is runcmd.ExitError { // if err is runcmd.ExitError {
// eprintln(err) // eprintln(err)
// exit(err.code()) // exit(err.code())
@@ -107,14 +132,15 @@ pub fn (mut c Command) run() ! {
// panic(err) // panic(err)
// } // }
// } // }
// println(bad_out)
// // &runcmd.ExitError{ // // &runcmd.ExitError{
// // state: exit status 1 // // state: exit status 1
// // stderr: 'Error!' // // stderr: 'Error!'
// // } // // }
// ``` // ```
pub fn (mut c Command) output() !string { pub fn (mut c Command) output() !string {
mut out := strings.new_builder(4096) mut out := strings.new_builder(2048)
mut err := strings.new_builder(4096) mut err := strings.new_builder(2048)
c.redirect_stdio = true c.redirect_stdio = true
c.stdout = out c.stdout = out
c.stderr = err c.stderr = err
@@ -135,8 +161,10 @@ pub fn (mut c Command) output() !string {
// reading from the corresponding file descriptors is done concurrently. // reading from the corresponding file descriptors is done concurrently.
// Example: // Example:
// ```v // ```v
// import runcmd
// mut cmd := runcmd.new('sh', '-c', 'echo Hello, STDOUT!; echo Hello, STDERR! >&2') // mut cmd := runcmd.new('sh', '-c', 'echo Hello, STDOUT!; echo Hello, STDERR! >&2')
// output := cmd.combined_output()! // output := cmd.combined_output()!
// println(output)
// // Hello, STDOUT! // // Hello, STDOUT!
// // Hello, STDERR! // // Hello, STDERR!
// ``` // ```
@@ -155,7 +183,7 @@ pub fn (mut c Command) combined_output() !string {
// to complete and release associated resources. // to complete and release associated resources.
// Note: `.state` field is not set after `start()` call. // Note: `.state` field is not set after `start()` call.
pub fn (mut c Command) start() !int { pub fn (mut c Command) start() !int {
if !isnil(c.process) { if c.process != none {
return error('runcmd: process already started') return error('runcmd: process already started')
} }
@@ -166,7 +194,7 @@ pub fn (mut c Command) start() !int {
pipes[2] = pipe()! // stderr pipes[2] = pipe()! // stderr
} }
post_fork_parent_cb := fn [mut c, pipes] (mut p Process) ! { parent_pipes_hook := fn [mut c, pipes] (mut p Process) ! {
if !c.redirect_stdio { if !c.redirect_stdio {
return return
} }
@@ -178,7 +206,8 @@ pub fn (mut c Command) start() !int {
fd_close(pipes[2].w)! fd_close(pipes[2].w)!
} }
post_fork_child_cb := fn [mut c, pipes] (mut p Process) ! { child_pipes_hook := fn [mut c, pipes] (mut p Process) ! {
printdbg('child pipes hook!')
if !c.redirect_stdio { if !c.redirect_stdio {
return return
} }
@@ -193,6 +222,9 @@ pub fn (mut c Command) start() !int {
fd_close(pipes[2].w)! fd_close(pipes[2].w)!
} }
mut pre_exec_hooks := [child_pipes_hook]
pre_exec_hooks << c.pre_exec_hooks
if c.redirect_stdio { if c.redirect_stdio {
if c.stdin != none { if c.stdin != none {
c.stdio_copy_fns << fn [mut c] () ! { c.stdio_copy_fns << fn [mut c] () ! {
@@ -230,17 +262,21 @@ pub fn (mut c Command) start() !int {
// Prepare and start child process. // Prepare and start child process.
path := look_path(c.path)! path := look_path(c.path)!
printdbg('${@METHOD}: executable found: ${path}')
c.path = path c.path = path
c.process = &Process{ c.process = &Process{
path: path path: path
argv: c.args argv: c.args
env: if c.env.len == 0 { os.environ() } else { c.env } env: if c.env.len == 0 { os.environ() } else { c.env }
dir: if c.dir == '' { os.getwd() } else { c.dir } dir: c.dir
post_fork_parent_cb: post_fork_parent_cb post_fork: [parent_pipes_hook]
post_fork_child_cb: post_fork_child_cb pre_exec: pre_exec_hooks
} }
pid := c.process.start()! mut pid := -1
if c.process != none {
pid = c.process.start()!
}
// Start I/O copy callbacks. // Start I/O copy callbacks.
if c.stdio_copy_fns.len > 0 { if c.stdio_copy_fns.len > 0 {
@@ -252,21 +288,47 @@ pub fn (mut c Command) start() !int {
} }
} }
// TODO: Handle context here... if c.ctx != none {
printdbg('${@METHOD}: start watching for context')
go c.ctx_watch()
}
return pid return pid
} }
fn (mut c Command) ctx_watch() {
mut ch := chan int{}
if c.ctx != none {
ch = c.ctx.done()
}
for {
select {
_ := <-ch {
printdbg('${@METHOD}: context is canceled/done')
if c.cancel != none {
printdbg('${@METHOD}: cancel command now!')
c.cancel() or { eprintln('error canceling command: ${err}') }
printdbg('${@METHOD}: command canceled!')
}
return
}
}
}
}
// wait waits to previously started command is finished. After call see the `.state` // 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. // 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 // `wait()` will return an error if the process has not been started or wait has
// already been called. // already been called.
pub fn (mut c Command) wait() ! { pub fn (mut c Command) wait() ! {
if isnil(c.process) { if c.process == none {
return error('runcmd: wait for non-started process') return error('runcmd: wait for non-started process')
} else if c.state != ProcessState{} { } else if c.state != ProcessState{} {
return error('runcmd: wait already called') return error('runcmd: wait already called')
} }
c.state = c.process.wait()! if c.process != none {
c.state = c.process.wait()!
}
unsafe { c.release()! } unsafe { c.release()! }
} }
@@ -289,8 +351,8 @@ pub fn (mut c Command) release() ! {
} }
// stdin returns an open file descriptor associated with the standard // stdin returns an open file descriptor associated with the standard
// input stream of the child process. This descriptor is writable only // input stream of the child process. This descriptor is write-only for
// by the parent process. // the parent process.
pub fn (c Command) stdin() !WriteFd { pub fn (c Command) stdin() !WriteFd {
return if c.stdio[0] != -1 { return if c.stdio[0] != -1 {
WriteFd{c.stdio[0]} WriteFd{c.stdio[0]}

View File

@@ -0,0 +1,35 @@
import context
import runcmd
import time
fn main() {
// Create context with cancel.
mut bg := context.background()
mut ctx, cancel := context.with_cancel(mut bg)
// Create new command with context.
mut cmd := runcmd.with_context(ctx, 'sleep', '120')
// Start a command.
println('Start command!')
cmd.start()!
// Sleep a bit for demonstration.
time.sleep(1 * time.second)
// Cancel command.
//
// In a real application, cancel() might be initiated by the user.
// For example, a command might take too long to execute and need
// to be canceled.
//
// See also command_with_timeout.v example.
println('Cancel command!')
cancel()
// Wait for command.
cmd.wait()!
// Since command has been terminated, the state would be: `signal: 15 (SIGTERM)`
println('Child state: ${cmd.state}')
}

View File

@@ -0,0 +1,46 @@
import context
import runcmd
import time
fn main() {
// Create context with cancel.
mut bg := context.background()
mut ctx, cancel := context.with_cancel(mut bg)
// Create new command as usual.
mut cmd := runcmd.new('sleep', '120')
// Set the context...
cmd.ctx = ctx
// ...and custom command cancel function.
cmd.cancel = fn [mut cmd] () ! {
if cmd.process != none {
println('Killing ${cmd.process.pid()}!')
cmd.process.kill()!
}
}
// Start a command.
println('Start command!')
cmd.start()!
// Sleep a bit for demonstration.
time.sleep(1 * time.second)
// Cancel command.
//
// In a real application, cancel() might be initiated by the user.
// For example, a command might take too long to execute and need
// to be canceled.
//
// See also command_with_timeout.v example.
println('Cancel command!')
cancel()
// Wait for command.
cmd.wait()!
// Since command has been killed, the state would be: `signal: 9 (SIGKILL)`
println('Child state: ${cmd.state}')
}

View File

@@ -0,0 +1,68 @@
/*
Note: chroot() call requires privilege escalation in the operating system.
Therefore, to run this example, run: `sudo v run command_with_chroot.c.v`
*/
import os
import runcmd
import term
fn C.chroot(&char) i32
fn main() {
// Create new root filesystem for demonstration.
new_root := '/tmp/new_root'
// Create dirtree and copy `ls` utility with shared objects...
paths := {
0: os.join_path(new_root, 'usr', 'bin')
1: os.join_path(new_root, 'usr', 'lib')
2: os.join_path(new_root, 'lib64')
}
for _, path in paths {
os.mkdir_all(path)!
}
os.cp('/usr/bin/ls', paths[0])!
os.cp('/usr/lib/libcap.so.2', paths[1])!
os.cp('/usr/lib/libc.so.6', paths[1])!
os.cp('/lib64/ld-linux-x86-64.so.2', paths[2])!
// Create a test file in the new root.
os.write_file(os.join_path_single(new_root, 'HELLO_FROM_CHROOT'), 'TEST')!
// Cleanup demo root filesystem at exit.
defer {
os.rmdir_all(new_root) or {}
}
// Prepare the command.
mut cmd := runcmd.new('ls', '-alFh', '/')
// Add pre-exec hook to perform chroot().
cmd.pre_exec_hooks << fn [new_root] (mut p runcmd.Process) ! {
if C.chroot(&char(new_root.str)) == -1 {
return os.last_error()
}
}
// Run command and read its output.
out := cmd.output() or {
if err is runcmd.ExitError {
eprintln(err)
exit(err.code())
} else {
panic(err)
}
}
// Expected output:
//
// total 4.0K
// drwxr-xr-x 4 0 0 100 Jan 8 06:39 ./
// drwxr-xr-x 4 0 0 100 Jan 8 06:39 ../
// -rw-r--r-- 1 0 0 4 Jan 8 06:39 HELLO_FROM_CHROOT
// drwxr-xr-x 2 0 0 60 Jan 8 06:39 lib64/
// drwxr-xr-x 4 0 0 80 Jan 8 06:39 usr/
println('Command output: ${term.yellow(out)}')
println('Child state: ${cmd.state}')
}

View File

@@ -0,0 +1,28 @@
import context
import runcmd
import time
fn main() {
// Create context with cancel.
mut bg := context.background()
mut ctx, _ := context.with_timeout(mut bg, 10 * time.second)
// Create new command with context.
mut cmd := runcmd.with_context(ctx, 'sleep', '120')
// Start a command.
cmd.start()!
started := time.now()
println('Command started at ${started}')
// Wait for command.
cmd.wait()!
// The `sleep 120` command would run for two minutes without a timeout.
// But in this example, it will time out after 10 seconds.
finished := time.now()
println('Command finished at ${finished} after ${finished - started}')
// Since command has been terminated, the state would be: `signal: 15 (SIGTERM)`
println('Child state: ${cmd.state}')
}

View File

@@ -1,4 +1,3 @@
import io.string_reader
import strings import strings
import runcmd import runcmd
@@ -6,12 +5,11 @@ fn main() {
input := 'Hello from parent process!' input := 'Hello from parent process!'
// Prepare reader and writer. // Prepare reader and writer.
//
// * `reader` reads input from the parent process; it will be copied to the // * `reader` reads input from the parent process; it will be copied to the
// standard input of the child process. // standard input of the child process.
// * `writer` accepts data from the child process; it will be copied from the // * `writer` accepts data from the child process; it will be copied from the
// standard output of the child process. // standard output of the child process.
mut reader := string_reader.StringReader.new(reader: runcmd.buffer(input.bytes()), source: input) mut reader := runcmd.buffer(input.bytes())
mut writer := strings.new_builder(4096) mut writer := strings.new_builder(4096)
// Prepare the command. // Prepare the command.

View File

@@ -2,7 +2,7 @@ module runcmd
import os import os
pub type ProcCallbackFn = fn (mut p Process) ! pub type ProcessHookFn = fn (mut p Process) !
pub struct Process { pub struct Process {
pub: pub:
@@ -18,14 +18,13 @@ pub:
// Working directory for the child process. // Working directory for the child process.
dir string dir string
// The *_cb fields stores callback functions that will be executed respectively: // The pre_* and post_* fields store the functions that will be executed, respectively:
// - before calling fork(); // - before fork() call in the parent process;
// - after calling fork() in the parent process, until the function exits; // - after fork() call in the parent process;
// - after calling fork() in the child process, until the working directory is // - after fork() call and before execve() call in the child process.
// changed and execve() is called. pre_fork []ProcessHookFn
pre_fork_cb ProcCallbackFn = fn (mut p Process) ! {} post_fork []ProcessHookFn
post_fork_parent_cb ProcCallbackFn = fn (mut p Process) ! {} pre_exec []ProcessHookFn
post_fork_child_cb ProcCallbackFn = fn (mut p Process) ! {}
mut: mut:
pid int = -1 pid int = -1
} }
@@ -98,34 +97,57 @@ pub fn (s ProcessState) str() string {
// [execve(3p)](https://man7.org/linux/man-pages/man3/exec.3p.html) // [execve(3p)](https://man7.org/linux/man-pages/man3/exec.3p.html)
// calls. Return value is the child process identifier. // calls. Return value is the child process identifier.
pub fn (mut p Process) start() !int { pub fn (mut p Process) start() !int {
printdbg('${@METHOD}: current pid before fork() = ${v_getpid()}') if p.pid != -1 {
printdbg('${@METHOD}: executing pre-fork callback') return error('runcmd: process already started')
p.pre_fork_cb(mut p)! }
printdbg('${@METHOD}: executing pre-fork hooks (parent)')
for hook in p.pre_fork {
hook(mut p)!
}
pid := os.fork() pid := os.fork()
p.pid = pid p.pid = pid
printdbg('${@METHOD}: pid after fork() = ${pid}') if pid == -1 {
return os.last_error()
}
printdbg('${@METHOD}: child pid = ${pid}')
if pid != 0 { if pid != 0 {
// //
// This is the parent process after the fork // This is the parent process
// //
printdbg('${@METHOD}: executing post-fork parent callback')
p.post_fork_parent_cb(mut p)! printdbg('${@METHOD}: executing post-fork hooks (parent)')
return pid for hook in p.post_fork {
hook(mut p)!
}
} else {
//
// This is the child process
//
printdbg('${@METHOD}: executing pre-exec hooks (child)')
for hook in p.pre_exec {
hook(mut p)!
}
mut env := []string{}
for k, v in p.env {
env << k + '=' + v
}
// If the working directory change was performed in a pre-exec hook,
// then calling os.chdir() again should be avoided. So current working
// directory could be checked here.
if p.dir != '' && os.getwd() != p.dir {
os.chdir(p.dir)!
}
os.execve(p.path, p.argv, env)!
} }
//
// 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 return pid
} }
@@ -154,7 +176,7 @@ pub fn (p &Process) signal(sig os.Signal) ! {
} }
} }
// kill send SIGKILL to the child process. // kill sends SIGKILL to the child process.
pub fn (p &Process) kill() ! { pub fn (p &Process) kill() ! {
p.signal(.kill)! p.signal(.kill)!
} }

View File

@@ -1,8 +1,9 @@
module runcmd module runcmd
import context
import os import os
// new creates new Command instance with given command name and arguments. // new creates new command with given command name and arguments.
pub fn new(name string, arg ...string) &Command { pub fn new(name string, arg ...string) &Command {
return &Command{ return &Command{
path: name path: name
@@ -10,6 +11,18 @@ pub fn new(name string, arg ...string) &Command {
} }
} }
// with_context creates new command with context, command name and arguments.
pub fn with_context(ctx context.Context, name string, arg ...string) &Command {
mut cmd := new(name, ...arg)
cmd.ctx = ctx
cmd.cancel = fn [mut cmd] () ! {
if cmd.process != none {
cmd.process.signal(.term)!
}
}
return cmd
}
// is_present returns true if cmd is present on system. cmd may be a command // is_present returns true if cmd is present on system. cmd may be a command
// name or filepath (relative or absolute). // name or filepath (relative or absolute).
// The result relies on `look_path()` output, see its docs for command search // The result relies on `look_path()` output, see its docs for command search
@@ -20,9 +33,10 @@ pub fn is_present(cmd string) bool {
} }
// look_path returns the absolute path to executable file. cmd may be a command // 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 // name or filepath (relative or absolute). The command is searched in PATH. If
// PATH search is not performed, instead the path will be resolved and the file // the name contains a slash, then the PATH search is not performed, instead the
// existence and its permissions will be checked (execution must be allowed). // path will be resolved and the file existence will be checked. In both cases
// the file must have the execute permission bit enabled.
// Note: To use executables located in the current working directory use './file' // Note: To use executables located in the current working directory use './file'
// instead of just 'file'. Searching for executable files in the current directory // instead of just 'file'. Searching for executable files in the current directory
// is disabled for security reasons. See https://go.dev/blog/path-security. // is disabled for security reasons. See https://go.dev/blog/path-security.

3
v.mod
View File

@@ -1,7 +1,8 @@
Module { Module {
name: 'runcmd' name: 'runcmd'
description: 'Run external commands' description: 'Run external commands'
version: '0.1.0' version: '0.3.0'
license: 'Unlicense' license: 'Unlicense'
repo_url: 'https://github.com/gechandesu/runcmd'
dependencies: [] dependencies: []
} }