Files
panel/pkg/shell/progress.go
T
耗子andClaude Fable 5.1 f0b3ef1b67 refactor(shell): 统一进程组处理,补测试,redis 密码改走环境变量
- 新增 Command 直连 exec 且套上同一套独立进程组、整组 SIGKILL 与宽限期,11 处自己拼 exec.Cmd 只调 ApplyEnv 的调用点改为使用它,ApplyEnv 不再导出
- webhook 脚本原先无 ctx 无进程组,脚本起守护进程占着管道会把请求挂死
- 删除零调用的 ExecfWithOutput/ExecfWithPipe,IsPTYError 改私有,NewPTYTurn 去掉没人用的变参
- ExecfAsync 失败改走 slog;ExecWithPipe 把 Wait 的错误传给读端
- redis-cli 密码改走 REDISCLI_AUTH,大 key 扫描任务在运行时从配置取密码,不再落进任务表
- pip/npm/go 镜像地址加引号;fstab 的 grep/sed 模式做正则转义
- 补 Quote、stderr、取消杀孙子进程、宽限期截断、超时、管道错误传播六个测试

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 02:17:40 +08:00

77 lines
1.5 KiB
Go

package shell
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"sync/atomic"
"time"
)
// ExecWithStdinProgress 以外部命令消费 stdin,同时按周期通过 progress 回调输出进度
func ExecWithStdinProgress(
ctx context.Context,
name string, args, env []string,
stdin io.Reader, total int64,
interval time.Duration,
progress func(written, total int64, rate float64),
) (string, error) {
cmd := Command(ctx, name, args...)
cmd.Env = append(cmd.Env, env...)
pr := &progressReader{r: stdin}
cmd.Stdin = pr
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return "", err
}
done := make(chan struct{})
if progress != nil && interval > 0 {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
start := time.Now()
for {
select {
case <-done:
return
case <-ticker.C:
w := pr.written.Load()
elapsed := time.Since(start).Seconds()
rate := 0.0
if elapsed > 0 {
rate = float64(w) / elapsed
}
progress(w, total, rate)
}
}
}()
}
err := cmd.Wait()
close(done)
if err != nil {
return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %w, stderr: %s", name, err, strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(stdout.String()), nil
}
type progressReader struct {
r io.Reader
written atomic.Int64
}
func (p *progressReader) Read(b []byte) (int, error) {
n, err := p.r.Read(b)
if n > 0 {
p.written.Add(int64(n))
}
return n, err
}