From 516890c49a7c5cd18b9fc9834049ec69d680087a Mon Sep 17 00:00:00 2001 From: pptx704 Date: Tue, 14 Apr 2026 03:57:01 +0600 Subject: [PATCH] Add background process execution API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start long-running processes (web servers, daemons) without blocking the HTTP request. Leverages envd's existing background process support (context.Background(), List, Connect, SendSignal RPCs) and wires it through the host agent and control plane layers. New API surface: - POST /v1/capsules/{id}/exec with background:true → 202 {pid, tag} - GET /v1/capsules/{id}/processes → list running processes - DELETE /v1/capsules/{id}/processes/{selector} → kill by PID or tag - WS /v1/capsules/{id}/processes/{selector}/stream → reconnect to output The {selector} param auto-detects: numeric = PID, string = tag. Tags are auto-generated as "proc-" + 8 hex chars if not provided. --- internal/api/handlers_exec.go | 61 +- internal/api/handlers_process.go | 266 ++++++ internal/api/openapi.yaml | 174 +++- internal/api/server.go | 7 + internal/envdclient/process.go | 187 ++++ internal/hostagent/server.go | 149 ++++ internal/sandbox/manager.go | 73 +- proto/hostagent/gen/hostagent.pb.go | 832 ++++++++++++++++-- .../hostagentv1connect/hostagent.connect.go | 126 +++ proto/hostagent/hostagent.proto | 74 ++ 10 files changed, 1876 insertions(+), 73 deletions(-) create mode 100644 internal/api/handlers_process.go create mode 100644 internal/envdclient/process.go 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; + } +}