From c7a60a80f862579b99b0fd70afd0a6be43a710ec Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 17 Sep 2024 23:10:09 -0700 Subject: [PATCH] initwshrpc in electron (#391) --- cmd/wsh/cmd/wshcmd-root.go | 2 -- cmd/wsh/cmd/wshcmd-web.go | 55 ++++++++++++++++++++++++++++-- emain/emain-web.ts | 37 ++++++++++---------- emain/emain-wsh.ts | 31 +++++++++++++++++ emain/emain.ts | 8 +++++ frontend/app/store/ws.ts | 31 ++++++++++++++--- frontend/app/store/wshclient.ts | 21 +++++++----- frontend/app/store/wshclientapi.ts | 10 ++++++ frontend/app/store/wshrouter.ts | 42 ++++++++--------------- frontend/app/store/wshrpcutil.ts | 16 ++++++++- frontend/types/gotypes.d.ts | 23 +++++++++++++ package.json | 4 ++- pkg/web/ws.go | 10 ++++-- pkg/wshrpc/wshclient/wshclient.go | 12 +++++++ pkg/wshrpc/wshrpctypes.go | 26 ++++++++++++++ pkg/wshrpc/wshserver/wshserver.go | 21 ++++++++++++ pkg/wshutil/wshrouter.go | 1 + yarn.lock | 13 ++++++- 18 files changed, 296 insertions(+), 67 deletions(-) create mode 100644 emain/emain-wsh.ts diff --git a/cmd/wsh/cmd/wshcmd-root.go b/cmd/wsh/cmd/wshcmd-root.go index 9d8c746c5..411ff08b2 100644 --- a/cmd/wsh/cmd/wshcmd-root.go +++ b/cmd/wsh/cmd/wshcmd-root.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "log" "os" "regexp" "runtime/debug" @@ -177,7 +176,6 @@ func Execute() { }() err := rootCmd.Execute() if err != nil { - log.Printf("[error] %v\n", err) wshutil.DoShutdown("", 1, true) return } diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go index c85203db1..bd2985931 100644 --- a/cmd/wsh/cmd/wshcmd-web.go +++ b/cmd/wsh/cmd/wshcmd-web.go @@ -4,12 +4,14 @@ package cmd import ( + "encoding/json" "fmt" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" + "github.com/wavetermdev/waveterm/pkg/wshutil" ) var webCmd = &cobra.Command{ @@ -26,15 +28,16 @@ var webOpenCmd = &cobra.Command{ } var webGetCmd = &cobra.Command{ - Use: "get [--inner] [--all] css-selector", + Use: "get [--inner] [--all] [--json] blockid css-selector", Short: "get the html for a css selector", - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(2), Hidden: true, RunE: webGetRun, } var webGetInner bool var webGetAll bool +var webGetJson bool var webOpenMagnified bool func init() { @@ -42,11 +45,59 @@ func init() { webCmd.AddCommand(webOpenCmd) webGetCmd.Flags().BoolVarP(&webGetInner, "inner", "", false, "get inner html (instead of outer)") webGetCmd.Flags().BoolVarP(&webGetAll, "all", "", false, "get all matches (querySelectorAll)") + webGetCmd.Flags().BoolVarP(&webGetJson, "json", "", false, "output as json") webCmd.AddCommand(webGetCmd) rootCmd.AddCommand(webCmd) } func webGetRun(cmd *cobra.Command, args []string) error { + oref := args[0] + if oref == "" { + return fmt.Errorf("blockid not specified") + } + err := validateEasyORef(oref) + if err != nil { + return err + } + fullORef, err := resolveSimpleId(oref) + if err != nil { + return fmt.Errorf("resolving blockid: %w", err) + } + blockInfo, err := wshclient.BlockInfoCommand(RpcClient, fullORef.OID, nil) + if err != nil { + return fmt.Errorf("getting block info: %w", err) + } + if blockInfo.Meta.GetString(waveobj.MetaKey_View, "") != "web" { + return fmt.Errorf("block %s is not a web block", fullORef.OID) + } + data := wshrpc.CommandWebSelectorData{ + WindowId: blockInfo.WindowId, + BlockId: fullORef.OID, + TabId: blockInfo.TabId, + Selector: args[1], + Opts: &wshrpc.WebSelectorOpts{ + Inner: webGetInner, + All: webGetAll, + }, + } + output, err := wshclient.WebSelectorCommand(RpcClient, data, &wshrpc.RpcOpts{ + Route: wshutil.ElectronRoute, + Timeout: 5000, + }) + if err != nil { + return err + } + if webGetJson { + barr, err := json.MarshalIndent(output, "", " ") + if err != nil { + return fmt.Errorf("json encoding: %w", err) + } + WriteStdout("%s\n", string(barr)) + } else { + for _, item := range output { + WriteStdout("%s\n", item) + } + } return nil } diff --git a/emain/emain-web.ts b/emain/emain-web.ts index d5193c68b..842e266bf 100644 --- a/emain/emain-web.ts +++ b/emain/emain-web.ts @@ -3,11 +3,7 @@ import { BrowserWindow, ipcMain, webContents, WebContents } from "electron"; -export async function getWebContentsByBlockId( - win: BrowserWindow, - tabId: string, - blockId: string -): Promise { +export function getWebContentsByBlockId(win: BrowserWindow, tabId: string, blockId: string): Promise { const prtn = new Promise((resolve, reject) => { const randId = Math.floor(Math.random() * 1000000000).toString(); const respCh = `getWebContentsByBlockId-${randId}`; @@ -46,18 +42,23 @@ export async function webGetSelector(wc: WebContents, selector: string, opts?: W if (!wc || !selector) { return null; } - try { - const escapedSelector = escapeSelector(selector); - const queryMethod = opts?.all ? "querySelectorAll" : "querySelector"; - const prop = opts?.inner ? "innerHTML" : "outerHTML"; - const execExpr = ` - Array.from(document.${queryMethod}("${escapedSelector}") || []).map(el => el.${prop}); - `; - - const results = await wc.executeJavaScript(execExpr); - return results; - } catch (e) { - console.error("webGetSelector error", e); - return null; + const escapedSelector = escapeSelector(selector); + const queryMethod = opts?.all ? "querySelectorAll" : "querySelector"; + const prop = opts?.inner ? "innerHTML" : "outerHTML"; + const execExpr = ` + (() => { + const toArr = x => (x instanceof NodeList) ? Array.from(x) : (x ? [x] : []); + try { + const result = document.${queryMethod}("${escapedSelector}"); + const value = toArr(result).map(el => el.${prop}); + return { value }; + } catch (error) { + return { error: error.message }; + } + })()`; + const results = await wc.executeJavaScript(execExpr); + if (results.error) { + throw new Error(results.error); } + return results.value; } diff --git a/emain/emain-wsh.ts b/emain/emain-wsh.ts new file mode 100644 index 000000000..db62e3ba8 --- /dev/null +++ b/emain/emain-wsh.ts @@ -0,0 +1,31 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; +import electron from "electron"; +import { getWebContentsByBlockId, webGetSelector } from "emain/emain-web"; + +type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise }; + +export class ElectronWshClientType extends WshClient { + constructor() { + super("electron"); + } + + async handle_webselector(rh: RpcResponseHelper, data: CommandWebSelectorData): Promise { + if (!data.tabid || !data.blockid || !data.windowid) { + throw new Error("tabid and blockid are required"); + } + const windows = electron.BrowserWindow.getAllWindows(); + const win = windows.find((w) => (w as WaveBrowserWindow).waveWindowId === data.windowid); + if (win == null) { + throw new Error(`no window found with id ${data.windowid}`); + } + const wc = await getWebContentsByBlockId(win, data.tabid, data.blockid); + if (wc == null) { + throw new Error(`no webcontents found with blockid ${data.blockid}`); + } + const rtn = await webGetSelector(wc, data.selector, data.opts); + return rtn; + } +} diff --git a/emain/emain.ts b/emain/emain.ts index 60dc7dd1e..51154f872 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -1,7 +1,9 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { initElectronWshrpc } from "@/app/store/wshrpcutil"; import * as electron from "electron"; +import { ElectronWshClientType } from "emain/emain-wsh"; import { FastAverageColor } from "fast-average-color"; import fs from "fs"; import * as child_process from "node:child_process"; @@ -33,6 +35,7 @@ import { } from "./platform"; import { configureAutoUpdater, updater } from "./updater"; +let ElectronWshClient = new ElectronWshClientType(); const electronApp = electron.app; let WaveVersion = "unknown"; // set by WAVESRV-ESTART let WaveBuildTime = 0; // set by WAVESRV-ESTART @@ -857,6 +860,11 @@ async function appMain() { await relaunchBrowserWindows(); await configureAutoUpdater(); setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe + try { + initElectronWshrpc(ElectronWshClient, AuthKey); + } catch (e) { + console.log("error initializing wshrpc", e); + } globalIsStarting = false; diff --git a/frontend/app/store/ws.ts b/frontend/app/store/ws.ts index 5ab134ead..5062af8f6 100644 --- a/frontend/app/store/ws.ts +++ b/frontend/app/store/ws.ts @@ -3,10 +3,22 @@ import debug from "debug"; import { sprintf } from "sprintf-js"; +import type { WebSocket as ElectronWebSocketType } from "ws"; + +let ElectronWebSocket: typeof ElectronWebSocketType; +const AuthKeyHeader = "X-AuthKey"; + +if (typeof window === "undefined") { + try { + const WebSocket = require("ws") as typeof ElectronWebSocketType; + ElectronWebSocket = WebSocket; + } catch (e) {} +} const dlog = debug("wave:ws"); -const MaxWebSocketSendSize = 1024 * 1024; // 1MB +const WarnWebSocketSendSize = 1024 * 1024; // 1MB +const MaxWebSocketSendSize = 5 * 1024 * 1024; // 5MB const reconnectHandlers: (() => void)[] = []; function addWSReconnectHandler(handler: () => void) { @@ -23,7 +35,7 @@ function removeWSReconnectHandler(handler: () => void) { type WSEventCallback = (arg0: WSEventType) => void; class WSControl { - wsConn: any; + wsConn: WebSocket | ElectronWebSocketType; open: boolean; opening: boolean = false; reconnectTimes: number = 0; @@ -35,12 +47,14 @@ class WSControl { wsLog: string[] = []; baseHostPort: string; lastReconnectTime: number = 0; + authKey: string = null; // used only by electron - constructor(baseHostPort: string, windowId: string, messageCallback: WSEventCallback) { + constructor(baseHostPort: string, windowId: string, messageCallback: WSEventCallback, authKey?: string) { this.baseHostPort = baseHostPort; this.messageCallback = messageCallback; this.windowId = windowId; this.open = false; + this.authKey = authKey; setInterval(this.sendPing.bind(this), 5000); } @@ -51,7 +65,13 @@ class WSControl { this.lastReconnectTime = Date.now(); dlog("try reconnect:", desc); this.opening = true; - this.wsConn = new WebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId); + if (ElectronWebSocket) { + this.wsConn = new ElectronWebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId, { + headers: { [AuthKeyHeader]: this.authKey }, + }); + } else { + this.wsConn = new WebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId); + } this.wsConn.onopen = this.onopen.bind(this); this.wsConn.onmessage = this.onmessage.bind(this); this.wsConn.onclose = this.onclose.bind(this); @@ -172,6 +192,9 @@ class WSControl { console.log("ws message too large", byteSize, data.wscommand, msg.substring(0, 100)); return; } + if (byteSize > WarnWebSocketSendSize) { + console.log("ws message large", byteSize, data.wscommand, msg.substring(0, 100)); + } this.wsConn.send(msg); } diff --git a/frontend/app/store/wshclient.ts b/frontend/app/store/wshclient.ts index ec74c15f0..93e7a7e38 100644 --- a/frontend/app/store/wshclient.ts +++ b/frontend/app/store/wshclient.ts @@ -92,16 +92,21 @@ class WshClient { // TODO implement a timeout (setTimeout + sendResponse) const helper = new RpcResponseHelper(this, msg); const handlerName = `handle_${msg.command}`; - let prtn: any = null; - if (handlerName in this) { - prtn = this[handlerName](helper, msg.data); - return; - } else { - prtn = this.handle_default(helper, msg); - } try { + let result: any = null; + let prtn: any = null; + if (handlerName in this) { + prtn = this[handlerName](helper, msg.data); + } else { + prtn = this.handle_default(helper, msg); + } if (prtn instanceof Promise) { - await prtn; + result = await prtn; + } else { + result = prtn; + } + if (!helper.done) { + helper.sendResponse({ data: result }); } } catch (e) { if (!helper.done) { diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 4687eeab7..5d7f194cd 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -12,6 +12,11 @@ class RpcApiType { return client.wshRpcCall("authenticate", data, opts); } + // command "blockinfo" [call] + BlockInfoCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("blockinfo", data, opts); + } + // command "connconnect" [call] ConnConnectCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { return client.wshRpcCall("connconnect", data, opts); @@ -207,6 +212,11 @@ class RpcApiType { return client.wshRpcCall("test", data, opts); } + // command "webselector" [call] + WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise { + return client.wshRpcCall("webselector", data, opts); + } + } export const RpcApi = new RpcApiType(); diff --git a/frontend/app/store/wshrouter.ts b/frontend/app/store/wshrouter.ts index 87a77cf57..2679c9d10 100644 --- a/frontend/app/store/wshrouter.ts +++ b/frontend/app/store/wshrouter.ts @@ -31,6 +31,9 @@ class WshRouter { constructor(upstreamClient: AbstractWshClient) { this.routeMap = new Map(); this.rpcMap = new Map(); + if (upstreamClient == null) { + throw new Error("upstream client cannot be null"); + } this.upstreamClient = upstreamClient; } @@ -46,34 +49,17 @@ class WshRouter { } // returns true if the message was sent - _sendRoutedMessage(msg: RpcMessage, destRouteId: string): boolean { + _sendRoutedMessage(msg: RpcMessage, destRouteId: string) { const client = this.routeMap.get(destRouteId); if (client) { client.recvRpcMessage(msg); - return true; - } - if (!this.upstreamClient) { - // there should always be an upstream client - return false; - } - this.upstreamClient?.recvRpcMessage(msg); - return true; - } - - _handleNoRoute(msg: RpcMessage) { - dlog("no route for message", msg); - if (util.isBlank(msg.reqid)) { - // send a message instead - if (msg.command == "message") { - return; - } - const nrMsg = { command: "message", route: msg.source, data: { message: `no route for ${msg.route}` } }; - this._sendRoutedMessage(nrMsg, SysRouteName); return; } - // send an error response - const nrMsg = { resid: msg.reqid, error: `no route for ${msg.route}` }; - this._sendRoutedMessage(nrMsg, msg.source); + // there should always an upstream client + if (!this.upstreamClient) { + throw new Error(`no upstream client for message: ${msg}`); + } + this.upstreamClient?.recvRpcMessage(msg); } _registerRouteInfo(reqid: string, sourceRouteId: string, destRouteId: string) { @@ -102,18 +88,17 @@ class WshRouter { } if (!util.isBlank(msg.command)) { // send + register routeinfo - const ok = this._sendRoutedMessage(msg, msg.route); - if (!ok) { - this._handleNoRoute(msg); - return; + if (!util.isBlank(msg.reqid)) { + this._registerRouteInfo(msg.reqid, msg.source, msg.route); } - this._registerRouteInfo(msg.reqid, msg.source, msg.route); + this._sendRoutedMessage(msg, msg.route); return; } if (!util.isBlank(msg.reqid)) { const routeInfo = this.rpcMap.get(msg.reqid); if (!routeInfo) { // no route info, discard + dlog("no route info for reqid, discarding", msg); return; } this._sendRoutedMessage(msg, routeInfo.destRouteId); @@ -123,6 +108,7 @@ class WshRouter { const routeInfo = this.rpcMap.get(msg.resid); if (!routeInfo) { // no route info, discard + dlog("no route info for resid, discarding", msg); return; } this._sendRoutedMessage(msg, routeInfo.sourceRouteId); diff --git a/frontend/app/store/wshrpcutil.ts b/frontend/app/store/wshrpcutil.ts index 44f0577a6..1f5c1c797 100644 --- a/frontend/app/store/wshrpcutil.ts +++ b/frontend/app/store/wshrpcutil.ts @@ -114,11 +114,24 @@ if (globalThis.window != null) { globalThis["consumeGenerator"] = consumeGenerator; } +function initElectronWshrpc(electronClient: WshClient, authKey: string) { + DefaultRouter = new WshRouter(new UpstreamWshRpcProxy()); + const handleFn = (event: WSEventType) => { + DefaultRouter.recvRpcMessage(event.data); + }; + globalWS = new WSControl(getWSServerEndpoint(), "electron", handleFn, authKey); + globalWS.connectNow("connectWshrpc"); + DefaultRouter.registerRoute(electronClient.routeId, electronClient); + addWSReconnectHandler(() => { + DefaultRouter.reannounceRoutes(); + }); + addWSReconnectHandler(wpsReconnectHandler); +} + function initWshrpc(windowId: string): WSControl { DefaultRouter = new WshRouter(new UpstreamWshRpcProxy()); const handleFn = (event: WSEventType) => { DefaultRouter.recvRpcMessage(event.data); - // handleIncomingRpcMessage(globalOpenRpcs, event); }; globalWS = new WSControl(getWSServerEndpoint(), windowId, handleFn); globalWS.connectNow("connectWshrpc"); @@ -144,6 +157,7 @@ class UpstreamWshRpcProxy implements AbstractWshClient { export { DefaultRouter, + initElectronWshrpc, initWshrpc, sendRawRpcMessage, sendRpcCommand, diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 935bb24d6..d58e16d66 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -25,6 +25,14 @@ declare global { meta?: MetaType; }; + // wshrpc.BlockInfoData + type BlockInfoData = { + blockid: string; + tabid: string; + windowid: string; + meta: MetaType; + }; + // webcmd.BlockInputWSCommand type BlockInputWSCommand = { wscommand: "blockinput"; @@ -147,6 +155,15 @@ declare global { meta: MetaType; }; + // wshrpc.CommandWebSelectorData + type CommandWebSelectorData = { + windowid: string; + blockid: string; + tabid: string; + selector: string; + opts?: WebSelectorOpts; + }; + // wconfig.ConfigError type ConfigError = { file: string; @@ -641,6 +658,12 @@ declare global { updates?: WaveObjUpdate[]; }; + // wshrpc.WebSelectorOpts + type WebSelectorOpts = { + all?: boolean; + inner?: boolean; + }; + // wconfig.WidgetConfigType type WidgetConfigType = { "display:order"?: number; diff --git a/package.json b/package.json index a0fd39b14..ee5080555 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/throttle-debounce": "^5", "@types/tinycolor2": "^1", "@types/uuid": "^10.0.0", + "@types/ws": "^8", "@vitejs/plugin-react-swc": "^3.7.0", "@vitest/coverage-istanbul": "^2.1.1", "electron": "^32.1.0", @@ -127,7 +128,8 @@ "throttle-debounce": "^5.0.2", "tinycolor2": "^1.6.0", "use-device-pixel-ratio": "^1.1.2", - "winston": "^3.14.2" + "winston": "^3.14.2", + "ws": "^8.18.0" }, "packageManager": "yarn@4.4.1" } diff --git a/pkg/web/ws.go b/pkg/web/ws.go index 1965a8779..4c605fa27 100644 --- a/pkg/web/ws.go +++ b/pkg/web/ws.go @@ -262,12 +262,18 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error { outputCh := make(chan any, 100) closeCh := make(chan any) eventbus.RegisterWSChannel(wsConnId, windowId, outputCh) + var routeId string + if windowId == wshutil.ElectronRoute { + routeId = wshutil.ElectronRoute + } else { + routeId = wshutil.MakeWindowRouteId(windowId) + } defer eventbus.UnregisterWSChannel(wsConnId) // we create a wshproxy to handle rpc messages to/from the window wproxy := wshutil.MakeRpcProxy() - wshutil.DefaultRouter.RegisterRoute(wshutil.MakeWindowRouteId(windowId), wproxy) + wshutil.DefaultRouter.RegisterRoute(routeId, wproxy) defer func() { - wshutil.DefaultRouter.UnregisterRoute(wshutil.MakeWindowRouteId(windowId)) + wshutil.DefaultRouter.UnregisterRoute(routeId) close(wproxy.ToRemoteCh) }() // WshServerFactoryFn(rpcInputCh, rpcOutputCh, wshrpc.RpcContext{}) diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index c3becfbda..71b89ff9d 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -19,6 +19,12 @@ func AuthenticateCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) ( return resp, err } +// command "blockinfo", wshserver.BlockInfoCommand +func BlockInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*wshrpc.BlockInfoData, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.BlockInfoData](w, "blockinfo", data, opts) + return resp, err +} + // command "connconnect", wshserver.ConnConnectCommand func ConnConnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts) @@ -248,4 +254,10 @@ func TestCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { return err } +// command "webselector", wshserver.WebSelectorCommand +func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, opts *wshrpc.RpcOpts) ([]string, error) { + resp, err := sendRpcRequestCallHelper[[]string](w, "webselector", data, opts) + return resp, err +} + diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 678b9498a..0e25854ae 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -40,6 +40,7 @@ const ( Command_FileAppend = "fileappend" Command_FileAppendIJson = "fileappendijson" Command_ResolveIds = "resolveids" + Command_BlockInfo = "blockinfo" Command_CreateBlock = "createblock" Command_DeleteBlock = "deleteblock" Command_FileWrite = "filewrite" @@ -65,6 +66,8 @@ const ( Command_ConnConnect = "connconnect" Command_ConnDisconnect = "conndisconnect" Command_ConnList = "connlist" + + Command_WebSelector = "webselector" ) type RespOrErrorUnion[T any] struct { @@ -101,6 +104,7 @@ type WshRpcInterface interface { StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData] TestCommand(ctx context.Context, data string) error SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error + BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error) // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) @@ -120,6 +124,8 @@ type WshRpcInterface interface { RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error) RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData] + + WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error) } // for frontend @@ -348,3 +354,23 @@ type ConnStatus struct { ActiveConnNum int `json:"activeconnnum"` Error string `json:"error,omitempty"` } + +type WebSelectorOpts struct { + All bool `json:"all,omitempty"` + Inner bool `json:"inner,omitempty"` +} + +type CommandWebSelectorData struct { + WindowId string `json:"windowid"` + BlockId string `json:"blockid" wshcontext:"BlockId"` + TabId string `json:"tabid" wshcontext:"TabId"` + Selector string `json:"selector"` + Opts *WebSelectorOpts `json:"opts,omitempty"` +} + +type BlockInfoData struct { + BlockId string `json:"blockid"` + TabId string `json:"tabid"` + WindowId string `json:"windowid"` + Meta waveobj.MetaMapType `json:"meta"` +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index ec633de0a..890c81726 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -526,3 +526,24 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin func (ws *WshServer) ConnListCommand(ctx context.Context) ([]string, error) { return conncontroller.GetConnectionsList() } + +func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wshrpc.BlockInfoData, error) { + blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) + if err != nil { + return nil, fmt.Errorf("error getting block: %w", err) + } + tabId, err := wstore.DBFindTabForBlockId(ctx, blockId) + if err != nil { + return nil, fmt.Errorf("error finding tab for block: %w", err) + } + windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error finding window for tab: %w", err) + } + return &wshrpc.BlockInfoData{ + BlockId: blockId, + TabId: tabId, + WindowId: windowId, + Meta: blockData.Meta, + }, nil +} diff --git a/pkg/wshutil/wshrouter.go b/pkg/wshutil/wshrouter.go index 6bd8afb59..26fdb8a39 100644 --- a/pkg/wshutil/wshrouter.go +++ b/pkg/wshutil/wshrouter.go @@ -18,6 +18,7 @@ import ( const DefaultRoute = "wavesrv" const SysRoute = "sys" // this route doesn't exist, just a placeholder for system messages +const ElectronRoute = "electron" // this works like a network switch diff --git a/yarn.lock b/yarn.lock index 73f463cc8..9bc8a1fbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,6 +2594,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9 + languageName: node + linkType: hard + "@types/yauzl@npm:^2.9.1": version: 2.10.3 resolution: "@types/yauzl@npm:2.10.3" @@ -10210,6 +10219,7 @@ __metadata: "@types/throttle-debounce": "npm:^5" "@types/tinycolor2": "npm:^1" "@types/uuid": "npm:^10.0.0" + "@types/ws": "npm:^8" "@vitejs/plugin-react-swc": "npm:^3.7.0" "@vitest/coverage-istanbul": "npm:^2.1.1" "@xterm/addon-fit": "npm:^0.10.0" @@ -10277,6 +10287,7 @@ __metadata: vite-tsconfig-paths: "npm:^5.0.1" vitest: "npm:^2.1.1" winston: "npm:^3.14.2" + ws: "npm:^8.18.0" languageName: unknown linkType: soft @@ -11218,7 +11229,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3": +"ws@npm:^8.18.0, ws@npm:^8.2.3": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: