wsh rpc working (#55)

lots of iterations on an RPC protocol. getting wsh working with a
getmeta/setmeta command in addition to html mode.
This commit is contained in:
Mike Sawka 2024-06-17 09:58:28 -07:00 committed by GitHub
parent d0c4f5c46f
commit e46906d423
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 902 additions and 198 deletions

View File

@ -4,8 +4,10 @@
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "log"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
@ -33,10 +35,29 @@ func getMetaRun(cmd *cobra.Command, args []string) {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)
return return
} }
setTermRawMode()
fullORef, err := resolveSimpleId(oref)
if err != nil {
fmt.Printf("error resolving oref: %v\r\n", err)
return
}
getMetaWshCmd := &wshutil.BlockGetMetaCommand{ getMetaWshCmd := &wshutil.BlockGetMetaCommand{
Command: wshutil.BlockCommand_SetMeta, Command: wshutil.BlockCommand_SetMeta,
OID: oref, ORef: fullORef,
} }
barr, _ := wshutil.EncodeWaveOSCMessage(getMetaWshCmd) resp, err := RpcClient.SendRpcRequest(getMetaWshCmd, 2000)
os.Stdout.Write(barr) if err != nil {
log.Printf("error getting metadata: %v\r\n", err)
return
}
outArr, err := json.MarshalIndent(resp, "", " ")
if err != nil {
log.Printf("error formatting metadata: %v\r\n", err)
return
}
outStr := string(outArr)
outStr = strings.ReplaceAll(outStr, "\n", "\r\n")
fmt.Print(outStr)
fmt.Print("\r\n")
} }

View File

@ -5,7 +5,6 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,12 +13,18 @@ func init() {
rootCmd.AddCommand(htmlCmd) rootCmd.AddCommand(htmlCmd)
} }
var htmlCmd = &cobra.Command{
Use: "html",
Short: "Launch a demo html-mode terminal",
Run: htmlRun,
}
func htmlRun(cmd *cobra.Command, args []string) { func htmlRun(cmd *cobra.Command, args []string) {
defer doShutdown("normal exit", 0) defer doShutdown("normal exit", 0)
setTermHtmlMode() setTermHtmlMode()
for { for {
var buf [1]byte var buf [1]byte
_, err := os.Stdin.Read(buf[:]) _, err := WrappedStdin.Read(buf[:])
if err != nil { if err != nil {
doShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1) doShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1)
} }
@ -33,9 +38,3 @@ func htmlRun(cmd *cobra.Command, args []string) {
} }
} }
} }
var htmlCmd = &cobra.Command{
Use: "html",
Short: "Launch a demo html-mode terminal",
Run: htmlRun,
}

View File

