diff --git a/cmd.v b/cmd.v index c1625a4..e894f4a 100644 --- a/cmd.v +++ b/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()! } } diff --git a/examples/command_with_cancel.v b/examples/command_with_cancel.v new file mode 100644 index 0000000..dcc9453 --- /dev/null +++ b/examples/command_with_cancel.v @@ -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}') +} diff --git a/examples/command_with_cancel_custom.v b/examples/command_with_cancel_custom.v new file mode 100644 index 0000000..2b649bb --- /dev/null +++ b/examples/command_with_cancel_custom.v @@ -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}') +} diff --git a/examples/command_with_timeout.v b/examples/command_with_timeout.v new file mode 100644 index 0000000..aee8071 --- /dev/null +++ b/examples/command_with_timeout.v @@ -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}') +} diff --git a/runcmd.v b/runcmd.v index fb1a739..dc8a4ca 100644 --- a/runcmd.v +++ b/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