feat: contexts support

This commit is contained in:
ge
2026-01-03 19:17:50 +03:00
parent b790cfef0a
commit 055dab663e
5 changed files with 176 additions and 9 deletions

60
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:
@@ -52,15 +55,29 @@ 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
// 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.
@@ -155,7 +172,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')
} }
@@ -240,7 +257,10 @@ pub fn (mut c Command) start() !int {
post_fork_child_cb: post_fork_child_cb 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. // Start I/O copy callbacks.
if c.stdio_copy_fns.len > 0 { 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 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')
} }
if c.process != none {
c.state = c.process.wait()! c.state = c.process.wait()!
}
unsafe { c.release()! } unsafe { c.release()! }
} }

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,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}')
}

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