github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/commands.go (about) 1 package sandbox 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "sync/atomic" 8 "time" 9 10 "connectrpc.com/connect" 11 12 "github.com/qiniu/go-sdk/v7/sandbox/internal/envdapi/process" 13 "github.com/qiniu/go-sdk/v7/sandbox/internal/envdapi/process/processconnect" 14 ) 15 16 // CommandResult 命令执行结果。 17 type CommandResult struct { 18 ExitCode int 19 Stdout string 20 Stderr string 21 Error string 22 } 23 24 // CommandHandle 后台命令句柄。 25 type CommandHandle struct { 26 // pid 进程 ID。Start 路径下由后台事件流 goroutine 写入, 27 // 因此使用 atomic 保证与 PID()/Kill() 的并发读取之间无数据竞态。 28 pid atomic.Uint32 29 30 commands *Commands 31 cancel context.CancelFunc 32 done chan struct{} 33 pidCh chan struct{} 34 pidOnce sync.Once 35 result *CommandResult 36 37 mu sync.Mutex 38 onStdout func(data []byte) 39 onStderr func(data []byte) 40 onPtyData func(data []byte) 41 } 42 43 // markPIDReady 标记进程 PID 就绪:首次调用写入 pid 并关闭 pidCh, 44 // 后续调用为 no-op。Start 路径在收到 Start 事件时调用; 45 // Connect 路径在构造 handle 后立即以已知 PID 调用, 46 // 避免共享事件流处理器后续再收到 Start 事件时重复 close(pidCh) 引发 panic。 47 func (h *CommandHandle) markPIDReady(pid uint32) { 48 h.pidOnce.Do(func() { 49 h.pid.Store(pid) 50 close(h.pidCh) 51 }) 52 } 53 54 // PID 返回进程 ID。 55 func (h *CommandHandle) PID() uint32 { 56 return h.pid.Load() 57 } 58 59 // Wait 等待命令完成并返回结果。 60 func (h *CommandHandle) Wait() (*CommandResult, error) { 61 <-h.done 62 if h.result == nil { 63 return nil, fmt.Errorf("command terminated without result") 64 } 65 return h.result, nil 66 } 67 68 // Kill 终止命令。 69 func (h *CommandHandle) Kill(ctx context.Context) error { 70 return h.commands.Kill(ctx, h.pid.Load()) 71 } 72 73 // WaitPID 等待进程 PID 被分配。 74 // 当进程流收到 Start 事件后返回 PID;若 ctx 取消则返回错误。 75 func (h *CommandHandle) WaitPID(ctx context.Context) (uint32, error) { 76 select { 77 case <-h.pidCh: 78 return h.pid.Load(), nil 79 case <-ctx.Done(): 80 return 0, ctx.Err() 81 } 82 } 83 84 // ProcessInfo 进程信息。 85 type ProcessInfo struct { 86 PID uint32 87 Tag *string 88 Cmd string 89 Args []string 90 Envs map[string]string 91 Cwd *string 92 } 93 94 // CommandOption 命令选项。 95 type CommandOption func(*commandOpts) 96 97 type commandOpts struct { 98 envs map[string]string 99 cwd string 100 user string 101 tag string 102 onStdout func(data []byte) 103 onStderr func(data []byte) 104 onPtyData func(data []byte) 105 timeout time.Duration 106 stdin bool 107 } 108 109 // WithEnvs 设置命令的环境变量。 110 func WithEnvs(envs map[string]string) CommandOption { 111 return func(o *commandOpts) { o.envs = envs } 112 } 113 114 // WithCwd 设置命令的工作目录。 115 func WithCwd(cwd string) CommandOption { 116 return func(o *commandOpts) { o.cwd = cwd } 117 } 118 119 // WithCommandUser 设置执行命令的用户。 120 func WithCommandUser(user string) CommandOption { 121 return func(o *commandOpts) { o.user = user } 122 } 123 124 // WithTag 设置进程标签,用于后续通过标签连接进程。 125 func WithTag(tag string) CommandOption { 126 return func(o *commandOpts) { o.tag = tag } 127 } 128 129 // WithOnStdout 设置 stdout 数据回调。仅用于标准命令的 stdout 输出。 130 // PTY 会话应使用 WithOnPtyData 接收输出。 131 func WithOnStdout(fn func(data []byte)) CommandOption { 132 return func(o *commandOpts) { o.onStdout = fn } 133 } 134 135 // WithOnStderr 设置 stderr 数据回调。 136 func WithOnStderr(fn func(data []byte)) CommandOption { 137 return func(o *commandOpts) { o.onStderr = fn } 138 } 139 140 // WithOnPtyData 设置 PTY 数据回调。用于接收 PTY 会话的输出数据。 141 // 若未设置,Pty.Create 会回退使用 WithOnStdout 回调以保持兼容。 142 func WithOnPtyData(fn func(data []byte)) CommandOption { 143 return func(o *commandOpts) { o.onPtyData = fn } 144 } 145 146 // WithTimeout 设置命令超时时间。 147 func WithTimeout(timeout time.Duration) CommandOption { 148 return func(o *commandOpts) { o.timeout = timeout } 149 } 150 151 // WithStdin 启用进程的标准输入。 152 // 启用后可通过 Commands.SendStdin 向进程发送数据,通过 Commands.CloseStdin 发送 EOF。 153 func WithStdin() CommandOption { 154 return func(o *commandOpts) { o.stdin = true } 155 } 156 157 func applyCommandOpts(opts []CommandOption) *commandOpts { 158 o := &commandOpts{user: DefaultUser} 159 for _, fn := range opts { 160 fn(o) 161 } 162 return o 163 } 164 165 // Commands 提供沙箱命令执行能力。 166 type Commands struct { 167 sandbox *Sandbox 168 rpc processconnect.ProcessClient 169 } 170 171 // newCommands 创建 Commands 实例。 172 func newCommands(s *Sandbox, rpc processconnect.ProcessClient) *Commands { 173 return &Commands{sandbox: s, rpc: rpc} 174 } 175 176 // pidSelector 返回按 PID 选择进程的 ProcessSelector。 177 func pidSelector(pid uint32) *process.ProcessSelector { 178 return &process.ProcessSelector{Selector: &process.ProcessSelector_Pid{Pid: pid}} 179 } 180 181 // Run 在沙箱中执行命令并等待完成。返回执行结果。 182 // 注意: stdout 和 stderr 在内存中累积,长时间运行或大量输出的命令 183 // 应使用 Start() + WithOnStdout/WithOnStderr 流式回调处理输出。 184 func (c *Commands) Run(ctx context.Context, cmd string, opts ...CommandOption) (*CommandResult, error) { 185 handle, err := c.Start(ctx, cmd, opts...) 186 if err != nil { 187 return nil, err 188 } 189 return handle.Wait() 190 } 191 192 // Start 在沙箱中后台启动命令。返回 CommandHandle 可用于等待完成。 193 // cmd 以 /bin/bash -l -c <cmd> 形式执行,支持 shell 语法(管道、重定向等), 194 // 会加载登录 shell 环境(/etc/profile 及用户 profile)。 195 func (c *Commands) Start(ctx context.Context, cmd string, opts ...CommandOption) (*CommandHandle, error) { 196 o := applyCommandOpts(opts) 197 198 cmdCtx := ctx 199 var cmdCancel context.CancelFunc 200 if o.timeout > 0 { 201 cmdCtx, cmdCancel = context.WithTimeout(ctx, o.timeout) 202 } else { 203 cmdCtx, cmdCancel = context.WithCancel(ctx) 204 } 205 206 startReq := &process.StartRequest{ 207 Process: &process.ProcessConfig{ 208 Cmd: "/bin/bash", 209 Args: []string{"-l", "-c", cmd}, 210 Envs: o.envs, 211 }, 212 } 213 if o.cwd != "" { 214 startReq.Process.Cwd = &o.cwd 215 } 216 if o.tag != "" { 217 startReq.Tag = &o.tag 218 } 219 startReq.Stdin = &o.stdin 220 221 req := connect.NewRequest(startReq) 222 c.sandbox.setEnvdAuth(req, o.user) 223 224 stream, err := c.rpc.Start(cmdCtx, req) 225 if err != nil { 226 cmdCancel() 227 return nil, fmt.Errorf("start command: %w", err) 228 } 229 230 handle := &CommandHandle{ 231 commands: c, 232 cancel: cmdCancel, 233 done: make(chan struct{}), 234 pidCh: make(chan struct{}), 235 onStdout: o.onStdout, 236 onStderr: o.onStderr, 237 } 238 239 go processEventStream(stream, handle) 240 241 return handle, nil 242 } 243 244 // eventMessage 是 StartResponse 和 ConnectResponse 的公共接口。 245 type eventMessage interface { 246 GetEvent() *process.ProcessEvent 247 } 248 249 // streamReceiver 抽象 ConnectRPC 服务端流的读取操作。 250 type streamReceiver[T eventMessage] interface { 251 Receive() bool 252 Msg() T 253 Err() error 254 } 255 256 // processEventStream 处理进程事件流(Start 和 Connect 共用)。 257 func processEventStream[T eventMessage](stream streamReceiver[T], handle *CommandHandle) { 258 defer close(handle.done) 259 260 var stdout, stderr []byte 261 for stream.Receive() { 262 event := stream.Msg().GetEvent() 263 if event == nil { 264 continue 265 } 266 switch ev := event.Event.(type) { 267 case *process.ProcessEvent_Start: 268 handle.markPIDReady(ev.Start.Pid) 269 case *process.ProcessEvent_Data: 270 if data := ev.Data.GetStdout(); len(data) > 0 { 271 stdout = append(stdout, data...) 272 handle.mu.Lock() 273 fn := handle.onStdout 274 handle.mu.Unlock() 275 if fn != nil { 276 fn(data) 277 } 278 } 279 if data := ev.Data.GetStderr(); len(data) > 0 { 280 stderr = append(stderr, data...) 281 handle.mu.Lock() 282 fn := handle.onStderr 283 handle.mu.Unlock() 284 if fn != nil { 285 fn(data) 286 } 287 } 288 if data := ev.Data.GetPty(); len(data) > 0 { 289 handle.mu.Lock() 290 fn := handle.onPtyData 291 handle.mu.Unlock() 292 if fn != nil { 293 fn(data) 294 } 295 } 296 case *process.ProcessEvent_End: 297 handle.result = &CommandResult{ 298 ExitCode: int(ev.End.ExitCode), 299 Stdout: string(stdout), 300 Stderr: string(stderr), 301 } 302 if ev.End.Error != nil { 303 handle.result.Error = *ev.End.Error 304 } 305 } 306 } 307 308 // 如果流结束但没有收到 EndEvent,创建一个错误结果 309 if handle.result == nil { 310 errMsg := "" 311 if err := stream.Err(); err != nil { 312 errMsg = err.Error() 313 } 314 handle.result = &CommandResult{ 315 ExitCode: -1, 316 Stdout: string(stdout), 317 Stderr: string(stderr), 318 Error: errMsg, 319 } 320 } 321 } 322 323 // Connect 连接到正在运行的进程。 324 func (c *Commands) Connect(ctx context.Context, pid uint32) (*CommandHandle, error) { 325 connectCtx, connectCancel := context.WithCancel(ctx) 326 327 req := connect.NewRequest(&process.ConnectRequest{ 328 Process: pidSelector(pid), 329 }) 330 c.sandbox.setEnvdAuth(req, DefaultUser) 331 332 stream, err := c.rpc.Connect(connectCtx, req) 333 if err != nil { 334 connectCancel() 335 return nil, fmt.Errorf("connect to process: %w", err) 336 } 337 338 handle := &CommandHandle{ 339 commands: c, 340 cancel: connectCancel, 341 done: make(chan struct{}), 342 pidCh: make(chan struct{}), 343 } 344 // PID 已知,立即标记就绪;后续若服务端在流里再发 Start 事件, 345 // markPIDReady 内部的 sync.Once 会保证不会重复 close(pidCh)。 346 handle.markPIDReady(pid) 347 348 go processEventStream(stream, handle) 349 350 return handle, nil 351 } 352 353 // List 列出所有运行中的进程。 354 func (c *Commands) List(ctx context.Context) ([]ProcessInfo, error) { 355 req := connect.NewRequest(&process.ListRequest{}) 356 c.sandbox.setEnvdAuth(req, DefaultUser) 357 358 resp, err := c.rpc.List(ctx, req) 359 if err != nil { 360 return nil, fmt.Errorf("list processes: %w", err) 361 } 362 363 var infos []ProcessInfo 364 for _, p := range resp.Msg.Processes { 365 info := ProcessInfo{ 366 PID: p.Pid, 367 Tag: p.Tag, 368 } 369 if p.Config != nil { 370 info.Cmd = p.Config.Cmd 371 info.Args = p.Config.Args 372 info.Envs = p.Config.Envs 373 info.Cwd = p.Config.Cwd 374 } 375 infos = append(infos, info) 376 } 377 return infos, nil 378 } 379 380 // SendStdin 向进程发送标准输入。 381 func (c *Commands) SendStdin(ctx context.Context, pid uint32, data []byte) error { 382 req := connect.NewRequest(&process.SendInputRequest{ 383 Process: pidSelector(pid), 384 Input: &process.ProcessInput{ 385 Input: &process.ProcessInput_Stdin{Stdin: data}, 386 }, 387 }) 388 c.sandbox.setEnvdAuth(req, DefaultUser) 389 390 _, err := c.rpc.SendInput(ctx, req) 391 if err != nil { 392 return fmt.Errorf("send stdin: %w", err) 393 } 394 return nil 395 } 396 397 // CloseStdin 关闭进程的标准输入管道,向进程发送 EOF 信号。 398 // 仅适用于非 PTY 进程。PTY 进程应通过 SendStdin 发送 Ctrl+D (0x04)。 399 // 若服务端尚未支持此 RPC,将返回 Unimplemented 错误,调用方可安全忽略。 400 func (c *Commands) CloseStdin(ctx context.Context, pid uint32) error { 401 req := connect.NewRequest(&process.CloseStdinRequest{ 402 Process: pidSelector(pid), 403 }) 404 c.sandbox.setEnvdAuth(req, DefaultUser) 405 406 _, err := c.rpc.CloseStdin(ctx, req) 407 if err != nil { 408 return fmt.Errorf("close stdin: %w", err) 409 } 410 return nil 411 } 412 413 // Kill 终止指定进程。 414 func (c *Commands) Kill(ctx context.Context, pid uint32) error { 415 req := connect.NewRequest(&process.SendSignalRequest{ 416 Process: pidSelector(pid), 417 Signal: process.Signal_SIGNAL_SIGKILL, 418 }) 419 c.sandbox.setEnvdAuth(req, DefaultUser) 420 421 _, err := c.rpc.SendSignal(ctx, req) 422 if err != nil { 423 return fmt.Errorf("kill process: %w", err) 424 } 425 return nil 426 }