跳转到内容

进程

介绍

Goravel 围绕 Go 标准 os/exec 包提供了一个富有表现力且优雅的 API,允许你从应用程序中无缝调用外部命令。 默认情况下,Go 的进程处理可能很冗长;Goravel 的 Process facade 简化了这一常见任务,为执行命令、处理输出和管理异步进程提供了流畅的接口。

调用进程

运行进程

要运行一个进程,你可以使用 RunStart 方法。 Run 方法将执行命令并等待其完成,而 Start 方法异步触发进程并立即返回控制权。

以下是执行阻塞命令的方式:

go
import (
    "fmt"

    "goravel/app/facades"
)

func main() {
    result := facades.Process().Run("echo", "Hello, World!")
    if result.Failed() {
        panic(result.Error())
    }

    fmt.Println(result.Output())
}

如果你想直接运行字符串命令(而不将其拆分为参数),可以将命令作为字符串传递给 Run 方法,/bin/sh -c(Linux/macOS)或 cmd /C(Windows)将会被添加到命令之前。 注意,只有当字符串命令包含空格或 &|- 时,该机制才会被触发。

go
result := facades.Process().Run("echo Hello, World!")
// /bin/sh -c ""echo Hello, World!"" on Linux/macOS
// cmd /c "echo Hello, World!" on Windows

Run 方法返回一个 Result 接口。 Result 接口提供了访问命令输出和状态的方式:

go
result := facades.Process().Run("ls", "-la")

result.Command()     // string: The original command
result.Error()       // error: The error returned by the command execution
result.ErrorOutput() // string: Output from Stderr
result.ExitCode()    // int: The exit code (e.g., 0, 1)
result.Failed()      // bool: True if the exit code was not 0
result.Output()      // string: Output from Stdout

选项

如果想要定义命令的运行方式,例如其运行位置或环境变量。 Process facade 为此提供了一个流畅的 API。

Path

你可以使用 Path 方法为命令指定工作目录。 如果不设置此项,进程将在应用程序的当前工作目录中执行。

go
result := facades.Process().Path("/var/www/html").Run("ls", "-la")

Timeout

为了防止进程无限期挂起,你可以设置超时时间。 如果进程运行时间超过指定的持续时间,它将被终止。

go
import "time"

result := facades.Process().Timeout(10 * time.Minute).Run("sleep", "20")

环境变量

可以使用 Env 方法将自定义环境变量传递给进程。 该进程也将继承系统的环境变量。

go
// Passes FOO=BAR along with existing system envs
result := facades.Process().Env(map[string]string{
    "FOO": "BAR",
    "API_KEY": "secret",
}).Run("printenv")

Input (Stdin)

如果期望从标准输入(stdin)接收输入,可以使用 Input 方法。 该方法接收一个 io.Reader

go
import "strings"

// Pipes "Hello Goravel" into the cat command
result := facades.Process().
    Input(strings.NewReader("Hello Goravel")).
    Run("cat")

输出

你可以在命令执行后使用 resultOutput(标准输出)和 ErrorOutput(标准错误)方法访问进程输出。

go
result := facades.Process().Run("ls", "-la")

fmt.Println(result.Output())
fmt.Println(result.ErrorOutput())

如果你需要实时处理输出(流式传输),可以使用 OnOutput 方法注册回调。 回调接收两个参数:输出类型(stdout 或 stderr)和包含输出数据的字节切片。

go
import (
    "fmt"
    "github.com/goravel/framework/contracts/process"
)

result := facades.Process().OnOutput(func(typ process.OutputType, b []byte) {
    // Handle real-time streaming here
    fmt.Print(string(b))
}).Run("ls", "-la")

如果你只需要在执行后验证输出是否包含特定字符串,可以使用 SeeInOutputSeeInErrorOutput 辅助方法。

go
result := facades.Process().Run("ls", "-la")

if result.SeeInOutput("go.mod") {
    // The file exists
}

禁用进程输出

如果进程写入大量数据,你可能希望控制其存储方式。

