mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat: interactive terminal — WS-bridged docker exec PTY (#46)
GET /services/:id/terminal upgrades to a WebSocket attached to a `docker exec` PTY in the service's running replica. JSON frames both ways: stdin/resize out, stdout/exit/error in. Shell probe tries each candidate (bash→sh) since `command -v` needs a shell to run; distroless images get a clean "no shell" error instead of an OCI failure dump. - docker.Client gains ExecAttach + ExecResize wrappers - Frontend: xterm.js replaces the one-off console section — live PTY, fit-addon resizes forwarded over the socket - CLI: `containr shell <id>` bridges the local TTY (raw mode, SIGWINCH resize forwarding); `containr exec` keeps the one-off command path - Access mirrors service logs: project manage scope required Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5eac4a382b
commit
612520bbff
13 files changed
+570
-75
No files matched your search
@@ -0,0 +1,113 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// ShellCmd opens an interactive shell inside a service's running container.
|
||||
var ShellCmd = &cobra.Command{
|
||||
Use: "shell <service-id>",
|
||||
Short: "Interactive shell inside the service container",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
serviceID := args[0]
|
||||
cli, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := strings.TrimRight(cli.BaseURL, "/")
|
||||
wsURL := strings.Replace(base, "http", "ws", 1) + "/services/" + serviceID + "/terminal"
|
||||
|
||||
header := http.Header{}
|
||||
header.Set("Authorization", "Bearer "+cli.Token)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("terminal: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fd := int(os.Stdin.Fd())
|
||||
raw := term.IsTerminal(fd)
|
||||
var oldState *term.State
|
||||
if raw {
|
||||
oldState, err = term.MakeRaw(fd)
|
||||
if err == nil {
|
||||
defer term.Restore(fd, oldState)
|
||||
}
|
||||
if w, h, err := term.GetSize(fd); err == nil {
|
||||
_ = conn.WriteJSON(map[string]interface{}{"type": "resize", "cols": w, "rows": h})
|
||||
}
|
||||
// Forward SIGWINCH resizes.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGWINCH)
|
||||
defer signal.Stop(sigCh)
|
||||
go func() {
|
||||
for range sigCh {
|
||||
if w, h, err := term.GetSize(fd); err == nil {
|
||||
_ = conn.WriteJSON(map[string]interface{}{"type": "resize", "cols": w, "rows": h})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
// stdin → ws
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if n > 0 {
|
||||
if werr := conn.WriteJSON(map[string]interface{}{
|
||||
"type": "stdin", "data": string(buf[:n]),
|
||||
}); werr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// ws → stdout
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
var frame struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := conn.ReadJSON(&frame); err != nil {
|
||||
return
|
||||
}
|
||||
switch frame.Type {
|
||||
case "stdout":
|
||||
_, _ = os.Stdout.WriteString(frame.Data)
|
||||
case "exit":
|
||||
fmt.Fprintf(os.Stderr, "\r\n[session ended — exit %d]\r\n", frame.Code)
|
||||
return
|
||||
case "error":
|
||||
fmt.Fprintf(os.Stderr, "\r\nerror: %s\r\n", frame.Message)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(24 * time.Hour):
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user