mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
terminal working, not prod ready, but it works
This commit is contained in:
parent
5b2a5eb5eb
commit
86b2596214
@ -44,7 +44,8 @@ function getThemeFromCSSVars(el: Element): ITheme {
|
|||||||
|
|
||||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [term, setTerm] = React.useState<Terminal>(null);
|
const [term, setTerm] = React.useState<Terminal | null>(null);
|
||||||
|
const [blockStarted, setBlockStarted] = React.useState<boolean>(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!connectElemRef.current) {
|
if (!connectElemRef.current) {
|
||||||
@ -58,16 +59,27 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
fontWeight: "normal",
|
fontWeight: "normal",
|
||||||
fontWeightBold: "bold",
|
fontWeightBold: "bold",
|
||||||
});
|
});
|
||||||
|
setTerm(term);
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
term.open(connectElemRef.current);
|
term.open(connectElemRef.current);
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
term.write("Hello, world!\r\n");
|
term.write("Hello, world!\r\n");
|
||||||
setTerm(term);
|
console.log(term);
|
||||||
|
term.onData((data) => {
|
||||||
|
const b64data = btoa(data);
|
||||||
|
const inputCmd = { command: "input", blockid: blockId, inputdata64: b64data };
|
||||||
|
BlockService.SendCommand(blockId, inputCmd);
|
||||||
|
});
|
||||||
|
|
||||||
// resize observer
|
// resize observer
|
||||||
const rszObs = new ResizeObserver(() => {
|
const rszObs = new ResizeObserver(() => {
|
||||||
|
const oldRows = term.rows;
|
||||||
|
const oldCols = term.cols;
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
if (oldRows !== term.rows || oldCols !== term.cols) {
|
||||||
|
BlockService.SendCommand(blockId, { command: "input", termsize: { rows: term.rows, cols: term.cols } });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
rszObs.observe(connectElemRef.current);
|
rszObs.observe(connectElemRef.current);
|
||||||
|
|
||||||
@ -88,7 +100,10 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
|
|
||||||
async function handleRunClick() {
|
async function handleRunClick() {
|
||||||
try {
|
try {
|
||||||
|
if (!blockStarted) {
|
||||||
await BlockService.StartBlock(blockId);
|
await BlockService.StartBlock(blockId);
|
||||||
|
setBlockStarted(true);
|
||||||
|
}
|
||||||
let termSize = { rows: term.rows, cols: term.cols };
|
let termSize = { rows: term.rows, cols: term.cols };
|
||||||
await BlockService.SendCommand(blockId, { command: "run", cmdstr: "ls -l", termsize: termSize });
|
await BlockService.SendCommand(blockId, { command: "run", cmdstr: "ls -l", termsize: termSize });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -96,12 +111,28 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleStartTerminalClick() {
|
||||||
|
try {
|
||||||
|
if (!blockStarted) {
|
||||||
|
await BlockService.StartBlock(blockId);
|
||||||
|
setBlockStarted(true);
|
||||||
|
}
|
||||||
|
let termSize = { rows: term.rows, cols: term.cols };
|
||||||
|
await BlockService.SendCommand(blockId, { command: "runshell", termsize: termSize });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("start terminal click error: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="view-term">
|
<div className="view-term">
|
||||||
<div className="term-header">
|
<div className="term-header">
|
||||||
<div>Terminal</div>
|
<div>Terminal</div>
|
||||||
<Button className="term-inline" onClick={() => handleRunClick()}>
|
<Button className="term-inline" onClick={() => handleRunClick()}>
|
||||||
Run
|
Run `ls`
|
||||||
|
</Button>
|
||||||
|
<Button className="term-inline" onClick={() => handleStartTerminalClick()}>
|
||||||
|
Start Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||||
|
92
pkg/blockcontroller/blockcommand.go
Normal file
92
pkg/blockcontroller/blockcommand.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package blockcontroller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CommandKey = "command"
|
||||||
|
|
||||||
|
const (
|
||||||
|
BlockCommand_Message = "message"
|
||||||
|
BlockCommand_Run = "run"
|
||||||
|
BlockCommand_Input = "input"
|
||||||
|
BlockCommand_RunShell = "runshell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CommandToTypeMap = map[string]reflect.Type{
|
||||||
|
BlockCommand_Message: reflect.TypeOf(MessageCommand{}),
|
||||||
|
BlockCommand_Run: reflect.TypeOf(RunCommand{}),
|
||||||
|
BlockCommand_Input: reflect.TypeOf(InputCommand{}),
|
||||||
|
BlockCommand_RunShell: reflect.TypeOf(RunShellCommand{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockCommand interface {
|
||||||
|
GetCommand() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCmdMap(cmdMap map[string]any) (BlockCommand, error) {
|
||||||
|
cmdType, ok := cmdMap[CommandKey].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no %s field in command map", CommandKey)
|
||||||
|
}
|
||||||
|
mapJson, err := json.Marshal(cmdMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshalling command map: %w", err)
|
||||||
|
}
|
||||||
|
rtype := CommandToTypeMap[cmdType]
|
||||||
|
if rtype == nil {
|
||||||
|
return nil, fmt.Errorf("unknown command type %q", cmdType)
|
||||||
|
}
|
||||||
|
cmd := reflect.New(rtype).Interface()
|
||||||
|
err = json.Unmarshal(mapJson, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshalling command: %w", err)
|
||||||
|
}
|
||||||
|
return cmd.(BlockCommand), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageCommand struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MessageCommand) GetCommand() string {
|
||||||
|
return BlockCommand_Message
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunCommand struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
CmdStr string `json:"cmdstr"`
|
||||||
|
TermSize shellexec.TermSize `json:"termsize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunCommand) GetCommand() string {
|
||||||
|
return BlockCommand_Run
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputCommand struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
InputData64 string `json:"inputdata64"`
|
||||||
|
SigName string `json:"signame,omitempty"`
|
||||||
|
TermSize *shellexec.TermSize `json:"termsize,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *InputCommand) GetCommand() string {
|
||||||
|
return BlockCommand_Input
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunShellCommand struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
TermSize shellexec.TermSize `json:"termsize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rsc *RunShellCommand) GetCommand() string {
|
||||||
|
return BlockCommand_RunShell
|
||||||
|
}
|
@ -5,12 +5,13 @@ package blockcontroller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
||||||
@ -20,64 +21,31 @@ import (
|
|||||||
var globalLock = &sync.Mutex{}
|
var globalLock = &sync.Mutex{}
|
||||||
var blockControllerMap = make(map[string]*BlockController)
|
var blockControllerMap = make(map[string]*BlockController)
|
||||||
|
|
||||||
type BlockCommand interface {
|
|
||||||
GetCommand() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageCommand struct {
|
|
||||||
Command string `json:"command"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
type BlockController struct {
|
||||||
|
Lock *sync.Mutex
|
||||||
BlockId string
|
BlockId string
|
||||||
InputCh chan BlockCommand
|
InputCh chan BlockCommand
|
||||||
|
ShellProc *shellexec.ShellProc
|
||||||
|
ShellInputCh chan *InputCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCmdMap(cmdMap map[string]any) (BlockCommand, error) {
|
func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error {
|
||||||
cmdType, ok := cmdMap["command"].(string)
|
bc.Lock.Lock()
|
||||||
if !ok {
|
defer bc.Lock.Unlock()
|
||||||
return nil, fmt.Errorf("no command field in command map")
|
if bc.ShellProc != nil {
|
||||||
}
|
return fmt.Errorf("shell process already running")
|
||||||
mapJson, err := json.Marshal(cmdMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error marshalling command map: %w", err)
|
|
||||||
}
|
|
||||||
switch cmdType {
|
|
||||||
case "message":
|
|
||||||
var cmd MessageCommand
|
|
||||||
err := json.Unmarshal(mapJson, &cmd)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
bc.ShellProc = shellProc
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) StartShellCommand(rc *RunCommand) error {
|
func (bc *BlockController) getShellProc() *shellexec.ShellProc {
|
||||||
|
bc.Lock.Lock()
|
||||||
|
defer bc.Lock.Unlock()
|
||||||
|
return bc.ShellProc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BlockController) DoRunCommand(rc *RunCommand) error {
|
||||||
cmdStr := rc.CmdStr
|
cmdStr := rc.CmdStr
|
||||||
shellPath := shellutil.DetectLocalShellPath()
|
shellPath := shellutil.DetectLocalShellPath()
|
||||||
ecmd := exec.Command(shellPath, "-c", cmdStr)
|
ecmd := exec.Command(shellPath, "-c", cmdStr)
|
||||||
@ -104,6 +72,71 @@ func (bc *BlockController) StartShellCommand(rc *RunCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bc *BlockController) DoRunShellCommand(rc *RunShellCommand) error {
|
||||||
|
if bc.getShellProc() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
shellProc, err := shellexec.StartShellProc(rc.TermSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = bc.setShellProc(shellProc)
|
||||||
|
if err != nil {
|
||||||
|
bc.ShellProc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
shellInputCh := make(chan *InputCommand)
|
||||||
|
bc.ShellInputCh = shellInputCh
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
// needs synchronization
|
||||||
|
bc.ShellProc.Close()
|
||||||
|
close(bc.ShellInputCh)
|
||||||
|
bc.ShellProc = nil
|
||||||
|
bc.ShellInputCh = nil
|
||||||
|
}()
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
nr, err := bc.ShellProc.Pty.Read(buf)
|
||||||
|
eventbus.SendEvent(application.WailsEvent{
|
||||||
|
Name: "block:ptydata",
|
||||||
|
Data: map[string]any{
|
||||||
|
"blockid": bc.BlockId,
|
||||||
|
"blockfile": "main",
|
||||||
|
"ptydata": base64.StdEncoding.EncodeToString(buf[:nr]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading from shell: %v\n", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for ic := range shellInputCh {
|
||||||
|
if ic.InputData64 != "" {
|
||||||
|
inputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(ic.InputData64)))
|
||||||
|
nw, err := base64.StdEncoding.Decode(inputBuf, []byte(ic.InputData64))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error decoding input data: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bc.ShellProc.Pty.Write(inputBuf[:nw])
|
||||||
|
}
|
||||||
|
if ic.TermSize != nil {
|
||||||
|
err := pty.Setsize(bc.ShellProc.Pty, &pty.Winsize{Rows: uint16(ic.TermSize.Rows), Cols: uint16(ic.TermSize.Cols)})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error setting term size: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bc *BlockController) Run() {
|
func (bc *BlockController) Run() {
|
||||||
defer func() {
|
defer func() {
|
||||||
eventbus.SendEvent(application.WailsEvent{
|
eventbus.SendEvent(application.WailsEvent{
|
||||||
@ -132,12 +165,28 @@ func (bc *BlockController) Run() {
|
|||||||
case *RunCommand:
|
case *RunCommand:
|
||||||
fmt.Printf("RUN: %s | %q\n", bc.BlockId, cmd.CmdStr)
|
fmt.Printf("RUN: %s | %q\n", bc.BlockId, cmd.CmdStr)
|
||||||
go func() {
|
go func() {
|
||||||
err := bc.StartShellCommand(cmd)
|
err := bc.DoRunCommand(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error running shell command: %v\n", err)
|
log.Printf("error running shell command: %v\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
case *InputCommand:
|
||||||
|
fmt.Printf("INPUT: %s | %q\n", bc.BlockId, cmd.InputData64)
|
||||||
|
if bc.ShellInputCh != nil {
|
||||||
|
bc.ShellInputCh <- cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
case *RunShellCommand:
|
||||||
|
fmt.Printf("RUNSHELL: %s\n", bc.BlockId)
|
||||||
|
if bc.ShellProc != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := bc.DoRunShellCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error running shell: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
default:
|
default:
|
||||||
fmt.Printf("unknown command type %T\n", cmd)
|
fmt.Printf("unknown command type %T\n", cmd)
|
||||||
}
|
}
|
||||||
@ -151,6 +200,7 @@ func StartBlockController(blockId string) *BlockController {
|
|||||||
return existingBC
|
return existingBC
|
||||||
}
|
}
|
||||||
bc := &BlockController{
|
bc := &BlockController{
|
||||||
|
Lock: &sync.Mutex{},
|
||||||
BlockId: blockId,
|
BlockId: blockId,
|
||||||
InputCh: make(chan BlockCommand),
|
InputCh: make(chan BlockCommand),
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,51 @@ type TermSize struct {
|
|||||||
Cols int `json:"cols"`
|
Cols int `json:"cols"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShellProc struct {
|
||||||
|
Cmd *exec.Cmd
|
||||||
|
Pty *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *ShellProc) Close() {
|
||||||
|
sp.Cmd.Process.Kill()
|
||||||
|
go func() {
|
||||||
|
sp.Cmd.Process.Wait()
|
||||||
|
sp.Pty.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartShellProc(termSize TermSize) (*ShellProc, error) {
|
||||||
|
shellPath := shellutil.DetectLocalShellPath()
|
||||||
|
ecmd := exec.Command(shellPath, "-i", "-l")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
ecmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
ecmd.SysProcAttr.Setsid = true
|
||||||
|
ecmd.SysProcAttr.Setctty = true
|
||||||
|
err = ecmd.Start()
|
||||||
|
cmdTty.Close()
|
||||||
|
if err != nil {
|
||||||
|
cmdPty.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ShellProc{Cmd: ecmd, Pty: cmdPty}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
|
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
|
||||||
ecmd.Env = os.Environ()
|
ecmd.Env = os.Environ()
|
||||||
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
|
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellEnvVars(shellutil.DefaultTermType))
|
||||||
|
Loading…
Reference in New Issue
Block a user