@ -5,6 +5,7 @@ package cmd
import ( import (
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@ -12,6 +13,7 @@ import (
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -30,8 +32,11 @@ var (
var shutdownOnce sync.Once var shutdownOnce sync.Once
var origTermState *term.State var origTermState *term.State
var madeRaw bool
var usingHtmlMode bool var usingHtmlMode bool
var shutdownSignalHandlersInstalled bool var shutdownSignalHandlersInstalled bool
var WrappedStdin io.Reader
var RpcClient *wshutil.WshRpc
func doShutdown(reason string, exitCode int) { func doShutdown(reason string, exitCode int) {
shutdownOnce.Do(func() { shutdownOnce.Do(func() {
@ -42,8 +47,8 @@ func doShutdown(reason string, exitCode int) {
Command: wshutil.BlockCommand_SetMeta, Command: wshutil.BlockCommand_SetMeta,
Meta: map[string]any{"term:mode": nil}, Meta: map[string]any{"term:mode": nil},
} }
barr, _ := wshutil.EncodeWaveOSCMessage(cmd) RpcClient.SendCommand(cmd)
os.Stdout.Write(barr) time.Sleep(10 * time.Millisecond)
} }
if origTermState != nil { if origTermState != nil {
term.Restore(int(os.Stdin.Fd()), origTermState) term.Restore(int(os.Stdin.Fd()), origTermState)
@ -51,20 +56,42 @@ func doShutdown(reason string, exitCode int) {
}) })
} }
func setTermHtmlMode() { // returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
installShutdownSignalHandlers() func setupRpcClient(handlerFn wshutil.CommandHandlerFnType) {
log.Printf("setup rpc client\r\n")
messageCh := make(chan wshutil.RpcMessage)
ptyBuf := wshutil.MakePtyBuffer(wshutil.WaveServerOSCPrefix, os.Stdin, messageCh)
rpcClient, outputCh := wshutil.MakeWshRpc(wshutil.WaveOSC, messageCh, handlerFn)
go func() {
for barr := range outputCh {
os.Stdout.Write(barr)
}
}()
WrappedStdin = ptyBuf
RpcClient = rpcClient
}
func setTermRawMode() {
if madeRaw {
return
}
origState, err := term.MakeRaw(int(os.Stdin.Fd())) origState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error setting raw mode: %v\n", err) fmt.Fprintf(os.Stderr, "Error setting raw mode: %v\n", err)
return return
} }
origTermState = origState origTermState = origState
madeRaw = true
}
func setTermHtmlMode() {
installShutdownSignalHandlers()
setTermRawMode()
cmd := &wshutil.BlockSetMetaCommand{ cmd := &wshutil.BlockSetMetaCommand{
Command: wshutil.BlockCommand_SetMeta, Command: wshutil.BlockCommand_SetMeta,
Meta: map[string]any{"term:mode": "html"}, Meta: map[string]any{"term:mode": "html"},
} }
barr, _ := wshutil.EncodeWaveOSCMessage(cmd) RpcClient.SendCommand(cmd)
os.Stdout.Write(barr)
usingHtmlMode = true usingHtmlMode = true
} }
@ -85,7 +112,7 @@ func installShutdownSignalHandlers() {
var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`) var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`)
func validateEasyORef(oref string) error { func validateEasyORef(oref string) error {
if strings.Index(oref, ":") >= 0 { if strings.Contains(oref, ":") {
_, err := waveobj.ParseORef(oref) _, err := waveobj.ParseORef(oref)
if err != nil { if err != nil {
return fmt.Errorf("invalid ORef: %v", err) return fmt.Errorf("invalid ORef: %v", err)
@ -105,7 +132,31 @@ func validateEasyORef(oref string) error {
return nil return nil
} }
func isFullORef(orefStr string) bool {
_, err := waveobj.ParseORef(orefStr)
return err == nil
}
func resolveSimpleId(id string) (string, error) {
if isFullORef(id) {
return id, nil
}
resolveCmd := &wshutil.ResolveIdsCommand{
Command: wshutil.Command_ResolveIds,
Ids: []string{id},
}
resp, err := RpcClient.SendRpcRequest(resolveCmd, 2000)
if err != nil {
return "", err
}
if resp[id] == nil {
return "", fmt.Errorf("id not found: %q", id)
}
return resp[id].(string), nil
}
// Execute executes the root command. // Execute executes the root command.
func Execute() error { func Execute() error {
setupRpcClient(nil)
return rootCmd.Execute() return rootCmd.Execute()
} }

View File

@ -6,7 +6,6 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strconv" "strconv"
"strings" "strings"
@ -76,11 +75,21 @@ func setMetaRun(cmd *cobra.Command, args []string) {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)
return return
} }
setTermRawMode()
fullORef, err := resolveSimpleId(oref)
if err != nil {
fmt.Printf("error resolving oref: %v\n", err)
return
}
setMetaWshCmd := &wshutil.BlockSetMetaCommand{ setMetaWshCmd := &wshutil.BlockSetMetaCommand{
Command: wshutil.BlockCommand_SetMeta, Command: wshutil.BlockCommand_SetMeta,
OID: oref, ORef: fullORef,
Meta: meta, Meta: meta,
} }
barr, _ := wshutil.EncodeWaveOSCMessage(setMetaWshCmd) _, err = RpcClient.SendRpcRequest(setMetaWshCmd, 2000)
os.Stdout.Write(barr) if err != nil {
fmt.Printf("error setting metadata: %v\n", err)
return
}
fmt.Print("metadata set\r\n")
} }

View File

@ -47,6 +47,9 @@ export default defineConfig({
}, },
}, },
}, },
server: {
open: false,
},
plugins: [ plugins: [
react({}), react({}),
tsconfigPaths(), tsconfigPaths(),

View File

@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import * as electron from "electron"; import * as electron from "electron";
import fs from "fs";
import * as child_process from "node:child_process"; import * as child_process from "node:child_process";
import os from "os";
import * as path from "path"; import * as path from "path";
import * as readline from "readline"; import * as readline from "readline";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import * as services from "../frontend/app/store/services"; import * as services from "../frontend/app/store/services";
import os from "os";
import fs from "fs";
const electronApp = electron.app; const electronApp = electron.app;
const isDev = process.env.WAVETERM_DEV; const isDev = process.env.WAVETERM_DEV;
@ -65,7 +65,7 @@ function getWaveSrvPath(): string {
function getWaveSrvPathWin(): string { function getWaveSrvPathWin(): string {
const appPath = path.join(getGoAppBasePath(), "bin", "wavesrv.exe"); const appPath = path.join(getGoAppBasePath(), "bin", "wavesrv.exe");
return `& "${appPath}"` return `& "${appPath}"`;
} }
function getWaveSrvCwd(): string { function getWaveSrvCwd(): string {
@ -85,12 +85,12 @@ function runWaveSrv(): Promise<boolean> {
envCopy[WaveDevVarName] = "1"; envCopy[WaveDevVarName] = "1";
} }
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString(); envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
let waveSrvCmd: string; let waveSrvCmd: string;
if (process.platform === "win32") { if (process.platform === "win32") {
waveSrvCmd = getWaveSrvPathWin(); waveSrvCmd = getWaveSrvPathWin();
} else { } else {
waveSrvCmd = getWaveSrvPath(); waveSrvCmd = getWaveSrvPath();
} }
console.log("trying to run local server", waveSrvCmd); console.log("trying to run local server", waveSrvCmd);
const proc = child_process.spawn(getWaveSrvPath(), { const proc = child_process.spawn(getWaveSrvPath(), {
cwd: getWaveSrvCwd(), cwd: getWaveSrvCwd(),
@ -121,23 +121,29 @@ function runWaveSrv(): Promise<boolean> {
terminal: false, terminal: false,
}); });
rlStderr.on("line", (line) => { rlStderr.on("line", (line) => {
if (line.includes("WAVESRV-ESTART")) { if (line.includes("WAVESRV-ESTART")) {
waveSrvReadyResolve(true); waveSrvReadyResolve(true);
return; return;
} }
console.log(line); console.log(line);
}); });
return rtnPromise; return rtnPromise;
} }
function mainResizeHandler(_: any, win: Electron.BrowserWindow) { async function mainResizeHandler(_: any, windowId: string, win: Electron.BrowserWindow) {
if (win == null || win.isDestroyed() || win.fullScreen) { if (win == null || win.isDestroyed() || win.fullScreen) {
return; return;
} }
const bounds = win.getBounds(); const bounds = win.getBounds();
const winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x }; try {
const url = new URL(getBaseHostPort() + "/api/set-winsize"); await services.WindowService.SetWindowPosAndSize(
// TODO windowId,
{ x: bounds.x, y: bounds.y },
{ width: bounds.width, height: bounds.height }
);
} catch (e) {
console.log("error resizing window", e);
}
} }
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) { function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
@ -182,12 +188,29 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
} }
function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserWindow { function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserWindow {
const primaryDisplay = electron.screen.getPrimaryDisplay();
let winHeight = waveWindow.winsize.height;
let winWidth = waveWindow.winsize.width;
if (winHeight > primaryDisplay.workAreaSize.height) {
winHeight = primaryDisplay.workAreaSize.height;
}
if (winWidth > primaryDisplay.workAreaSize.width) {
winWidth = primaryDisplay.workAreaSize.width;
}
let winX = waveWindow.pos.x;
let winY = waveWindow.pos.y;
if (winX + winWidth > primaryDisplay.workAreaSize.width) {
winX = Math.floor((primaryDisplay.workAreaSize.width - winWidth) / 2);
}
if (winY + winHeight > primaryDisplay.workAreaSize.height) {
winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2);
}
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
x: 200, x: winX,
y: 200, y: winY,
titleBarStyle: "hiddenInset", titleBarStyle: "hiddenInset",
width: waveWindow.winsize.width, width: winWidth,
height: waveWindow.winsize.height, height: winHeight,
minWidth: 500, minWidth: 500,
minHeight: 300, minHeight: 300,
icon: icon:
@ -221,11 +244,11 @@ function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserW
win.webContents.on("will-frame-navigate", shFrameNavHandler); win.webContents.on("will-frame-navigate", shFrameNavHandler);
win.on( win.on(
"resize", "resize",
debounce(400, (e) => mainResizeHandler(e, win)) debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
); );
win.on( win.on(
"move", "move",
debounce(400, (e) => mainResizeHandler(e, win)) debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
); );
win.webContents.on("zoom-changed", (e) => { win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed"); win.webContents.send("zoom-changed");
@ -257,10 +280,10 @@ electron.ipcMain.on("isDevServer", () => {
electronApp.quit(); electronApp.quit();
return; return;
} }
const waveHomeDir = getWaveHomeDir(); const waveHomeDir = getWaveHomeDir();
if (!fs.existsSync(waveHomeDir)) { if (!fs.existsSync(waveHomeDir)) {
fs.mkdirSync(waveHomeDir); fs.mkdirSync(waveHomeDir);
} }
try { try {
await runWaveSrv(); await runWaveSrv();
} catch (e) { } catch (e) {
@ -270,7 +293,7 @@ electron.ipcMain.on("isDevServer", () => {
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms"); console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
console.log("get client data"); console.log("get client data");
let clientData = await services.ClientService.GetClientData().catch(e => console.log(e)) as Client; let clientData = (await services.ClientService.GetClientData().catch((e) => console.log(e))) as Client;
console.log("client data ready"); console.log("client data ready");
let windowData: WaveWindow = (await services.ObjectService.GetObject( let windowData: WaveWindow = (await services.ObjectService.GetObject(
"window:" + clientData.mainwindowid "window:" + clientData.mainwindowid

View File

@ -98,3 +98,13 @@ class ObjectServiceType {
export const ObjectService = new ObjectServiceType() export const ObjectService = new ObjectServiceType()
// windowservice.WindowService (window)
class WindowServiceType {
// @returns object updates
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
}
}
export const WindowService = new WindowServiceType()

View File

@ -30,7 +30,7 @@ declare global {
type BlockCommand = { type BlockCommand = {
command: string; command: string;
} & ( BlockAppendIJsonCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockMessageCommand | BlockAppendFileCommand ); } & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | BlockSetViewCommand );
// wstore.BlockDef // wstore.BlockDef
type BlockDef = { type BlockDef = {
@ -40,6 +40,12 @@ declare global {
meta?: MetaType; meta?: MetaType;
}; };
// wshutil.BlockGetMetaCommand
type BlockGetMetaCommand = {
command: "getmeta";
oid: string;
};
// wshutil.BlockInputCommand // wshutil.BlockInputCommand
type BlockInputCommand = { type BlockInputCommand = {
command: "controller:input"; command: "controller:input";
@ -57,6 +63,7 @@ declare global {
// wshutil.BlockSetMetaCommand // wshutil.BlockSetMetaCommand
type BlockSetMetaCommand = { type BlockSetMetaCommand = {
command: "setmeta"; command: "setmeta";
oid?: string;
meta: MetaType; meta: MetaType;
}; };

View File

@ -39,6 +39,12 @@ const DefaultTimeout = 2 * time.Second
var globalLock = &sync.Mutex{} var globalLock = &sync.Mutex{}
var blockControllerMap = make(map[string]*BlockController) var blockControllerMap = make(map[string]*BlockController)
type BlockInputUnion struct {
InputData []byte `json:"inputdata,omitempty"`
SigName string `json:"signame,omitempty"`
TermSize *shellexec.TermSize `json:"termsize,omitempty"`
}
type BlockController struct { type BlockController struct {
Lock *sync.Mutex Lock *sync.Mutex
BlockId string BlockId string
@ -47,7 +53,7 @@ type BlockController struct {
Status string Status string
CreatedHtmlFile bool CreatedHtmlFile bool
ShellProc *shellexec.ShellProc ShellProc *shellexec.ShellProc
ShellInputCh chan *wshutil.BlockInputCommand ShellInputCh chan *BlockInputUnion
} }
func (bc *BlockController) WithLock(f func()) { func (bc *BlockController) WithLock(f func()) {
@ -159,6 +165,114 @@ func (bc *BlockController) resetTerminalState() {
} }
} }
func resolveSimpleId(ctx context.Context, simpleId string) (*waveobj.ORef, error) {
if strings.Contains(simpleId, ":") {
rtn, err := waveobj.ParseORef(simpleId)
if err != nil {
return nil, fmt.Errorf("error parsing simple id: %w", err)
}
return &rtn, nil
}
return wstore.DBResolveEasyOID(ctx, simpleId)
}
func staticHandleGetMeta(ctx context.Context, cmd *wshutil.BlockGetMetaCommand) (map[string]any, error) {
oref, err := waveobj.ParseORef(cmd.ORef)
if err != nil {
return nil, fmt.Errorf("error parsing oref: %w", err)
}
obj, err := wstore.DBGetORef(ctx, oref)
if err != nil {
return nil, fmt.Errorf("error getting object: %w", err)
}
if obj == nil {
return nil, fmt.Errorf("object not found: %s", oref)
}
return waveobj.GetMeta(obj), nil
}
func staticHandleSetMeta(ctx context.Context, cmd *wshutil.BlockSetMetaCommand, curBlockId string) (map[string]any, error) {
var oref *waveobj.ORef
if cmd.ORef != "" {
orefVal, err := waveobj.ParseORef(cmd.ORef)
if err != nil {
return nil, fmt.Errorf("error parsing oref: %w", err)
}
oref = &orefVal
} else {
orefVal := waveobj.MakeORef(wstore.OType_Block, curBlockId)
oref = &orefVal
}
log.Printf("SETMETA: %s | %v\n", oref, cmd.Meta)
obj, err := wstore.DBGetORef(ctx, *oref)
if err != nil {
return nil, fmt.Errorf("error getting object: %w", err)
}
if obj == nil {
return nil, nil
}
meta := waveobj.GetMeta(obj)
if meta == nil {
meta = make(map[string]any)
}
for k, v := range cmd.Meta {
if v == nil {
delete(meta, k)
continue
}
meta[k] = v
}
waveobj.SetMeta(obj, meta)
err = wstore.DBUpdate(ctx, obj)
if err != nil {
return nil, fmt.Errorf("error updating block: %w", err)
}
// send a waveobj:update event
updatedBlock, err := wstore.DBGetORef(ctx, *oref)
if err != nil {
return nil, fmt.Errorf("error getting object (2): %w", err)
}
eventbus.SendEvent(eventbus.WSEventType{
EventType: "waveobj:update",
ORef: oref.String(),
Data: wstore.WaveObjUpdate{
UpdateType: wstore.UpdateType_Update,
OType: updatedBlock.GetOType(),
OID: waveobj.GetOID(updatedBlock),
Obj: updatedBlock,
},
})
return nil, nil
}
func staticHandleResolveIds(ctx context.Context, cmd *wshutil.ResolveIdsCommand) (map[string]any, error) {
rtn := make(map[string]any)
for _, simpleId := range cmd.Ids {
oref, err := resolveSimpleId(ctx, simpleId)
if err != nil || oref == nil {
continue
}
rtn[simpleId] = oref.String()
}
return rtn, nil
}
func (bc *BlockController) waveOSCMessageHandler(ctx context.Context, cmd wshutil.BlockCommand, respFn wshutil.ResponseFnType) (wshutil.ResponseDataType, error) {
if strings.HasPrefix(cmd.GetCommand(), "controller:") {
bc.InputCh <- cmd
return nil, nil
}
switch cmd.GetCommand() {
case wshutil.BlockCommand_GetMeta:
return staticHandleGetMeta(ctx, cmd.(*wshutil.BlockGetMetaCommand))
case wshutil.Command_ResolveIds:
return staticHandleResolveIds(ctx, cmd.(*wshutil.ResolveIdsCommand))
}
ProcessStaticCommand(bc.BlockId, cmd)
return nil, nil
}
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error { func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
// create a circular blockfile for the output // create a circular blockfile for the output
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
@ -183,20 +297,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
bc.ShellProc.Close() bc.ShellProc.Close()
return err return err
} }
shellInputCh := make(chan *wshutil.BlockInputCommand) shellInputCh := make(chan *BlockInputUnion, 32)
bc.ShellInputCh = shellInputCh bc.ShellInputCh = shellInputCh
commandCh := make(chan wshutil.BlockCommand, 32) messageCh := make(chan wshutil.RpcMessage, 32)
ptyBuffer := wshutil.MakePtyBuffer(bc.ShellProc.Pty, commandCh) ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Pty, messageCh)
go func() { _, outputCh := wshutil.MakeWshRpc(wshutil.WaveServerOSC, messageCh, bc.waveOSCMessageHandler)
for cmd := range commandCh {
if strings.HasPrefix(cmd.GetCommand(), "controller:") {
bc.InputCh <- cmd
} else {
ProcessStaticCommand(bc.BlockId, cmd)
}
}
}()
go func() { go func() {
// handles regular output from the pty (goes to the blockfile and xterm)
defer func() { defer func() {
// needs synchronization // needs synchronization
bc.ShellProc.Close() bc.ShellProc.Close()
@ -223,15 +330,10 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
} }
}() }()
go func() { go func() {
// handles input from the shellInputCh, sent to pty
for ic := range shellInputCh { for ic := range shellInputCh {
if ic.InputData64 != "" { if len(ic.InputData) > 0 {
inputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(ic.InputData64))) bc.ShellProc.Pty.Write(ic.InputData)
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 { if ic.TermSize != nil {
log.Printf("SETTERMSIZE: %dx%d\n", ic.TermSize.Rows, ic.TermSize.Cols) log.Printf("SETTERMSIZE: %dx%d\n", ic.TermSize.Rows, ic.TermSize.Cols)
@ -240,6 +342,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
log.Printf("error setting term size: %v\n", err) log.Printf("error setting term size: %v\n", err)
} }
} }
// TODO signals
}
}()
go func() {
// handles outputCh -> shellInputCh
for out := range outputCh {
shellInputCh <- &BlockInputUnion{InputData: out}
} }
}() }()
return nil return nil
@ -277,10 +386,24 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
for genCmd := range bc.InputCh { for genCmd := range bc.InputCh {
switch cmd := genCmd.(type) { switch cmd := genCmd.(type) {
case *wshutil.BlockInputCommand: case *wshutil.BlockInputCommand:
log.Printf("INPUT: %s | %q\n", bc.BlockId, cmd.InputData64) if bc.ShellInputCh == nil {
if bc.ShellInputCh != nil { continue
bc.ShellInputCh <- cmd
} }
inputUnion := &BlockInputUnion{
SigName: cmd.SigName,
TermSize: cmd.TermSize,
}
if len(cmd.InputData64) > 0 {
inputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(cmd.InputData64)))
nw, err := base64.StdEncoding.Decode(inputBuf, []byte(cmd.InputData64))
if err != nil {
log.Printf("error decoding input data: %v\n", err)
continue
}
inputUnion.InputData = inputBuf[:nw]
}
log.Printf("INPUT: %s | %q\n", bc.BlockId, string(inputUnion.InputData))
bc.ShellInputCh <- inputUnion
default: default:
log.Printf("unknown command type %T\n", cmd) log.Printf("unknown command type %T\n", cmd)
} }
@ -363,44 +486,12 @@ func ProcessStaticCommand(blockId string, cmdGen wshutil.BlockCommand) error {
}) })
return nil return nil
case *wshutil.BlockSetMetaCommand: case *wshutil.BlockSetMetaCommand:
log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) _, err := staticHandleSetMeta(ctx, cmd, blockId)
block, err := wstore.DBGet[*wstore.Block](ctx, blockId)
if err != nil { if err != nil {
return fmt.Errorf("error getting block: %w", err) return err
} }
if block == nil {
return nil
}
if block.Meta == nil {
block.Meta = make(map[string]any)
}
for k, v := range cmd.Meta {
if v == nil {
delete(block.Meta, k)
continue
}
block.Meta[k] = v
}
err = wstore.DBUpdate(ctx, block)
if err != nil {
return fmt.Errorf("error updating block: %w", err)
}
// send a waveobj:update event
updatedBlock, err := wstore.DBGet[*wstore.Block](ctx, blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
eventbus.SendEvent(eventbus.WSEventType{
EventType: "waveobj:update",
ORef: waveobj.MakeORef(wstore.OType_Block, blockId).String(),
Data: wstore.WaveObjUpdate{
UpdateType: wstore.UpdateType_Update,
OType: wstore.OType_Block,
OID: blockId,
Obj: updatedBlock,
},
})
return nil return nil
case *wshutil.BlockMessageCommand: case *wshutil.BlockMessageCommand:
log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message) log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message)
return nil return nil

View File

@ -5,7 +5,6 @@ package objectservice
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -72,26 +71,6 @@ func (svc *ObjectService) GetObjects(orefStrArr []string) ([]waveobj.WaveObj, er
return wstore.DBSelectORefs(ctx, orefArr) return wstore.DBSelectORefs(ctx, orefArr)
} }
func updatesRtn(ctx context.Context, rtnVal map[string]any) (any, error) {
updates := wstore.ContextGetUpdates(ctx)
if len(updates) == 0 {
return nil, nil
}
updateArr := make([]wstore.WaveObjUpdate, 0, len(updates))
for _, update := range updates {
updateArr = append(updateArr, update)
}
jval, err := json.Marshal(updateArr)
if err != nil {
return nil, fmt.Errorf("error converting updates to JSON: %w", err)
}
if rtnVal == nil {
rtnVal = make(map[string]any)
}
rtnVal["updates"] = json.RawMessage(jval)
return rtnVal, nil
}
func (svc *ObjectService) AddTabToWorkspace_Meta() tsgenmeta.MethodMeta { func (svc *ObjectService) AddTabToWorkspace_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{ return tsgenmeta.MethodMeta{
ArgNames: []string{"uiContext", "tabName", "activateTab"}, ArgNames: []string{"uiContext", "tabName", "activateTab"},

View File

@ -13,7 +13,9 @@ import (
"github.com/wavetermdev/thenextwave/pkg/service/clientservice" "github.com/wavetermdev/thenextwave/pkg/service/clientservice"
"github.com/wavetermdev/thenextwave/pkg/service/fileservice" "github.com/wavetermdev/thenextwave/pkg/service/fileservice"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice" "github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/service/windowservice"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/web/webcmd" "github.com/wavetermdev/thenextwave/pkg/web/webcmd"
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
@ -25,6 +27,7 @@ var ServiceMap = map[string]any{
"object": &objectservice.ObjectService{}, "object": &objectservice.ObjectService{},
"file": &fileservice.FileService{}, "file": &fileservice.FileService{},
"client": &clientservice.ClientService{}, "client": &clientservice.ClientService{},
"window": &windowservice.WindowService{},
} }
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem() var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
@ -85,7 +88,7 @@ func convertNumber(argType reflect.Type, jsonArg float64) (any, error) {
func convertComplex(argType reflect.Type, jsonArg any) (any, error) { func convertComplex(argType reflect.Type, jsonArg any) (any, error) {
nativeArgVal := reflect.New(argType) nativeArgVal := reflect.New(argType)
err := waveobj.DoMapStucture(nativeArgVal.Interface(), jsonArg) err := utilfn.DoMapStucture(nativeArgVal.Interface(), jsonArg)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,34 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package windowservice
import (
"context"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
type WindowService struct{}
func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) {
if pos == nil && size == nil {
return nil, nil
}
ctx = wstore.ContextWithUpdates(ctx)
win, err := wstore.DBMustGet[*wstore.Window](ctx, windowId)
if err != nil {
return nil, err
}
if pos != nil {
win.Pos = *pos
}
if size != nil {
win.WinSize = *size
}
err = wstore.DBUpdate(ctx, win)
if err != nil {
return nil, err
}
return wstore.ContextGetUpdatesRtn(ctx), nil
}

View File

@ -23,6 +23,8 @@ import (
"strings" "strings"
"syscall" "syscall"
"unicode/utf8" "unicode/utf8"
"github.com/mitchellh/mapstructure"
) )
var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
@ -725,3 +727,16 @@ func IndentString(indent string, str string) string {
} }
return rtn.String() return rtn.String()
} }
// does a mapstructure using "json" tags
func DoMapStucture(out any, input any) error {
dconfig := &mapstructure.DecoderConfig{
Result: out,
TagName: "json",
}
decoder, err := mapstructure.NewDecoder(dconfig)
if err != nil {
return err
}
return decoder.Decode(input)
}

View File

@ -184,18 +184,6 @@ func SetMeta(waveObj WaveObj, meta map[string]any) {
reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Set(reflect.ValueOf(meta)) reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Set(reflect.ValueOf(meta))
} }
func DoMapStucture(out any, input any) error {
dconfig := &mapstructure.DecoderConfig{
Result: out,
TagName: "json",
}
decoder, err := mapstructure.NewDecoder(dconfig)
if err != nil {
return err
}
return decoder.Decode(input)
}
func ToJsonMap(w WaveObj) (map[string]any, error) { func ToJsonMap(w WaveObj) (map[string]any, error) {
if w == nil { if w == nil {
return nil, nil return nil, nil

View File

@ -9,7 +9,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
) )
const ( const (
@ -48,7 +48,7 @@ func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
switch cmdType { switch cmdType {
case WSCommand_SetBlockTermSize: case WSCommand_SetBlockTermSize:
var cmd SetBlockTermSizeWSCommand var cmd SetBlockTermSizeWSCommand
err := waveobj.DoMapStucture(&cmd, cmdMap) err := utilfn.DoMapStucture(&cmd, cmdMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err) return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err)
} }

View File

@ -0,0 +1,102 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wshutil
import (
"encoding/json"
"fmt"
)
type RpcMessageUnmarshalHelper struct {
Command string
ReqId string
ResId string
M map[string]any
Req *RpcRequest
Res *RpcResponse
}
func (helper *RpcMessageUnmarshalHelper) UnmarshalJSON(data []byte) error {
var rmap map[string]any
if err := json.Unmarshal(data, &rmap); err != nil {
return err
}
if command, ok := rmap["command"].(string); ok {
helper.Command = command
}
if reqid, ok := rmap["reqid"].(string); ok {
helper.ReqId = reqid
}
if resid, ok := rmap["resid"].(string); ok {
helper.ResId = resid
}
if helper.ReqId != "" && helper.ResId != "" {
return fmt.Errorf("both reqid and resid cannot be set")
}
if helper.Command == "" && helper.ResId == "" {
return fmt.Errorf("either command or resid must be set")
}
helper.M = rmap
if helper.Command != "" {
// ok, this is a request, so lets parse it
req, err := helper.parseRequest()
if err != nil {
return fmt.Errorf("error parsing request: %w", err)
}
helper.Req = req
} else {
// this is a response, parse it
res, err := helper.parseResponse()
if err != nil {
return fmt.Errorf("error parsing response: %w", err)
}
helper.Res = res
}
return nil
}
func (helper *RpcMessageUnmarshalHelper) parseRequest() (*RpcRequest, error) {
req := &RpcRequest{
ReqId: helper.ReqId,
}
if helper.M["timeoutms"] != nil {
timeoutMs, ok := helper.M["timeoutms"].(float64)
if !ok {
return nil, fmt.Errorf("timeoutms field is not a number")
}
req.TimeoutMs = int(timeoutMs)
}
cmd, err := ParseCmdMap(helper.M)
if err != nil {
return nil, fmt.Errorf("error parsing command: %w", err)
}
req.Command = cmd
return req, nil
}
func (helper *RpcMessageUnmarshalHelper) parseResponse() (*RpcResponse, error) {
rtn := &RpcResponse{
ResId: helper.ResId,
Data: helper.M,
}
if helper.M["error"] != nil {
errStr, ok := helper.M["error"].(string)
if !ok {
return nil, fmt.Errorf("error field is not a string")
}
rtn.Error = errStr
}
if helper.M["cont"] != nil {
cont, ok := helper.M["cont"].(bool)
if !ok {
return nil, fmt.Errorf("cont field is not a bool")
}
rtn.Cont = cont
}
delete(rtn.Data, "resid")
delete(rtn.Data, "error")
delete(rtn.Data, "cont")
return rtn, nil
}

View File

@ -1,3 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wshutil package wshutil
import ( import (
@ -5,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"sync" "sync"
) )
@ -22,19 +26,25 @@ type PtyBuffer struct {
DataBuf *bytes.Buffer DataBuf *bytes.Buffer
EscMode string EscMode string
EscSeqBuf []byte EscSeqBuf []byte
OSCPrefix string
InputReader io.Reader InputReader io.Reader
CommandCh chan BlockCommand MessageCh chan RpcMessage
AtEOF bool AtEOF bool
Err error Err error
} }
func MakePtyBuffer(input io.Reader, commandCh chan BlockCommand) *PtyBuffer { // closes messageCh when input is closed (or error)
func MakePtyBuffer(oscPrefix string, input io.Reader, messageCh chan RpcMessage) *PtyBuffer {
if len(oscPrefix) != WaveOSCPrefixLen {
panic(fmt.Sprintf("invalid OSC prefix length: %d", len(oscPrefix)))
}
b := &PtyBuffer{ b := &PtyBuffer{
CVar: sync.NewCond(&sync.Mutex{}), CVar: sync.NewCond(&sync.Mutex{}),
DataBuf: &bytes.Buffer{}, DataBuf: &bytes.Buffer{},
OSCPrefix: oscPrefix,
EscMode: Mode_Normal, EscMode: Mode_Normal,
InputReader: input, InputReader: input,
CommandCh: commandCh, MessageCh: messageCh,
} }
go b.run() go b.run()
return b return b
@ -57,22 +67,21 @@ func (b *PtyBuffer) setEOF() {
} }
func (b *PtyBuffer) processWaveEscSeq(escSeq []byte) { func (b *PtyBuffer) processWaveEscSeq(escSeq []byte) {
jmsg := make(map[string]any) var helper RpcMessageUnmarshalHelper
err := json.Unmarshal(escSeq, &jmsg) err := json.Unmarshal(escSeq, &helper)
if err != nil { if err != nil {
b.setErr(fmt.Errorf("error unmarshalling Wave OSC sequence data: %w", err)) log.Printf("error unmarshalling Wave OSC sequence data: %v\n", err)
return return
} }
cmd, err := ParseCmdMap(jmsg) if helper.Req != nil {
if err != nil { b.MessageCh <- helper.Req
b.setErr(fmt.Errorf("error parsing Wave OSC command: %w", err)) } else {
return b.MessageCh <- helper.Res
} }
b.CommandCh <- cmd
} }
func (b *PtyBuffer) run() { func (b *PtyBuffer) run() {
defer close(b.CommandCh) defer close(b.MessageCh)
buf := make([]byte, 4096) buf := make([]byte, 4096)
for { for {
n, err := b.InputReader.Read(buf) n, err := b.InputReader.Read(buf)
@ -101,7 +110,7 @@ func (b *PtyBuffer) processData(data []byte) {
} else if ch == BEL || ch == ST { } else if ch == BEL || ch == ST {
// terminates the escpae sequence (is a valid Wave OSC command) // terminates the escpae sequence (is a valid Wave OSC command)
b.EscMode = Mode_Normal b.EscMode = Mode_Normal
waveEscSeq := b.EscSeqBuf[len(WaveOSCPrefix):] waveEscSeq := b.EscSeqBuf[WaveOSCPrefixLen:]
b.EscSeqBuf = nil b.EscSeqBuf = nil
b.processWaveEscSeq(waveEscSeq) b.processWaveEscSeq(waveEscSeq)
} else { } else {
@ -115,21 +124,22 @@ func (b *PtyBuffer) processData(data []byte) {
b.EscMode = Mode_Normal b.EscMode = Mode_Normal
outputBuf = append(outputBuf, b.EscSeqBuf...) outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch) outputBuf = append(outputBuf, ch)
} else { b.EscSeqBuf = nil
if ch == WaveOSCPrefixBytes[len(b.EscSeqBuf)] { continue
// we're still building what could be a Wave OSC sequence }
b.EscSeqBuf = append(b.EscSeqBuf, ch) if ch != b.OSCPrefix[len(b.EscSeqBuf)] {
} else { // this is not a Wave OSC sequence, just an escape sequence
// this is not a Wave OSC sequence, just an escape sequence b.EscMode = Mode_Normal
b.EscMode = Mode_Normal outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, b.EscSeqBuf...) outputBuf = append(outputBuf, ch)
outputBuf = append(outputBuf, ch) b.EscSeqBuf = nil
continue continue
} }
// check to see if we have a full Wave OSC prefix // we're still building what could be a Wave OSC sequence
if len(b.EscSeqBuf) == len(WaveOSCPrefixBytes) { b.EscSeqBuf = append(b.EscSeqBuf, ch)
b.EscMode = Mode_WaveEsc // check to see if we have a full Wave OSC prefix
} if len(b.EscSeqBuf) == len(b.OSCPrefix) {
b.EscMode = Mode_WaveEsc
} }
continue continue
} }

View File

@ -23,6 +23,7 @@ const (
BlockCommand_Input = "controller:input" BlockCommand_Input = "controller:input"
BlockCommand_AppendBlockFile = "blockfile:append" BlockCommand_AppendBlockFile = "blockfile:append"
BlockCommand_AppendIJson = "blockfile:appendijson" BlockCommand_AppendIJson = "blockfile:appendijson"
Command_ResolveIds = "resolveids"
) )
var CommandToTypeMap = map[string]reflect.Type{ var CommandToTypeMap = map[string]reflect.Type{
@ -33,6 +34,7 @@ var CommandToTypeMap = map[string]reflect.Type{
BlockCommand_Message: reflect.TypeOf(BlockMessageCommand{}), BlockCommand_Message: reflect.TypeOf(BlockMessageCommand{}),
BlockCommand_AppendBlockFile: reflect.TypeOf(BlockAppendFileCommand{}), BlockCommand_AppendBlockFile: reflect.TypeOf(BlockAppendFileCommand{}),
BlockCommand_AppendIJson: reflect.TypeOf(BlockAppendIJsonCommand{}), BlockCommand_AppendIJson: reflect.TypeOf(BlockAppendIJsonCommand{}),
Command_ResolveIds: reflect.TypeOf(ResolveIdsCommand{}),
} }
func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta { func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta {
@ -91,6 +93,15 @@ func (ic *BlockInputCommand) GetCommand() string {
return BlockCommand_Input return BlockCommand_Input
} }
type ResolveIdsCommand struct {
Command string `json:"command" tstype:"\"resolveids\""`
Ids []string `json:"ids"`
}
func (ric *ResolveIdsCommand) GetCommand() string {
return Command_ResolveIds
}
type BlockSetViewCommand struct { type BlockSetViewCommand struct {
Command string `json:"command" tstype:"\"setview\""` Command string `json:"command" tstype:"\"setview\""`
View string `json:"view"` View string `json:"view"`
@ -102,8 +113,7 @@ func (svc *BlockSetViewCommand) GetCommand() string {
type BlockGetMetaCommand struct { type BlockGetMetaCommand struct {
Command string `json:"command" tstype:"\"getmeta\""` Command string `json:"command" tstype:"\"getmeta\""`
RpcId string `json:"rpcid"` ORef string `json:"oref"` // oref string
OID string `json:"oid"` // allows oref, 8-char oid, or full uuid
} }
func (gmc *BlockGetMetaCommand) GetCommand() string { func (gmc *BlockGetMetaCommand) GetCommand() string {
@ -112,7 +122,7 @@ func (gmc *BlockGetMetaCommand) GetCommand() string {
type BlockSetMetaCommand struct { type BlockSetMetaCommand struct {
Command string `json:"command" tstype:"\"setmeta\""` Command string `json:"command" tstype:"\"setmeta\""`
OID string `json:"oid"` // allows oref, 8-char oid, or full uuid ORef string `json:"oref,omitempty"` // allows oref, 8-char oid, or full uuid (empty is current block)
Meta map[string]any `json:"meta"` Meta map[string]any `json:"meta"`
} }

330
pkg/wshutil/wshrpc.go Normal file
View File

@ -0,0 +1,330 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wshutil
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
)
const DefaultTimeoutMs = 5000
const RespChSize = 32
const DefaultOutputChSize = 32
type ResponseDataType = map[string]any
type ResponseFnType = func(ResponseDataType) error
type CommandHandlerFnType = func(context.Context, BlockCommand, ResponseFnType) (ResponseDataType, error)
type RpcMessage interface {
IsRpcRequest() bool
}
type WshRpc struct {
Lock *sync.Mutex
InputCh chan RpcMessage
OutputCh chan []byte
OSCEsc string // either 23198 or 23199
RpcMap map[string]*rpcData
HandlerFn CommandHandlerFnType
}
type RpcRequest struct {
ReqId string
TimeoutMs int
Command BlockCommand
}
func (r *RpcRequest) IsRpcRequest() bool {
return true
}
func (r *RpcRequest) MarshalJSON() ([]byte, error) {
if r == nil {
return []byte("null"), nil
}
rtn := make(map[string]any)
utilfn.DoMapStucture(&rtn, r.Command)
rtn["command"] = r.Command.GetCommand()
if r.ReqId != "" {
rtn["reqid"] = r.ReqId
} else {
delete(rtn, "reqid")
}
if r.TimeoutMs != 0 {
rtn["timeoutms"] = float64(r.TimeoutMs)
} else {
delete(rtn, "timeoutms")
}
return json.Marshal(rtn)
}
type RpcResponse struct {
ResId string `json:"resid"`
Error string `json:"error,omitempty"`
Cont bool `json:"cont,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
func (r *RpcResponse) IsRpcRequest() bool {
return false
}
func (r *RpcResponse) MarshalJSON() ([]byte, error) {
rtn := make(map[string]any)
// rest goes first (since other fields will overwrite)
for k, v := range r.Data {
rtn[k] = v
}
rtn["resid"] = r.ResId
if r.Error != "" {
rtn["error"] = r.Error
} else {
delete(rtn, "error")
}
if r.Cont {
rtn["cont"] = true
} else {
delete(rtn, "cont")
}
return json.Marshal(rtn)
}
type rpcData struct {
ResCh chan *RpcResponse
Ctx context.Context
CancelFn context.CancelFunc
}
// oscEsc is the OSC escape sequence to use for *sending* messages
// closes outputCh when inputCh is closed/done
func MakeWshRpc(oscEsc string, inputCh chan RpcMessage, commandHandlerFn CommandHandlerFnType) (*WshRpc, chan []byte) {
if len(oscEsc) != 5 {
panic("oscEsc must be 5 characters")
}
outputCh := make(chan []byte, DefaultOutputChSize)
rtn := &WshRpc{
Lock: &sync.Mutex{},
InputCh: inputCh,
OutputCh: outputCh,
OSCEsc: oscEsc,
RpcMap: make(map[string]*rpcData),
HandlerFn: commandHandlerFn,
}
go rtn.runServer()
return rtn, outputCh
}
func (w *WshRpc) handleRequest(req *RpcRequest) {
defer func() {
if r := recover(); r != nil {
errResp := &RpcResponse{
ResId: req.ReqId,
Error: fmt.Sprintf("panic: %v", r),
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, errResp)
if err != nil {
return
}
w.OutputCh <- barr
}
}()
respFn := func(resp ResponseDataType) error {
if req.ReqId == "" {
// request is not expecting a response
return nil
}
respMsg := &RpcResponse{
ResId: req.ReqId,
Cont: true,
Data: resp,
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, respMsg)
if err != nil {
return fmt.Errorf("error marshalling response to json: %w", err)
}
w.OutputCh <- barr
return nil
}
timeoutMs := req.TimeoutMs
if timeoutMs <= 0 {
timeoutMs = DefaultTimeoutMs
}
ctx, cancelFn := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
defer cancelFn()
respData, err := w.HandlerFn(ctx, req.Command, respFn)
log.Printf("handler for %q returned resp: %v\n", req.Command.GetCommand(), respData)
if req.ReqId == "" {
// no response expected
if err != nil {
log.Printf("error handling request (no response): %v\n", err)
}
return
}
if err != nil {
errResp := &RpcResponse{
ResId: req.ReqId,
Error: err.Error(),
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, errResp)
if err != nil {
return
}
w.OutputCh <- barr
return
}
respMsg := &RpcResponse{
ResId: req.ReqId,
Data: respData,
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, respMsg)
if err != nil {
respMsg := &RpcResponse{
ResId: req.ReqId,
Error: err.Error(),
}
barr, _ = EncodeWaveOSCMessageEx(w.OSCEsc, respMsg)
}
w.OutputCh <- barr
}
func (w *WshRpc) runServer() {
defer close(w.OutputCh)
for msg := range w.InputCh {
if msg.IsRpcRequest() {
if w.HandlerFn == nil {
continue
}
req := msg.(*RpcRequest)
w.handleRequest(req)
} else {
resp := msg.(*RpcResponse)
respCh := w.getResponseCh(resp.ResId)
if respCh == nil {
continue
}
respCh <- resp
if !resp.Cont {
w.unregisterRpc(resp.ResId, nil)
}
}
}
}
func (w *WshRpc) getResponseCh(resId string) chan *RpcResponse {
if resId == "" {
return nil
}
w.Lock.Lock()
defer w.Lock.Unlock()
rd := w.RpcMap[resId]
if rd == nil {
return nil
}
return rd.ResCh
}
func (w *WshRpc) SetHandler(handler CommandHandlerFnType) {
w.Lock.Lock()
defer w.Lock.Unlock()
w.HandlerFn = handler
}
// no response
func (w *WshRpc) SendCommand(cmd BlockCommand) error {
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, &RpcRequest{Command: cmd})
if err != nil {
return fmt.Errorf("error marshalling request to json: %w", err)
}
w.OutputCh <- barr
return nil
}
func (w *WshRpc) registerRpc(reqId string, timeoutMs int) chan *RpcResponse {
w.Lock.Lock()
defer w.Lock.Unlock()
if timeoutMs <= 0 {
timeoutMs = DefaultTimeoutMs
}
ctx, cancelFn := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
rpcCh := make(chan *RpcResponse, RespChSize)
w.RpcMap[reqId] = &rpcData{
ResCh: rpcCh,
Ctx: ctx,
CancelFn: cancelFn,
}
go func() {
<-ctx.Done()
w.unregisterRpc(reqId, fmt.Errorf("EC-TIME: timeout waiting for response"))
}()
return rpcCh
}
func (w *WshRpc) unregisterRpc(reqId string, err error) {
w.Lock.Lock()
defer w.Lock.Unlock()
rd := w.RpcMap[reqId]
if rd != nil {
if err != nil {
errResp := &RpcResponse{
ResId: reqId,
Error: err.Error(),
}
rd.ResCh <- errResp
}
close(rd.ResCh)
rd.CancelFn()
}
delete(w.RpcMap, reqId)
}
// single response
func (w *WshRpc) SendRpcRequest(cmd BlockCommand, timeoutMs int) (map[string]any, error) {
if timeoutMs < 0 {
return nil, fmt.Errorf("timeout must be >= 0")
}
req := &RpcRequest{
Command: cmd,
ReqId: uuid.New().String(),
TimeoutMs: timeoutMs,
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, req)
if err != nil {
return nil, fmt.Errorf("error marshalling request to ANSI esc: %w", err)
}
rpcCh := w.registerRpc(req.ReqId, timeoutMs)
defer w.unregisterRpc(req.ReqId, nil)
w.OutputCh <- barr
resp := <-rpcCh
if resp.Error != "" {
return nil, errors.New(resp.Error)
}
return resp.Data, nil
}
// streaming response
func (w *WshRpc) SendRpcRequestEx(cmd BlockCommand, timeoutMs int) (chan *RpcResponse, error) {
if timeoutMs < 0 {
return nil, fmt.Errorf("timeout must be >= 0")
}
req := &RpcRequest{
Command: cmd,
ReqId: uuid.New().String(),
TimeoutMs: timeoutMs,
}
barr, err := EncodeWaveOSCMessageEx(w.OSCEsc, req)
if err != nil {
return nil, fmt.Errorf("error marshalling request to json: %w", err)
}
rpcCh := w.registerRpc(req.ReqId, timeoutMs)
w.OutputCh <- barr
return rpcCh, nil
}

View File

@ -11,18 +11,19 @@ import (
"reflect" "reflect"
) )
// these should both be 5 characters
const WaveOSC = "23198" const WaveOSC = "23198"
const WaveServerOSC = "23199"
const WaveOSCPrefixLen = 5 + 3 // \x1b] + WaveOSC + ; + \x07
const WaveOSCPrefix = "\x1b]" + WaveOSC + ";" const WaveOSCPrefix = "\x1b]" + WaveOSC + ";"
const WaveResponseOSC = "23199" const WaveServerOSCPrefix = "\x1b]" + WaveServerOSC + ";"
const WaveResponseOSCPrefix = "\x1b]" + WaveResponseOSC + ";"
const HexChars = "0123456789ABCDEF" const HexChars = "0123456789ABCDEF"
const BEL = 0x07 const BEL = 0x07
const ST = 0x9c const ST = 0x9c
const ESC = 0x1b const ESC = 0x1b
var WaveOSCPrefixBytes = []byte(WaveOSCPrefix)
// OSC escape types // OSC escape types
// OSC 23198 ; (JSON | base64-JSON) ST // OSC 23198 ; (JSON | base64-JSON) ST
// JSON = must escape all ASCII control characters ([\x00-\x1F\x7F]) // JSON = must escape all ASCII control characters ([\x00-\x1F\x7F])
@ -31,19 +32,37 @@ var WaveOSCPrefixBytes = []byte(WaveOSCPrefix)
// for responses (terminal -> program), we'll use OSC 23199 // for responses (terminal -> program), we'll use OSC 23199
// same json format // same json format
func EncodeWaveOSCMessage(cmd BlockCommand) ([]byte, error) { func copyOscPrefix(dst []byte, oscNum string) {
if cmd.GetCommand() == "" { dst[0] = ESC
return nil, fmt.Errorf("command field not set in struct") dst[1] = ']'
copy(dst[2:], oscNum)
dst[len(oscNum)+2] = ';'
}
func oscPrefixLen(oscNum string) int {
return 3 + len(oscNum)
}
func makeOscPrefix(oscNum string) []byte {
output := make([]byte, oscPrefixLen(oscNum))
copyOscPrefix(output, oscNum)
return output
}
func EncodeWaveReq(cmd BlockCommand) ([]byte, error) {
req := &RpcRequest{Command: cmd}
return EncodeWaveOSCMessage(req)
}
func EncodeWaveOSCMessage(msg RpcMessage) ([]byte, error) {
return EncodeWaveOSCMessageEx(WaveOSC, msg)
}
func EncodeWaveOSCMessageEx(oscNum string, msg RpcMessage) ([]byte, error) {
if msg == nil {
return nil, fmt.Errorf("nil message")
} }
ctype, ok := CommandToTypeMap[cmd.GetCommand()] barr, err := json.Marshal(msg)
if !ok {
return nil, fmt.Errorf("unknown command type %q", cmd.GetCommand())
}
cmdType := reflect.TypeOf(cmd)
if cmdType != ctype && (cmdType.Kind() == reflect.Pointer && cmdType.Elem() != ctype) {
return nil, fmt.Errorf("command type does not match %q", cmd.GetCommand())
}
barr, err := json.Marshal(cmd)
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshalling message to json: %w", err) return nil, fmt.Errorf("error marshalling message to json: %w", err)
} }
@ -57,15 +76,15 @@ func EncodeWaveOSCMessage(cmd BlockCommand) ([]byte, error) {
if !hasControlChars { if !hasControlChars {
// If no control characters, directly construct the output // If no control characters, directly construct the output
// \x1b] (2) + WaveOSC + ; (1) + message + \x07 (1) // \x1b] (2) + WaveOSC + ; (1) + message + \x07 (1)
output := make([]byte, len(WaveOSCPrefix)+len(barr)+1) output := make([]byte, oscPrefixLen(oscNum)+len(barr)+1)
copy(output, WaveOSCPrefixBytes) copyOscPrefix(output, oscNum)
copy(output[len(WaveOSCPrefix):], barr) copy(output[oscPrefixLen(oscNum):], barr)
output[len(output)-1] = BEL output[len(output)-1] = BEL
return output, nil return output, nil
} }
var buf bytes.Buffer var buf bytes.Buffer
buf.Write(WaveOSCPrefixBytes) buf.Write(makeOscPrefix(oscNum))
escSeq := [6]byte{'\\', 'u', '0', '0', '0', '0'} escSeq := [6]byte{'\\', 'u', '0', '0', '0', '0'}
for _, b := range barr { for _, b := range barr {
if b < 0x20 || b == 0x7f { if b < 0x20 || b == 0x7f {