diff --git a/frontend/app/view/term.tsx b/frontend/app/view/term.tsx index 72a55f29d..693d24e5d 100644 --- a/frontend/app/view/term.tsx +++ b/frontend/app/view/term.tsx @@ -64,29 +64,33 @@ const TerminalView = ({ blockId }: { blockId: string }) => { fitAddon.fit(); term.write("Hello, world!\r\n"); setTerm(term); - return () => { - term.dispose(); - }; - }, [connectElemRef.current]); - React.useEffect(() => { - if (!term) { - return; - } + + // resize observer + const rszObs = new ResizeObserver(() => { + fitAddon.fit(); + }); + rszObs.observe(connectElemRef.current); + + // block subject const blockSubject = getBlockSubject(blockId); blockSubject.subscribe((data) => { // base64 decode const decodedData = base64ToArray(data.ptydata); term.write(decodedData); }); + return () => { + term.dispose(); + rszObs.disconnect(); blockSubject.release(); }; - }, [term]); + }, [connectElemRef.current]); async function handleRunClick() { try { await BlockService.StartBlock(blockId); - await BlockService.SendCommand(blockId, { command: "message", message: "Run clicked" }); + let termSize = { rows: term.rows, cols: term.cols }; + await BlockService.SendCommand(blockId, { command: "run", cmdstr: "ls -l", termsize: termSize }); } catch (e) { console.log("run click error: ", e); } diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 09fc4ad71..4e8de7c6f 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -7,10 +7,14 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log" + "os/exec" "sync" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wavetermdev/thenextwave/pkg/eventbus" + "github.com/wavetermdev/thenextwave/pkg/shellexec" + "github.com/wavetermdev/thenextwave/pkg/util/shellutil" ) var globalLock = &sync.Mutex{} @@ -29,6 +33,16 @@ func (mc *MessageCommand) GetCommand() string { return "message" } +type RunCommand struct { + Command string `json:"command"` + CmdStr string `json:"cmdstr"` + TermSize shellexec.TermSize `json:"termsize"` +} + +func (rc *RunCommand) GetCommand() string { + return "run" +} + type BlockController struct { BlockId string InputCh chan BlockCommand @@ -51,11 +65,45 @@ func ParseCmdMap(cmdMap map[string]any) (BlockCommand, error) { return nil, fmt.Errorf("error unmarshalling message command: %w", err) } return &cmd, nil + case "run": + var cmd RunCommand + err := json.Unmarshal(mapJson, &cmd) + if err != nil { + return nil, fmt.Errorf("error unmarshalling run command: %w", err) + } + return &cmd, nil default: return nil, fmt.Errorf("unknown command type %q", cmdType) } } +func (bc *BlockController) StartShellCommand(rc *RunCommand) error { + cmdStr := rc.CmdStr + shellPath := shellutil.DetectLocalShellPath() + ecmd := exec.Command(shellPath, "-c", cmdStr) + log.Printf("running shell command: %q %q\n", shellPath, cmdStr) + barr, err := shellexec.RunSimpleCmdInPty(ecmd, rc.TermSize) + if err != nil { + return err + } + for len(barr) > 0 { + part := barr + if len(part) > 4096 { + part = part[:4096] + } + eventbus.SendEvent(application.WailsEvent{ + Name: "block:ptydata", + Data: map[string]any{ + "blockid": bc.BlockId, + "blockfile": "main", + "ptydata": base64.StdEncoding.EncodeToString(part), + }, + }) + barr = barr[len(part):] + } + return nil +} + func (bc *BlockController) Run() { defer func() { eventbus.SendEvent(application.WailsEvent{ @@ -81,6 +129,14 @@ func (bc *BlockController) Run() { "ptydata": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("message %d\r\n", messageCount))), }, }) + case *RunCommand: + fmt.Printf("RUN: %s | %q\n", bc.BlockId, cmd.CmdStr) + go func() { + err := bc.StartShellCommand(cmd) + if err != nil { + log.Printf("error running shell command: %v\n", err) + } + }() default: fmt.Printf("unknown command type %T\n", cmd) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 35859cddd..8adab5705 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -15,14 +15,26 @@ import ( "github.com/wavetermdev/thenextwave/pkg/util/shellutil" ) -func RunSimpleCmdInPty(ecmd *exec.Cmd) ([]byte, error) { +type TermSize struct { + Rows int `json:"rows"` + Cols int `json:"cols"` +} + +func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) { ecmd.Env = os.Environ() shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType)) cmdPty, cmdTty, err := pty.Open() if err != nil { return nil, fmt.Errorf("opening new pty: %w", err) } - pty.Setsize(cmdPty, &pty.Winsize{Rows: shellutil.DefaultTermRows, Cols: shellutil.DefaultTermCols}) + if termSize.Rows == 0 || termSize.Cols == 0 { + termSize.Rows = shellutil.DefaultTermRows + termSize.Cols = shellutil.DefaultTermCols + } + if termSize.Rows <= 0 || termSize.Cols <= 0 { + return nil, fmt.Errorf("invalid term size: %v", termSize) + } + pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)}) ecmd.Stdin = cmdTty ecmd.Stdout = cmdTty ecmd.Stderr = cmdTty diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 429d0f171..ce635826f 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -4,9 +4,15 @@ package shellutil import ( + "context" "os" "os/exec" + "os/user" + "regexp" + "runtime" "strings" + "sync" + "time" "github.com/wavetermdev/thenextwave/pkg/wavebase" ) @@ -15,6 +21,55 @@ const DefaultTermType = "xterm-256color" const DefaultTermRows = 24 const DefaultTermCols = 80 +var cachedMacUserShell string +var macUserShellOnce = &sync.Once{} +var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`) + +const DefaultShellPath = "/bin/bash" + +func DetectLocalShellPath() string { + shellPath := GetMacUserShell() + if shellPath == "" { + shellPath = os.Getenv("SHELL") + } + if shellPath == "" { + return DefaultShellPath + } + return shellPath +} + +func GetMacUserShell() string { + if runtime.GOOS != "darwin" { + return "" + } + macUserShellOnce.Do(func() { + cachedMacUserShell = internalMacUserShell() + }) + return cachedMacUserShell +} + +// dscl . -read /User/[username] UserShell +// defaults to /bin/bash +func internalMacUserShell() string { + osUser, err := user.Current() + if err != nil { + return DefaultShellPath + } + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + userStr := "/Users/" + osUser.Username + out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput() + if err != nil { + return DefaultShellPath + } + outStr := strings.TrimSpace(string(out)) + m := userShellRegexp.FindStringSubmatch(outStr) + if m == nil { + return DefaultShellPath + } + return m[1] +} + func WaveshellEnvVars(termType string) map[string]string { rtn := make(map[string]string) if termType != "" {