diff --git a/internal/api/handlers_exec.go b/internal/api/handlers_exec.go index f540e31..b7d2f94 100644 --- a/internal/api/handlers_exec.go +++ b/internal/api/handlers_exec.go @@ -29,9 +29,13 @@ func newExecHandler(db *db.Queries, pool *lifecycle.HostClientPool) *execHandler } type execRequest struct { - Cmd string `json:"cmd"` - Args []string `json:"args"` - TimeoutSec int32 `json:"timeout_sec"` + Cmd string `json:"cmd"` + Args []string `json:"args"` + TimeoutSec int32 `json:"timeout_sec"` + Background bool `json:"background"` + Tag string `json:"tag"` + Envs map[string]string `json:"envs"` + Cwd string `json:"cwd"` } type execResponse struct { @@ -45,6 +49,13 @@ type execResponse struct { Encoding string `json:"encoding"` } +type backgroundExecResponse struct { + SandboxID string `json:"sandbox_id"` + Cmd string `json:"cmd"` + PID uint32 `json:"pid"` + Tag string `json:"tag"` +} + // Exec handles POST /v1/capsules/{id}/exec. func (h *execHandler) Exec(w http.ResponseWriter, r *http.Request) { sandboxIDStr := chi.URLParam(r, "id") @@ -78,14 +89,54 @@ func (h *execHandler) Exec(w http.ResponseWriter, r *http.Request) { return } - start := time.Now() - agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) if err != nil { writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") return } + // Background mode: start process and return immediately. + if req.Background { + tag := req.Tag + if tag == "" { + tag = "proc-" + id.NewPtyTag() + } + + bgResp, err := agent.StartBackground(ctx, connect.NewRequest(&pb.StartBackgroundRequest{ + SandboxId: sandboxIDStr, + Tag: tag, + Cmd: req.Cmd, + Args: req.Args, + Envs: req.Envs, + Cwd: req.Cwd, + })) + if err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + if err := h.db.UpdateLastActive(ctx, db.UpdateLastActiveParams{ + ID: sandboxID, + LastActiveAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }); err != nil { + slog.Warn("failed to update last_active_at", "id", sandboxIDStr, "error", err) + } + + writeJSON(w, http.StatusAccepted, backgroundExecResponse{ + SandboxID: sandboxIDStr, + Cmd: req.Cmd, + PID: bgResp.Msg.Pid, + Tag: bgResp.Msg.Tag, + }) + return + } + + start := time.Now() + resp, err := agent.Exec(ctx, connect.NewRequest(&pb.ExecRequest{ SandboxId: sandboxIDStr, Cmd: req.Cmd, diff --git a/internal/api/handlers_process.go b/internal/api/handlers_process.go new file mode 100644 index 0000000..7c99221 --- /dev/null +++ b/internal/api/handlers_process.go @@ -0,0 +1,266 @@ +package api + +import ( + "context" + "log/slog" + "net/http" + "strconv" + "time" + + "connectrpc.com/connect" + "github.com/go-chi/chi/v5" + "github.com/gorilla/websocket" + "github.com/jackc/pgx/v5/pgtype" + + "git.omukk.dev/wrenn/wrenn/internal/auth" + "git.omukk.dev/wrenn/wrenn/internal/db" + "git.omukk.dev/wrenn/wrenn/internal/id" + "git.omukk.dev/wrenn/wrenn/internal/lifecycle" + pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen" +) + +type processHandler struct { + db *db.Queries + pool *lifecycle.HostClientPool +} + +func newProcessHandler(db *db.Queries, pool *lifecycle.HostClientPool) *processHandler { + return &processHandler{db: db, pool: pool} +} + +// processResponse is a single entry in the process list. +type processResponse struct { + PID uint32 `json:"pid"` + Tag string `json:"tag,omitempty"` + Cmd string `json:"cmd"` + Args []string `json:"args,omitempty"` +} + +// processListResponse wraps the list of processes. +type processListResponse struct { + Processes []processResponse `json:"processes"` +} + +// ListProcesses handles GET /v1/capsules/{id}/processes. +func (h *processHandler) ListProcesses(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running (status: "+sb.Status+")") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + resp, err := agent.ListProcesses(ctx, connect.NewRequest(&pb.ListProcessesRequest{ + SandboxId: sandboxIDStr, + })) + if err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + procs := make([]processResponse, 0, len(resp.Msg.Processes)) + for _, p := range resp.Msg.Processes { + procs = append(procs, processResponse{ + PID: p.Pid, + Tag: p.Tag, + Cmd: p.Cmd, + Args: p.Args, + }) + } + + writeJSON(w, http.StatusOK, processListResponse{Processes: procs}) +} + +// KillProcess handles DELETE /v1/capsules/{id}/processes/{selector}. +// The selector can be a numeric PID or a string tag. +func (h *processHandler) KillProcess(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + selectorStr := chi.URLParam(r, "selector") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running (status: "+sb.Status+")") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + // Build the kill request with PID or tag selector. + killReq := &pb.KillProcessRequest{ + SandboxId: sandboxIDStr, + Signal: "SIGKILL", + } + if sig := r.URL.Query().Get("signal"); sig == "SIGTERM" { + killReq.Signal = "SIGTERM" + } + + if pid, err := strconv.ParseUint(selectorStr, 10, 32); err == nil { + killReq.Selector = &pb.KillProcessRequest_Pid{Pid: uint32(pid)} + } else { + killReq.Selector = &pb.KillProcessRequest_Tag{Tag: selectorStr} + } + + if _, err := agent.KillProcess(ctx, connect.NewRequest(killReq)); err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// wsProcessOut is the JSON message sent to the WebSocket client. +type wsProcessOut struct { + Type string `json:"type"` // "start", "stdout", "stderr", "exit", "error" + PID uint32 `json:"pid,omitempty"` // only for "start" + Data string `json:"data,omitempty"` // only for "stdout", "stderr", "error" + ExitCode *int32 `json:"exit_code,omitempty"` // only for "exit" +} + +// ConnectProcess handles WS /v1/capsules/{id}/processes/{selector}/stream. +func (h *processHandler) ConnectProcess(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + selectorStr := chi.URLParam(r, "selector") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running (status: "+sb.Status+")") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + slog.Error("process stream websocket upgrade failed", "error", err) + return + } + defer conn.Close() + + // Build the connect request with PID or tag selector. + connectReq := &pb.ConnectProcessRequest{ + SandboxId: sandboxIDStr, + } + if pid, err := strconv.ParseUint(selectorStr, 10, 32); err == nil { + connectReq.Selector = &pb.ConnectProcessRequest_Pid{Pid: uint32(pid)} + } else { + connectReq.Selector = &pb.ConnectProcessRequest_Tag{Tag: selectorStr} + } + + streamCtx, cancel := context.WithCancel(ctx) + defer cancel() + + stream, err := agent.ConnectProcess(streamCtx, connect.NewRequest(connectReq)) + if err != nil { + sendProcessWSError(conn, "failed to connect to process: "+err.Error()) + return + } + defer stream.Close() + + // Listen for client disconnect in a goroutine. + go func() { + for { + _, _, err := conn.ReadMessage() + if err != nil { + cancel() + return + } + } + }() + + // Forward stream events to WebSocket. + for stream.Receive() { + resp := stream.Msg() + switch ev := resp.Event.(type) { + case *pb.ConnectProcessResponse_Start: + writeWSJSON(conn, wsProcessOut{Type: "start", PID: ev.Start.Pid}) + + case *pb.ConnectProcessResponse_Data: + switch o := ev.Data.Output.(type) { + case *pb.ExecStreamData_Stdout: + writeWSJSON(conn, wsProcessOut{Type: "stdout", Data: string(o.Stdout)}) + case *pb.ExecStreamData_Stderr: + writeWSJSON(conn, wsProcessOut{Type: "stderr", Data: string(o.Stderr)}) + } + + case *pb.ConnectProcessResponse_End: + exitCode := ev.End.ExitCode + writeWSJSON(conn, wsProcessOut{Type: "exit", ExitCode: &exitCode}) + } + } + + if err := stream.Err(); err != nil { + if streamCtx.Err() == nil { + sendProcessWSError(conn, err.Error()) + } + } + + // Update last active using a fresh context. + updateCtx, updateCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer updateCancel() + if err := h.db.UpdateLastActive(updateCtx, db.UpdateLastActiveParams{ + ID: sandboxID, + LastActiveAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }); err != nil { + slog.Warn("failed to update last active after process stream", "sandbox_id", sandboxIDStr, "error", err) + } +} + +func sendProcessWSError(conn *websocket.Conn, msg string) { + writeWSJSON(conn, wsProcessOut{Type: "error", Data: msg}) +} diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml index b6bd643..031cefd 100644 --- a/internal/api/openapi.yaml +++ b/internal/api/openapi.yaml @@ -699,11 +699,17 @@ paths: $ref: "#/components/schemas/ExecRequest" responses: "200": - description: Command output + description: Command output (foreground exec) content: application/json: schema: $ref: "#/components/schemas/ExecResponse" + "202": + description: Background process started + content: + application/json: + schema: + $ref: "#/components/schemas/BackgroundExecResponse" "404": description: Capsule not found content: @@ -717,6 +723,122 @@ paths: schema: $ref: "#/components/schemas/Error" + /v1/capsules/{id}/processes: + parameters: + - name: id + in: path + required: true + schema: + type: string + + get: + summary: List running processes + operationId: listProcesses + tags: [capsules] + security: + - apiKeyAuth: [] + description: | + Returns all running processes inside the capsule, including background + processes and any processes started by templates or init scripts. + responses: + "200": + description: Process list + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessListResponse" + "404": + description: Capsule not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Capsule not running + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/capsules/{id}/processes/{selector}: + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: selector + in: path + required: true + description: Process PID (numeric) or tag (string) + schema: + type: string + + delete: + summary: Kill a process + operationId: killProcess + tags: [capsules] + security: + - apiKeyAuth: [] + parameters: + - name: signal + in: query + required: false + description: Signal to send (SIGKILL or SIGTERM, default SIGKILL) + schema: + type: string + enum: [SIGKILL, SIGTERM] + default: SIGKILL + responses: + "204": + description: Process killed + "404": + description: Capsule or process not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Capsule not running + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/capsules/{id}/processes/{selector}/stream: + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: selector + in: path + required: true + description: Process PID (numeric) or tag (string) + schema: + type: string + + get: + summary: Stream process output via WebSocket + operationId: connectProcess + tags: [capsules] + security: + - apiKeyAuth: [] + description: | + Opens a WebSocket connection to stream stdout/stderr from a running + background process. The selector can be a numeric PID or a string tag. + + Server sends JSON messages: + - `{"type": "start", "pid": 42}` — connected to process + - `{"type": "stdout", "data": "..."}` — stdout output + - `{"type": "stderr", "data": "..."}` — stderr output + - `{"type": "exit", "exit_code": 0}` — process exited + - `{"type": "error", "data": "..."}` — error message + responses: + "101": + description: WebSocket upgrade + /v1/capsules/{id}/ping: parameters: - name: id @@ -2153,6 +2275,56 @@ components: timeout_sec: type: integer default: 30 + description: Timeout in seconds (foreground exec only, default 30) + background: + type: boolean + default: false + description: If true, starts the process in the background and returns immediately with a PID and tag (HTTP 202) + tag: + type: string + description: Optional user-chosen tag for the background process. Auto-generated if omitted. Only used when background is true. + envs: + type: object + additionalProperties: + type: string + description: Environment variables for the process (background exec only) + cwd: + type: string + description: Working directory for the process (background exec only) + + BackgroundExecResponse: + type: object + properties: + sandbox_id: + type: string + cmd: + type: string + pid: + type: integer + tag: + type: string + + ProcessEntry: + type: object + properties: + pid: + type: integer + tag: + type: string + cmd: + type: string + args: + type: array + items: + type: string + + ProcessListResponse: + type: object + properties: + processes: + type: array + items: + $ref: "#/components/schemas/ProcessEntry" ExecResponse: type: object diff --git a/internal/api/server.go b/internal/api/server.go index 582c2f5..1069d22 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -74,6 +74,7 @@ func New( buildH := newBuildHandler(buildSvc, queries, pool) channelH := newChannelHandler(channelSvc, al) ptyH := newPtyHandler(queries, pool) + processH := newProcessHandler(queries, pool) adminCapsules := newAdminCapsuleHandler(sandboxSvc, queries, pool, al) // OpenAPI spec and docs. @@ -141,6 +142,9 @@ func New( r.Post("/files/remove", fsH.Remove) r.Get("/metrics", metricsH.GetMetrics) r.Get("/pty", ptyH.PtySession) + r.Get("/processes", processH.ListProcesses) + r.Delete("/processes/{selector}", processH.KillProcess) + r.Get("/processes/{selector}/stream", processH.ConnectProcess) }) }) @@ -224,6 +228,9 @@ func New( r.Post("/files/remove", fsH.Remove) r.Get("/metrics", metricsH.GetMetrics) r.Get("/pty", ptyH.PtySession) + r.Get("/processes", processH.ListProcesses) + r.Delete("/processes/{selector}", processH.KillProcess) + r.Get("/processes/{selector}/stream", processH.ConnectProcess) }) }) diff --git a/internal/envdclient/process.go b/internal/envdclient/process.go new file mode 100644 index 0000000..adb807b --- /dev/null +++ b/internal/envdclient/process.go @@ -0,0 +1,187 @@ +package envdclient + +import ( + "context" + "fmt" + "io" + "log/slog" + + "connectrpc.com/connect" + + envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen" +) + +// ProcessInfo holds metadata about a running process inside the sandbox. +type ProcessInfo struct { + PID uint32 + Tag string + Cmd string + Args []string +} + +// StartBackground starts a process that runs independently of the RPC stream. +// It opens a Start stream, reads the first StartEvent to obtain the PID, +// then closes the stream. The process continues running inside the VM because +// envd binds it to context.Background(). +func (c *Client) StartBackground(ctx context.Context, tag, cmd string, args []string, envs map[string]string, cwd string) (uint32, error) { + stdin := false + cfg := &envdpb.ProcessConfig{ + Cmd: cmd, + Args: args, + Envs: envs, + } + if cwd != "" { + cfg.Cwd = &cwd + } + + req := connect.NewRequest(&envdpb.StartRequest{ + Process: cfg, + Tag: &tag, + Stdin: &stdin, + }) + + stream, err := c.process.Start(ctx, req) + if err != nil { + return 0, fmt.Errorf("start background process: %w", err) + } + defer stream.Close() + + // Read events until we get the StartEvent with the PID. + for stream.Receive() { + msg := stream.Msg() + if msg.Event == nil { + continue + } + if start, ok := msg.Event.GetEvent().(*envdpb.ProcessEvent_Start); ok { + return start.Start.GetPid(), nil + } + } + + if err := stream.Err(); err != nil && err != io.EOF { + return 0, fmt.Errorf("start background process stream: %w", err) + } + + return 0, fmt.Errorf("start background process: no start event received") +} + +// ConnectProcess re-attaches to a running process by PID or tag and returns +// a channel of streaming events. The channel is closed when the process ends +// or the context is cancelled. +func (c *Client) ConnectProcess(ctx context.Context, pid uint32, tag string) (<-chan ExecStreamEvent, error) { + var selector *envdpb.ProcessSelector + if tag != "" { + selector = &envdpb.ProcessSelector{ + Selector: &envdpb.ProcessSelector_Tag{Tag: tag}, + } + } else { + selector = &envdpb.ProcessSelector{ + Selector: &envdpb.ProcessSelector_Pid{Pid: pid}, + } + } + + stream, err := c.process.Connect(ctx, connect.NewRequest(&envdpb.ConnectRequest{ + Process: selector, + })) + if err != nil { + return nil, fmt.Errorf("connect process: %w", err) + } + + ch := make(chan ExecStreamEvent, 16) + go func() { + defer close(ch) + defer stream.Close() + + for stream.Receive() { + msg := stream.Msg() + if msg.Event == nil { + continue + } + + var ev ExecStreamEvent + switch e := msg.Event.GetEvent().(type) { + case *envdpb.ProcessEvent_Start: + ev = ExecStreamEvent{Type: "start", PID: e.Start.GetPid()} + + case *envdpb.ProcessEvent_Data: + switch o := e.Data.GetOutput().(type) { + case *envdpb.ProcessEvent_DataEvent_Stdout: + ev = ExecStreamEvent{Type: "stdout", Data: o.Stdout} + case *envdpb.ProcessEvent_DataEvent_Stderr: + ev = ExecStreamEvent{Type: "stderr", Data: o.Stderr} + default: + continue + } + + case *envdpb.ProcessEvent_End: + ev = ExecStreamEvent{Type: "end", ExitCode: e.End.GetExitCode()} + if e.End.Error != nil { + ev.Error = e.End.GetError() + } + + case *envdpb.ProcessEvent_Keepalive: + continue + } + + select { + case ch <- ev: + case <-ctx.Done(): + return + } + } + + if err := stream.Err(); err != nil && err != io.EOF { + slog.Debug("connect process stream error", "error", err) + } + }() + + return ch, nil +} + +// ListProcesses returns all running processes inside the sandbox. +func (c *Client) ListProcesses(ctx context.Context) ([]ProcessInfo, error) { + resp, err := c.process.List(ctx, connect.NewRequest(&envdpb.ListRequest{})) + if err != nil { + return nil, fmt.Errorf("list processes: %w", err) + } + + procs := make([]ProcessInfo, 0, len(resp.Msg.Processes)) + for _, p := range resp.Msg.Processes { + info := ProcessInfo{ + PID: p.Pid, + } + if p.Tag != nil { + info.Tag = *p.Tag + } + if p.Config != nil { + info.Cmd = p.Config.Cmd + info.Args = p.Config.Args + } + procs = append(procs, info) + } + + return procs, nil +} + +// KillProcess sends a signal to a process identified by PID or tag. +func (c *Client) KillProcess(ctx context.Context, pid uint32, tag string, signal envdpb.Signal) error { + var selector *envdpb.ProcessSelector + if tag != "" { + selector = &envdpb.ProcessSelector{ + Selector: &envdpb.ProcessSelector_Tag{Tag: tag}, + } + } else { + selector = &envdpb.ProcessSelector{ + Selector: &envdpb.ProcessSelector_Pid{Pid: pid}, + } + } + + _, err := c.process.SendSignal(ctx, connect.NewRequest(&envdpb.SendSignalRequest{ + Process: selector, + Signal: signal, + })) + if err != nil { + return fmt.Errorf("kill process: %w", err) + } + + return nil +} diff --git a/internal/hostagent/server.go b/internal/hostagent/server.go index ff13954..7b88965 100644 --- a/internal/hostagent/server.go +++ b/internal/hostagent/server.go @@ -752,3 +752,152 @@ func entryInfoToPB(e *envdpb.EntryInfo) *pb.FileEntry { return entry } + +// ── Background Processes ──────────────────────────────────────────── + +func (s *Server) StartBackground( + ctx context.Context, + req *connect.Request[pb.StartBackgroundRequest], +) (*connect.Response[pb.StartBackgroundResponse], error) { + msg := req.Msg + + pid, err := s.mgr.StartBackground(ctx, msg.SandboxId, msg.Tag, msg.Cmd, msg.Args, msg.Envs, msg.Cwd) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("start background: %w", err)) + } + + return connect.NewResponse(&pb.StartBackgroundResponse{ + Pid: pid, + Tag: msg.Tag, + }), nil +} + +func (s *Server) ListProcesses( + ctx context.Context, + req *connect.Request[pb.ListProcessesRequest], +) (*connect.Response[pb.ListProcessesResponse], error) { + procs, err := s.mgr.ListProcesses(ctx, req.Msg.SandboxId) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("list processes: %w", err)) + } + + entries := make([]*pb.ProcessEntry, 0, len(procs)) + for _, p := range procs { + entries = append(entries, &pb.ProcessEntry{ + Pid: p.PID, + Tag: p.Tag, + Cmd: p.Cmd, + Args: p.Args, + }) + } + + return connect.NewResponse(&pb.ListProcessesResponse{ + Processes: entries, + }), nil +} + +func (s *Server) KillProcess( + ctx context.Context, + req *connect.Request[pb.KillProcessRequest], +) (*connect.Response[pb.KillProcessResponse], error) { + msg := req.Msg + + // Resolve PID/tag selector. + var pid uint32 + var tag string + switch sel := msg.Selector.(type) { + case *pb.KillProcessRequest_Pid: + pid = sel.Pid + case *pb.KillProcessRequest_Tag: + tag = sel.Tag + default: + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("pid or tag is required")) + } + + // Map signal string to envd enum. + var signal envdpb.Signal + switch msg.Signal { + case "", "SIGKILL": + signal = envdpb.Signal_SIGNAL_SIGKILL + case "SIGTERM": + signal = envdpb.Signal_SIGNAL_SIGTERM + default: + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unsupported signal: %s (use SIGKILL or SIGTERM)", msg.Signal)) + } + + if err := s.mgr.KillProcess(ctx, msg.SandboxId, pid, tag, signal); err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("kill process: %w", err)) + } + + return connect.NewResponse(&pb.KillProcessResponse{}), nil +} + +func (s *Server) ConnectProcess( + ctx context.Context, + req *connect.Request[pb.ConnectProcessRequest], + stream *connect.ServerStream[pb.ConnectProcessResponse], +) error { + msg := req.Msg + + var pid uint32 + var tag string + switch sel := msg.Selector.(type) { + case *pb.ConnectProcessRequest_Pid: + pid = sel.Pid + case *pb.ConnectProcessRequest_Tag: + tag = sel.Tag + default: + return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("pid or tag is required")) + } + + events, err := s.mgr.ConnectProcess(ctx, msg.SandboxId, pid, tag) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return connect.NewError(connect.CodeNotFound, err) + } + return connect.NewError(connect.CodeInternal, fmt.Errorf("connect process: %w", err)) + } + + for ev := range events { + var resp pb.ConnectProcessResponse + switch ev.Type { + case "start": + resp.Event = &pb.ConnectProcessResponse_Start{ + Start: &pb.ExecStreamStart{Pid: ev.PID}, + } + case "stdout": + resp.Event = &pb.ConnectProcessResponse_Data{ + Data: &pb.ExecStreamData{ + Output: &pb.ExecStreamData_Stdout{Stdout: ev.Data}, + }, + } + case "stderr": + resp.Event = &pb.ConnectProcessResponse_Data{ + Data: &pb.ExecStreamData{ + Output: &pb.ExecStreamData_Stderr{Stderr: ev.Data}, + }, + } + case "end": + resp.Event = &pb.ConnectProcessResponse_End{ + End: &pb.ExecStreamEnd{ + ExitCode: ev.ExitCode, + Error: ev.Error, + }, + } + } + if err := stream.Send(&resp); err != nil { + return err + } + } + + return nil +} diff --git a/internal/sandbox/manager.go b/internal/sandbox/manager.go index 206b5fb..3dde053 100644 --- a/internal/sandbox/manager.go +++ b/internal/sandbox/manager.go @@ -24,12 +24,13 @@ import ( "git.omukk.dev/wrenn/wrenn/internal/snapshot" "git.omukk.dev/wrenn/wrenn/internal/uffd" "git.omukk.dev/wrenn/wrenn/internal/vm" + envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen" ) // Config holds the paths and defaults for the sandbox manager. type Config struct { - WrennDir string // root directory (e.g. /var/lib/wrenn); all sub-paths derived via layout package - EnvdTimeout time.Duration + WrennDir string // root directory (e.g. /var/lib/wrenn); all sub-paths derived via layout package + EnvdTimeout time.Duration DefaultRootfsSizeMB int // target size for template rootfs images; 0 → DefaultDiskSizeMB } @@ -1328,6 +1329,74 @@ func (m *Manager) PtyKill(ctx context.Context, sandboxID, tag string) error { return sb.client.PtyKill(ctx, tag) } +// StartBackground starts a background process inside a sandbox. +func (m *Manager) StartBackground(ctx context.Context, sandboxID, tag, cmd string, args []string, envs map[string]string, cwd string) (uint32, error) { + sb, err := m.get(sandboxID) + if err != nil { + return 0, err + } + if sb.Status != models.StatusRunning { + return 0, fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status) + } + + m.mu.Lock() + sb.LastActiveAt = time.Now() + m.mu.Unlock() + + return sb.client.StartBackground(ctx, tag, cmd, args, envs, cwd) +} + +// ConnectProcess re-attaches to a running process inside a sandbox. +func (m *Manager) ConnectProcess(ctx context.Context, sandboxID string, pid uint32, tag string) (<-chan envdclient.ExecStreamEvent, error) { + sb, err := m.get(sandboxID) + if err != nil { + return nil, err + } + if sb.Status != models.StatusRunning { + return nil, fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status) + } + + m.mu.Lock() + sb.LastActiveAt = time.Now() + m.mu.Unlock() + + return sb.client.ConnectProcess(ctx, pid, tag) +} + +// ListProcesses returns all running processes inside a sandbox. +func (m *Manager) ListProcesses(ctx context.Context, sandboxID string) ([]envdclient.ProcessInfo, error) { + sb, err := m.get(sandboxID) + if err != nil { + return nil, err + } + if sb.Status != models.StatusRunning { + return nil, fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status) + } + + m.mu.Lock() + sb.LastActiveAt = time.Now() + m.mu.Unlock() + + return sb.client.ListProcesses(ctx) +} + +// KillProcess sends a signal to a process inside a sandbox. +func (m *Manager) KillProcess(ctx context.Context, sandboxID string, pid uint32, tag string, signal envdpb.Signal) error { + sb, err := m.get(sandboxID) + if err != nil { + return err + } + if sb.Status != models.StatusRunning { + return fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status) + } + + m.mu.Lock() + sb.LastActiveAt = time.Now() + m.mu.Unlock() + + return sb.client.KillProcess(ctx, pid, tag, signal) +} + // AcquireProxyConn atomically looks up a sandbox by ID and registers an // in-flight proxy connection. Returns the sandbox's host-reachable IP, the // connection tracker, and true on success. The caller must call diff --git a/proto/hostagent/gen/hostagent.pb.go b/proto/hostagent/gen/hostagent.pb.go index ac864ff..fc6b2e0 100644 --- a/proto/hostagent/gen/hostagent.pb.go +++ b/proto/hostagent/gen/hostagent.pb.go @@ -3461,6 +3461,623 @@ func (*PtyKillResponse) Descriptor() ([]byte, []int) { return file_hostagent_proto_rawDescGZIP(), []int{59} } +type StartBackgroundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Cmd string `protobuf:"bytes,2,opt,name=cmd,proto3" json:"cmd,omitempty"` + Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` + // User-chosen tag for the process. If empty, the host agent generates one. + Tag string `protobuf:"bytes,4,opt,name=tag,proto3" json:"tag,omitempty"` + Envs map[string]string `protobuf:"bytes,5,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Cwd string `protobuf:"bytes,6,opt,name=cwd,proto3" json:"cwd,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartBackgroundRequest) Reset() { + *x = StartBackgroundRequest{} + mi := &file_hostagent_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartBackgroundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartBackgroundRequest) ProtoMessage() {} + +func (x *StartBackgroundRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartBackgroundRequest.ProtoReflect.Descriptor instead. +func (*StartBackgroundRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{60} +} + +func (x *StartBackgroundRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *StartBackgroundRequest) GetCmd() string { + if x != nil { + return x.Cmd + } + return "" +} + +func (x *StartBackgroundRequest) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *StartBackgroundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *StartBackgroundRequest) GetEnvs() map[string]string { + if x != nil { + return x.Envs + } + return nil +} + +func (x *StartBackgroundRequest) GetCwd() string { + if x != nil { + return x.Cwd + } + return "" +} + +type StartBackgroundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pid uint32 `protobuf:"varint,1,opt,name=pid,proto3" json:"pid,omitempty"` + Tag string `protobuf:"bytes,2,opt,name=tag,proto3" json:"tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartBackgroundResponse) Reset() { + *x = StartBackgroundResponse{} + mi := &file_hostagent_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartBackgroundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartBackgroundResponse) ProtoMessage() {} + +func (x *StartBackgroundResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartBackgroundResponse.ProtoReflect.Descriptor instead. +func (*StartBackgroundResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{61} +} + +func (x *StartBackgroundResponse) GetPid() uint32 { + if x != nil { + return x.Pid + } + return 0 +} + +func (x *StartBackgroundResponse) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type ListProcessesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProcessesRequest) Reset() { + *x = ListProcessesRequest{} + mi := &file_hostagent_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProcessesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProcessesRequest) ProtoMessage() {} + +func (x *ListProcessesRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProcessesRequest.ProtoReflect.Descriptor instead. +func (*ListProcessesRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{62} +} + +func (x *ListProcessesRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +type ProcessEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pid uint32 `protobuf:"varint,1,opt,name=pid,proto3" json:"pid,omitempty"` + Tag string `protobuf:"bytes,2,opt,name=tag,proto3" json:"tag,omitempty"` + Cmd string `protobuf:"bytes,3,opt,name=cmd,proto3" json:"cmd,omitempty"` + Args []string `protobuf:"bytes,4,rep,name=args,proto3" json:"args,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProcessEntry) Reset() { + *x = ProcessEntry{} + mi := &file_hostagent_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProcessEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProcessEntry) ProtoMessage() {} + +func (x *ProcessEntry) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProcessEntry.ProtoReflect.Descriptor instead. +func (*ProcessEntry) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{63} +} + +func (x *ProcessEntry) GetPid() uint32 { + if x != nil { + return x.Pid + } + return 0 +} + +func (x *ProcessEntry) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *ProcessEntry) GetCmd() string { + if x != nil { + return x.Cmd + } + return "" +} + +func (x *ProcessEntry) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +type ListProcessesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Processes []*ProcessEntry `protobuf:"bytes,1,rep,name=processes,proto3" json:"processes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProcessesResponse) Reset() { + *x = ListProcessesResponse{} + mi := &file_hostagent_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProcessesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProcessesResponse) ProtoMessage() {} + +func (x *ListProcessesResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProcessesResponse.ProtoReflect.Descriptor instead. +func (*ListProcessesResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{64} +} + +func (x *ListProcessesResponse) GetProcesses() []*ProcessEntry { + if x != nil { + return x.Processes + } + return nil +} + +type KillProcessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Types that are valid to be assigned to Selector: + // + // *KillProcessRequest_Pid + // *KillProcessRequest_Tag + Selector isKillProcessRequest_Selector `protobuf_oneof:"selector"` + // Signal to send: "SIGTERM" or "SIGKILL" (default: "SIGKILL"). + Signal string `protobuf:"bytes,4,opt,name=signal,proto3" json:"signal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KillProcessRequest) Reset() { + *x = KillProcessRequest{} + mi := &file_hostagent_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KillProcessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KillProcessRequest) ProtoMessage() {} + +func (x *KillProcessRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KillProcessRequest.ProtoReflect.Descriptor instead. +func (*KillProcessRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{65} +} + +func (x *KillProcessRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *KillProcessRequest) GetSelector() isKillProcessRequest_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *KillProcessRequest) GetPid() uint32 { + if x != nil { + if x, ok := x.Selector.(*KillProcessRequest_Pid); ok { + return x.Pid + } + } + return 0 +} + +func (x *KillProcessRequest) GetTag() string { + if x != nil { + if x, ok := x.Selector.(*KillProcessRequest_Tag); ok { + return x.Tag + } + } + return "" +} + +func (x *KillProcessRequest) GetSignal() string { + if x != nil { + return x.Signal + } + return "" +} + +type isKillProcessRequest_Selector interface { + isKillProcessRequest_Selector() +} + +type KillProcessRequest_Pid struct { + Pid uint32 `protobuf:"varint,2,opt,name=pid,proto3,oneof"` +} + +type KillProcessRequest_Tag struct { + Tag string `protobuf:"bytes,3,opt,name=tag,proto3,oneof"` +} + +func (*KillProcessRequest_Pid) isKillProcessRequest_Selector() {} + +func (*KillProcessRequest_Tag) isKillProcessRequest_Selector() {} + +type KillProcessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KillProcessResponse) Reset() { + *x = KillProcessResponse{} + mi := &file_hostagent_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KillProcessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KillProcessResponse) ProtoMessage() {} + +func (x *KillProcessResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KillProcessResponse.ProtoReflect.Descriptor instead. +func (*KillProcessResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{66} +} + +type ConnectProcessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Types that are valid to be assigned to Selector: + // + // *ConnectProcessRequest_Pid + // *ConnectProcessRequest_Tag + Selector isConnectProcessRequest_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectProcessRequest) Reset() { + *x = ConnectProcessRequest{} + mi := &file_hostagent_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectProcessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectProcessRequest) ProtoMessage() {} + +func (x *ConnectProcessRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectProcessRequest.ProtoReflect.Descriptor instead. +func (*ConnectProcessRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{67} +} + +func (x *ConnectProcessRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ConnectProcessRequest) GetSelector() isConnectProcessRequest_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *ConnectProcessRequest) GetPid() uint32 { + if x != nil { + if x, ok := x.Selector.(*ConnectProcessRequest_Pid); ok { + return x.Pid + } + } + return 0 +} + +func (x *ConnectProcessRequest) GetTag() string { + if x != nil { + if x, ok := x.Selector.(*ConnectProcessRequest_Tag); ok { + return x.Tag + } + } + return "" +} + +type isConnectProcessRequest_Selector interface { + isConnectProcessRequest_Selector() +} + +type ConnectProcessRequest_Pid struct { + Pid uint32 `protobuf:"varint,2,opt,name=pid,proto3,oneof"` +} + +type ConnectProcessRequest_Tag struct { + Tag string `protobuf:"bytes,3,opt,name=tag,proto3,oneof"` +} + +func (*ConnectProcessRequest_Pid) isConnectProcessRequest_Selector() {} + +func (*ConnectProcessRequest_Tag) isConnectProcessRequest_Selector() {} + +// Reuses ExecStream event types for symmetry. +type ConnectProcessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *ConnectProcessResponse_Start + // *ConnectProcessResponse_Data + // *ConnectProcessResponse_End + Event isConnectProcessResponse_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectProcessResponse) Reset() { + *x = ConnectProcessResponse{} + mi := &file_hostagent_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectProcessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectProcessResponse) ProtoMessage() {} + +func (x *ConnectProcessResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectProcessResponse.ProtoReflect.Descriptor instead. +func (*ConnectProcessResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{68} +} + +func (x *ConnectProcessResponse) GetEvent() isConnectProcessResponse_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *ConnectProcessResponse) GetStart() *ExecStreamStart { + if x != nil { + if x, ok := x.Event.(*ConnectProcessResponse_Start); ok { + return x.Start + } + } + return nil +} + +func (x *ConnectProcessResponse) GetData() *ExecStreamData { + if x != nil { + if x, ok := x.Event.(*ConnectProcessResponse_Data); ok { + return x.Data + } + } + return nil +} + +func (x *ConnectProcessResponse) GetEnd() *ExecStreamEnd { + if x != nil { + if x, ok := x.Event.(*ConnectProcessResponse_End); ok { + return x.End + } + } + return nil +} + +type isConnectProcessResponse_Event interface { + isConnectProcessResponse_Event() +} + +type ConnectProcessResponse_Start struct { + Start *ExecStreamStart `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +} + +type ConnectProcessResponse_Data struct { + Data *ExecStreamData `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +type ConnectProcessResponse_End struct { + End *ExecStreamEnd `protobuf:"bytes,3,opt,name=end,proto3,oneof"` +} + +func (*ConnectProcessResponse_Start) isConnectProcessResponse_Event() {} + +func (*ConnectProcessResponse_Data) isConnectProcessResponse_Event() {} + +func (*ConnectProcessResponse_End) isConnectProcessResponse_Event() {} + var File_hostagent_proto protoreflect.FileDescriptor const file_hostagent_proto_rawDesc = "" + @@ -3725,7 +4342,52 @@ const file_hostagent_proto_rawDesc = "" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" + "\x03tag\x18\x02 \x01(\tR\x03tag\"\x11\n" + - "\x0fPtyKillResponse2\xe6\x10\n" + + "\x0fPtyKillResponse\"\xfe\x01\n" + + "\x16StartBackgroundRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" + + "\x03cmd\x18\x02 \x01(\tR\x03cmd\x12\x12\n" + + "\x04args\x18\x03 \x03(\tR\x04args\x12\x10\n" + + "\x03tag\x18\x04 \x01(\tR\x03tag\x12B\n" + + "\x04envs\x18\x05 \x03(\v2..hostagent.v1.StartBackgroundRequest.EnvsEntryR\x04envs\x12\x10\n" + + "\x03cwd\x18\x06 \x01(\tR\x03cwd\x1a7\n" + + "\tEnvsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"=\n" + + "\x17StartBackgroundResponse\x12\x10\n" + + "\x03pid\x18\x01 \x01(\rR\x03pid\x12\x10\n" + + "\x03tag\x18\x02 \x01(\tR\x03tag\"5\n" + + "\x14ListProcessesRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"X\n" + + "\fProcessEntry\x12\x10\n" + + "\x03pid\x18\x01 \x01(\rR\x03pid\x12\x10\n" + + "\x03tag\x18\x02 \x01(\tR\x03tag\x12\x10\n" + + "\x03cmd\x18\x03 \x01(\tR\x03cmd\x12\x12\n" + + "\x04args\x18\x04 \x03(\tR\x04args\"Q\n" + + "\x15ListProcessesResponse\x128\n" + + "\tprocesses\x18\x01 \x03(\v2\x1a.hostagent.v1.ProcessEntryR\tprocesses\"\x7f\n" + + "\x12KillProcessRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x03pid\x18\x02 \x01(\rH\x00R\x03pid\x12\x12\n" + + "\x03tag\x18\x03 \x01(\tH\x00R\x03tag\x12\x16\n" + + "\x06signal\x18\x04 \x01(\tR\x06signalB\n" + + "\n" + + "\bselector\"\x15\n" + + "\x13KillProcessResponse\"j\n" + + "\x15ConnectProcessRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x03pid\x18\x02 \x01(\rH\x00R\x03pid\x12\x12\n" + + "\x03tag\x18\x03 \x01(\tH\x00R\x03tagB\n" + + "\n" + + "\bselector\"\xbd\x01\n" + + "\x16ConnectProcessResponse\x125\n" + + "\x05start\x18\x01 \x01(\v2\x1d.hostagent.v1.ExecStreamStartH\x00R\x05start\x122\n" + + "\x04data\x18\x02 \x01(\v2\x1c.hostagent.v1.ExecStreamDataH\x00R\x04data\x12/\n" + + "\x03end\x18\x03 \x01(\v2\x1b.hostagent.v1.ExecStreamEndH\x00R\x03endB\a\n" + + "\x05event2\xd3\x13\n" + "\x10HostAgentService\x12X\n" + "\rCreateSandbox\x12\".hostagent.v1.CreateSandboxRequest\x1a#.hostagent.v1.CreateSandboxResponse\x12[\n" + "\x0eDestroySandbox\x12#.hostagent.v1.DestroySandboxRequest\x1a$.hostagent.v1.DestroySandboxResponse\x12U\n" + @@ -3753,7 +4415,11 @@ const file_hostagent_proto_rawDesc = "" + "\tPtyAttach\x12\x1e.hostagent.v1.PtyAttachRequest\x1a\x1f.hostagent.v1.PtyAttachResponse0\x01\x12U\n" + "\fPtySendInput\x12!.hostagent.v1.PtySendInputRequest\x1a\".hostagent.v1.PtySendInputResponse\x12L\n" + "\tPtyResize\x12\x1e.hostagent.v1.PtyResizeRequest\x1a\x1f.hostagent.v1.PtyResizeResponse\x12F\n" + - "\aPtyKill\x12\x1c.hostagent.v1.PtyKillRequest\x1a\x1d.hostagent.v1.PtyKillResponseB\xae\x01\n" + + "\aPtyKill\x12\x1c.hostagent.v1.PtyKillRequest\x1a\x1d.hostagent.v1.PtyKillResponse\x12^\n" + + "\x0fStartBackground\x12$.hostagent.v1.StartBackgroundRequest\x1a%.hostagent.v1.StartBackgroundResponse\x12X\n" + + "\rListProcesses\x12\".hostagent.v1.ListProcessesRequest\x1a#.hostagent.v1.ListProcessesResponse\x12R\n" + + "\vKillProcess\x12 .hostagent.v1.KillProcessRequest\x1a!.hostagent.v1.KillProcessResponse\x12]\n" + + "\x0eConnectProcess\x12#.hostagent.v1.ConnectProcessRequest\x1a$.hostagent.v1.ConnectProcessResponse0\x01B\xae\x01\n" + "\x10com.hostagent.v1B\x0eHostagentProtoP\x01Z9git.omukk.dev/wrenn/wrenn/proto/hostagent/gen;hostagentv1\xa2\x02\x03HXX\xaa\x02\fHostagent.V1\xca\x02\fHostagent\\V1\xe2\x02\x18Hostagent\\V1\\GPBMetadata\xea\x02\rHostagent::V1b\x06proto3" var ( @@ -3768,7 +4434,7 @@ func file_hostagent_proto_rawDescGZIP() []byte { return file_hostagent_proto_rawDescData } -var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 63) +var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 73) var file_hostagent_proto_goTypes = []any{ (*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest (*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse @@ -3830,13 +4496,23 @@ var file_hostagent_proto_goTypes = []any{ (*PtyResizeResponse)(nil), // 57: hostagent.v1.PtyResizeResponse (*PtyKillRequest)(nil), // 58: hostagent.v1.PtyKillRequest (*PtyKillResponse)(nil), // 59: hostagent.v1.PtyKillResponse - nil, // 60: hostagent.v1.CreateSandboxRequest.DefaultEnvEntry - nil, // 61: hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry - nil, // 62: hostagent.v1.PtyAttachRequest.EnvsEntry + (*StartBackgroundRequest)(nil), // 60: hostagent.v1.StartBackgroundRequest + (*StartBackgroundResponse)(nil), // 61: hostagent.v1.StartBackgroundResponse + (*ListProcessesRequest)(nil), // 62: hostagent.v1.ListProcessesRequest + (*ProcessEntry)(nil), // 63: hostagent.v1.ProcessEntry + (*ListProcessesResponse)(nil), // 64: hostagent.v1.ListProcessesResponse + (*KillProcessRequest)(nil), // 65: hostagent.v1.KillProcessRequest + (*KillProcessResponse)(nil), // 66: hostagent.v1.KillProcessResponse + (*ConnectProcessRequest)(nil), // 67: hostagent.v1.ConnectProcessRequest + (*ConnectProcessResponse)(nil), // 68: hostagent.v1.ConnectProcessResponse + nil, // 69: hostagent.v1.CreateSandboxRequest.DefaultEnvEntry + nil, // 70: hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry + nil, // 71: hostagent.v1.PtyAttachRequest.EnvsEntry + nil, // 72: hostagent.v1.StartBackgroundRequest.EnvsEntry } var file_hostagent_proto_depIdxs = []int32{ - 60, // 0: hostagent.v1.CreateSandboxRequest.default_env:type_name -> hostagent.v1.CreateSandboxRequest.DefaultEnvEntry - 61, // 1: hostagent.v1.ResumeSandboxRequest.default_env:type_name -> hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry + 69, // 0: hostagent.v1.CreateSandboxRequest.default_env:type_name -> hostagent.v1.CreateSandboxRequest.DefaultEnvEntry + 70, // 1: hostagent.v1.ResumeSandboxRequest.default_env:type_name -> hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry 16, // 2: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo 23, // 3: hostagent.v1.ExecStreamResponse.start:type_name -> hostagent.v1.ExecStreamStart 24, // 4: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData @@ -3848,65 +4524,78 @@ var file_hostagent_proto_depIdxs = []int32{ 42, // 10: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint 42, // 11: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint 42, // 12: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint - 62, // 13: hostagent.v1.PtyAttachRequest.envs:type_name -> hostagent.v1.PtyAttachRequest.EnvsEntry + 71, // 13: hostagent.v1.PtyAttachRequest.envs:type_name -> hostagent.v1.PtyAttachRequest.EnvsEntry 51, // 14: hostagent.v1.PtyAttachResponse.started:type_name -> hostagent.v1.PtyStarted 52, // 15: hostagent.v1.PtyAttachResponse.output:type_name -> hostagent.v1.PtyOutput 53, // 16: hostagent.v1.PtyAttachResponse.exited:type_name -> hostagent.v1.PtyExited - 0, // 17: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest - 2, // 18: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest - 4, // 19: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest - 6, // 20: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest - 12, // 21: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest - 14, // 22: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest - 17, // 23: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest - 19, // 24: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest - 31, // 25: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest - 34, // 26: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest - 36, // 27: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest - 8, // 28: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest - 10, // 29: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest - 21, // 30: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest - 26, // 31: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest - 29, // 32: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest - 38, // 33: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest - 40, // 34: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest - 43, // 35: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest - 45, // 36: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest - 47, // 37: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest - 49, // 38: hostagent.v1.HostAgentService.PtyAttach:input_type -> hostagent.v1.PtyAttachRequest - 54, // 39: hostagent.v1.HostAgentService.PtySendInput:input_type -> hostagent.v1.PtySendInputRequest - 56, // 40: hostagent.v1.HostAgentService.PtyResize:input_type -> hostagent.v1.PtyResizeRequest - 58, // 41: hostagent.v1.HostAgentService.PtyKill:input_type -> hostagent.v1.PtyKillRequest - 1, // 42: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse - 3, // 43: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse - 5, // 44: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse - 7, // 45: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse - 13, // 46: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse - 15, // 47: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse - 18, // 48: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse - 20, // 49: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse - 32, // 50: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse - 35, // 51: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse - 37, // 52: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse - 9, // 53: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse - 11, // 54: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse - 22, // 55: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse - 28, // 56: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse - 30, // 57: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse - 39, // 58: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse - 41, // 59: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse - 44, // 60: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse - 46, // 61: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse - 48, // 62: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse - 50, // 63: hostagent.v1.HostAgentService.PtyAttach:output_type -> hostagent.v1.PtyAttachResponse - 55, // 64: hostagent.v1.HostAgentService.PtySendInput:output_type -> hostagent.v1.PtySendInputResponse - 57, // 65: hostagent.v1.HostAgentService.PtyResize:output_type -> hostagent.v1.PtyResizeResponse - 59, // 66: hostagent.v1.HostAgentService.PtyKill:output_type -> hostagent.v1.PtyKillResponse - 42, // [42:67] is the sub-list for method output_type - 17, // [17:42] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 72, // 17: hostagent.v1.StartBackgroundRequest.envs:type_name -> hostagent.v1.StartBackgroundRequest.EnvsEntry + 63, // 18: hostagent.v1.ListProcessesResponse.processes:type_name -> hostagent.v1.ProcessEntry + 23, // 19: hostagent.v1.ConnectProcessResponse.start:type_name -> hostagent.v1.ExecStreamStart + 24, // 20: hostagent.v1.ConnectProcessResponse.data:type_name -> hostagent.v1.ExecStreamData + 25, // 21: hostagent.v1.ConnectProcessResponse.end:type_name -> hostagent.v1.ExecStreamEnd + 0, // 22: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest + 2, // 23: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest + 4, // 24: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest + 6, // 25: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest + 12, // 26: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest + 14, // 27: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest + 17, // 28: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest + 19, // 29: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest + 31, // 30: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest + 34, // 31: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest + 36, // 32: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest + 8, // 33: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest + 10, // 34: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest + 21, // 35: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest + 26, // 36: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest + 29, // 37: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest + 38, // 38: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest + 40, // 39: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest + 43, // 40: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest + 45, // 41: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest + 47, // 42: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest + 49, // 43: hostagent.v1.HostAgentService.PtyAttach:input_type -> hostagent.v1.PtyAttachRequest + 54, // 44: hostagent.v1.HostAgentService.PtySendInput:input_type -> hostagent.v1.PtySendInputRequest + 56, // 45: hostagent.v1.HostAgentService.PtyResize:input_type -> hostagent.v1.PtyResizeRequest + 58, // 46: hostagent.v1.HostAgentService.PtyKill:input_type -> hostagent.v1.PtyKillRequest + 60, // 47: hostagent.v1.HostAgentService.StartBackground:input_type -> hostagent.v1.StartBackgroundRequest + 62, // 48: hostagent.v1.HostAgentService.ListProcesses:input_type -> hostagent.v1.ListProcessesRequest + 65, // 49: hostagent.v1.HostAgentService.KillProcess:input_type -> hostagent.v1.KillProcessRequest + 67, // 50: hostagent.v1.HostAgentService.ConnectProcess:input_type -> hostagent.v1.ConnectProcessRequest + 1, // 51: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse + 3, // 52: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse + 5, // 53: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse + 7, // 54: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse + 13, // 55: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse + 15, // 56: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse + 18, // 57: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse + 20, // 58: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse + 32, // 59: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse + 35, // 60: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse + 37, // 61: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse + 9, // 62: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse + 11, // 63: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse + 22, // 64: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse + 28, // 65: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse + 30, // 66: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse + 39, // 67: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse + 41, // 68: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse + 44, // 69: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse + 46, // 70: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse + 48, // 71: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse + 50, // 72: hostagent.v1.HostAgentService.PtyAttach:output_type -> hostagent.v1.PtyAttachResponse + 55, // 73: hostagent.v1.HostAgentService.PtySendInput:output_type -> hostagent.v1.PtySendInputResponse + 57, // 74: hostagent.v1.HostAgentService.PtyResize:output_type -> hostagent.v1.PtyResizeResponse + 59, // 75: hostagent.v1.HostAgentService.PtyKill:output_type -> hostagent.v1.PtyKillResponse + 61, // 76: hostagent.v1.HostAgentService.StartBackground:output_type -> hostagent.v1.StartBackgroundResponse + 64, // 77: hostagent.v1.HostAgentService.ListProcesses:output_type -> hostagent.v1.ListProcessesResponse + 66, // 78: hostagent.v1.HostAgentService.KillProcess:output_type -> hostagent.v1.KillProcessResponse + 68, // 79: hostagent.v1.HostAgentService.ConnectProcess:output_type -> hostagent.v1.ConnectProcessResponse + 51, // [51:80] is the sub-list for method output_type + 22, // [22:51] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_hostagent_proto_init() } @@ -3933,13 +4622,26 @@ func file_hostagent_proto_init() { (*PtyAttachResponse_Output)(nil), (*PtyAttachResponse_Exited)(nil), } + file_hostagent_proto_msgTypes[65].OneofWrappers = []any{ + (*KillProcessRequest_Pid)(nil), + (*KillProcessRequest_Tag)(nil), + } + file_hostagent_proto_msgTypes[67].OneofWrappers = []any{ + (*ConnectProcessRequest_Pid)(nil), + (*ConnectProcessRequest_Tag)(nil), + } + file_hostagent_proto_msgTypes[68].OneofWrappers = []any{ + (*ConnectProcessResponse_Start)(nil), + (*ConnectProcessResponse_Data)(nil), + (*ConnectProcessResponse_End)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)), NumEnums: 0, - NumMessages: 63, + NumMessages: 73, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go b/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go index 0a91ab6..16f30ad 100644 --- a/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go +++ b/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go @@ -107,6 +107,18 @@ const ( // HostAgentServicePtyKillProcedure is the fully-qualified name of the HostAgentService's PtyKill // RPC. HostAgentServicePtyKillProcedure = "/hostagent.v1.HostAgentService/PtyKill" + // HostAgentServiceStartBackgroundProcedure is the fully-qualified name of the HostAgentService's + // StartBackground RPC. + HostAgentServiceStartBackgroundProcedure = "/hostagent.v1.HostAgentService/StartBackground" + // HostAgentServiceListProcessesProcedure is the fully-qualified name of the HostAgentService's + // ListProcesses RPC. + HostAgentServiceListProcessesProcedure = "/hostagent.v1.HostAgentService/ListProcesses" + // HostAgentServiceKillProcessProcedure is the fully-qualified name of the HostAgentService's + // KillProcess RPC. + HostAgentServiceKillProcessProcedure = "/hostagent.v1.HostAgentService/KillProcess" + // HostAgentServiceConnectProcessProcedure is the fully-qualified name of the HostAgentService's + // ConnectProcess RPC. + HostAgentServiceConnectProcessProcedure = "/hostagent.v1.HostAgentService/ConnectProcess" ) // HostAgentServiceClient is a client for the hostagent.v1.HostAgentService service. @@ -172,6 +184,15 @@ type HostAgentServiceClient interface { PtyResize(context.Context, *connect.Request[gen.PtyResizeRequest]) (*connect.Response[gen.PtyResizeResponse], error) // PtyKill sends a signal to a PTY process. PtyKill(context.Context, *connect.Request[gen.PtyKillRequest]) (*connect.Response[gen.PtyKillResponse], error) + // StartBackground starts a process in the background and returns immediately + // with the PID and tag. The process survives RPC disconnection. + StartBackground(context.Context, *connect.Request[gen.StartBackgroundRequest]) (*connect.Response[gen.StartBackgroundResponse], error) + // ListProcesses returns all running processes inside a sandbox. + ListProcesses(context.Context, *connect.Request[gen.ListProcessesRequest]) (*connect.Response[gen.ListProcessesResponse], error) + // KillProcess sends a signal to a process identified by PID or tag. + KillProcess(context.Context, *connect.Request[gen.KillProcessRequest]) (*connect.Response[gen.KillProcessResponse], error) + // ConnectProcess re-attaches to a running process and streams its output. + ConnectProcess(context.Context, *connect.Request[gen.ConnectProcessRequest]) (*connect.ServerStreamForClient[gen.ConnectProcessResponse], error) } // NewHostAgentServiceClient constructs a client for the hostagent.v1.HostAgentService service. By @@ -335,6 +356,30 @@ func NewHostAgentServiceClient(httpClient connect.HTTPClient, baseURL string, op connect.WithSchema(hostAgentServiceMethods.ByName("PtyKill")), connect.WithClientOptions(opts...), ), + startBackground: connect.NewClient[gen.StartBackgroundRequest, gen.StartBackgroundResponse]( + httpClient, + baseURL+HostAgentServiceStartBackgroundProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("StartBackground")), + connect.WithClientOptions(opts...), + ), + listProcesses: connect.NewClient[gen.ListProcessesRequest, gen.ListProcessesResponse]( + httpClient, + baseURL+HostAgentServiceListProcessesProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("ListProcesses")), + connect.WithClientOptions(opts...), + ), + killProcess: connect.NewClient[gen.KillProcessRequest, gen.KillProcessResponse]( + httpClient, + baseURL+HostAgentServiceKillProcessProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("KillProcess")), + connect.WithClientOptions(opts...), + ), + connectProcess: connect.NewClient[gen.ConnectProcessRequest, gen.ConnectProcessResponse]( + httpClient, + baseURL+HostAgentServiceConnectProcessProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("ConnectProcess")), + connect.WithClientOptions(opts...), + ), } } @@ -365,6 +410,10 @@ type hostAgentServiceClient struct { ptySendInput *connect.Client[gen.PtySendInputRequest, gen.PtySendInputResponse] ptyResize *connect.Client[gen.PtyResizeRequest, gen.PtyResizeResponse] ptyKill *connect.Client[gen.PtyKillRequest, gen.PtyKillResponse] + startBackground *connect.Client[gen.StartBackgroundRequest, gen.StartBackgroundResponse] + listProcesses *connect.Client[gen.ListProcessesRequest, gen.ListProcessesResponse] + killProcess *connect.Client[gen.KillProcessRequest, gen.KillProcessResponse] + connectProcess *connect.Client[gen.ConnectProcessRequest, gen.ConnectProcessResponse] } // CreateSandbox calls hostagent.v1.HostAgentService.CreateSandbox. @@ -492,6 +541,26 @@ func (c *hostAgentServiceClient) PtyKill(ctx context.Context, req *connect.Reque return c.ptyKill.CallUnary(ctx, req) } +// StartBackground calls hostagent.v1.HostAgentService.StartBackground. +func (c *hostAgentServiceClient) StartBackground(ctx context.Context, req *connect.Request[gen.StartBackgroundRequest]) (*connect.Response[gen.StartBackgroundResponse], error) { + return c.startBackground.CallUnary(ctx, req) +} + +// ListProcesses calls hostagent.v1.HostAgentService.ListProcesses. +func (c *hostAgentServiceClient) ListProcesses(ctx context.Context, req *connect.Request[gen.ListProcessesRequest]) (*connect.Response[gen.ListProcessesResponse], error) { + return c.listProcesses.CallUnary(ctx, req) +} + +// KillProcess calls hostagent.v1.HostAgentService.KillProcess. +func (c *hostAgentServiceClient) KillProcess(ctx context.Context, req *connect.Request[gen.KillProcessRequest]) (*connect.Response[gen.KillProcessResponse], error) { + return c.killProcess.CallUnary(ctx, req) +} + +// ConnectProcess calls hostagent.v1.HostAgentService.ConnectProcess. +func (c *hostAgentServiceClient) ConnectProcess(ctx context.Context, req *connect.Request[gen.ConnectProcessRequest]) (*connect.ServerStreamForClient[gen.ConnectProcessResponse], error) { + return c.connectProcess.CallServerStream(ctx, req) +} + // HostAgentServiceHandler is an implementation of the hostagent.v1.HostAgentService service. type HostAgentServiceHandler interface { // CreateSandbox boots a new microVM with the given configuration. @@ -555,6 +624,15 @@ type HostAgentServiceHandler interface { PtyResize(context.Context, *connect.Request[gen.PtyResizeRequest]) (*connect.Response[gen.PtyResizeResponse], error) // PtyKill sends a signal to a PTY process. PtyKill(context.Context, *connect.Request[gen.PtyKillRequest]) (*connect.Response[gen.PtyKillResponse], error) + // StartBackground starts a process in the background and returns immediately + // with the PID and tag. The process survives RPC disconnection. + StartBackground(context.Context, *connect.Request[gen.StartBackgroundRequest]) (*connect.Response[gen.StartBackgroundResponse], error) + // ListProcesses returns all running processes inside a sandbox. + ListProcesses(context.Context, *connect.Request[gen.ListProcessesRequest]) (*connect.Response[gen.ListProcessesResponse], error) + // KillProcess sends a signal to a process identified by PID or tag. + KillProcess(context.Context, *connect.Request[gen.KillProcessRequest]) (*connect.Response[gen.KillProcessResponse], error) + // ConnectProcess re-attaches to a running process and streams its output. + ConnectProcess(context.Context, *connect.Request[gen.ConnectProcessRequest], *connect.ServerStream[gen.ConnectProcessResponse]) error } // NewHostAgentServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -714,6 +792,30 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han connect.WithSchema(hostAgentServiceMethods.ByName("PtyKill")), connect.WithHandlerOptions(opts...), ) + hostAgentServiceStartBackgroundHandler := connect.NewUnaryHandler( + HostAgentServiceStartBackgroundProcedure, + svc.StartBackground, + connect.WithSchema(hostAgentServiceMethods.ByName("StartBackground")), + connect.WithHandlerOptions(opts...), + ) + hostAgentServiceListProcessesHandler := connect.NewUnaryHandler( + HostAgentServiceListProcessesProcedure, + svc.ListProcesses, + connect.WithSchema(hostAgentServiceMethods.ByName("ListProcesses")), + connect.WithHandlerOptions(opts...), + ) + hostAgentServiceKillProcessHandler := connect.NewUnaryHandler( + HostAgentServiceKillProcessProcedure, + svc.KillProcess, + connect.WithSchema(hostAgentServiceMethods.ByName("KillProcess")), + connect.WithHandlerOptions(opts...), + ) + hostAgentServiceConnectProcessHandler := connect.NewServerStreamHandler( + HostAgentServiceConnectProcessProcedure, + svc.ConnectProcess, + connect.WithSchema(hostAgentServiceMethods.ByName("ConnectProcess")), + connect.WithHandlerOptions(opts...), + ) return "/hostagent.v1.HostAgentService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case HostAgentServiceCreateSandboxProcedure: @@ -766,6 +868,14 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han hostAgentServicePtyResizeHandler.ServeHTTP(w, r) case HostAgentServicePtyKillProcedure: hostAgentServicePtyKillHandler.ServeHTTP(w, r) + case HostAgentServiceStartBackgroundProcedure: + hostAgentServiceStartBackgroundHandler.ServeHTTP(w, r) + case HostAgentServiceListProcessesProcedure: + hostAgentServiceListProcessesHandler.ServeHTTP(w, r) + case HostAgentServiceKillProcessProcedure: + hostAgentServiceKillProcessHandler.ServeHTTP(w, r) + case HostAgentServiceConnectProcessProcedure: + hostAgentServiceConnectProcessHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -874,3 +984,19 @@ func (UnimplementedHostAgentServiceHandler) PtyResize(context.Context, *connect. func (UnimplementedHostAgentServiceHandler) PtyKill(context.Context, *connect.Request[gen.PtyKillRequest]) (*connect.Response[gen.PtyKillResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.PtyKill is not implemented")) } + +func (UnimplementedHostAgentServiceHandler) StartBackground(context.Context, *connect.Request[gen.StartBackgroundRequest]) (*connect.Response[gen.StartBackgroundResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.StartBackground is not implemented")) +} + +func (UnimplementedHostAgentServiceHandler) ListProcesses(context.Context, *connect.Request[gen.ListProcessesRequest]) (*connect.Response[gen.ListProcessesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ListProcesses is not implemented")) +} + +func (UnimplementedHostAgentServiceHandler) KillProcess(context.Context, *connect.Request[gen.KillProcessRequest]) (*connect.Response[gen.KillProcessResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.KillProcess is not implemented")) +} + +func (UnimplementedHostAgentServiceHandler) ConnectProcess(context.Context, *connect.Request[gen.ConnectProcessRequest], *connect.ServerStream[gen.ConnectProcessResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ConnectProcess is not implemented")) +} diff --git a/proto/hostagent/hostagent.proto b/proto/hostagent/hostagent.proto index 6b6306e..48b9ded 100644 --- a/proto/hostagent/hostagent.proto +++ b/proto/hostagent/hostagent.proto @@ -91,6 +91,19 @@ service HostAgentService { // PtyKill sends a signal to a PTY process. rpc PtyKill(PtyKillRequest) returns (PtyKillResponse); + // StartBackground starts a process in the background and returns immediately + // with the PID and tag. The process survives RPC disconnection. + rpc StartBackground(StartBackgroundRequest) returns (StartBackgroundResponse); + + // ListProcesses returns all running processes inside a sandbox. + rpc ListProcesses(ListProcessesRequest) returns (ListProcessesResponse); + + // KillProcess sends a signal to a process identified by PID or tag. + rpc KillProcess(KillProcessRequest) returns (KillProcessResponse); + + // ConnectProcess re-attaches to a running process and streams its output. + rpc ConnectProcess(ConnectProcessRequest) returns (stream ConnectProcessResponse); + } message CreateSandboxRequest { @@ -476,3 +489,64 @@ message PtyKillRequest { } message PtyKillResponse {} + +// ── Background Processes ─────────────────────────────────────────── + +message StartBackgroundRequest { + string sandbox_id = 1; + string cmd = 2; + repeated string args = 3; + // User-chosen tag for the process. If empty, the host agent generates one. + string tag = 4; + map envs = 5; + string cwd = 6; +} + +message StartBackgroundResponse { + uint32 pid = 1; + string tag = 2; +} + +message ListProcessesRequest { + string sandbox_id = 1; +} + +message ProcessEntry { + uint32 pid = 1; + string tag = 2; + string cmd = 3; + repeated string args = 4; +} + +message ListProcessesResponse { + repeated ProcessEntry processes = 1; +} + +message KillProcessRequest { + string sandbox_id = 1; + oneof selector { + uint32 pid = 2; + string tag = 3; + } + // Signal to send: "SIGTERM" or "SIGKILL" (default: "SIGKILL"). + string signal = 4; +} + +message KillProcessResponse {} + +message ConnectProcessRequest { + string sandbox_id = 1; + oneof selector { + uint32 pid = 2; + string tag = 3; + } +} + +// Reuses ExecStream event types for symmetry. +message ConnectProcessResponse { + oneof event { + ExecStreamStart start = 1; + ExecStreamData data = 2; + ExecStreamEnd end = 3; + } +}