Compare commits
No commits in common. "main" and "v1.5.5" have entirely different histories.
@ -1,12 +1,5 @@
|
|||||||
# CHANGELOG - shell
|
# CHANGELOG - shell
|
||||||
|
|
||||||
## v1.5.6 (2026-08-24)
|
|
||||||
- **进程生命周期**:
|
|
||||||
- `Process.Kill` 改为并发安全,并在返回前等待子进程回收完成。
|
|
||||||
- 优雅退出超时后升级为 `SIGKILL`,避免旧进程占用端口时启动替代进程。
|
|
||||||
- 异步进程 I/O 改用 `os/exec` 原生管道,避免退出期间 `Wait` 与管道关闭互相等待。
|
|
||||||
- **依赖更新**: 对齐 `cast v1.5.5`。
|
|
||||||
|
|
||||||
## v1.5.5 (2026-07-18)
|
## v1.5.5 (2026-07-18)
|
||||||
- **依赖校验修复**: 更新 `go.sum` 为当前可验证的依赖 checksum。
|
- **依赖校验修复**: 更新 `go.sum` 为当前可验证的依赖 checksum。
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
#### 数据结构
|
#### 数据结构
|
||||||
- **`Process`**: 代表由 `Start` 启动的运行中命令
|
- **`Process`**: 代表由 `Start` 启动的运行中命令
|
||||||
- `Kill() error`: 并发安全地终止进程组;先发送 SIGTERM,超时后发送 SIGKILL,返回前确保子进程已回收
|
- `Kill() error`: 终止进程组(SIGTERM → 200ms → SIGKILL),Windows 直接 Kill
|
||||||
- `Read(data []byte) (int, error)`: 读取缓冲的 stdout 数据(上限 512KB,超出丢弃旧数据)
|
- `Read(data []byte) (int, error)`: 读取缓冲的 stdout 数据(上限 512KB,超出丢弃旧数据)
|
||||||
- `Write(data []byte) (int, error)`: 向进程 stdin 写入数据
|
- `Write(data []byte) (int, error)`: 向进程 stdin 写入数据
|
||||||
- `Check() error`: 检查进程是否仍在运行,已退出则返回错误
|
- `Check() error`: 检查进程是否仍在运行,已退出则返回错误
|
||||||
|
|||||||
6
TEST.md
6
TEST.md
@ -1,10 +1,5 @@
|
|||||||
# 测试报告 (shell)
|
# 测试报告 (shell)
|
||||||
|
|
||||||
## v1.5.6 验证
|
|
||||||
- 验证 `Process.Kill` 并发调用只执行一次终止流程。
|
|
||||||
- 验证 `Kill` 返回前子进程已完成回收。
|
|
||||||
- 全量测试、竞态测试和基准测试均通过。
|
|
||||||
|
|
||||||
## v1.5.5 验证
|
## v1.5.5 验证
|
||||||
- 独立模块解析、全量测试与基准测试均通过。
|
- 独立模块解析、全量测试与基准测试均通过。
|
||||||
|
|
||||||
@ -19,7 +14,6 @@
|
|||||||
- '&&' 短路逻辑验证(失败中断)
|
- '&&' 短路逻辑验证(失败中断)
|
||||||
- 异步进程启动与管理 (Start / Process)
|
- 异步进程启动与管理 (Start / Process)
|
||||||
- 进程生命周期控制 (Kill / Read / Write / Check)
|
- 进程生命周期控制 (Kill / Read / Write / Check)
|
||||||
- 并发调用 Kill 时只执行一次终止流程,并在返回前完成子进程回收
|
|
||||||
|
|
||||||
## Benchmark 结果
|
## Benchmark 结果
|
||||||
- 目前暂未添加复杂基准测试,Shell 模块执行受限于 OS 系统调用开销。
|
- 目前暂未添加复杂基准测试,Shell 模块执行受限于 OS 系统调用开销。
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -2,6 +2,6 @@ module apigo.cc/go/shell
|
|||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require apigo.cc/go/cast v1.5.5
|
require apigo.cc/go/cast v1.5.3
|
||||||
|
|
||||||
require apigo.cc/go/jsmod v1.5.3
|
require apigo.cc/go/jsmod v1.5.3
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -1,4 +1,4 @@
|
|||||||
apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw=
|
apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw=
|
||||||
apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
||||||
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
||||||
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
|
|||||||
88
run.go
88
run.go
@ -1,10 +1,8 @@
|
|||||||
package shell
|
package shell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -121,7 +119,6 @@ func InteractiveRun(name string, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type writerFunc func([]byte)
|
type writerFunc func([]byte)
|
||||||
|
|
||||||
func (w writerFunc) Write(p []byte) (n int, err error) {
|
func (w writerFunc) Write(p []byte) (n int, err error) {
|
||||||
w(p)
|
w(p)
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
@ -241,7 +238,6 @@ type Process struct {
|
|||||||
stdoutBuf bytes.Buffer
|
stdoutBuf bytes.Buffer
|
||||||
stderrBuf bytes.Buffer
|
stderrBuf bytes.Buffer
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
killMu sync.Mutex
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,20 +261,14 @@ func Start(name string, args []string, opts *Options) (*Process, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stdinW, err := cmd.StdinPipe()
|
stdinR, stdinW := io.Pipe()
|
||||||
if err != nil {
|
cmd.Stdin = stdinR
|
||||||
return nil, err
|
|
||||||
}
|
stdoutR, stdoutW := io.Pipe()
|
||||||
stdoutR, err := cmd.StdoutPipe()
|
cmd.Stdout = stdoutW
|
||||||
if err != nil {
|
|
||||||
stdinW.Close()
|
stderrR, stderrW := io.Pipe()
|
||||||
return nil, err
|
cmd.Stderr = stderrW
|
||||||
}
|
|
||||||
stderrR, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
stdinW.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
@ -286,6 +276,8 @@ func Start(name string, args []string, opts *Options) (*Process, error) {
|
|||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
stdinW.Close()
|
stdinW.Close()
|
||||||
|
stdoutW.Close()
|
||||||
|
stderrW.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,72 +340,30 @@ func Start(name string, args []string, opts *Options) (*Process, error) {
|
|||||||
go func() {
|
go func() {
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
stdinW.Close()
|
stdinW.Close()
|
||||||
|
stdoutW.Close()
|
||||||
|
stderrW.Close()
|
||||||
close(p.done)
|
close(p.done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill terminates the process group and waits until the child is reaped.
|
// Kill terminates the process group. SIGTERM first for graceful shutdown,
|
||||||
// It allows up to two seconds for graceful shutdown before sending SIGKILL.
|
// then SIGKILL after 200ms. On Windows, uses os.Process.Kill directly.
|
||||||
func (p *Process) Kill() error {
|
func (p *Process) Kill() error {
|
||||||
p.killMu.Lock()
|
|
||||||
defer p.killMu.Unlock()
|
|
||||||
|
|
||||||
if p.cmd.Process == nil {
|
if p.cmd.Process == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if p.waitDone(0) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
return p.cmd.Process.Kill()
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.waitDone(-1)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
pgid, err := syscall.Getpgid(p.cmd.Process.Pid)
|
pgid, err := syscall.Getpgid(p.cmd.Process.Pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, syscall.ESRCH) || p.waitDone(0) {
|
return p.cmd.Process.Kill()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = syscall.Kill(-pgid, syscall.SIGTERM); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.waitDone(2 * time.Second) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err = syscall.Kill(-pgid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.waitDone(-1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Process) waitDone(timeout time.Duration) bool {
|
|
||||||
if timeout < 0 {
|
|
||||||
<-p.done
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if timeout == 0 {
|
|
||||||
select {
|
|
||||||
case <-p.done:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
select {
|
|
||||||
case <-p.done:
|
|
||||||
return true
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
_ = 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
|
// Read reads buffered stdout data from the process. The buffer retains up to
|
||||||
|
|||||||
28
run_test.go
28
run_test.go
@ -3,7 +3,6 @@ package shell_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -129,30 +128,3 @@ func TestRunCommandMixedPipeAndChain(t *testing.T) {
|
|||||||
t.Errorf("Expected 'hello' and 'world' in output, got: %s", output)
|
t.Errorf("Expected 'hello' and 'world' in output, got: %s", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessKillWaitsAndIsConcurrentSafe(t *testing.T) {
|
|
||||||
proc, err := shell.Start("sh", []string{"-c", "while :; do read line || :; done"}, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errs := make(chan error, 4)
|
|
||||||
for range 4 {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
errs <- proc.Kill()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
close(errs)
|
|
||||||
for err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Kill failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := proc.Check(); err == nil {
|
|
||||||
t.Fatal("process should be reaped before Kill returns")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user