Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12690e8b91 |
@ -1,5 +1,13 @@
|
|||||||
# CHANGELOG - shell
|
# 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` 优雅终止:先 SIGTERM,200ms 后 SIGKILL;Windows 直接 Kill。
|
||||||
|
|
||||||
## v1.5.3 (2026-06-21)
|
## v1.5.3 (2026-06-21)
|
||||||
- **重构与错误堆栈支持**:
|
- **重构与错误堆栈支持**:
|
||||||
- 重构 `js_export.go`,将 `Run` 和 `RunCommand` 注册为具名函数,并动态使用 `jsmod.MakeError` 包裹错误。
|
- 重构 `js_export.go`,将 `Run` 和 `RunCommand` 注册为具名函数,并动态使用 `jsmod.MakeError` 包裹错误。
|
||||||
|
|||||||
26
README.md
26
README.md
@ -47,6 +47,18 @@
|
|||||||
- **`RunCommand(command string, opts *Options) (*CommandResult, error)`**: 支持解析 `|` 管道与 `&&` 短路逻辑的复合命令执行。
|
- **`RunCommand(command string, opts *Options) (*CommandResult, error)`**: 支持解析 `|` 管道与 `&&` 短路逻辑的复合命令执行。
|
||||||
- **`Pipeline(commands [][]string, opts *Options) (*CommandResult, error)`**: 执行多命令并行管道,高性能 IO 传输。
|
- **`Pipeline(commands [][]string, opts *Options) (*CommandResult, error)`**: 执行多命令并行管道,高性能 IO 传输。
|
||||||
|
|
||||||
|
### 异步进程管理 (Start)
|
||||||
|
|
||||||
|
#### 数据结构
|
||||||
|
- **`Process`**: 代表由 `Start` 启动的运行中命令
|
||||||
|
- `Kill() error`: 终止进程组(SIGTERM → 200ms → SIGKILL),Windows 直接 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
|
```go
|
||||||
@ -58,6 +70,20 @@ res, err := shell.Run("grep", []string{"pattern", "file.log"}, &shell.Options{
|
|||||||
|
|
||||||
// 2. 管道与短路逻辑组合
|
// 2. 管道与短路逻辑组合
|
||||||
res, err := shell.RunCommand("cat file.txt | grep 'foo' && echo 'found'", nil)
|
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)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 测试与质量保证
|
## 测试与质量保证
|
||||||
|
|||||||
2
TEST.md
2
TEST.md
@ -9,6 +9,8 @@
|
|||||||
- 管道命令执行 (Pipeline)
|
- 管道命令执行 (Pipeline)
|
||||||
- 完整命令字符串支持(包含 '|' 与 '&&' 逻辑)
|
- 完整命令字符串支持(包含 '|' 与 '&&' 逻辑)
|
||||||
- '&&' 短路逻辑验证(失败中断)
|
- '&&' 短路逻辑验证(失败中断)
|
||||||
|
- 异步进程启动与管理 (Start / Process)
|
||||||
|
- 进程生命周期控制 (Kill / Read / Write / Check)
|
||||||
|
|
||||||
## Benchmark 结果
|
## Benchmark 结果
|
||||||
- 目前暂未添加复杂基准测试,Shell 模块执行受限于 OS 系统调用开销。
|
- 目前暂未添加复杂基准测试,Shell 模块执行受限于 OS 系统调用开销。
|
||||||
166
run.go
166
run.go
@ -10,6 +10,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user