Add multiple terminals per task and saved commands that start one
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
func (s *Server) ListSavedCommands(w http.ResponseWriter, r *http.Request) {
|
||||
cmds, err := s.store.SavedCommands(r.Context(), actor(r))
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cmds)
|
||||
}
|
||||
|
||||
func (s *Server) CreateSavedCommand(w http.ResponseWriter, r *http.Request) {
|
||||
s.saveCommand(w, r, "", http.StatusCreated)
|
||||
}
|
||||
|
||||
func (s *Server) UpdateSavedCommand(w http.ResponseWriter, r *http.Request, commandID string) {
|
||||
s.saveCommand(w, r, commandID, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) saveCommand(w http.ResponseWriter, r *http.Request, id string, status int) {
|
||||
req := api.SavedCommandSave{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Command) == "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("a saved command needs a name and a command"))
|
||||
return
|
||||
}
|
||||
c, err := s.store.SaveSavedCommand(r.Context(), actor(r), id, req.Name, req.Command, actor(r))
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, status, c)
|
||||
}
|
||||
|
||||
func (s *Server) DeleteSavedCommand(w http.ResponseWriter, r *http.Request, commandID string) {
|
||||
err := s.store.DeleteSavedCommand(r.Context(), actor(r), commandID, actor(r))
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -54,22 +54,21 @@ func New(dataDir string, portFirst int, ts *tsnet.Server) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
terminals, err := terminal.New(filepath.Join(dataDir, "zsh"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lc, err := ts.LocalClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{
|
||||
hub: newHub(),
|
||||
terminals: terminals,
|
||||
tailnet: lc,
|
||||
hostname: ts.Hostname,
|
||||
startedAt: time.Now().UTC(),
|
||||
users: map[string]string{},
|
||||
}
|
||||
s.terminals, err = terminal.New(filepath.Join(dataDir, "zsh"), s.terminalsChanged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.clients = clients.New(s.clientsChanged)
|
||||
|
||||
s.store, err = store.Open(filepath.Join(dataDir, "byre.db"), portFirst, s.publish)
|
||||
|
||||
@@ -18,16 +18,78 @@ const (
|
||||
terminalExited = websocket.StatusCode(4000)
|
||||
)
|
||||
|
||||
func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID string, params api.GetTaskTerminalParams) {
|
||||
var newTerminalSize = api.TerminalSize{Cols: 80, Rows: 24}
|
||||
|
||||
func (s *Server) terminalsChanged(taskID string) {
|
||||
s.store.Broadcast(api.Event{Kind: "terminals.changed", TaskId: &taskID, Actor: "byre", Payload: map[string]any{}})
|
||||
}
|
||||
|
||||
func taskTerminal(term *terminal.Terminal) api.TaskTerminal {
|
||||
return api.TaskTerminal{Id: term.ID, Name: term.Name, StartedAt: term.Started().UTC()}
|
||||
}
|
||||
|
||||
func (s *Server) ListTaskTerminals(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
_, ok := s.activeTask(w, r, taskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out := []api.TaskTerminal{}
|
||||
for _, term := range s.terminals.List(taskID) {
|
||||
out = append(out, taskTerminal(term))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) StartTaskTerminal(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
t, ok := s.activeTask(w, r, taskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req := api.TerminalStart{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
name, command := "Shell", ""
|
||||
if req.CommandId != nil {
|
||||
c, err := s.store.SavedCommand(r.Context(), actor(r), *req.CommandId)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
name, command = c.Name, c.Command
|
||||
}
|
||||
term, err := s.terminals.Start(t, name, command, newTerminalSize)
|
||||
if err != nil {
|
||||
slog.Error("start terminal", "task", taskID, "err", err)
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, taskTerminal(term))
|
||||
}
|
||||
|
||||
func (s *Server) CloseTaskTerminal(w http.ResponseWriter, r *http.Request, taskID, terminalID string) {
|
||||
if !s.terminals.Kill(taskID, terminalID) {
|
||||
writeError(w, http.StatusNotFound, fmt.Errorf("no terminal %s in task %s", terminalID, taskID))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID, terminalID string, params api.GetTaskTerminalParams) {
|
||||
_, ok := s.activeTask(w, r, taskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
size := api.TerminalSize{Cols: params.Cols, Rows: params.Rows}
|
||||
if !terminal.ValidSize(size) {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("cols and rows must be between 1 and %d", terminal.MaxSize))
|
||||
return
|
||||
}
|
||||
term := s.terminals.Get(taskID, terminalID)
|
||||
if term == nil {
|
||||
writeError(w, http.StatusNotFound, fmt.Errorf("no terminal %s in task %s", terminalID, taskID))
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
@@ -37,12 +99,6 @@ func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID
|
||||
defer conn.CloseNow()
|
||||
conn.SetReadLimit(terminalReadLimit)
|
||||
|
||||
term, err := s.terminals.Attach(t, size)
|
||||
if err != nil {
|
||||
slog.Error("start terminal", "task", taskID, "err", err)
|
||||
conn.Close(websocket.StatusInternalError, "couldn't start the shell; see byre's log")
|
||||
return
|
||||
}
|
||||
term.Resize(size)
|
||||
|
||||
backlog, out := term.Subscribe()
|
||||
|
||||
Reference in New Issue
Block a user