mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
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:
parent
d0c4f5c46f
commit
e46906d423
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react({}),
|
react({}),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
|
@ -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 {
|
||||||
@ -130,14 +130,20 @@ function runWaveSrv(): Promise<boolean> {
|
|||||||
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");
|
||||||
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
9
frontend/types/gotypes.d.ts
vendored
9
frontend/types/gotypes.d.ts
vendored
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
34
pkg/service/windowservice/windowservice.go
Normal file
34
pkg/service/windowservice/windowservice.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
102
pkg/wshutil/unmarshalhelper.go
Normal file
102
pkg/wshutil/unmarshalhelper.go
Normal 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
|
||||||
|
}
|
@ -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,22 +124,23 @@ 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
|
||||||
}
|
}
|
||||||
|
// we're still building what could be a Wave OSC sequence
|
||||||
|
b.EscSeqBuf = append(b.EscSeqBuf, ch)
|
||||||
// check to see if we have a full Wave OSC prefix
|
// check to see if we have a full Wave OSC prefix
|
||||||
if len(b.EscSeqBuf) == len(WaveOSCPrefixBytes) {
|
if len(b.EscSeqBuf) == len(b.OSCPrefix) {
|
||||||
b.EscMode = Mode_WaveEsc
|
b.EscMode = Mode_WaveEsc
|
||||||
}
|
}
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Mode_Normal
|
// Mode_Normal
|
||||||
|
@ -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
330
pkg/wshutil/wshrpc.go
Normal 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
|
||||||
|
}
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user