使用 Quietly 将防止输出在执行期间输出到控制台或日志,但数据仍将被收集并可通过 result.Output() 访问。

如果你完全不需要访问最终输出并希望节省内存,可以使用 DisableBuffering。 这会阻止输出存储在结果对象中,不过你仍然可以使用 OnOutput 实时检查流。

go
// Captures output but doesn't print it during execution
facades.Process().Quietly().Run("...")

// Does not capture output (saves memory), but allows streaming
facades.Process().DisableBuffering().OnOutput(func(typ process.OutputType, b []byte) {
    // ...
}).Run("...")

管道

有时你需要将一个进程的输出传输到另一个进程的输入。 通过 Pipe 方法可以很容易实现,该方法允许你同步地将多个命令链接在一起。

go
import "github.com/goravel/framework/contracts/process"

result := facades.Process().Pipe(func(pipe process.Pipe) {
    pipe.Command("echo", "Hello, World!")
    pipe.Command("grep World") // string command: /bin/sh -c "grep World"
    pipe.Command("tr", "a-z", "A-Z") 
}).Run()

WARNING

诸如 TimeoutEnvInput 之类的进程选项必须在调用 Pipe 方法之后进行配置。 在 Pipe 调用之前应用的任何配置都将被忽略。

go
// 正确:配置在 Pipe 之后应用
facades.Process().Pipe(...).Timeout(10 * time.Second).Run()

// 错误:超时将被忽略
facades.Process().Timeout(10 * time.Second).Pipe(...).Run()

输出与 Keys

你可以使用 OnOutput 方法实时检查管道的输出。 当与管道一起使用时,回调签名会包含一个 key(字符串),允许你识别哪个命令产生了输出。

默认情况下,key 是命令的数字索引。 你还可以使用 As 方法为每个命令分配一个可读的标签,这对于调试复杂的管道非常有用。

go
facades.Process().Pipe(func(pipe process.Pipe) {
    pipe.Command("cat", "access.log").As("source")
    pipe.Command("grep", "error").As("filter")
}).OnOutput(func(typ process.OutputType, line []byte, key string) {
    // 'key' will be "source" or "filter"
}).Run()

异步进程

虽然 Run 方法等待进程完成,但 Start 可用于异步调用进程。 这允许进程在后台运行,而应用程序继续执行其他任务。 Start 方法返回一个 Running 接口。

go
import "time"

running, err := facades.Process().Timeout(10 * time.Second).Start("sleep", "5")

// Continue doing other work...

result := running.Wait()

要检查进程是否已完成而不阻塞,可以使用 Done 方法。 这会返回一个标准的 Go 通道,当进程退出时关闭,使其非常适合在 select 语句中使用。

go
running, err := facades.Process().Start("sleep", "5")

select {
case <-running.Done():
    // Process finished successfully
case <-time.After(1 * time.Second):
    // Custom logic if it takes too long
}

result := running.Wait()

WARNING

即使你使用 Done 通道来检测完成,之后也必须调用 Wait()。 这确保进程被操作系统正确“捕获”并清理底层资源。

进程 ID 与信号

可以使用 PID 方法检索正在运行的进程的操作系统进程 ID(PID)。

go
running, err := facades.Process().Start("ls", "-la")

println(running.PID())

发送信号

Goravel 提供了与进程生命周期交互的方法。 可以使用 Signal 方法发送特定的操作系统信号,或使用 Stop 辅助函数尝试优雅地关闭进程。

Stop 方法会发送终止信号(默认为 SIGTERM)。 如果进程在提供的超时时间内未退出,它将被强制终止(SIGKILL)。

go
import (
    "os"
    "time"
)

running, err := facades.Process().Start("sleep", "60")

// Manually send a signal
running.Signal(os.Interrupt)

// Attempt to stop gracefully, wait 5 seconds, then force kill
running.Stop(5 * time.Second)

检查进程状态

可以使用 Running 方法检查进程的当前状态。 这对于调试或健康检查特别有用,因为它可以检查进程当前是否处于活动状态。

