mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-23 14:54:13 +03:00
feat: contexts support
This commit is contained in:
62
cmd.v
62
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:
|
||||
@@ -52,15 +55,29 @@ 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
|
||||
|
||||
// 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.
|
||||
@@ -155,7 +172,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')
|
||||
}
|
||||
|
||||
@@ -240,7 +257,10 @@ pub fn (mut c Command) start() !int {
|
||||
post_fork_child_cb: post_fork_child_cb
|
||||
}
|
||||
|
||||
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 +272,47 @@ pub fn (mut c Command) start() !int {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle context here...
|
||||
if c.ctx != none && c.cancel != 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()! }
|
||||
}
|
||||
|
||||
|
||||
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}')
|
||||
}
|
||||
27
examples/command_with_timeout.v
Normal file
27
examples/command_with_timeout.v
Normal file
@@ -0,0 +1,27 @@
|
||||
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.
|
||||
started := time.now()
|
||||
println('Start command at ${started}')
|
||||
cmd.start()!
|
||||
|
||||
// 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.
|
||||
println('Command finished after ${time.now() - started}')
|
||||
|
||||
// Since command has been terminated, the state would be: `signal: 15 (SIGTERM)`
|
||||
println('Child state: ${cmd.state}')
|
||||
}
|
||||
15
runcmd.v
15
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
|
||||
|
||||
Reference in New Issue
Block a user