feat: Add Process Management Support#1171
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1171 +/- ##
==========================================
+ Coverage 68.52% 68.73% +0.21%
==========================================
Files 223 231 +8
Lines 14466 14957 +491
==========================================
+ Hits 9913 10281 +368
- Misses 4178 4272 +94
- Partials 375 404 +29 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: 0ac3643 | Previous: 5249efe | Ratio |
|---|---|---|---|
Benchmark_Fatal |
4e-7 ns/op 0 B/op 0 allocs/op |
2e-7 ns/op 0 B/op 0 allocs/op |
2 |
Benchmark_Fatal - ns/op |
4e-7 ns/op |
2e-7 ns/op |
2 |
This comment was automatically generated by workflow using github-action-benchmark.
| func setSysProcAttr(cmd *exec.Cmd) { | ||
| cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | ||
| } |
There was a problem hiding this comment.
Currently, we have only one method, but there are multiple instances where we will need OS-specific features like TTY.
| @@ -0,0 +1,104 @@ | |||
| //go:build windows | |||
There was a problem hiding this comment.
We need to write different test cases for Windows, so instead of relying on runtime.GOOS, we are going to rely on build tags.
|
|
||
| // Actively probe the OS by sending a null signal. | ||
| // A nil error means the OS found the process (running or zombie). | ||
| err := r.cmd.Process.Signal(unix.Signal(0)) |
There was a problem hiding this comment.
Windows does not fully support POSIX signals. As suggested, instead of using syscall, we use golang.org/x/sys/unix on Unix-like systems and rely on build tags to achieve cross-platform functionality.
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a new process management system to the Goravel framework, enabling developers to execute and control external processes programmatically. The implementation provides a fluent API for running commands with various configuration options, monitoring process execution, and handling input/output streams.
- Adds platform-specific process execution with Unix and Windows support
- Implements process lifecycle management including graceful termination
- Provides output capturing and real-time monitoring capabilities
Reviewed Changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/process/*.go | Defines interfaces for Process, Running, and Result abstractions |
| process/process.go | Core process implementation with fluent configuration API |
| process/running*.go | Platform-specific process monitoring and control logic |
| process/result*.go | Process execution result handling and output inspection |
| process/output_writer.go | Line-based output streaming for real-time callbacks |
| process/*_test.go | Comprehensive test coverage for all platforms and scenarios |
| mocks/process/*.go | Generated mock implementations for testing |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| type Process interface { | ||
| Command(name string, arg ...string) Process | ||
| Env(vars map[string]string) Process | ||
| Input(in io.Reader) Process |
There was a problem hiding this comment.
in io.Reader vs string, which is better?
There was a problem hiding this comment.
Personally, I think using io.Reader as input is more flexible than using a plain string. It allows the process to handle various data sources efficiently—such as files, network streams, or buffers—without needing to load everything into memory. This makes the code more versatile and scalable.
There was a problem hiding this comment.
io.Reader is better because it’s the standard way in Go to handle input from different sources like files, terminal, or network. If you only use string, the function becomes less flexible. When you already have a string, you can easily turn it into an io.Reader with strings.NewReader().
There was a problem hiding this comment.
Agree, can we add some quick functions to build different io.Reader?
There was a problem hiding this comment.
@hwbrzzl Each stdlib type has its own way to get an io.Reader, so we don’t need to handle it:
string→strings.NewReader("hello")[]byte→bytes.NewReader(data)*os.File→ already implementsio.Reader
There was a problem hiding this comment.
Okay, the quick functions is optional for now.
process/process_unix_test.go
Outdated
| { | ||
| name: "echo to stdout", | ||
| setup: func(p *Process) { | ||
| p.Command("sh", "-c", "printf 'hello'").Quietly() |
There was a problem hiding this comment.
Can this be p.Command("sh -c printf 'hello'").Quietly()?
There was a problem hiding this comment.
No, exec.CommandContext takes the command name and its arguments separately. I also don’t recommend writing a wrapper just to handle this.
There was a problem hiding this comment.
Actually, as a user, why he needs to care about the exec.CommandContext command, he just wants to run a command like: facades.Process.Run("sh -c printf 'hello'"). And is the Command function necessary?
There was a problem hiding this comment.
@hwbrzzl Process is a generic implementation, not tied to the Goravel framework itself. Run will just be wrapper functions around process.New(), so the user won’t need to call Command directly.
Example:
func Run(name string, args ...string) process.Result {
return New().Command(name, args...).Run()
}Usage:
facades.Process().Run("sh", "-c", "printf 'hello'")We should still respect the exec.CommandContext syntax because Go does not split arguments like a shell. The command name and each argument must be passed separately. This keeps it consistent with the standard library and avoids unexpected bugs when handling arguments.
There was a problem hiding this comment.
Sorry, I mean facades.Process().Run("sh -c printf 'hello'"), instead of facades.Process().Command("sh", "-c", "printf 'hello'").Run().
There was a problem hiding this comment.
Yes, splitting the command is the standard action for the Go library. However, I think it would be better to support these two ways: Run("sh -c printf 'hello'") and Run("sh", "-c", "printf 'hello'"). I was sick of splitting the command myself when I implemented other features previously.
There was a problem hiding this comment.
Take the example command "sh -c printf 'hello'". How can you split it into "sh", "-c", "printf 'hello'"? If you simply split by spaces, you'll get "sh", "-c", "printf", and "'hello'", which is not correct. 🤔
There was a problem hiding this comment.
Good point, is it good to judge sh specifically? Anyway, it's optional for this PR, we can optimize it in the future.
There was a problem hiding this comment.
I believe that if we provide a feature, it should work in all cases; it shouldn't work only conditionally unless there's a specific limitation.(We can provide a different package to convert a single string to {name, args...})
There was a problem hiding this comment.
Yes, the key is to transform the command, there should be a rule to do that.
There was a problem hiding this comment.
Pull Request Overview
This PR implements a comprehensive process management system for the Goravel framework, enabling execution and control of external processes with support for timeouts, environment variables, stdin/stdout/stderr handling, and platform-specific signal management.
- Adds core process execution capabilities with synchronous (
Run) and asynchronous (Start) methods - Implements platform-specific process control for Unix and Windows systems
- Provides output streaming and buffering with configurable callbacks
Reviewed Changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/process/*.go | Define interfaces for Process, Running, and Result abstractions |
| process/process.go | Core process implementation with configuration methods and execution logic |
| process/running*.go | Platform-specific running process management with signal handling |
| process/result.go | Process execution result with exit codes and output access |
| process/output_writer.go | Line-based output streaming for real-time process monitoring |
| process/*_test.go | Comprehensive test coverage for all platforms and scenarios |
| mocks/process/*.go | Generated mock implementations for testing |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| return r | ||
| } | ||
|
|
||
| func (r *Process) run(name string, args ...string) (contractsprocess.Result, error) { |
There was a problem hiding this comment.
@kkumar-gcc I found there is an Error function in contractsprocess.Result, and an error is returned here as well. It's a bit complicated to judge these two errors. Can the Result.Error() be returned here directly instead of nil?
There was a problem hiding this comment.
Yes, both serve different purposes. The first error indicates that something went wrong before the process even started for example, an invalid configuration or a setup issue.
On the other hand, the error inside Result means the process did start, but the command itself failed during execution.
| } | ||
| } | ||
|
|
||
| func (r *Running) Wait() contractsprocess.Result { |
There was a problem hiding this comment.
How about returning an error here instead of result.Error() in other places?
| func (r *Running) Wait() contractsprocess.Result { | |
| func (r *Running) Wait() (contractsprocess.Result, error) { |
There was a problem hiding this comment.
Well, the error was added since you requested access to the internal error in Result. We can go with this approach too. I’ll create a PR to fix this.
📑 Description
Closes goravel/goravel#567
This PR implements a single process invocation. In the next couple of PRs, I will implement more advanced features like
PipeandPool. Currently, we can directly useprocess.New(). In upcoming PRs, I will also add a process facade.Run a command and get output
Start and control a running process
Common options (chainable)
Notes
Quietly()is set.Startreturns a handle you canWait()on andStop()with a timeout.✅ Checks