feat(shell): 新增 Start 方法支持异步进程管理(by AI)

- 新增 Start(name, args, opts) 异步启动命令,立即返回 *Process
- Process 提供 Kill/Read/Write/Check 方法管理子进程生命周期
- stdout/stderr 默认流式输出到终端,同时缓冲供 Read 读取
- Kill 优雅终止:先 SIGTERM,200ms 后 SIGKILL
- 更新 CHANGELOG、README、TEST 文档

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
This commit is contained in:
AI Engineer 2026-06-21 14:44:33 +08:00
parent 85bb755cad
commit 7d037bc9c1
4 changed files with 202 additions and 0 deletions

View File

@ -1,5 +1,13 @@
# CHANGELOG - shell
## v1.5.4 (2026-06-21)
- **新增 `Start` 方法和 `Process` 结构体**:
- `Start(name, args, opts)` 异步启动命令,立即返回 `*Process`,不等待命令完成。
- `Process` 提供 `Kill`/`Read`/`Write`/`Check` 方法,支持子进程生命周期管理和实时 I/O 交互。
- stdout/stderr 默认流式输出到父终端,同时缓冲(上限 512KB以供 `Read` 读取。
- `CatchSignal` 选项支持信号自动转发到子进程组。
- `Kill` 优雅终止:先 SIGTERM200ms 后 SIGKILLWindows 直接 Kill。
## v1.5.3 (2026-06-21)
- **重构与错误堆栈支持**:
- 重构 `js_export.go`,将 `Run``RunCommand` 注册为具名函数,并动态使用 `jsmod.MakeError` 包裹错误。

View File

@ -47,6 +47,18 @@
- **`RunCommand(command string, opts *Options) (*CommandResult, error)`**: 支持解析 `|` 管道与 `&&` 短路逻辑的复合命令执行。
- **`Pipeline(commands [][]string, opts *Options) (*CommandResult, error)`**: 执行多命令并行管道,高性能 IO 传输。
### 异步进程管理 (Start)
#### 数据结构
- **`Process`**: 代表由 `Start` 启动的运行中命令
- `Kill() error`: 终止进程组SIGTERM → 200ms → SIGKILLWindows 直接 Kill
- `Read(data []byte) (int, error)`: 读取缓冲的 stdout 数据(上限 512KB超出丢弃旧数据
- `Write(data []byte) (int, error)`: 向进程 stdin 写入数据
- `Check() error`: 检查进程是否仍在运行,已退出则返回错误
#### 函数接口
- **`Start(name string, args []string, opts *Options) (*Process, error)`**: 异步启动命令,立即返回 `*Process`。stdout/stderr 默认流式输出到终端,可通过 `opts.OnStdout`/`opts.OnStderr` 自定义回调。
## 使用示例
```go
@ -58,6 +70,20 @@ res, err := shell.Run("grep", []string{"pattern", "file.log"}, &shell.Options{
// 2. 管道与短路逻辑组合
res, err := shell.RunCommand("cat file.txt | grep 'foo' && echo 'found'", nil)
// 3. 异步启动长期运行的服务
proc, err := shell.Start("go", []string{"run", "server.go"}, &shell.Options{
Dir: "./app",
OnStdout: func(data []byte) { log.Print(string(data)) },
})
if err != nil { panic(err) }
defer proc.Kill()
// 写入 stdin
proc.Write([]byte("input\n"))
// 检查运行状态
if err := proc.Check(); err != nil {
fmt.Println("process stopped:", err)
}
```
## 测试与质量保证

View File

@ -9,6 +9,8 @@
- 管道命令执行 (Pipeline)
- 完整命令字符串支持(包含 '|' 与 '&&' 逻辑)
- '&&' 短路逻辑验证(失败中断)
- 异步进程启动与管理 (Start / Process)
- 进程生命周期控制 (Kill / Read / Write / Check)
## Benchmark 结果
- 目前暂未添加复杂基准测试Shell 模块执行受限于 OS 系统调用开销。

166
run.go
View File

@ -10,6 +10,7 @@ import (
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"time"
)
@ -228,3 +229,168 @@ func setupSignalHandling(cmd *exec.Cmd) {
}
}()
}
// Process represents a running command started by Start.
// It provides methods to manage the subprocess lifecycle and I/O.
type Process struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdoutBuf bytes.Buffer
stderrBuf bytes.Buffer
mu sync.Mutex
done chan struct{}
}
// Start starts a command and returns immediately, unlike Run which waits for completion.
// The returned Process provides Kill/Read/Write/Check for lifecycle management.
// If CatchSignal is set in opts, OS signals are automatically forwarded to the child.
// By default, stdout/stderr are streamed to the parent's terminal in addition to
// being buffered for Read calls.
func Start(name string, args []string, opts *Options) (*Process, error) {
cmd := exec.Command(name, args...)
if opts != nil {
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
if opts.Verbose {
fmt.Println(Style(TextCyan, "Start:", name, strings.Join(args, " ")))
}
if opts.Env != nil {
cmd.Env = append(os.Environ(), opts.Env...)
}
}
stdinR, stdinW := io.Pipe()
cmd.Stdin = stdinR
stdoutR, stdoutW := io.Pipe()
cmd.Stdout = stdoutW
stderrR, stderrW := io.Pipe()
cmd.Stderr = stderrW
if runtime.GOOS != "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
if err := cmd.Start(); err != nil {
stdinW.Close()
stdoutW.Close()
stderrW.Close()
return nil, err
}
p := &Process{
cmd: cmd,
stdin: stdinW,
done: make(chan struct{}),
}
go func() {
buf := make([]byte, 4096)
for {
n, err := stdoutR.Read(buf)
if n > 0 {
p.mu.Lock()
if p.stdoutBuf.Len() > 512*1024 {
p.stdoutBuf.Next(p.stdoutBuf.Len() / 2)
}
p.stdoutBuf.Write(buf[:n])
p.mu.Unlock()
if opts != nil && opts.OnStdout != nil {
opts.OnStdout(buf[:n])
} else {
os.Stdout.Write(buf[:n])
}
}
if err != nil {
break
}
}
}()
go func() {
buf := make([]byte, 4096)
for {
n, err := stderrR.Read(buf)
if n > 0 {
p.mu.Lock()
if p.stderrBuf.Len() > 512*1024 {
p.stderrBuf.Next(p.stderrBuf.Len() / 2)
}
p.stderrBuf.Write(buf[:n])
p.mu.Unlock()
if opts != nil && opts.OnStderr != nil {
opts.OnStderr(buf[:n])
} else {
os.Stderr.Write(buf[:n])
}
}
if err != nil {
break
}
}
}()
if opts != nil && opts.CatchSignal {
setupSignalHandling(cmd)
}
go func() {
cmd.Wait()
stdinW.Close()
stdoutW.Close()
stderrW.Close()
close(p.done)
}()
return p, nil
}
// Kill terminates the process group. SIGTERM first for graceful shutdown,
// then SIGKILL after 200ms. On Windows, uses os.Process.Kill directly.
func (p *Process) Kill() error {
if p.cmd.Process == nil {
return nil
}
if runtime.GOOS == "windows" {
return p.cmd.Process.Kill()
}
pgid, err := syscall.Getpgid(p.cmd.Process.Pid)
if err != nil {
return p.cmd.Process.Kill()
}
_ = syscall.Kill(-pgid, syscall.SIGTERM)
time.Sleep(200 * time.Millisecond)
return syscall.Kill(-pgid, syscall.SIGKILL)
}
// Read reads buffered stdout data from the process. The buffer retains up to
// 512KB of recent output; older data is discarded when the limit is exceeded.
func (p *Process) Read(data []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.stdoutBuf.Read(data)
}
// Write sends data to the process's stdin.
func (p *Process) Write(data []byte) (int, error) {
return p.stdin.Write(data)
}
// Check returns nil if the process is still running.
// If the process has exited, it returns an error with exit details.
func (p *Process) Check() error {
select {
case <-p.done:
if p.cmd.ProcessState != nil {
if code := p.cmd.ProcessState.ExitCode(); code != 0 {
return fmt.Errorf("process exited with code %d", code)
}
}
return fmt.Errorf("process completed")
default:
return nil
}
}