Skip to content

feat: Add Process Management Support#1171

Merged
krishankumar01 merged 24 commits intomasterfrom
kkumar-gcc/#567
Aug 27, 2025
Merged

feat: Add Process Management Support#1171
krishankumar01 merged 24 commits intomasterfrom
kkumar-gcc/#567

Conversation

@krishankumar01
Copy link
Member

@krishankumar01 krishankumar01 commented Aug 23, 2025

📑 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 Pipe and Pool. Currently, we can directly use process.New(). In upcoming PRs, I will also add a process facade.

Note: Currently, TTY is adding a pipe to os.Stdin, but in the next PR I will add a fully functional TTY using github.com/creack/pty. This will only be available on Unix-like systems.

Run a command and get output

package main

import (
    "context"
    "fmt"

    "github.com/goravel/framework/process"
)

func main() {
    res, err := process.New().
        Quietly().
        Run("sh", "-c", "printf 'hello'")
    if err != nil { panic(err) }

    fmt.Println(res.Successful()) // true
    fmt.Println(res.Output())     // "hello"
}

Start and control a running process

ctx := context.Background()

r, err := process.New().
    WithContext(ctx).
    Start("sh", "-c", "printf 'hello'")
if err != nil { panic(err) }

_ = r.Stop(500 * time.Millisecond) // try graceful stop (cross‑platform)
res := r.Wait()                     // block until exit
fmt.Println(res.ExitCode())

Common options (chainable)

res, err := process.New().
    Env(map[string]string{"FOO": "BAR"}).     // add environment vars
    Path("/tmp").                               // working directory
    Input(strings.NewReader("stdin data")).     // provide stdin
    Timeout(2 * time.Second).                    // kill after timeout
    Quietly().                                   // don't mirror to current stdout/stderr
    OnOutput(func(typ process.OutputType, b []byte) {
        // per-line callback for stdout/stderr
    }).
    WithContext(context.Background()).
    Run("sh", "-c", "echo $FOO && pwd")

Notes

  • Output is mirrored to current stdout/stderr unless Quietly() is set.
  • Start returns a handle you can Wait() on and Stop() with a timeout.

✅ Checks

  • Added test cases for my code

Copilot AI review requested due to automatic review settings August 23, 2025 13:14
@krishankumar01 krishankumar01 requested a review from a team as a code owner August 23, 2025 13:14

This comment was marked as outdated.

@codecov
Copy link

codecov bot commented Aug 23, 2025

Codecov Report

❌ Patch coverage is 75.45788% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.73%. Comparing base (828daf7) to head (b2bcaaf).
⚠️ Report is 13 commits behind head on master.

Files with missing lines Patch % Lines
process/running.go 70.00% 14 Missing and 7 partials ⚠️
process/running_unix.go 41.66% 16 Missing and 5 partials ⚠️
process/process.go 78.40% 16 Missing and 3 partials ⚠️
process/output_writer.go 80.00% 4 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@krishankumar01 krishankumar01 marked this pull request as draft August 23, 2025 13:57
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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.

Comment on lines +11 to +13
func setSysProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@krishankumar01 krishankumar01 marked this pull request as ready for review August 24, 2025 10:55
@krishankumar01 krishankumar01 requested a review from Copilot August 24, 2025 11:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This comment was marked as outdated.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing 👍

type Process interface {
Command(name string, arg ...string) Process
Env(vars map[string]string) Process
Input(in io.Reader) Process
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in io.Reader vs string, which is better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, can we add some quick functions to build different io.Reader?

Copy link
Member Author

@krishankumar01 krishankumar01 Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hwbrzzl Each stdlib type has its own way to get an io.Reader, so we don’t need to handle it:

  • stringstrings.NewReader("hello")
  • []bytebytes.NewReader(data)
  • *os.File → already implements io.Reader

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, the quick functions is optional for now.

{
name: "echo to stdout",
setup: func(p *Process) {
p.Command("sh", "-c", "printf 'hello'").Quietly()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be p.Command("sh -c printf 'hello'").Quietly()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, exec.CommandContext takes the command name and its arguments separately. I also don’t recommend writing a wrapper just to handle this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member Author

@krishankumar01 krishankumar01 Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I mean facades.Process().Run("sh -c printf 'hello'"), instead of facades.Process().Command("sh", "-c", "printf 'hello'").Run().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, is it good to judge sh specifically? Anyway, it's optional for this PR, we can optimize it in the future.

Copy link
Member Author

@krishankumar01 krishankumar01 Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the key is to transform the command, there should be a rule to do that.

@hwbrzzl hwbrzzl requested a review from Copilot August 27, 2025 06:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, awesome, thanks!

@krishankumar01 krishankumar01 merged commit 030010c into master Aug 27, 2025
21 of 22 checks passed
@krishankumar01 krishankumar01 deleted the kkumar-gcc/#567 branch August 27, 2025 07:57
return r
}

func (r *Process) run(name string, args ...string) (contractsprocess.Result, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about returning an error here instead of result.Error() in other places?

Suggested change
func (r *Running) Wait() contractsprocess.Result {
func (r *Running) Wait() (contractsprocess.Result, error) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@krishankumar01 krishankumar01 mentioned this pull request Oct 26, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] Add Process Management Support

4 participants