feat,breaking: add pre_exec_hooks support, rework Process hooks

This commit is contained in:
ge
2026-01-08 09:56:47 +03:00
parent a3b5d4def5
commit e4c4c9ef87
2 changed files with 67 additions and 38 deletions

52
cmd.v
View File

@@ -27,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
@@ -69,6 +69,11 @@ pub mut:
// if context is timed out or canceled. // if context is timed out or canceled.
cancel ?CommandCancelFn 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 holds the underlying Process once started.
process ?&Process process ?&Process
@@ -94,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
@@ -110,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())
@@ -124,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
@@ -152,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!
// ``` // ```
@@ -183,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
} }
@@ -195,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
} }
@@ -210,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] () ! {
@@ -247,14 +262,15 @@ 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
} }
mut pid := -1 mut pid := -1
@@ -335,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

@@ -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
} }
@@ -101,37 +100,51 @@ pub fn (mut p Process) start() !int {
if p.pid != -1 { if p.pid != -1 {
return error('runcmd: process already started') return error('runcmd: process already started')
} }
printdbg('${@METHOD}: current pid before fork() = ${v_getpid()}')
printdbg('${@METHOD}: executing pre-fork callback') printdbg('${@METHOD}: executing pre-fork hooks (parent)')
p.pre_fork_cb(mut p)! for hook in p.pre_fork {
hook(mut p)!
}
pid := os.fork() pid := os.fork()
p.pid = pid p.pid = pid
if pid == -1 { if pid == -1 {
return os.last_error() return os.last_error()
} }
printdbg('${@METHOD}: pid after fork() = ${pid}')
printdbg('${@METHOD}: child pid = ${pid}')
if pid != 0 { if pid != 0 {
// //
// This is the parent process // This is the parent process
// //
printdbg('${@METHOD}: executing post-fork parent callback') printdbg('${@METHOD}: executing post-fork hooks (parent)')
p.post_fork_parent_cb(mut p)! for hook in p.post_fork {
hook(mut p)!
}
} else { } else {
// //
// This is the child process // This is the child process
// //
printdbg('${@METHOD}: executing post-fork child callback') printdbg('${@METHOD}: executing pre-exec hooks (child)')
p.post_fork_child_cb(mut p)! for hook in p.pre_exec {
if p.dir != '' { hook(mut p)!
os.chdir(p.dir)!
} }
mut env := []string{} mut env := []string{}
for k, v in p.env { for k, v in p.env {
env << k + '=' + v 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)! os.execve(p.path, p.argv, env)!
} }
@@ -163,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)!
} }