go
// Snapshot check (useful for logs or metrics)
if running.Running() {
    fmt.Println("Process is still active...")
}

TIP

如果你需要在进程完成时执行代码,请勿轮询 Running()。 请使用 Done() 通道或 Wait() 方法,这比重复检查状态要高效得多。

并发进程

Goravel 使得管理并发进程池变得容易,允许你同时执行多个命令。 这对于批处理或并行运行独立任务特别有用。

进程池

要运行进程池,可以使用 Pool 方法。 它接受一个闭包,你可以在其中定义要执行的命令。

默认情况下,Pool 方法会等待所有进程完成,并返回一个按进程名称(或索引)键控的结果。

go
results, err := facades.Process().Pool(func(pool process.Pool) {
    pool.Command("sleep", "1").As("first")
    pool.Command("sleep 2").As("second") // string command: /bin/sh -c "sleep 2"
}).Run()

if err != nil {
    panic(err)
}

// Access results by their assigned key
println(results["first"].Output())
println(results["second"].Output())

命名进程

默认情况下,池中的进程按其数字索引(例如 "0"、"1")。 但是,为了清晰和更容易访问结果,你应该使用 As 方法为每个进程分配一个唯一的名称:

go
pool.Command("cat", "system.log").As("system")

选项

Pool 提供了几种方法来控制整个批处理的执行行为。

并发数

你可以使用 Concurrency 方法控制同时运行的最大进程数。

go
facades.Process().Pool(func(pool process.Pool) {
    // Define 10 commands...
}).Concurrency(2).Run()

超时

你可以使用 Timeout 方法为整个池的执行强制执行全局超时。 如果池的执行时间超过此持续时间,所有正在运行的进程将被终止。

go
facades.Process().Pool(...).Timeout(1 * time.Minute).Run()

异步池

如果你需要在应用程序执行其他任务时在后台运行池,可以使用 Start 方法代替 Run。 这将返回一个 RunningPool 接口。

go
runningPool, err := facades.Process().Pool(func(pool process.Pool) {
    pool.Command("sleep", "5").As("long_task")
}).Start()

// Check if the pool is still running
if runningPool.Running() {
    fmt.Println("Pool is active...")
}

// Wait for all processes to finish and gather results
results := runningPool.Wait()

与运行中的进程池交互

RunningPool 接口提供了几种方法来管理进行池:

  • PIDs():返回一个按命令名称为键的进程 ID Map。
  • Signal(os.Signal):向池中所有正在运行的进程发送信号。
  • Stop(timeout, signal):优雅地停止所有进程。
  • Done():返回一个在池完成时关闭的通道,对 select 语句很有用。
go
select {
case <-runningPool.Done():
    // All processes finished
case <-time.After(10 * time.Second):
    // Force stop all processes if they take too long
    runningPool.Stop(1 * time.Second)
}

输出

可以使用 OnOutput 方法实时检查池的输出。

WARNING

OnOutput 回调可能会从多个 goroutine 并发调用。 确保你的回调逻辑是线程安全的。

go
facades.Process().Pool(func(pool process.Pool) {
    pool.Command("ping", "google.com").As("ping")
}).OnOutput(func(typ process.OutputType, line []byte, key string) {
    // key will be "ping"
    fmt.Printf("[%s] %s", key, string(line))
}).Run()

配置

在进程池内部,每个命令都支持类似于单个进程的配置方法:

  • Path(string):设置工作目录。
  • Env(map[string]string):设置环境变量。
  • Input(io.Reader):设置标准输入。
  • Timeout(time.Duration):设置特定命令的超时时间。
  • Quietly():禁用此命令的输出。
  • DisableBuffering():禁用内存缓冲(对高容量输出很有用)。
go
facades.Process().Pool(func(pool process.Pool) {
    pool.Command("find", "/", "-name", "*.log").
        As("search").
        Path("/var/www").
        Timeout(10 * time.Second).
        DisableBuffering()
}).Run()

基于 MIT 许可发布