mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-23 23:04:13 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a43070aa95 | |||
| fdc57e846a | |||
| e4c4c9ef87 | |||
| a3b5d4def5 | |||
| 4ccc80f4a5 | |||
| 21ebb3dbfa | |||
| 04faf33542 | |||
| acd7a0cfc6 | |||
| 4df4fcec47 | |||
| 8a4262dc1a | |||
| 3cd5e40ac8 | |||
| 055dab663e | |||
| b790cfef0a |
48
.github/workflows/docs.yaml
vendored
Normal file
48
.github/workflows/docs.yaml
vendored
Normal 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
27
.github/workflows/test.yaml
vendored
Normal 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 .
|
||||
35
README.md
35
README.md
@@ -1,25 +1,23 @@
|
||||
# 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`?
|
||||
|
||||
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:
|
||||
The standard V `os` module already contains many tools for running external commands,
|
||||
but I don't like any of them. So let's overview.
|
||||
|
||||
* `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.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.
|
||||
* `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.
|
||||
|
||||
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
|
||||
@@ -38,20 +36,6 @@ cmd.run()! // Start and wait for process.
|
||||
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
|
||||
@@ -64,12 +48,13 @@ println(pid)
|
||||
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.
|
||||
To learn more about the `runcmd`'s capabilities and usage, see the [examples](examples)
|
||||
directory. **Examples are very important**.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [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().
|
||||
- [ ] Better error handling and more tests...
|
||||
|
||||
|
||||
114
cmd.v
114
cmd.v
@@ -1,11 +1,14 @@
|
||||
module runcmd
|
||||
|
||||
import context
|
||||
import io
|
||||
import os
|
||||
import strings
|
||||
|
||||
type IOCopyFn = fn () !
|
||||
|
||||
pub type CommandCancelFn = fn () !
|
||||
|
||||
@[heap]
|
||||
pub struct Command {
|
||||
pub mut:
|
||||
@@ -24,9 +27,9 @@ pub mut:
|
||||
// `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 specifies the working directory for the child process.
|
||||
// If not specified, the current working directory of parent
|
||||
// will be used.
|
||||
dir string
|
||||
|
||||
// If true create pipes for standart in/out/err streams and
|
||||
@@ -52,15 +55,34 @@ pub mut:
|
||||
stdout ?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.
|
||||
// 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.
|
||||
@@ -77,7 +99,7 @@ mut:
|
||||
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.
|
||||
// `run()` is shorthand for:
|
||||
// ```v
|
||||
@@ -93,12 +115,15 @@ pub fn (mut c Command) run() ! {
|
||||
// status is non-zero `ExitError` error is returned.
|
||||
// Example:
|
||||
// ```v
|
||||
// import runcmd
|
||||
//
|
||||
// mut okcmd := runcmd.new('sh', '-c', 'echo Hello, World!')
|
||||
// output := okcmd.output()!
|
||||
// ok_out := okcmd.output()!
|
||||
// println(ok_out)
|
||||
// // Hello, World!
|
||||
//
|
||||
// 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 {
|
||||
// eprintln(err)
|
||||
// exit(err.code())
|
||||
@@ -107,14 +132,15 @@ pub fn (mut c Command) run() ! {
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
// println(bad_out)
|
||||
// // &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)
|
||||
mut out := strings.new_builder(2048)
|
||||
mut err := strings.new_builder(2048)
|
||||
c.redirect_stdio = true
|
||||
c.stdout = out
|
||||
c.stderr = err
|
||||
@@ -135,8 +161,10 @@ pub fn (mut c Command) output() !string {
|
||||
// reading from the corresponding file descriptors is done concurrently.
|
||||
// Example:
|
||||
// ```v
|
||||
// import runcmd
|
||||
// mut cmd := runcmd.new('sh', '-c', 'echo Hello, STDOUT!; echo Hello, STDERR! >&2')
|
||||
// output := cmd.combined_output()!
|
||||
// println(output)
|
||||
// // Hello, STDOUT!
|
||||
// // Hello, STDERR!
|
||||
// ```
|
||||
@@ -155,7 +183,7 @@ pub fn (mut c Command) combined_output() !string {
|
||||
// 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) {
|
||||
if c.process != none {
|
||||
return error('runcmd: process already started')
|
||||
}
|
||||
|
||||
@@ -166,7 +194,7 @@ pub fn (mut c Command) start() !int {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -178,7 +206,8 @@ pub fn (mut c Command) start() !int {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -193,6 +222,9 @@ pub fn (mut c Command) start() !int {
|
||||
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.stdin != none {
|
||||
c.stdio_copy_fns << fn [mut c] () ! {
|
||||
@@ -230,17 +262,21 @@ pub fn (mut c Command) start() !int {
|
||||
|
||||
// Prepare and start child process.
|
||||
path := look_path(c.path)!
|
||||
printdbg('${@METHOD}: executable found: ${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
|
||||
path: path
|
||||
argv: c.args
|
||||
env: if c.env.len == 0 { os.environ() } else { c.env }
|
||||
dir: c.dir
|
||||
post_fork: [parent_pipes_hook]
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
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`
|
||||
// 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) {
|
||||
if c.process == none {
|
||||
return error('runcmd: wait for non-started process')
|
||||
} else if c.state != ProcessState{} {
|
||||
return error('runcmd: wait already called')
|
||||
}
|
||||
c.state = c.process.wait()!
|
||||
if c.process != none {
|
||||
c.state = c.process.wait()!
|
||||
}
|
||||
unsafe { c.release()! }
|
||||
}
|
||||
|
||||
@@ -289,8 +351,8 @@ pub fn (mut c Command) release() ! {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// input stream of the child process. This descriptor is write-only for
|
||||
// the parent process.
|
||||
pub fn (c Command) stdin() !WriteFd {
|
||||
return if c.stdio[0] != -1 {
|
||||
WriteFd{c.stdio[0]}
|
||||
|
||||
35
examples/command_with_cancel.v
Normal file
35
examples/command_with_cancel.v
Normal 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}')
|
||||
}
|
||||
46
examples/command_with_cancel_custom.v
Normal file
46
examples/command_with_cancel_custom.v
Normal 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}')
|
||||
}
|
||||
68
examples/command_with_chroot.c.v
Normal file
68
examples/command_with_chroot.c.v
Normal 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}')
|
||||
}
|
||||
28
examples/command_with_timeout.v
Normal file
28
examples/command_with_timeout.v
Normal 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}')
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import io.string_reader
|
||||
import strings
|
||||
import runcmd
|
||||
|
||||
@@ -6,12 +5,11 @@ 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 reader := runcmd.buffer(input.bytes())
|
||||
mut writer := strings.new_builder(4096)
|
||||
|
||||
// Prepare the command.
|
||||
|
||||
84
proc.c.v
84
proc.c.v
@@ -2,7 +2,7 @@ module runcmd
|
||||
|
||||
import os
|
||||
|
||||
pub type ProcCallbackFn = fn (mut p Process) !
|
||||
pub type ProcessHookFn = fn (mut p Process) !
|
||||
|
||||
pub struct Process {
|
||||
pub:
|
||||
@@ -18,14 +18,13 @@ pub:
|
||||
// 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) ! {}
|
||||
// The pre_* and post_* fields store the functions that will be executed, respectively:
|
||||
// - before fork() call in the parent process;
|
||||
// - after fork() call in the parent process;
|
||||
// - after fork() call and before execve() call in the child process.
|
||||
pre_fork []ProcessHookFn
|
||||
post_fork []ProcessHookFn
|
||||
pre_exec []ProcessHookFn
|
||||
mut:
|
||||
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)
|
||||
// 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)!
|
||||
if p.pid != -1 {
|
||||
return error('runcmd: process already started')
|
||||
}
|
||||
|
||||
printdbg('${@METHOD}: executing pre-fork hooks (parent)')
|
||||
for hook in p.pre_fork {
|
||||
hook(mut p)!
|
||||
}
|
||||
|
||||
pid := os.fork()
|
||||
p.pid = pid
|
||||
printdbg('${@METHOD}: pid after fork() = ${pid}')
|
||||
if pid == -1 {
|
||||
return os.last_error()
|
||||
}
|
||||
|
||||
printdbg('${@METHOD}: child pid = ${pid}')
|
||||
|
||||
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)!
|
||||
return pid
|
||||
|
||||
printdbg('${@METHOD}: executing post-fork hooks (parent)')
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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() ! {
|
||||
p.signal(.kill)!
|
||||
}
|
||||
|
||||
22
runcmd.v
22
runcmd.v
@@ -1,8 +1,9 @@
|
||||
module runcmd
|
||||
|
||||
import context
|
||||
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 {
|
||||
return &Command{
|
||||
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
|
||||
// name or filepath (relative or absolute).
|
||||
// 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
|
||||
// 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).
|
||||
// name or filepath (relative or absolute). The command is searched in PATH. If
|
||||
// the name contains a slash, then the PATH search is not performed, instead the
|
||||
// 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'
|
||||
// instead of just 'file'. Searching for executable files in the current directory
|
||||
// is disabled for security reasons. See https://go.dev/blog/path-security.
|
||||
|
||||
Reference in New Issue
Block a user