checkpoint on domain sockets + update background colors + transparency (#160)

This commit is contained in:
Mike Sawka 2024-07-26 13:30:11 -07:00 committed by GitHub
parent f1837d6fae
commit 9df9c99fbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1000 additions and 436 deletions

View File

@ -8,11 +8,12 @@ import (
"os" "os"
"reflect" "reflect"
"sort" "sort"
"strings"
"github.com/wavetermdev/thenextwave/pkg/service" "github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/tsgen" "github.com/wavetermdev/thenextwave/pkg/tsgen"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshserver" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
) )
func generateTypesFile(tsTypesMap map[reflect.Type]string) error { func generateTypesFile(tsTypesMap map[reflect.Type]string) error {
@ -43,6 +44,10 @@ func generateTypesFile(tsTypesMap map[reflect.Type]string) error {
return iname < jname return iname < jname
}) })
for _, key := range keys { for _, key := range keys {
// don't output generic types
if strings.Index(key.Name(), "[") != -1 {
continue
}
tsCode := tsTypesMap[key] tsCode := tsTypesMap[key]
istr := utilfn.IndentString(" ", tsCode) istr := utilfn.IndentString(" ", tsCode)
fmt.Fprint(fd, istr) fmt.Fprint(fd, istr)
@ -79,16 +84,17 @@ func generateWshServerFile(tsTypeMap map[reflect.Type]string) error {
return err return err
} }
defer fd.Close() defer fd.Close()
declMap := wshrpc.GenerateWshCommandDeclMap()
fmt.Fprintf(os.Stderr, "generating wshserver file to %s\n", fd.Name()) fmt.Fprintf(os.Stderr, "generating wshserver file to %s\n", fd.Name())
fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n") fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n") fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n") fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n")
fmt.Fprintf(fd, "import * as WOS from \"./wos\";\n\n") fmt.Fprintf(fd, "import * as WOS from \"./wos\";\n\n")
orderedKeys := utilfn.GetOrderedMapKeys(wshserver.WshServerCommandToDeclMap) orderedKeys := utilfn.GetOrderedMapKeys(declMap)
fmt.Fprintf(fd, "// WshServerCommandToDeclMap\n") fmt.Fprintf(fd, "// WshServerCommandToDeclMap\n")
fmt.Fprintf(fd, "class WshServerType {\n") fmt.Fprintf(fd, "class WshServerType {\n")
for _, methodDecl := range orderedKeys { for _, methodDecl := range orderedKeys {
methodDecl := wshserver.WshServerCommandToDeclMap[methodDecl] methodDecl := declMap[methodDecl]
methodStr := tsgen.GenerateWshServerMethod(methodDecl, tsTypeMap) methodStr := tsgen.GenerateWshServerMethod(methodDecl, tsTypeMap)
fmt.Fprint(fd, methodStr) fmt.Fprint(fd, methodStr)
fmt.Fprintf(fd, "\n") fmt.Fprintf(fd, "\n")

View File

@ -8,11 +8,10 @@ import (
"os" "os"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshserver" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshutil"
) )
func genMethod_ResponseStream(fd *os.File, methodDecl *wshserver.WshServerMethodDecl) { func genMethod_ResponseStream(fd *os.File, methodDecl *wshrpc.WshRpcMethodDecl) {
fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
var dataType string var dataType string
dataVarName := "nil" dataVarName := "nil"
@ -29,7 +28,7 @@ func genMethod_ResponseStream(fd *os.File, methodDecl *wshserver.WshServerMethod
fmt.Fprintf(fd, "}\n\n") fmt.Fprintf(fd, "}\n\n")
} }
func genMethod_Call(fd *os.File, methodDecl *wshserver.WshServerMethodDecl) { func genMethod_Call(fd *os.File, methodDecl *wshrpc.WshRpcMethodDecl) {
fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
var dataType string var dataType string
dataVarName := "nil" dataVarName := "nil"
@ -70,14 +69,14 @@ func main() {
fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshutil\"\n") fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshutil\"\n")
fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshrpc\"\n") fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshrpc\"\n")
fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/waveobj\"\n") fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/waveobj\"\n")
fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/waveai\"\n")
fmt.Fprintf(fd, ")\n\n") fmt.Fprintf(fd, ")\n\n")
for _, key := range utilfn.GetOrderedMapKeys(wshserver.WshServerCommandToDeclMap) { wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
methodDecl := wshserver.WshServerCommandToDeclMap[key] for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {
if methodDecl.CommandType == wshutil.RpcType_ResponseStream { methodDecl := wshDeclMap[key]
if methodDecl.CommandType == wshrpc.RpcType_ResponseStream {
genMethod_ResponseStream(fd, methodDecl) genMethod_ResponseStream(fd, methodDecl)
} else if methodDecl.CommandType == wshutil.RpcType_Call { } else if methodDecl.CommandType == wshrpc.RpcType_Call {
genMethod_Call(fd, methodDecl) genMethod_Call(fd, methodDecl)
} else { } else {
panic("unsupported command type " + methodDecl.CommandType) panic("unsupported command type " + methodDecl.CommandType)

View File

@ -43,7 +43,7 @@ func runReadFile(cmd *cobra.Command, args []string) {
fmt.Fprintf(os.Stderr, "error resolving oref: %v\r\n", err) fmt.Fprintf(os.Stderr, "error resolving oref: %v\r\n", err)
return return
} }
resp64, err := wshclient.ReadFile(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[1]}, &wshrpc.WshRpcCommandOpts{Timeout: 5000}) resp64, err := wshclient.FileReadCommand(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[1]}, &wshrpc.WshRpcCommandOpts{Timeout: 5000})
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error reading file: %v\r\n", err) fmt.Fprintf(os.Stderr, "error reading file: %v\r\n", err)
return return

View File

@ -34,6 +34,7 @@ const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
}); });
let globalIsQuitting = false; let globalIsQuitting = false;
let globalIsStarting = true; let globalIsStarting = true;
let globalIsRelaunching = false;
const isDev = !electronApp.isPackaged; const isDev = !electronApp.isPackaged;
const isDevVite = isDev && process.env.ELECTRON_RENDERER_URL; const isDevVite = isDev && process.env.ELECTRON_RENDERER_URL;
@ -214,7 +215,8 @@ async function handleWSEvent(evtMsg: WSEventType) {
return; return;
} }
const clientData = await services.ClientService.GetClientData(); const clientData = await services.ClientService.GetClientData();
const newWin = createBrowserWindow(clientData.oid, windowData); const settings = await services.FileService.GetSettingsConfig();
const newWin = createBrowserWindow(clientData.oid, windowData, settings);
await newWin.readyPromise; await newWin.readyPromise;
newWin.show(); newWin.show();
} else if (evtMsg.eventtype == "electron:closewindow") { } else if (evtMsg.eventtype == "electron:closewindow") {
@ -290,7 +292,11 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
// note, this does not *show* the window. // note, this does not *show* the window.
// to show, await win.readyPromise and then win.show() // to show, await win.readyPromise and then win.show()
function createBrowserWindow(clientId: string, waveWindow: WaveWindow): WaveBrowserWindow { function createBrowserWindow(
clientId: string,
waveWindow: WaveWindow,
settings: SettingsConfigType
): WaveBrowserWindow {
let winBounds = { let winBounds = {
x: waveWindow.pos.x, x: waveWindow.pos.x,
y: waveWindow.pos.y, y: waveWindow.pos.y,
@ -298,7 +304,7 @@ function createBrowserWindow(clientId: string, waveWindow: WaveWindow): WaveBrow
height: waveWindow.winsize.height, height: waveWindow.winsize.height,
}; };
winBounds = ensureBoundsAreVisible(winBounds); winBounds = ensureBoundsAreVisible(winBounds);
const bwin = new electron.BrowserWindow({ const winOpts: Electron.BrowserWindowConstructorOptions = {
titleBarStyle: "hiddenInset", titleBarStyle: "hiddenInset",
x: winBounds.x, x: winBounds.x,
y: winBounds.y, y: winBounds.y,
@ -316,8 +322,14 @@ function createBrowserWindow(clientId: string, waveWindow: WaveWindow): WaveBrow
}, },
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: "#000000", };
}); const isTransparent = settings?.window?.transparent ?? true;
if (isTransparent) {
winOpts.transparent = true;
} else {
winOpts.backgroundColor = "#222222";
}
const bwin = new electron.BrowserWindow(winOpts);
(bwin as any).waveWindowId = waveWindow.oid; (bwin as any).waveWindowId = waveWindow.oid;
let readyResolve: (value: void) => void; let readyResolve: (value: void) => void;
(bwin as any).readyPromise = new Promise((resolve, _) => { (bwin as any).readyPromise = new Promise((resolve, _) => {
@ -519,7 +531,8 @@ electron.ipcMain.on("getEnv", (event, varName) => {
async function createNewWaveWindow() { async function createNewWaveWindow() {
const clientData = await services.ClientService.GetClientData(); const clientData = await services.ClientService.GetClientData();
const newWindow = await services.ClientService.MakeWindow(); const newWindow = await services.ClientService.MakeWindow();
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow); const settings = await services.FileService.GetSettingsConfig();
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, settings);
newBrowserWindow.show(); newBrowserWindow.show();
} }
@ -616,6 +629,12 @@ function makeAppMenu() {
{ {
role: "forceReload", role: "forceReload",
}, },
{
label: "Relaunch All Windows",
click: () => {
relaunchBrowserWindows();
},
},
{ {
role: "toggleDevTools", role: "toggleDevTools",
}, },
@ -663,6 +682,9 @@ function makeAppMenu() {
} }
electronApp.on("window-all-closed", () => { electronApp.on("window-all-closed", () => {
if (globalIsRelaunching) {
return;
}
if (unamePlatform !== "darwin") { if (unamePlatform !== "darwin") {
electronApp.quit(); electronApp.quit();
} }
@ -857,6 +879,36 @@ async function configureAutoUpdater() {
} }
// ====== AUTO-UPDATER ====== // // ====== AUTO-UPDATER ====== //
async function relaunchBrowserWindows() {
globalIsRelaunching = true;
const windows = electron.BrowserWindow.getAllWindows();
for (const window of windows) {
window.removeAllListeners();
window.close();
}
globalIsRelaunching = false;
const clientData = await services.ClientService.GetClientData();
const settings = await services.FileService.GetSettingsConfig();
const wins: WaveBrowserWindow[] = [];
for (const windowId of clientData.windowids.slice().reverse()) {
const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
if (windowData == null) {
services.WindowService.CloseWindow(windowId).catch((e) => {
/* ignore */
});
continue;
}
const win = createBrowserWindow(clientData.oid, windowData, settings);
wins.push(win);
}
for (const win of wins) {
await win.readyPromise;
console.log("show", win.waveWindowId);
win.show();
}
}
async function appMain() { async function appMain() {
const startTs = Date.now(); const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock(); const instanceLock = electronApp.requestSingleInstanceLock();
@ -877,27 +929,8 @@ async function appMain() {
} }
const ready = await waveSrvReady; const ready = await waveSrvReady;
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");
const clientData = await services.ClientService.GetClientData();
console.log("client data ready");
await electronApp.whenReady(); await electronApp.whenReady();
const wins: WaveBrowserWindow[] = []; relaunchBrowserWindows();
for (const windowId of clientData.windowids.slice().reverse()) {
const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
if (windowData == null) {
services.WindowService.CloseWindow(windowId).catch((e) => {
/* ignore */
});
continue;
}
const win = createBrowserWindow(clientData.oid, windowData);
wins.push(win);
}
for (const win of wins) {
await win.readyPromise;
console.log("show", win.waveWindowId);
win.show();
}
configureAutoUpdater(); configureAutoUpdater();
globalIsStarting = false; globalIsStarting = false;

View File

@ -14,6 +14,8 @@ body {
font: var(--base-font); font: var(--base-font);
overflow: hidden; overflow: hidden;
-webkit-font-smoothing: auto; -webkit-font-smoothing: auto;
backface-visibility: hidden;
transform: translateZ(0);
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {

View File

@ -16,6 +16,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
import { CenteredDiv } from "./element/quickelems"; import { CenteredDiv } from "./element/quickelems";
import clsx from "clsx"; import clsx from "clsx";
import Color from "color";
import "overlayscrollbars/overlayscrollbars.css"; import "overlayscrollbars/overlayscrollbars.css";
import "./app.less"; import "./app.less";
@ -200,6 +201,31 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) {
} }
} }
function AppSettingsUpdater() {
const settings = jotai.useAtomValue(atoms.settingsConfigAtom);
React.useEffect(() => {
let isTransparent = settings?.window?.transparent ?? true;
let opacity = util.boundNumber(settings?.window?.opacity ?? 0.8, 0, 1);
let baseBgColor = settings?.window?.bgcolor;
console.log("window settings", settings.window);
if (isTransparent) {
document.body.classList.add("is-transparent");
const rootStyles = getComputedStyle(document.documentElement);
if (baseBgColor == null) {
baseBgColor = rootStyles.getPropertyValue("--main-bg-color").trim();
}
const color = new Color(baseBgColor);
const rgbaColor = color.alpha(opacity).string();
document.body.style.backgroundColor = rgbaColor;
} else {
document.body.classList.remove("is-transparent");
document.body.style.opacity = null;
}
}, [settings?.window]);
return null;
}
const AppInner = () => { const AppInner = () => {
const client = jotai.useAtomValue(atoms.client); const client = jotai.useAtomValue(atoms.client);
const windowData = jotai.useAtomValue(atoms.waveWindow); const windowData = jotai.useAtomValue(atoms.waveWindow);
@ -251,6 +277,7 @@ const AppInner = () => {
const isFullScreen = jotai.useAtomValue(atoms.isFullScreen); const isFullScreen = jotai.useAtomValue(atoms.isFullScreen);
return ( return (
<div className={clsx("mainapp", PLATFORM, { fullscreen: isFullScreen })} onContextMenu={handleContextMenu}> <div className={clsx("mainapp", PLATFORM, { fullscreen: isFullScreen })} onContextMenu={handleContextMenu}>
<AppSettingsUpdater />
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Workspace /> <Workspace />
</DndProvider> </DndProvider>

View File

@ -59,7 +59,7 @@
padding: 2px; padding: 2px;
.block-frame-default-inner { .block-frame-default-inner {
background-color: rgba(0, 0, 0, 0.5); background-color: var(--block-bg-color);
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; border-radius: 8px;

View File

@ -7,14 +7,19 @@ import * as WOS from "./wos";
// WshServerCommandToDeclMap // WshServerCommandToDeclMap
class WshServerType { class WshServerType {
// command "controller:input" [call] // command "authenticate" [call]
BlockInputCommand(data: CommandBlockInputData, opts?: WshRpcCommandOpts): Promise<void> { AuthenticateCommand(data: string, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("controller:input", data, opts); return WOS.wshServerRpcHelper_call("authenticate", data, opts);
} }
// command "controller:restart" [call] // command "controllerinput" [call]
BlockRestartCommand(data: CommandBlockRestartData, opts?: WshRpcCommandOpts): Promise<void> { ControllerInputCommand(data: CommandBlockInputData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("controller:restart", data, opts); return WOS.wshServerRpcHelper_call("controllerinput", data, opts);
}
// command "controllerrestart" [call]
ControllerRestartCommand(data: CommandBlockRestartData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("controllerrestart", data, opts);
} }
// command "createblock" [call] // command "createblock" [call]
@ -27,24 +32,49 @@ class WshServerType {
return WOS.wshServerRpcHelper_call("deleteblock", data, opts); return WOS.wshServerRpcHelper_call("deleteblock", data, opts);
} }
// command "file:append" [call] // command "eventpublish" [call]
AppendFileCommand(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> { EventPublishCommand(data: WaveEvent, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("file:append", data, opts); return WOS.wshServerRpcHelper_call("eventpublish", data, opts);
} }
// command "file:appendijson" [call] // command "eventrecv" [call]
AppendIJsonCommand(data: CommandAppendIJsonData, opts?: WshRpcCommandOpts): Promise<void> { EventRecvCommand(data: WaveEvent, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("file:appendijson", data, opts); return WOS.wshServerRpcHelper_call("eventrecv", data, opts);
} }
// command "file:read" [call] // command "eventsub" [call]
ReadFile(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<string> { EventSubCommand(data: SubscriptionRequest, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("file:read", data, opts); return WOS.wshServerRpcHelper_call("eventsub", data, opts);
} }
// command "file:write" [call] // command "eventunsub" [call]
WriteFile(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> { EventUnsubCommand(data: SubscriptionRequest, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("file:write", data, opts); return WOS.wshServerRpcHelper_call("eventunsub", data, opts);
}
// command "eventunsuball" [call]
EventUnsubAllCommand(opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("eventunsuball", null, opts);
}
// command "fileappend" [call]
FileAppendCommand(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("fileappend", data, opts);
}
// command "fileappendijson" [call]
FileAppendIJsonCommand(data: CommandAppendIJsonData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("fileappendijson", data, opts);
}
// command "fileread" [call]
FileReadCommand(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<string> {
return WOS.wshServerRpcHelper_call("fileread", data, opts);
}
// command "filewrite" [call]
FileWriteCommand(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("filewrite", data, opts);
} }
// command "getmeta" [call] // command "getmeta" [call]
@ -68,18 +98,18 @@ class WshServerType {
} }
// command "setview" [call] // command "setview" [call]
BlockSetViewCommand(data: CommandBlockSetViewData, opts?: WshRpcCommandOpts): Promise<void> { SetViewCommand(data: CommandBlockSetViewData, opts?: WshRpcCommandOpts): Promise<void> {
return WOS.wshServerRpcHelper_call("setview", data, opts); return WOS.wshServerRpcHelper_call("setview", data, opts);
} }
// command "stream:waveai" [responsestream] // command "streamtest" [responsestream]
RespStreamWaveAi(data: OpenAiStreamRequest, opts?: WshRpcCommandOpts): AsyncGenerator<OpenAIPacketType, void, boolean> { StreamTestCommand(opts?: WshRpcCommandOpts): AsyncGenerator<number, void, boolean> {
return WOS.wshServerRpcHelper_responsestream("stream:waveai", data, opts); return WOS.wshServerRpcHelper_responsestream("streamtest", null, opts);
} }
// command "streamtest" [responsestream] // command "streamwaveai" [responsestream]
RespStreamTest(opts?: WshRpcCommandOpts): AsyncGenerator<number, void, boolean> { StreamWaveAiCommand(data: OpenAiStreamRequest, opts?: WshRpcCommandOpts): AsyncGenerator<OpenAIPacketType, void, boolean> {
return WOS.wshServerRpcHelper_responsestream("streamtest", null, opts); return WOS.wshServerRpcHelper_responsestream("streamwaveai", data, opts);
} }
} }

View File

@ -6,7 +6,7 @@
--title-font-size: 18px; --title-font-size: 18px;
--secondary-text-color: rgb(195, 200, 194); --secondary-text-color: rgb(195, 200, 194);
--grey-text-color: #666; --grey-text-color: #666;
--main-bg-color: #454444; --main-bg-color: rgb(34, 34, 34);
--border-color: #333333; --border-color: #333333;
--base-font: normal 14px / normal "Inter", sans-serif; --base-font: normal 14px / normal "Inter", sans-serif;
--fixed-font: normal 12px / normal "Hack", monospace; --fixed-font: normal 12px / normal "Hack", monospace;
@ -19,7 +19,7 @@
--warning-color: rgb(224, 185, 86); --warning-color: rgb(224, 185, 86);
--success-color: rgb(78, 154, 6); --success-color: rgb(78, 154, 6);
--hover-bg-color: rgba(255, 255, 255, 0.1); --hover-bg-color: rgba(255, 255, 255, 0.1);
--block-bg-color: rgba(255, 255, 255, 0.05); --block-bg-color: rgba(0, 0, 0, 0.5);
/* scrollbar colors */ /* scrollbar colors */
--scrollbar-background-color: transparent; --scrollbar-background-color: transparent;

View File

@ -214,7 +214,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
} }
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) { if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
// restart // restart
WshServer.BlockRestartCommand({ blockid: blockId }); WshServer.ControllerRestartCommand({ blockid: blockId });
return false; return false;
} }
} }
@ -263,7 +263,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
return false; return false;
} }
const b64data = btoa(asciiVal); const b64data = btoa(asciiVal);
WshServer.BlockInputCommand({ blockid: blockId, inputdata64: b64data }); WshServer.ControllerInputCommand({ blockid: blockId, inputdata64: b64data });
return true; return true;
}; };

View File

@ -76,7 +76,7 @@ export class TermWrap {
handleTermData(data: string) { handleTermData(data: string) {
const b64data = btoa(data); const b64data = btoa(data);
WshServer.BlockInputCommand({ blockid: this.blockId, inputdata64: b64data }); WshServer.ControllerInputCommand({ blockid: this.blockId, inputdata64: b64data });
} }
addFocusListener(focusFn: () => void) { addFocusListener(focusFn: () => void) {

View File

@ -137,7 +137,7 @@ export class WaveAiModel implements ViewModel {
opts: opts, opts: opts,
prompt: prompt, prompt: prompt,
}; };
const aiGen = WshServer.RespStreamWaveAi(beMsg); const aiGen = WshServer.StreamWaveAiCommand(beMsg);
let temp = async () => { let temp = async () => {
let fullMsg = ""; let fullMsg = "";
for await (const msg of aiGen) { for await (const msg of aiGen) {

View File

@ -187,7 +187,7 @@ declare global {
// waveobj.ORef // waveobj.ORef
type ORef = string; type ORef = string;
// waveai.OpenAIOptsType // wshrpc.OpenAIOptsType
type OpenAIOptsType = { type OpenAIOptsType = {
model: string; model: string;
apitoken: string; apitoken: string;
@ -197,7 +197,7 @@ declare global {
timeout?: number; timeout?: number;
}; };
// waveai.OpenAIPacketType // wshrpc.OpenAIPacketType
type OpenAIPacketType = { type OpenAIPacketType = {
type: string; type: string;
model?: string; model?: string;
@ -209,21 +209,21 @@ declare global {
error?: string; error?: string;
}; };
// waveai.OpenAIPromptMessageType // wshrpc.OpenAIPromptMessageType
type OpenAIPromptMessageType = { type OpenAIPromptMessageType = {
role: string; role: string;
content: string; content: string;
name?: string; name?: string;
}; };
// waveai.OpenAIUsageType // wshrpc.OpenAIUsageType
type OpenAIUsageType = { type OpenAIUsageType = {
prompt_tokens?: number; prompt_tokens?: number;
completion_tokens?: number; completion_tokens?: number;
total_tokens?: number; total_tokens?: number;
}; };
// waveai.OpenAiStreamRequest // wshrpc.OpenAiStreamRequest
type OpenAiStreamRequest = { type OpenAiStreamRequest = {
clientid?: string; clientid?: string;
opts: OpenAIOptsType; opts: OpenAIOptsType;
@ -270,6 +270,7 @@ declare global {
blockheader: BlockHeaderOpts; blockheader: BlockHeaderOpts;
autoupdate: AutoUpdateOpts; autoupdate: AutoUpdateOpts;
termthemes: {[key: string]: TermThemeType}; termthemes: {[key: string]: TermThemeType};
window: WindowSettingsType;
}; };
// wstore.StickerClickOptsType // wstore.StickerClickOptsType
@ -293,6 +294,13 @@ declare global {
display: StickerDisplayOptsType; display: StickerDisplayOptsType;
}; };
// wshrpc.SubscriptionRequest
type SubscriptionRequest = {
event: string;
scopes?: string[];
allscopes?: boolean;
};
// wstore.Tab // wstore.Tab
type Tab = WaveObj & { type Tab = WaveObj & {
name: string; name: string;
@ -428,6 +436,14 @@ declare global {
error: string; error: string;
}; };
// wshrpc.WaveEvent
type WaveEvent = {
event: string;
scopes?: string[];
sender?: string;
data?: any;
};
// filestore.WaveFile // filestore.WaveFile
type WaveFile = { type WaveFile = {
zoneid: string; zoneid: string;
@ -497,6 +513,13 @@ declare global {
height: number; height: number;
}; };
// wconfig.WindowSettingsType
type WindowSettingsType = {
transparent: boolean;
opacity: number;
bgcolor: string;
};
// wstore.Workspace // wstore.Workspace
type Workspace = WaveObj & { type Workspace = WaveObj & {
name: string; name: string;

View File

@ -34,6 +34,10 @@ function base64ToArray(b64: string): Uint8Array {
return rtnArr; return rtnArr;
} }
function boundNumber(num: number, min: number, max: number): number {
return Math.min(Math.max(num, min), max);
}
// works for json-like objects (arrays, objects, strings, numbers, booleans) // works for json-like objects (arrays, objects, strings, numbers, booleans)
function jsonDeepEqual(v1: any, v2: any): boolean { function jsonDeepEqual(v1: any, v2: any): boolean {
if (v1 === v2) { if (v1 === v2) {
@ -193,6 +197,7 @@ function getCrypto() {
export { export {
base64ToArray, base64ToArray,
base64ToString, base64ToString,
boundNumber,
fireAndForget, fireAndForget,
getCrypto, getCrypto,
getPromiseState, getPromiseState,

View File

@ -27,6 +27,7 @@ loadFonts();
(window as any).globalWS = globalWS; (window as any).globalWS = globalWS;
(window as any).WOS = WOS; (window as any).WOS = WOS;
(window as any).globalStore = globalStore; (window as any).globalStore = globalStore;
(window as any).globalAtoms = atoms;
(window as any).WshServer = WshServer; (window as any).WshServer = WshServer;
(window as any).isFullScreen = false; (window as any).isFullScreen = false;

View File

@ -76,11 +76,13 @@
"@table-nav/core": "^0.0.7", "@table-nav/core": "^0.0.7",
"@table-nav/react": "^0.0.7", "@table-nav/react": "^0.0.7",
"@tanstack/react-table": "^8.19.3", "@tanstack/react-table": "^8.19.3",
"@types/color": "^3.0.6",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0", "@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"color": "^4.2.3",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"electron-updater": "6.3.1", "electron-updater": "6.3.1",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",

View File

@ -6,7 +6,9 @@ package blockcontroller
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -21,12 +23,13 @@ import (
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
// set by main-server.go (for dependency inversion) // set by main-server.go (for dependency inversion)
var WshServerFactoryFn func(inputCh chan []byte, outputCh chan []byte, initialCtx wshutil.RpcContext) = nil var WshServerFactoryFn func(inputCh chan []byte, outputCh chan []byte, initialCtx wshrpc.RpcContext) = nil
const ( const (
BlockController_Shell = "shell" BlockController_Shell = "shell"
@ -205,6 +208,46 @@ func (bc *BlockController) resetTerminalState() {
} }
} }
func getMetaBool(meta map[string]any, key string, def bool) bool {
val, found := meta[key]
if !found {
return def
}
if val == nil {
return def
}
if bval, ok := val.(bool); ok {
return bval
}
return def
}
func getMetaStr(meta map[string]any, key string, def string) string {
val, found := meta[key]
if !found {
return def
}
if val == nil {
return def
}
if sval, ok := val.(string); ok {
return sval
}
return def
}
// every byte is 4-bits of randomness
func randomHexString(numHexDigits int) (string, error) {
numBytes := (numHexDigits + 1) / 2 // Calculate the number of bytes needed
bytes := make([]byte, numBytes)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
hexStr := hex.EncodeToString(bytes)
return hexStr[:numHexDigits], nil // Return the exact number of hex digits
}
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[string]any) error { func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[string]any) 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)
@ -232,12 +275,35 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
if shellProcErr != nil { if shellProcErr != nil {
return shellProcErr return shellProcErr
} }
var remoteDomainSocketName string
remoteName := getMetaStr(blockMeta, "connection", "")
isRemote := remoteName != ""
if isRemote {
randStr, err := randomHexString(16) // 64-bits of randomness
if err != nil {
return fmt.Errorf("error generating random string: %w", err)
}
remoteDomainSocketName = fmt.Sprintf("/tmp/waveterm-%s.sock", randStr)
}
var cmdStr string var cmdStr string
cmdOpts := shellexec.CommandOptsType{ cmdOpts := shellexec.CommandOptsType{
Env: make(map[string]string), Env: make(map[string]string),
} }
// temporary for blockid (will switch to a JWT at some point) if !getMetaBool(blockMeta, "nowsh", false) {
cmdOpts.Env["LC_WAVETERM_BLOCKID"] = bc.BlockId if isRemote {
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId}, remoteDomainSocketName)
if err != nil {
return fmt.Errorf("error making jwt token: %w", err)
}
cmdOpts.Env["WAVETERM_JWT"] = jwtStr
} else {
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId}, wavebase.GetDomainSocketName())
if err != nil {
return fmt.Errorf("error making jwt token: %w", err)
}
cmdOpts.Env["WAVETERM_JWT"] = jwtStr
}
}
if bc.ControllerType == BlockController_Shell { if bc.ControllerType == BlockController_Shell {
cmdOpts.Interactive = true cmdOpts.Interactive = true
cmdOpts.Login = true cmdOpts.Login = true
@ -284,11 +350,8 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
} else { } else {
return fmt.Errorf("unknown controller type %q", bc.ControllerType) return fmt.Errorf("unknown controller type %q", bc.ControllerType)
} }
// pty buffer equivalent for ssh? i think if i have the ecmd or session i can manage it with output
// pty write needs stdin, so if i provide that, i might be able to write that way
// need a way to handle setsize???
var shellProc *shellexec.ShellProc var shellProc *shellexec.ShellProc
if remoteName, ok := blockMeta["connection"].(string); ok && remoteName != "" { if remoteName != "" {
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, remoteName) shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, remoteName)
if err != nil { if err != nil {
return err return err
@ -309,7 +372,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
messageCh := make(chan []byte, 32) messageCh := make(chan []byte, 32)
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Pty, messageCh) ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Pty, messageCh)
outputCh := make(chan []byte, 32) outputCh := make(chan []byte, 32)
WshServerFactoryFn(messageCh, outputCh, wshutil.RpcContext{BlockId: bc.BlockId, TabId: bc.TabId}) WshServerFactoryFn(messageCh, outputCh, wshrpc.RpcContext{BlockId: bc.BlockId, TabId: bc.TabId})
go func() { go func() {
// handles regular output from the pty (goes to the blockfile and xterm) // handles regular output from the pty (goes to the blockfile and xterm)
defer func() { defer func() {

View File

@ -20,7 +20,6 @@ import (
"github.com/wavetermdev/thenextwave/pkg/wconfig" "github.com/wavetermdev/thenextwave/pkg/wconfig"
"github.com/wavetermdev/thenextwave/pkg/web/webcmd" "github.com/wavetermdev/thenextwave/pkg/web/webcmd"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshserver"
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -61,10 +60,13 @@ var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem()
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem() var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{}) var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{})
var orefRType = reflect.TypeOf((*waveobj.ORef)(nil)).Elem() var orefRType = reflect.TypeOf((*waveobj.ORef)(nil)).Elem()
var wshRpcInterfaceRType = reflect.TypeOf((*wshrpc.WshRpcInterface)(nil)).Elem()
func generateTSMethodTypes(method reflect.Method, tsTypesMap map[reflect.Type]string) error { func generateTSMethodTypes(method reflect.Method, tsTypesMap map[reflect.Type]string, skipFirstArg bool) error {
for idx := 1; idx < method.Type.NumIn(); idx++ { for idx := 0; idx < method.Type.NumIn(); idx++ {
// skip receiver if skipFirstArg && idx == 0 {
continue
}
inType := method.Type.In(idx) inType := method.Type.In(idx)
GenerateTSType(inType, tsTypesMap) GenerateTSType(inType, tsTypesMap)
} }
@ -159,14 +161,13 @@ var tsRenameMap = map[string]string{
func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) { func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) {
var buf bytes.Buffer var buf bytes.Buffer
waveObjType := reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
tsTypeName := rtype.Name() tsTypeName := rtype.Name()
if tsRename, ok := tsRenameMap[tsTypeName]; ok { if tsRename, ok := tsRenameMap[tsTypeName]; ok {
tsTypeName = tsRename tsTypeName = tsRename
} }
var isWaveObj bool var isWaveObj bool
buf.WriteString(fmt.Sprintf("// %s\n", rtype.String())) buf.WriteString(fmt.Sprintf("// %s\n", rtype.String()))
if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { if rtype.Implements(waveObjRType) || reflect.PointerTo(rtype).Implements(waveObjRType) {
isWaveObj = true isWaveObj = true
buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName)) buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName))
} else { } else {
@ -253,6 +254,9 @@ func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) {
if rtype == nil { if rtype == nil {
return return
} }
if rtype.Kind() == reflect.Chan {
rtype = rtype.Elem()
}
if rtype == metaRType { if rtype == metaRType {
tsTypesMap[metaRType] = GenerateMetaType() tsTypesMap[metaRType] = GenerateMetaType()
return return
@ -397,17 +401,17 @@ func GenerateServiceClass(serviceName string, serviceObj any, tsTypesMap map[ref
return sb.String() return sb.String()
} }
func GenerateWshServerMethod(methodDecl *wshserver.WshServerMethodDecl, tsTypesMap map[reflect.Type]string) string { func GenerateWshServerMethod(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
if methodDecl.CommandType == wshutil.RpcType_ResponseStream { if methodDecl.CommandType == wshrpc.RpcType_ResponseStream {
return GenerateWshServerMethod_ResponseStream(methodDecl, tsTypesMap) return GenerateWshServerMethod_ResponseStream(methodDecl, tsTypesMap)
} else if methodDecl.CommandType == wshutil.RpcType_Call { } else if methodDecl.CommandType == wshrpc.RpcType_Call {
return GenerateWshServerMethod_Call(methodDecl, tsTypesMap) return GenerateWshServerMethod_Call(methodDecl, tsTypesMap)
} else { } else {
panic(fmt.Sprintf("cannot generate wshserver commandtype %q", methodDecl.CommandType)) panic(fmt.Sprintf("cannot generate wshserver commandtype %q", methodDecl.CommandType))
} }
} }
func GenerateWshServerMethod_ResponseStream(methodDecl *wshserver.WshServerMethodDecl, tsTypesMap map[reflect.Type]string) string { func GenerateWshServerMethod_ResponseStream(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType)) sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType))
respType := "any" respType := "any"
@ -429,7 +433,7 @@ func GenerateWshServerMethod_ResponseStream(methodDecl *wshserver.WshServerMetho
return sb.String() return sb.String()
} }
func GenerateWshServerMethod_Call(methodDecl *wshserver.WshServerMethodDecl, tsTypesMap map[reflect.Type]string) string { func GenerateWshServerMethod_Call(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType)) sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType))
rtnType := "Promise<void>" rtnType := "Promise<void>"
@ -469,7 +473,7 @@ func GenerateServiceTypes(tsTypesMap map[reflect.Type]string) error {
serviceType := reflect.TypeOf(serviceObj) serviceType := reflect.TypeOf(serviceObj)
for midx := 0; midx < serviceType.NumMethod(); midx++ { for midx := 0; midx < serviceType.NumMethod(); midx++ {
method := serviceType.Method(midx) method := serviceType.Method(midx)
err := generateTSMethodTypes(method, tsTypesMap) err := generateTSMethodTypes(method, tsTypesMap, true)
if err != nil { if err != nil {
return fmt.Errorf("error generating TS method types for %s.%s: %v", serviceType, method.Name, err) return fmt.Errorf("error generating TS method types for %s.%s: %v", serviceType, method.Name, err)
} }
@ -480,16 +484,12 @@ func GenerateServiceTypes(tsTypesMap map[reflect.Type]string) error {
func GenerateWshServerTypes(tsTypesMap map[reflect.Type]string) error { func GenerateWshServerTypes(tsTypesMap map[reflect.Type]string) error {
GenerateTSType(reflect.TypeOf(wshrpc.WshRpcCommandOpts{}), tsTypesMap) GenerateTSType(reflect.TypeOf(wshrpc.WshRpcCommandOpts{}), tsTypesMap)
for _, methodDecl := range wshserver.WshServerCommandToDeclMap { rtype := wshRpcInterfaceRType
GenerateTSType(methodDecl.CommandDataType, tsTypesMap) for midx := 0; midx < rtype.NumMethod(); midx++ {
if methodDecl.DefaultResponseDataType != nil { method := rtype.Method(midx)
GenerateTSType(methodDecl.DefaultResponseDataType, tsTypesMap) err := generateTSMethodTypes(method, tsTypesMap, false)
} if err != nil {
for _, rtype := range methodDecl.RequestDataTypes { return fmt.Errorf("error generating TS method types for %s.%s: %v", rtype, method.Name, err)
GenerateTSType(rtype, tsTypesMap)
}
for _, rtype := range methodDecl.ResponseDataTypes {
GenerateTSType(rtype, tsTypesMap)
} }
} }
return nil return nil

View File

@ -800,3 +800,29 @@ func MoveSliceIdxToFront[T any](arr []T, idx int) []T {
rtn = append(rtn, arr[idx+1:]...) rtn = append(rtn, arr[idx+1:]...)
return rtn return rtn
} }
// matches a delimited string with a pattern string
// the pattern string can contain "*" to match a single part, or "**" to match the rest of the string
// note that "**" may only appear at the end of the string
func StarMatchString(pattern string, s string, delimiter string) bool {
patternParts := strings.Split(pattern, delimiter)
stringParts := strings.Split(s, delimiter)
pLen, sLen := len(patternParts), len(stringParts)
for i := 0; i < pLen; i++ {
if patternParts[i] == "**" {
// '**' must be at the end to be valid
return i == pLen-1
}
if i >= sLen {
// If string is exhausted but pattern is not
return false
}
if patternParts[i] != "*" && patternParts[i] != stringParts[i] {
// If current parts don't match and pattern part is not '*'
return false
}
}
// Check if both pattern and string are fully matched
return pLen == sLen
}

View File

@ -23,12 +23,6 @@ const OpenAIPacketStr = "openai"
const OpenAICloudReqStr = "openai-cloudreq" const OpenAICloudReqStr = "openai-cloudreq"
const PacketEOFStr = "EOF" const PacketEOFStr = "EOF"
type OpenAIUsageType struct {
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
}
type OpenAICmdInfoPacketOutputType struct { type OpenAICmdInfoPacketOutputType struct {
Model string `json:"model,omitempty"` Model string `json:"model,omitempty"`
Created int64 `json:"created,omitempty"` Created int64 `json:"created,omitempty"`
@ -37,19 +31,8 @@ type OpenAICmdInfoPacketOutputType struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type OpenAIPacketType struct { func MakeOpenAIPacket() *wshrpc.OpenAIPacketType {
Type string `json:"type"` return &wshrpc.OpenAIPacketType{Type: OpenAIPacketStr}
Model string `json:"model,omitempty"`
Created int64 `json:"created,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Usage *OpenAIUsageType `json:"usage,omitempty"`
Index int `json:"index,omitempty"`
Text string `json:"text,omitempty"`
Error string `json:"error,omitempty"`
}
func MakeOpenAIPacket() *OpenAIPacketType {
return &OpenAIPacketType{Type: OpenAIPacketStr}
} }
type OpenAICmdInfoChatMessage struct { type OpenAICmdInfoChatMessage struct {
@ -60,27 +43,12 @@ type OpenAICmdInfoChatMessage struct {
UserEngineeredQuery string `json:"userengineeredquery,omitempty"` UserEngineeredQuery string `json:"userengineeredquery,omitempty"`
} }
type OpenAIPromptMessageType struct {
Role string `json:"role"`
Content string `json:"content"`
Name string `json:"name,omitempty"`
}
type OpenAICloudReqPacketType struct { type OpenAICloudReqPacketType struct {
Type string `json:"type"` Type string `json:"type"`
ClientId string `json:"clientid"` ClientId string `json:"clientid"`
Prompt []OpenAIPromptMessageType `json:"prompt"` Prompt []wshrpc.OpenAIPromptMessageType `json:"prompt"`
MaxTokens int `json:"maxtokens,omitempty"` MaxTokens int `json:"maxtokens,omitempty"`
MaxChoices int `json:"maxchoices,omitempty"` MaxChoices int `json:"maxchoices,omitempty"`
}
type OpenAIOptsType struct {
Model string `json:"model"`
APIToken string `json:"apitoken"`
BaseURL string `json:"baseurl,omitempty"`
MaxTokens int `json:"maxtokens,omitempty"`
MaxChoices int `json:"maxchoices,omitempty"`
Timeout int `json:"timeout,omitempty"`
} }
func MakeOpenAICloudReqPacket() *OpenAICloudReqPacketType { func MakeOpenAICloudReqPacket() *OpenAICloudReqPacketType {
@ -89,12 +57,6 @@ func MakeOpenAICloudReqPacket() *OpenAICloudReqPacketType {
} }
} }
type OpenAiStreamRequest struct {
ClientId string `json:"clientid,omitempty"`
Opts *OpenAIOptsType `json:"opts"`
Prompt []OpenAIPromptMessageType `json:"prompt"`
}
func GetWSEndpoint() string { func GetWSEndpoint() string {
return PCloudWSEndpoint return PCloudWSEndpoint
if !wavebase.IsDevMode() { if !wavebase.IsDevMode() {
@ -116,18 +78,18 @@ const PCloudWSEndpointVarName = "PCLOUD_WS_ENDPOINT"
const CloudWebsocketConnectTimeout = 1 * time.Minute const CloudWebsocketConnectTimeout = 1 * time.Minute
func convertUsage(resp openaiapi.ChatCompletionResponse) *OpenAIUsageType { func convertUsage(resp openaiapi.ChatCompletionResponse) *wshrpc.OpenAIUsageType {
if resp.Usage.TotalTokens == 0 { if resp.Usage.TotalTokens == 0 {
return nil return nil
} }
return &OpenAIUsageType{ return &wshrpc.OpenAIUsageType{
PromptTokens: resp.Usage.PromptTokens, PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens, CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens, TotalTokens: resp.Usage.TotalTokens,
} }
} }
func ConvertPrompt(prompt []OpenAIPromptMessageType) []openaiapi.ChatCompletionMessage { func ConvertPrompt(prompt []wshrpc.OpenAIPromptMessageType) []openaiapi.ChatCompletionMessage {
var rtn []openaiapi.ChatCompletionMessage var rtn []openaiapi.ChatCompletionMessage
for _, p := range prompt { for _, p := range prompt {
msg := openaiapi.ChatCompletionMessage{Role: p.Role, Content: p.Content, Name: p.Name} msg := openaiapi.ChatCompletionMessage{Role: p.Role, Content: p.Content, Name: p.Name}
@ -136,31 +98,31 @@ func ConvertPrompt(prompt []OpenAIPromptMessageType) []openaiapi.ChatCompletionM
return rtn return rtn
} }
func RunCloudCompletionStream(ctx context.Context, request OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[OpenAIPacketType] { func RunCloudCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
rtn := make(chan wshrpc.RespOrErrorUnion[OpenAIPacketType]) rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType])
go func() { go func() {
log.Printf("start: %v", request) log.Printf("start: %v", request)
defer close(rtn) defer close(rtn)
if request.Opts == nil { if request.Opts == nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")}
return return
} }
websocketContext, dialCancelFn := context.WithTimeout(context.Background(), CloudWebsocketConnectTimeout) websocketContext, dialCancelFn := context.WithTimeout(context.Background(), CloudWebsocketConnectTimeout)
defer dialCancelFn() defer dialCancelFn()
conn, _, err := websocket.DefaultDialer.DialContext(websocketContext, GetWSEndpoint(), nil) conn, _, err := websocket.DefaultDialer.DialContext(websocketContext, GetWSEndpoint(), nil)
if err == context.DeadlineExceeded {
rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, timed out connecting to cloud server: %v", err)}
return
} else if err != nil {
rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket connect error: %v", err)}
return
}
defer func() { defer func() {
err = conn.Close() err = conn.Close()
if err != nil { if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("unable to close openai channel: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("unable to close openai channel: %v", err)}
} }
}() }()
if err == context.DeadlineExceeded {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, timed out connecting to cloud server: %v", err)}
return
} else if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket connect error: %v", err)}
return
}
reqPk := MakeOpenAICloudReqPacket() reqPk := MakeOpenAICloudReqPacket()
reqPk.ClientId = request.ClientId reqPk.ClientId = request.ClientId
reqPk.Prompt = request.Prompt reqPk.Prompt = request.Prompt
@ -168,12 +130,12 @@ func RunCloudCompletionStream(ctx context.Context, request OpenAiStreamRequest)
reqPk.MaxChoices = request.Opts.MaxChoices reqPk.MaxChoices = request.Opts.MaxChoices
configMessageBuf, err := json.Marshal(reqPk) configMessageBuf, err := json.Marshal(reqPk)
if err != nil { if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, packet marshal error: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, packet marshal error: %v", err)}
return return
} }
err = conn.WriteMessage(websocket.TextMessage, configMessageBuf) err = conn.WriteMessage(websocket.TextMessage, configMessageBuf)
if err != nil { if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket write config error: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket write config error: %v", err)}
return return
} }
for { for {
@ -184,14 +146,14 @@ func RunCloudCompletionStream(ctx context.Context, request OpenAiStreamRequest)
} }
if err != nil { if err != nil {
log.Printf("err received: %v", err) log.Printf("err received: %v", err)
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket error reading message: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket error reading message: %v", err)}
break break
} }
var streamResp *OpenAIPacketType var streamResp *wshrpc.OpenAIPacketType
err = json.Unmarshal(socketMessage, &streamResp) err = json.Unmarshal(socketMessage, &streamResp)
log.Printf("ai resp: %v", streamResp) log.Printf("ai resp: %v", streamResp)
if err != nil { if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket response json decode error: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket response json decode error: %v", err)}
break break
} }
if streamResp.Error == PacketEOFStr { if streamResp.Error == PacketEOFStr {
@ -199,30 +161,30 @@ func RunCloudCompletionStream(ctx context.Context, request OpenAiStreamRequest)
break break
} else if streamResp.Error != "" { } else if streamResp.Error != "" {
// use error from server directly // use error from server directly
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("%v", streamResp.Error)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("%v", streamResp.Error)}
break break
} }
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Response: *streamResp} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Response: *streamResp}
} }
}() }()
return rtn return rtn
} }
func RunLocalCompletionStream(ctx context.Context, request OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[OpenAIPacketType] { func RunLocalCompletionStream(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
rtn := make(chan wshrpc.RespOrErrorUnion[OpenAIPacketType]) rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType])
go func() { go func() {
log.Printf("start2: %v", request) log.Printf("start2: %v", request)
defer close(rtn) defer close(rtn)
if request.Opts == nil { if request.Opts == nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no openai opts found")}
return return
} }
if request.Opts.Model == "" { if request.Opts.Model == "" {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("no openai model specified")} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no openai model specified")}
return return
} }
if request.Opts.BaseURL == "" && request.Opts.APIToken == "" { if request.Opts.BaseURL == "" && request.Opts.APIToken == "" {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("no api token")} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("no api token")}
return return
} }
clientConfig := openaiapi.DefaultConfig(request.Opts.APIToken) clientConfig := openaiapi.DefaultConfig(request.Opts.APIToken)
@ -241,7 +203,7 @@ func RunLocalCompletionStream(ctx context.Context, request OpenAiStreamRequest)
} }
apiResp, err := client.CreateChatCompletionStream(ctx, req) apiResp, err := client.CreateChatCompletionStream(ctx, req)
if err != nil { if err != nil {
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("error calling openai API: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("error calling openai API: %v", err)}
return return
} }
sentHeader := false sentHeader := false
@ -253,14 +215,14 @@ func RunLocalCompletionStream(ctx context.Context, request OpenAiStreamRequest)
} }
if err != nil { if err != nil {
log.Printf("err received2: %v", err) log.Printf("err received2: %v", err)
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket error reading message: %v", err)} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Error: fmt.Errorf("OpenAI request, websocket error reading message: %v", err)}
break break
} }
if streamResp.Model != "" && !sentHeader { if streamResp.Model != "" && !sentHeader {
pk := MakeOpenAIPacket() pk := MakeOpenAIPacket()
pk.Model = streamResp.Model pk.Model = streamResp.Model
pk.Created = streamResp.Created pk.Created = streamResp.Created
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Response: *pk} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Response: *pk}
sentHeader = true sentHeader = true
} }
for _, choice := range streamResp.Choices { for _, choice := range streamResp.Choices {
@ -268,15 +230,15 @@ func RunLocalCompletionStream(ctx context.Context, request OpenAiStreamRequest)
pk.Index = choice.Index pk.Index = choice.Index
pk.Text = choice.Delta.Content pk.Text = choice.Delta.Content
pk.FinishReason = string(choice.FinishReason) pk.FinishReason = string(choice.FinishReason)
rtn <- wshrpc.RespOrErrorUnion[OpenAIPacketType]{Response: *pk} rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Response: *pk}
} }
} }
}() }()
return rtn return rtn
} }
func marshalResponse(resp openaiapi.ChatCompletionResponse) []*OpenAIPacketType { func marshalResponse(resp openaiapi.ChatCompletionResponse) []*wshrpc.OpenAIPacketType {
var rtn []*OpenAIPacketType var rtn []*wshrpc.OpenAIPacketType
headerPk := MakeOpenAIPacket() headerPk := MakeOpenAIPacket()
headerPk.Model = resp.Model headerPk.Model = resp.Model
headerPk.Created = resp.Created headerPk.Created = resp.Created
@ -292,14 +254,14 @@ func marshalResponse(resp openaiapi.ChatCompletionResponse) []*OpenAIPacketType
return rtn return rtn
} }
func CreateErrorPacket(errStr string) *OpenAIPacketType { func CreateErrorPacket(errStr string) *wshrpc.OpenAIPacketType {
errPk := MakeOpenAIPacket() errPk := MakeOpenAIPacket()
errPk.FinishReason = "error" errPk.FinishReason = "error"
errPk.Error = errStr errPk.Error = errStr
return errPk return errPk
} }
func CreateTextPacket(text string) *OpenAIPacketType { func CreateTextPacket(text string) *wshrpc.OpenAIPacketType {
pk := MakeOpenAIPacket() pk := MakeOpenAIPacket()
pk.Text = text pk.Text = text
return pk return pk

View File

@ -72,13 +72,21 @@ type TermThemesConfigType map[string]TermThemeType
// TODO add default term theme settings // TODO add default term theme settings
// note we pointers so we preserve nulls
type WindowSettingsType struct {
Transparent *bool `json:"transparent"`
Opacity *float64 `json:"opacity"`
BgColor *string `json:"bgcolor"`
}
type SettingsConfigType struct { type SettingsConfigType struct {
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
Term TerminalConfigType `json:"term"` Term TerminalConfigType `json:"term"`
Widgets []WidgetsConfigType `json:"widgets"` Widgets []WidgetsConfigType `json:"widgets"`
BlockHeader BlockHeaderOpts `json:"blockheader"` BlockHeader BlockHeaderOpts `json:"blockheader"`
AutoUpdate *AutoUpdateOpts `json:"autoupdate"` AutoUpdate *AutoUpdateOpts `json:"autoupdate"`
TermThemes TermThemesConfigType `json:"termthemes"` TermThemes TermThemesConfigType `json:"termthemes"`
WindowSettings WindowSettingsType `json:"window"`
} }
var DefaultTermDarkTheme = TermThemeType{ var DefaultTermDarkTheme = TermThemeType{

View File

@ -23,7 +23,7 @@ import (
) )
// set by main-server.go (for dependency inversion) // set by main-server.go (for dependency inversion)
var WshServerFactoryFn func(inputCh chan []byte, outputCh chan []byte, initialCtx wshutil.RpcContext) = nil var WshServerFactoryFn func(inputCh chan []byte, outputCh chan []byte, initialCtx wshrpc.RpcContext) = nil
const wsReadWaitTimeout = 15 * time.Second const wsReadWaitTimeout = 15 * time.Second
const wsWriteWaitTimeout = 10 * time.Second const wsWriteWaitTimeout = 10 * time.Second
@ -104,7 +104,7 @@ func processWSCommand(jmsg map[string]any, outputCh chan any, rpcInputCh chan []
TermSize: &cmd.TermSize, TermSize: &cmd.TermSize,
} }
rpcMsg := wshutil.RpcMessage{ rpcMsg := wshutil.RpcMessage{
Command: wshrpc.Command_BlockInput, Command: wshrpc.Command_ControllerInput,
Data: data, Data: data,
} }
msgBytes, err := json.Marshal(rpcMsg) msgBytes, err := json.Marshal(rpcMsg)
@ -121,7 +121,7 @@ func processWSCommand(jmsg map[string]any, outputCh chan any, rpcInputCh chan []
InputData64: cmd.InputData64, InputData64: cmd.InputData64,
} }
rpcMsg := wshutil.RpcMessage{ rpcMsg := wshutil.RpcMessage{
Command: wshrpc.Command_BlockInput, Command: wshrpc.Command_ControllerInput,
Data: data, Data: data,
} }
msgBytes, err := json.Marshal(rpcMsg) msgBytes, err := json.Marshal(rpcMsg)
@ -281,7 +281,7 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
rpcOutputCh := make(chan []byte, 32) rpcOutputCh := make(chan []byte, 32)
eventbus.RegisterWSChannel(wsConnId, windowId, outputCh) eventbus.RegisterWSChannel(wsConnId, windowId, outputCh)
defer eventbus.UnregisterWSChannel(wsConnId) defer eventbus.UnregisterWSChannel(wsConnId)
WshServerFactoryFn(rpcInputCh, rpcOutputCh, wshutil.RpcContext{WindowId: windowId}) WshServerFactoryFn(rpcInputCh, rpcOutputCh, wshrpc.RpcContext{WindowId: windowId})
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(2) wg.Add(2)
go func() { go func() {

View File

@ -5,44 +5,50 @@
package wps package wps
import ( import (
"strings"
"sync" "sync"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
) )
// this broker interface is mostly generic // this broker interface is mostly generic
// strong typing and event types can be defined elsewhere // strong typing and event types can be defined elsewhere
type WaveEvent struct {
Event string `json:"event"`
Scopes []string `json:"scopes,omitempty"`
Sender string `json:"sender,omitempty"`
Data any `json:"data,omitempty"`
}
type SubscriptionRequest struct {
Event string `json:"event"`
Scopes []string `json:"scopes,omitempty"`
AllScopes bool `json:"allscopes,omitempty"`
}
type Client interface { type Client interface {
ClientId() string ClientId() string
SendEvent(event WaveEvent) SendEvent(event wshrpc.WaveEvent)
} }
type BrokerSubscription struct { type BrokerSubscription struct {
AllSubs []string // clientids of client subscribed to "all" events AllSubs []string // clientids of client subscribed to "all" events
ScopeSubs map[string][]string // clientids of client subscribed to specific scopes ScopeSubs map[string][]string // clientids of client subscribed to specific scopes
StarSubs map[string][]string // clientids of client subscribed to star scope (scopes with "*" or "**" in them)
} }
type Broker struct { type BrokerType struct {
Lock *sync.Mutex Lock *sync.Mutex
ClientMap map[string]Client ClientMap map[string]Client
SubMap map[string]*BrokerSubscription SubMap map[string]*BrokerSubscription
} }
func (b *Broker) Subscribe(subscriber Client, sub SubscriptionRequest) { var Broker = &BrokerType{
Lock: &sync.Mutex{},
ClientMap: make(map[string]Client),
SubMap: make(map[string]*BrokerSubscription),
}
func scopeHasStarMatch(scope string) bool {
parts := strings.Split(scope, ":")
for _, part := range parts {
if part == "*" || part == "**" {
return true
}
}
return false
}
func (b *BrokerType) Subscribe(subscriber Client, sub wshrpc.SubscriptionRequest) {
b.Lock.Lock() b.Lock.Lock()
defer b.Lock.Unlock() defer b.Lock.Unlock()
clientId := subscriber.ClientId() clientId := subscriber.ClientId()
@ -51,6 +57,7 @@ func (b *Broker) Subscribe(subscriber Client, sub SubscriptionRequest) {
bs = &BrokerSubscription{ bs = &BrokerSubscription{
AllSubs: []string{}, AllSubs: []string{},
ScopeSubs: make(map[string][]string), ScopeSubs: make(map[string][]string),
StarSubs: make(map[string][]string),
} }
b.SubMap[sub.Event] = bs b.SubMap[sub.Event] = bs
} }
@ -58,17 +65,47 @@ func (b *Broker) Subscribe(subscriber Client, sub SubscriptionRequest) {
bs.AllSubs = utilfn.AddElemToSliceUniq(bs.AllSubs, clientId) bs.AllSubs = utilfn.AddElemToSliceUniq(bs.AllSubs, clientId)
} }
for _, scope := range sub.Scopes { for _, scope := range sub.Scopes {
scopeSubs := bs.ScopeSubs[scope] starMatch := scopeHasStarMatch(scope)
scopeSubs = utilfn.AddElemToSliceUniq(scopeSubs, clientId) if starMatch {
bs.ScopeSubs[scope] = scopeSubs addStrToScopeMap(bs.StarSubs, scope, clientId)
} else {
addStrToScopeMap(bs.ScopeSubs, scope, clientId)
}
} }
} }
func (bs *BrokerSubscription) IsEmpty() bool { func (bs *BrokerSubscription) IsEmpty() bool {
return len(bs.AllSubs) == 0 && len(bs.ScopeSubs) == 0 return len(bs.AllSubs) == 0 && len(bs.ScopeSubs) == 0 && len(bs.StarSubs) == 0
} }
func (b *Broker) Unsubscribe(subscriber Client, sub SubscriptionRequest) { func removeStrFromScopeMap(scopeMap map[string][]string, scope string, clientId string) {
scopeSubs := scopeMap[scope]
scopeSubs = utilfn.RemoveElemFromSlice(scopeSubs, clientId)
if len(scopeSubs) == 0 {
delete(scopeMap, scope)
} else {
scopeMap[scope] = scopeSubs
}
}
func removeStrFromScopeMapAll(scopeMap map[string][]string, clientId string) {
for scope, scopeSubs := range scopeMap {
scopeSubs = utilfn.RemoveElemFromSlice(scopeSubs, clientId)
if len(scopeSubs) == 0 {
delete(scopeMap, scope)
} else {
scopeMap[scope] = scopeSubs
}
}
}
func addStrToScopeMap(scopeMap map[string][]string, scope string, clientId string) {
scopeSubs := scopeMap[scope]
scopeSubs = utilfn.AddElemToSliceUniq(scopeSubs, clientId)
scopeMap[scope] = scopeSubs
}
func (b *BrokerType) Unsubscribe(subscriber Client, sub wshrpc.SubscriptionRequest) {
b.Lock.Lock() b.Lock.Lock()
defer b.Lock.Unlock() defer b.Lock.Unlock()
clientId := subscriber.ClientId() clientId := subscriber.ClientId()
@ -80,12 +117,11 @@ func (b *Broker) Unsubscribe(subscriber Client, sub SubscriptionRequest) {
bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, clientId) bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, clientId)
} }
for _, scope := range sub.Scopes { for _, scope := range sub.Scopes {
scopeSubs := bs.ScopeSubs[scope] starMatch := scopeHasStarMatch(scope)
scopeSubs = utilfn.RemoveElemFromSlice(scopeSubs, clientId) if starMatch {
if len(scopeSubs) == 0 { removeStrFromScopeMap(bs.StarSubs, scope, clientId)
delete(bs.ScopeSubs, scope)
} else { } else {
bs.ScopeSubs[scope] = scopeSubs removeStrFromScopeMap(bs.ScopeSubs, scope, clientId)
} }
} }
if bs.IsEmpty() { if bs.IsEmpty() {
@ -93,28 +129,22 @@ func (b *Broker) Unsubscribe(subscriber Client, sub SubscriptionRequest) {
} }
} }
func (b *Broker) UnsubscribeAll(subscriber Client) { func (b *BrokerType) UnsubscribeAll(subscriber Client) {
b.Lock.Lock() b.Lock.Lock()
defer b.Lock.Unlock() defer b.Lock.Unlock()
clientId := subscriber.ClientId() clientId := subscriber.ClientId()
delete(b.ClientMap, clientId) delete(b.ClientMap, clientId)
for eventType, bs := range b.SubMap { for eventType, bs := range b.SubMap {
bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, clientId) bs.AllSubs = utilfn.RemoveElemFromSlice(bs.AllSubs, clientId)
for scope, scopeSubs := range bs.ScopeSubs { removeStrFromScopeMapAll(bs.StarSubs, clientId)
scopeSubs = utilfn.RemoveElemFromSlice(scopeSubs, clientId) removeStrFromScopeMapAll(bs.ScopeSubs, clientId)
if len(scopeSubs) == 0 {
delete(bs.ScopeSubs, scope)
} else {
bs.ScopeSubs[scope] = scopeSubs
}
}
if bs.IsEmpty() { if bs.IsEmpty() {
delete(b.SubMap, eventType) delete(b.SubMap, eventType)
} }
} }
} }
func (b *Broker) Publish(subscriber Client, event WaveEvent) { func (b *BrokerType) Publish(event wshrpc.WaveEvent) {
clientIds := b.getMatchingClientIds(event) clientIds := b.getMatchingClientIds(event)
for _, clientId := range clientIds { for _, clientId := range clientIds {
client := b.ClientMap[clientId] client := b.ClientMap[clientId]
@ -124,7 +154,7 @@ func (b *Broker) Publish(subscriber Client, event WaveEvent) {
} }
} }
func (b *Broker) getMatchingClientIds(event WaveEvent) []string { func (b *BrokerType) getMatchingClientIds(event wshrpc.WaveEvent) []string {
b.Lock.Lock() b.Lock.Lock()
defer b.Lock.Unlock() defer b.Lock.Unlock()
bs := b.SubMap[event.Event] bs := b.SubMap[event.Event]
@ -139,6 +169,13 @@ func (b *Broker) getMatchingClientIds(event WaveEvent) []string {
for _, clientId := range bs.ScopeSubs[scope] { for _, clientId := range bs.ScopeSubs[scope] {
clientIds[clientId] = true clientIds[clientId] = true
} }
for starScope := range bs.StarSubs {
if utilfn.StarMatchString(starScope, scope, ":") {
for _, clientId := range bs.StarSubs[starScope] {
clientIds[clientId] = true
}
}
}
} }
var rtn []string var rtn []string
for clientId := range clientIds { for clientId := range clientIds {

View File

@ -9,24 +9,29 @@ import (
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/waveai"
) )
// command "controller:input", wshserver.BlockInputCommand // command "authenticate", wshserver.AuthenticateCommand
func BlockInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.WshRpcCommandOpts) error { func AuthenticateCommand(w *wshutil.WshRpc, data string, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "controller:input", data, opts) _, err := sendRpcRequestCallHelper[any](w, "authenticate", data, opts)
return err return err
} }
// command "controller:restart", wshserver.BlockRestartCommand // command "controllerinput", wshserver.ControllerInputCommand
func BlockRestartCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockRestartData, opts *wshrpc.WshRpcCommandOpts) error { func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "controller:restart", data, opts) _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts)
return err
}
// command "controllerrestart", wshserver.ControllerRestartCommand
func ControllerRestartCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockRestartData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "controllerrestart", data, opts)
return err return err
} }
// command "createblock", wshserver.CreateBlockCommand // command "createblock", wshserver.CreateBlockCommand
func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, opts *wshrpc.WshRpcCommandOpts) (*waveobj.ORef, error) { func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, opts *wshrpc.WshRpcCommandOpts) (waveobj.ORef, error) {
resp, err := sendRpcRequestCallHelper[*waveobj.ORef](w, "createblock", data, opts) resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createblock", data, opts)
return resp, err return resp, err
} }
@ -36,27 +41,57 @@ func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, o
return err return err
} }
// command "file:append", wshserver.AppendFileCommand // command "eventpublish", wshserver.EventPublishCommand
func AppendFileCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error { func EventPublishCommand(w *wshutil.WshRpc, data wshrpc.WaveEvent, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "file:append", data, opts) _, err := sendRpcRequestCallHelper[any](w, "eventpublish", data, opts)
return err return err
} }
// command "file:appendijson", wshserver.AppendIJsonCommand // command "eventrecv", wshserver.EventRecvCommand
func AppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonData, opts *wshrpc.WshRpcCommandOpts) error { func EventRecvCommand(w *wshutil.WshRpc, data wshrpc.WaveEvent, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "file:appendijson", data, opts) _, err := sendRpcRequestCallHelper[any](w, "eventrecv", data, opts)
return err return err
} }
// command "file:read", wshserver.ReadFile // command "eventsub", wshserver.EventSubCommand
func ReadFile(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) (string, error) { func EventSubCommand(w *wshutil.WshRpc, data wshrpc.SubscriptionRequest, opts *wshrpc.WshRpcCommandOpts) error {
resp, err := sendRpcRequestCallHelper[string](w, "file:read", data, opts) _, err := sendRpcRequestCallHelper[any](w, "eventsub", data, opts)
return err
}
// command "eventunsub", wshserver.EventUnsubCommand
func EventUnsubCommand(w *wshutil.WshRpc, data wshrpc.SubscriptionRequest, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "eventunsub", data, opts)
return err
}
// command "eventunsuball", wshserver.EventUnsubAllCommand
func EventUnsubAllCommand(w *wshutil.WshRpc, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "eventunsuball", nil, opts)
return err
}
// command "fileappend", wshserver.FileAppendCommand
func FileAppendCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "fileappend", data, opts)
return err
}
// command "fileappendijson", wshserver.FileAppendIJsonCommand
func FileAppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "fileappendijson", data, opts)
return err
}
// command "fileread", wshserver.FileReadCommand
func FileReadCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) (string, error) {
resp, err := sendRpcRequestCallHelper[string](w, "fileread", data, opts)
return resp, err return resp, err
} }
// command "file:write", wshserver.WriteFile // command "filewrite", wshserver.FileWriteCommand
func WriteFile(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error { func FileWriteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "file:write", data, opts) _, err := sendRpcRequestCallHelper[any](w, "filewrite", data, opts)
return err return err
} }
@ -84,20 +119,20 @@ func SetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandSetMetaData, opts *wsh
return err return err
} }
// command "setview", wshserver.BlockSetViewCommand // command "setview", wshserver.SetViewCommand
func BlockSetViewCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockSetViewData, opts *wshrpc.WshRpcCommandOpts) error { func SetViewCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockSetViewData, opts *wshrpc.WshRpcCommandOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "setview", data, opts) _, err := sendRpcRequestCallHelper[any](w, "setview", data, opts)
return err return err
} }
// command "stream:waveai", wshserver.RespStreamWaveAi // command "streamtest", wshserver.StreamTestCommand
func RespStreamWaveAi(w *wshutil.WshRpc, data waveai.OpenAiStreamRequest, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[waveai.OpenAIPacketType] { func StreamTestCommand(w *wshutil.WshRpc, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[int] {
return sendRpcRequestResponseStreamHelper[waveai.OpenAIPacketType](w, "stream:waveai", data, opts)
}
// command "streamtest", wshserver.RespStreamTest
func RespStreamTest(w *wshutil.WshRpc, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[int] {
return sendRpcRequestResponseStreamHelper[int](w, "streamtest", nil, opts) return sendRpcRequestResponseStreamHelper[int](w, "streamtest", nil, opts)
} }
// command "streamwaveai", wshserver.StreamWaveAiCommand
func StreamWaveAiCommand(w *wshutil.WshRpc, data wshrpc.OpenAiStreamRequest, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
return sendRpcRequestResponseStreamHelper[wshrpc.OpenAIPacketType](w, "streamwaveai", data, opts)
}

116
pkg/wshrpc/wshrpcmeta.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wshrpc
import (
"context"
"fmt"
"log"
"reflect"
"strings"
)
type WshRpcMethodDecl struct {
Command string
CommandType string
MethodName string
CommandDataType reflect.Type
DefaultResponseDataType reflect.Type
}
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var wshRpcInterfaceRType = reflect.TypeOf((*WshRpcInterface)(nil)).Elem()
func getWshCommandType(method reflect.Method) string {
if method.Type.NumOut() == 1 {
outType := method.Type.Out(0)
if outType.Kind() == reflect.Chan {
return RpcType_ResponseStream
}
}
return RpcType_Call
}
func getWshMethodResponseType(commandType string, method reflect.Method) reflect.Type {
switch commandType {
case RpcType_ResponseStream:
if method.Type.NumOut() != 1 {
panic(fmt.Sprintf("method %q has invalid number of return values for response stream", method.Name))
}
outType := method.Type.Out(0)
if outType.Kind() != reflect.Chan {
panic(fmt.Sprintf("method %q has invalid return type %s for response stream", method.Name, outType))
}
elemType := outType.Elem()
if !strings.HasPrefix(elemType.Name(), "RespOrErrorUnion") {
panic(fmt.Sprintf("method %q has invalid return element type %s for response stream (should be RespOrErrorUnion)", method.Name, elemType))
}
respField, found := elemType.FieldByName("Response")
if !found {
panic(fmt.Sprintf("method %q has invalid return element type %s for response stream (missing Response field)", method.Name, elemType))
}
return respField.Type
case RpcType_Call:
if method.Type.NumOut() > 1 {
return method.Type.Out(0)
}
return nil
default:
panic(fmt.Sprintf("unsupported command type %q", commandType))
}
}
func generateWshCommandDecl(method reflect.Method) *WshRpcMethodDecl {
if method.Type.NumIn() == 0 || method.Type.In(0) != contextRType {
panic(fmt.Sprintf("method %q does not have context as first argument", method.Name))
}
cmdStr := method.Name
decl := &WshRpcMethodDecl{}
// remove Command suffix
if !strings.HasSuffix(cmdStr, "Command") {
panic(fmt.Sprintf("method %q does not have Command suffix", cmdStr))
}
cmdStr = cmdStr[:len(cmdStr)-len("Command")]
decl.Command = strings.ToLower(cmdStr)
decl.CommandType = getWshCommandType(method)
decl.MethodName = method.Name
var cdataType reflect.Type
if method.Type.NumIn() > 1 {
cdataType = method.Type.In(1)
}
decl.CommandDataType = cdataType
decl.DefaultResponseDataType = getWshMethodResponseType(decl.CommandType, method)
return decl
}
func MakeMethodMapForImpl(impl any, declMap map[string]*WshRpcMethodDecl) map[string]reflect.Method {
rtype := reflect.TypeOf(impl)
rtnMap := make(map[string]reflect.Method)
for midx := 0; midx < rtype.NumMethod(); midx++ {
method := rtype.Method(midx)
if !strings.HasSuffix(method.Name, "Command") {
continue
}
commandName := strings.ToLower(method.Name[:len(method.Name)-len("Command")])
decl := declMap[commandName]
if decl == nil {
log.Printf("WARNING: method %q does not match a command method", method.Name)
continue
}
rtnMap[commandName] = method
}
return rtnMap
}
func GenerateWshCommandDeclMap() map[string]*WshRpcMethodDecl {
rtype := wshRpcInterfaceRType
rtnMap := make(map[string]*WshRpcMethodDecl)
for midx := 0; midx < rtype.NumMethod(); midx++ {
method := rtype.Method(midx)
decl := generateWshCommandDecl(method)
rtnMap[decl.Command] = decl
}
return rtnMap
}

View File

@ -5,45 +5,77 @@
package wshrpc package wshrpc
import ( import (
"context"
"reflect" "reflect"
"github.com/wavetermdev/thenextwave/pkg/ijson" "github.com/wavetermdev/thenextwave/pkg/ijson"
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
const ( const (
Command_Message = "message" RpcType_Call = "call" // single response (regular rpc)
Command_SetView = "setview" RpcType_ResponseStream = "responsestream" // stream of responses (streaming rpc)
Command_SetMeta = "setmeta" RpcType_StreamingRequest = "streamingrequest" // streaming request
Command_GetMeta = "getmeta" RpcType_Complex = "complex" // streaming request/response
Command_BlockInput = "controller:input" )
Command_Restart = "controller:restart"
Command_AppendFile = "file:append" const (
Command_AppendIJson = "file:appendijson" Command_Authenticate = "authenticate"
Command_ResolveIds = "resolveids" Command_Message = "message"
Command_CreateBlock = "createblock" Command_GetMeta = "getmeta"
Command_DeleteBlock = "deleteblock" Command_SetMeta = "setmeta"
Command_WriteFile = "file:write" Command_SetView = "setview"
Command_ReadFile = "file:read" Command_ControllerInput = "controllerinput"
Command_StreamWaveAi = "stream:waveai" Command_ControllerRestart = "controllerrestart"
Command_FileAppend = "fileappend"
Command_FileAppendIJson = "fileappendijson"
Command_ResolveIds = "resolveids"
Command_CreateBlock = "createblock"
Command_DeleteBlock = "deleteblock"
Command_FileWrite = "filewrite"
Command_FileRead = "fileread"
Command_EventPublish = "eventpublish"
Command_EventRecv = "eventrecv"
Command_EventSub = "eventsub"
Command_EventUnsub = "eventunsub"
Command_EventUnsubAll = "eventunsuball"
Command_StreamTest = "streamtest"
Command_StreamWaveAi = "streamwaveai"
) )
type MetaDataType = map[string]any type MetaDataType = map[string]any
var DataTypeMap = map[string]reflect.Type{
"meta": reflect.TypeOf(MetaDataType{}),
"resolveidsrtn": reflect.TypeOf(CommandResolveIdsRtnData{}),
"oref": reflect.TypeOf(waveobj.ORef{}),
}
type RespOrErrorUnion[T any] struct { type RespOrErrorUnion[T any] struct {
Response T Response T
Error error Error error
} }
type WshRpcInterface interface {
AuthenticateCommand(ctx context.Context, data string) error
MessageCommand(ctx context.Context, data CommandMessageData) error
GetMetaCommand(ctx context.Context, data CommandGetMetaData) (MetaDataType, error)
SetMetaCommand(ctx context.Context, data CommandSetMetaData) error
SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
ControllerRestartCommand(ctx context.Context, data CommandBlockRestartData) error
FileAppendCommand(ctx context.Context, data CommandFileData) error
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error)
DeleteBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
FileWriteCommand(ctx context.Context, data CommandFileData) error
FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
EventPublishCommand(ctx context.Context, data WaveEvent) error
EventRecvCommand(ctx context.Context, data WaveEvent) error
EventSubCommand(ctx context.Context, data SubscriptionRequest) error
EventUnsubCommand(ctx context.Context, data SubscriptionRequest) error
EventUnsubAllCommand(ctx context.Context) error
StreamTestCommand(ctx context.Context) chan RespOrErrorUnion[int]
StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType]
}
// for frontend // for frontend
type WshServerCommandMeta struct { type WshServerCommandMeta struct {
CommandType string `json:"commandtype"` CommandType string `json:"commandtype"`
@ -54,7 +86,13 @@ type WshRpcCommandOpts struct {
NoResponse bool `json:"noresponse"` NoResponse bool `json:"noresponse"`
} }
func HackRpcContextIntoData(dataPtr any, rpcContext wshutil.RpcContext) { type RpcContext struct {
BlockId string `json:"blockid,omitempty"`
TabId string `json:"tabid,omitempty"`
WindowId string `json:"windowid,omitempty"`
}
func HackRpcContextIntoData(dataPtr any, rpcContext RpcContext) {
dataVal := reflect.ValueOf(dataPtr).Elem() dataVal := reflect.ValueOf(dataPtr).Elem()
dataType := dataVal.Type() dataType := dataVal.Type()
for i := 0; i < dataVal.NumField(); i++ { for i := 0; i < dataVal.NumField(); i++ {
@ -141,3 +179,54 @@ type CommandAppendIJsonData struct {
type CommandDeleteBlockData struct { type CommandDeleteBlockData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"` BlockId string `json:"blockid" wshcontext:"BlockId"`
} }
type WaveEvent struct {
Event string `json:"event"`
Scopes []string `json:"scopes,omitempty"`
Sender string `json:"sender,omitempty"`
Data any `json:"data,omitempty"`
}
type SubscriptionRequest struct {
Event string `json:"event"`
Scopes []string `json:"scopes,omitempty"`
AllScopes bool `json:"allscopes,omitempty"`
}
type OpenAiStreamRequest struct {
ClientId string `json:"clientid,omitempty"`
Opts *OpenAIOptsType `json:"opts"`
Prompt []OpenAIPromptMessageType `json:"prompt"`
}
type OpenAIPromptMessageType struct {
Role string `json:"role"`
Content string `json:"content"`
Name string `json:"name,omitempty"`
}
type OpenAIOptsType struct {
Model string `json:"model"`
APIToken string `json:"apitoken"`
BaseURL string `json:"baseurl,omitempty"`
MaxTokens int `json:"maxtokens,omitempty"`
MaxChoices int `json:"maxchoices,omitempty"`
Timeout int `json:"timeout,omitempty"`
}
type OpenAIPacketType struct {
Type string `json:"type"`
Model string `json:"model,omitempty"`
Created int64 `json:"created,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Usage *OpenAIUsageType `json:"usage,omitempty"`
Index int `json:"index,omitempty"`
Text string `json:"text,omitempty"`
Error string `json:"error,omitempty"`
}
type OpenAIUsageType struct {
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
}

View File

@ -11,7 +11,6 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"reflect"
"strings" "strings"
"time" "time"
@ -20,45 +19,26 @@ import (
"github.com/wavetermdev/thenextwave/pkg/filestore" "github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/waveai" "github.com/wavetermdev/thenextwave/pkg/waveai"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wps"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshutil" "github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
var RespStreamTest_MethodDecl = &WshServerMethodDecl{ func (ws *WshServer) AuthenticateCommand(ctx context.Context, data string) error {
Command: "streamtest", w := wshutil.GetWshRpcFromContext(ctx)
CommandType: wshutil.RpcType_ResponseStream, if w == nil {
MethodName: "RespStreamTest", return fmt.Errorf("no wshrpc in context")
Method: reflect.ValueOf(WshServerImpl.RespStreamTest), }
CommandDataType: nil, newCtx, err := wshutil.ValidateAndExtractRpcContextFromToken(data)
DefaultResponseDataType: reflect.TypeOf((int)(0)), if err != nil {
} return fmt.Errorf("error validating token: %w", err)
}
var RespStreamWaveAi_MethodDecl = &WshServerMethodDecl{ if newCtx == nil {
Command: wshrpc.Command_StreamWaveAi, return fmt.Errorf("no context found in jwt token")
CommandType: wshutil.RpcType_ResponseStream, }
MethodName: "RespStreamWaveAi", w.SetRpcContext(*newCtx)
Method: reflect.ValueOf(WshServerImpl.RespStreamWaveAi), return nil
CommandDataType: reflect.TypeOf(waveai.OpenAiStreamRequest{}),
DefaultResponseDataType: reflect.TypeOf(waveai.OpenAIPacketType{}),
}
var WshServerCommandToDeclMap = map[string]*WshServerMethodDecl{
wshrpc.Command_Message: GetWshServerMethod(wshrpc.Command_Message, wshutil.RpcType_Call, "MessageCommand", WshServerImpl.MessageCommand),
wshrpc.Command_SetView: GetWshServerMethod(wshrpc.Command_SetView, wshutil.RpcType_Call, "BlockSetViewCommand", WshServerImpl.BlockSetViewCommand),
wshrpc.Command_SetMeta: GetWshServerMethod(wshrpc.Command_SetMeta, wshutil.RpcType_Call, "SetMetaCommand", WshServerImpl.SetMetaCommand),
wshrpc.Command_GetMeta: GetWshServerMethod(wshrpc.Command_GetMeta, wshutil.RpcType_Call, "GetMetaCommand", WshServerImpl.GetMetaCommand),
wshrpc.Command_ResolveIds: GetWshServerMethod(wshrpc.Command_ResolveIds, wshutil.RpcType_Call, "ResolveIdsCommand", WshServerImpl.ResolveIdsCommand),
wshrpc.Command_CreateBlock: GetWshServerMethod(wshrpc.Command_CreateBlock, wshutil.RpcType_Call, "CreateBlockCommand", WshServerImpl.CreateBlockCommand),
wshrpc.Command_Restart: GetWshServerMethod(wshrpc.Command_Restart, wshutil.RpcType_Call, "BlockRestartCommand", WshServerImpl.BlockRestartCommand),
wshrpc.Command_BlockInput: GetWshServerMethod(wshrpc.Command_BlockInput, wshutil.RpcType_Call, "BlockInputCommand", WshServerImpl.BlockInputCommand),
wshrpc.Command_AppendFile: GetWshServerMethod(wshrpc.Command_AppendFile, wshutil.RpcType_Call, "AppendFileCommand", WshServerImpl.AppendFileCommand),
wshrpc.Command_AppendIJson: GetWshServerMethod(wshrpc.Command_AppendIJson, wshutil.RpcType_Call, "AppendIJsonCommand", WshServerImpl.AppendIJsonCommand),
wshrpc.Command_DeleteBlock: GetWshServerMethod(wshrpc.Command_DeleteBlock, wshutil.RpcType_Call, "DeleteBlockCommand", WshServerImpl.DeleteBlockCommand),
wshrpc.Command_WriteFile: GetWshServerMethod(wshrpc.Command_WriteFile, wshutil.RpcType_Call, "WriteFile", WshServerImpl.WriteFile),
wshrpc.Command_ReadFile: GetWshServerMethod(wshrpc.Command_ReadFile, wshutil.RpcType_Call, "ReadFile", WshServerImpl.ReadFile),
wshrpc.Command_StreamWaveAi: RespStreamWaveAi_MethodDecl,
"streamtest": RespStreamTest_MethodDecl,
} }
// for testing // for testing
@ -68,7 +48,7 @@ func (ws *WshServer) MessageCommand(ctx context.Context, data wshrpc.CommandMess
} }
// for testing // for testing
func (ws *WshServer) RespStreamTest(ctx context.Context) chan wshrpc.RespOrErrorUnion[int] { func (ws *WshServer) StreamTestCommand(ctx context.Context) chan wshrpc.RespOrErrorUnion[int] {
rtn := make(chan wshrpc.RespOrErrorUnion[int]) rtn := make(chan wshrpc.RespOrErrorUnion[int])
go func() { go func() {
for i := 1; i <= 5; i++ { for i := 1; i <= 5; i++ {
@ -80,7 +60,7 @@ func (ws *WshServer) RespStreamTest(ctx context.Context) chan wshrpc.RespOrError
return rtn return rtn
} }
func (ws *WshServer) RespStreamWaveAi(ctx context.Context, request waveai.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[waveai.OpenAIPacketType] { func (ws *WshServer) StreamWaveAiCommand(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
if request.Opts.BaseURL == "" && request.Opts.APIToken == "" { if request.Opts.BaseURL == "" && request.Opts.APIToken == "" {
return waveai.RunCloudCompletionStream(ctx, request) return waveai.RunCloudCompletionStream(ctx, request)
} }
@ -224,7 +204,7 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
return &waveobj.ORef{OType: wstore.OType_Block, OID: blockData.OID}, nil return &waveobj.ORef{OType: wstore.OType_Block, OID: blockData.OID}, nil
} }
func (ws *WshServer) BlockSetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error { func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error {
log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View) log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View)
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
block, err := wstore.DBGet[*wstore.Block](ctx, data.BlockId) block, err := wstore.DBGet[*wstore.Block](ctx, data.BlockId)
@ -241,7 +221,7 @@ func (ws *WshServer) BlockSetViewCommand(ctx context.Context, data wshrpc.Comman
return nil return nil
} }
func (ws *WshServer) BlockRestartCommand(ctx context.Context, data wshrpc.CommandBlockRestartData) error { func (ws *WshServer) ControllerRestartCommand(ctx context.Context, data wshrpc.CommandBlockRestartData) error {
bc := blockcontroller.GetBlockController(data.BlockId) bc := blockcontroller.GetBlockController(data.BlockId)
if bc == nil { if bc == nil {
return fmt.Errorf("block controller not found for block %q", data.BlockId) return fmt.Errorf("block controller not found for block %q", data.BlockId)
@ -249,7 +229,7 @@ func (ws *WshServer) BlockRestartCommand(ctx context.Context, data wshrpc.Comman
return bc.RestartController() return bc.RestartController()
} }
func (ws *WshServer) BlockInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error { func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
bc := blockcontroller.GetBlockController(data.BlockId) bc := blockcontroller.GetBlockController(data.BlockId)
if bc == nil { if bc == nil {
return fmt.Errorf("block controller not found for block %q", data.BlockId) return fmt.Errorf("block controller not found for block %q", data.BlockId)
@ -269,7 +249,7 @@ func (ws *WshServer) BlockInputCommand(ctx context.Context, data wshrpc.CommandB
return bc.SendInput(inputUnion) return bc.SendInput(inputUnion)
} }
func (ws *WshServer) WriteFile(ctx context.Context, data wshrpc.CommandFileData) error { func (ws *WshServer) FileWriteCommand(ctx context.Context, data wshrpc.CommandFileData) error {
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64) dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
if err != nil { if err != nil {
return fmt.Errorf("error decoding data64: %w", err) return fmt.Errorf("error decoding data64: %w", err)
@ -290,7 +270,7 @@ func (ws *WshServer) WriteFile(ctx context.Context, data wshrpc.CommandFileData)
return nil return nil
} }
func (ws *WshServer) ReadFile(ctx context.Context, data wshrpc.CommandFileData) (string, error) { func (ws *WshServer) FileReadCommand(ctx context.Context, data wshrpc.CommandFileData) (string, error) {
_, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName) _, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err) return "", fmt.Errorf("error reading blockfile: %w", err)
@ -298,7 +278,7 @@ func (ws *WshServer) ReadFile(ctx context.Context, data wshrpc.CommandFileData)
return base64.StdEncoding.EncodeToString(dataBuf), nil return base64.StdEncoding.EncodeToString(dataBuf), nil
} }
func (ws *WshServer) AppendFileCommand(ctx context.Context, data wshrpc.CommandFileData) error { func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandFileData) error {
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64) dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
if err != nil { if err != nil {
return fmt.Errorf("error decoding data64: %w", err) return fmt.Errorf("error decoding data64: %w", err)
@ -320,7 +300,7 @@ func (ws *WshServer) AppendFileCommand(ctx context.Context, data wshrpc.CommandF
return nil return nil
} }
func (ws *WshServer) AppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error { func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error {
tryCreate := true tryCreate := true
if data.FileName == blockcontroller.BlockFile_Html && tryCreate { if data.FileName == blockcontroller.BlockFile_Html && tryCreate {
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true}) err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
@ -378,3 +358,46 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
sendWStoreUpdatesToEventBus(updates) sendWStoreUpdatesToEventBus(updates)
return nil return nil
} }
func (ws *WshServer) EventRecvCommand(ctx context.Context, data wshrpc.WaveEvent) error {
return nil
}
func (ws *WshServer) EventPublishCommand(ctx context.Context, data wshrpc.WaveEvent) error {
wrpc := wshutil.GetWshRpcFromContext(ctx)
if wrpc == nil {
return fmt.Errorf("no wshrpc in context")
}
if data.Sender == "" {
data.Sender = wrpc.ClientId()
}
wps.Broker.Publish(data)
return nil
}
func (ws *WshServer) EventSubCommand(ctx context.Context, data wshrpc.SubscriptionRequest) error {
wrpc := wshutil.GetWshRpcFromContext(ctx)
if wrpc == nil {
return fmt.Errorf("no wshrpc in context")
}
wps.Broker.Subscribe(wrpc, data)
return nil
}
func (ws *WshServer) EventUnsubCommand(ctx context.Context, data wshrpc.SubscriptionRequest) error {
wrpc := wshutil.GetWshRpcFromContext(ctx)
if wrpc == nil {
return fmt.Errorf("no wshrpc in context")
}
wps.Broker.Unsubscribe(wrpc, data)
return nil
}
func (ws *WshServer) EventUnsubAllCommand(ctx context.Context) error {
wrpc := wshutil.GetWshRpcFromContext(ctx)
if wrpc == nil {
return fmt.Errorf("no wshrpc in context")
}
wps.Broker.UnsubscribeAll(wrpc)
return nil
}

View File

@ -10,9 +10,7 @@ import (
"net" "net"
"os" "os"
"reflect" "reflect"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
@ -41,6 +39,7 @@ type WshServerMethodDecl struct {
var WshServerImpl = WshServer{} var WshServerImpl = WshServer{}
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem() var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var wshCommandDeclMap = wshrpc.GenerateWshCommandDeclMap()
func GetWshServerMethod(command string, commandType string, methodName string, methodFunc any) *WshServerMethodDecl { func GetWshServerMethod(command string, commandType string, methodName string, methodFunc any) *WshServerMethodDecl {
methodVal := reflect.ValueOf(methodFunc) methodVal := reflect.ValueOf(methodFunc)
@ -55,12 +54,16 @@ func GetWshServerMethod(command string, commandType string, methodName string, m
if methodType.NumOut() > 1 { if methodType.NumOut() > 1 {
defResponseType = methodType.Out(0) defResponseType = methodType.Out(0)
} }
var cdataType reflect.Type
if methodType.NumIn() > 1 {
cdataType = methodType.In(1)
}
rtn := &WshServerMethodDecl{ rtn := &WshServerMethodDecl{
Command: command, Command: command,
CommandType: commandType, CommandType: commandType,
MethodName: methodName, MethodName: methodName,
Method: methodVal, Method: methodVal,
CommandDataType: methodType.In(1), CommandDataType: cdataType,
DefaultResponseDataType: defResponseType, DefaultResponseDataType: defResponseType,
} }
return rtn return rtn
@ -89,7 +92,7 @@ func decodeRtnVals(rtnVals []reflect.Value) (any, error) {
func mainWshServerHandler(handler *wshutil.RpcResponseHandler) bool { func mainWshServerHandler(handler *wshutil.RpcResponseHandler) bool {
command := handler.GetCommand() command := handler.GetCommand()
methodDecl := WshServerCommandToDeclMap[command] methodDecl := wshCommandDeclMap[command]
if methodDecl == nil { if methodDecl == nil {
handler.SendResponseError(fmt.Errorf("command %q not found", command)) handler.SendResponseError(fmt.Errorf("command %q not found", command))
return true return true
@ -106,8 +109,18 @@ func mainWshServerHandler(handler *wshutil.RpcResponseHandler) bool {
wshrpc.HackRpcContextIntoData(commandData, handler.GetRpcContext()) wshrpc.HackRpcContextIntoData(commandData, handler.GetRpcContext())
callParams = append(callParams, reflect.ValueOf(commandData).Elem()) callParams = append(callParams, reflect.ValueOf(commandData).Elem())
} }
if methodDecl.CommandType == wshutil.RpcType_Call { implVal := reflect.ValueOf(&WshServerImpl)
rtnVals := methodDecl.Method.Call(callParams) implMethod := implVal.MethodByName(methodDecl.MethodName)
if !implMethod.IsValid() {
if !handler.NeedsResponse() {
// we also send an out of band message here since this is likely unexpected and will require debugging
handler.SendMessage(fmt.Sprintf("command %q method %q not found", handler.GetCommand(), methodDecl.MethodName))
}
handler.SendResponseError(fmt.Errorf("method %q not found", methodDecl.MethodName))
return true
}
if methodDecl.CommandType == wshrpc.RpcType_Call {
rtnVals := implMethod.Call(callParams)
rtnData, rtnErr := decodeRtnVals(rtnVals) rtnData, rtnErr := decodeRtnVals(rtnVals)
if rtnErr != nil { if rtnErr != nil {
handler.SendResponseError(rtnErr) handler.SendResponseError(rtnErr)
@ -115,8 +128,8 @@ func mainWshServerHandler(handler *wshutil.RpcResponseHandler) bool {
} }
handler.SendResponse(rtnData, true) handler.SendResponse(rtnData, true)
return true return true
} else if methodDecl.CommandType == wshutil.RpcType_ResponseStream { } else if methodDecl.CommandType == wshrpc.RpcType_ResponseStream {
rtnVals := methodDecl.Method.Call(callParams) rtnVals := implMethod.Call(callParams)
rtnChVal := rtnVals[0] rtnChVal := rtnVals[0]
if rtnChVal.IsNil() { if rtnChVal.IsNil() {
handler.SendResponse(nil, true) handler.SendResponse(nil, true)
@ -163,7 +176,7 @@ func runWshRpcWithStream(conn net.Conn) {
outputCh := make(chan []byte, DefaultOutputChSize) outputCh := make(chan []byte, DefaultOutputChSize)
go wshutil.AdaptMsgChToStream(outputCh, conn) go wshutil.AdaptMsgChToStream(outputCh, conn)
go wshutil.AdaptStreamToMsgCh(conn, inputCh) go wshutil.AdaptStreamToMsgCh(conn, inputCh)
wshutil.MakeWshRpc(inputCh, outputCh, wshutil.RpcContext{}, mainWshServerHandler) wshutil.MakeWshRpc(inputCh, outputCh, wshrpc.RpcContext{}, mainWshServerHandler)
} }
func RunWshRpcOverListener(listener net.Listener) { func RunWshRpcOverListener(listener net.Listener) {
@ -179,82 +192,6 @@ func RunWshRpcOverListener(listener net.Listener) {
}() }()
} }
func MakeClientJWTToken(rpcCtx wshutil.RpcContext, sockName string) (string, error) {
claims := jwt.MapClaims{}
claims["iat"] = time.Now().Unix()
claims["iss"] = "waveterm"
claims["sock"] = sockName
claims["exp"] = time.Now().Add(time.Hour * 24 * 365).Unix()
if rpcCtx.BlockId != "" {
claims["blockid"] = rpcCtx.BlockId
}
if rpcCtx.TabId != "" {
claims["tabid"] = rpcCtx.TabId
}
if rpcCtx.WindowId != "" {
claims["windowid"] = rpcCtx.WindowId
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(wavebase.JwtSecret))
if err != nil {
return "", fmt.Errorf("error signing token: %w", err)
}
return tokenStr, nil
}
func ValidateAndExtractRpcContextFromToken(tokenStr string) (wshutil.RpcContext, error) {
parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
token, err := parser.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return []byte(wavebase.JwtSecret), nil
})
if err != nil {
return wshutil.RpcContext{}, fmt.Errorf("error parsing token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return wshutil.RpcContext{}, fmt.Errorf("error getting claims from token")
}
// validate "exp" claim
if exp, ok := claims["exp"].(float64); ok {
if int64(exp) < time.Now().Unix() {
return wshutil.RpcContext{}, fmt.Errorf("token has expired")
}
} else {
return wshutil.RpcContext{}, fmt.Errorf("exp claim is missing or invalid")
}
// validate "iss" claim
if iss, ok := claims["iss"].(string); ok {
if iss != "waveterm" {
return wshutil.RpcContext{}, fmt.Errorf("unexpected issuer: %s", iss)
}
} else {
return wshutil.RpcContext{}, fmt.Errorf("iss claim is missing or invalid")
}
rpcCtx := wshutil.RpcContext{}
rpcCtx.BlockId = claims["blockid"].(string)
rpcCtx.TabId = claims["tabid"].(string)
rpcCtx.WindowId = claims["windowid"].(string)
return rpcCtx, nil
}
func ExtractUnverifiedSocketName(tokenStr string) (string, error) {
// this happens on the client who does not have access to the secret key
// we want to read the claims without validating the signature
token, _, err := new(jwt.Parser).ParseUnverified(tokenStr, jwt.MapClaims{})
if err != nil {
return "", fmt.Errorf("error parsing token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", fmt.Errorf("error getting claims from token")
}
sockName, ok := claims["sock"].(string)
if !ok {
return "", fmt.Errorf("sock claim is missing or invalid")
}
return sockName, nil
}
func RunDomainSocketWshServer() error { func RunDomainSocketWshServer() error {
sockName := wavebase.GetDomainSocketName() sockName := wavebase.GetDomainSocketName()
listener, err := MakeUnixListener(sockName) listener, err := MakeUnixListener(sockName)
@ -266,6 +203,6 @@ func RunDomainSocketWshServer() error {
return nil return nil
} }
func MakeWshServer(inputCh chan []byte, outputCh chan []byte, initialCtx wshutil.RpcContext) { func MakeWshServer(inputCh chan []byte, outputCh chan []byte, initialCtx wshrpc.RpcContext) {
wshutil.MakeWshRpc(inputCh, outputCh, initialCtx, mainWshServerHandler) wshutil.MakeWshRpc(inputCh, outputCh, initialCtx, mainWshServerHandler)
} }

View File

@ -15,19 +15,13 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
) )
const DefaultTimeoutMs = 5000 const DefaultTimeoutMs = 5000
const RespChSize = 32 const RespChSize = 32
const DefaultMessageChSize = 32 const DefaultMessageChSize = 32
const (
RpcType_Call = "call" // single response (regular rpc)
RpcType_ResponseStream = "responsestream" // stream of responses (streaming rpc)
RpcType_StreamingRequest = "streamingrequest" // streaming request
RpcType_Complex = "complex" // streaming request/response
)
type ResponseFnType = func(any) error type ResponseFnType = func(any) error
// returns true if handler is complete, false for an async handler // returns true if handler is complete, false for an async handler
@ -115,17 +109,12 @@ func (r *RpcMessage) Validate() error {
return fmt.Errorf("invalid packet: must have command, reqid, or resid set") return fmt.Errorf("invalid packet: must have command, reqid, or resid set")
} }
type RpcContext struct {
BlockId string `json:"blockid,omitempty"`
TabId string `json:"tabid,omitempty"`
WindowId string `json:"windowid,omitempty"`
}
type WshRpc struct { type WshRpc struct {
Lock *sync.Mutex Lock *sync.Mutex
clientId string
InputCh chan []byte InputCh chan []byte
OutputCh chan []byte OutputCh chan []byte
RpcContext *atomic.Pointer[RpcContext] RpcContext *atomic.Pointer[wshrpc.RpcContext]
RpcMap map[string]*rpcData RpcMap map[string]*rpcData
HandlerFn CommandHandlerFnType HandlerFn CommandHandlerFnType
@ -139,13 +128,14 @@ type rpcData struct {
// oscEsc is the OSC escape sequence to use for *sending* messages // oscEsc is the OSC escape sequence to use for *sending* messages
// closes outputCh when inputCh is closed/done // closes outputCh when inputCh is closed/done
func MakeWshRpc(inputCh chan []byte, outputCh chan []byte, rpcCtx RpcContext, commandHandlerFn CommandHandlerFnType) *WshRpc { func MakeWshRpc(inputCh chan []byte, outputCh chan []byte, rpcCtx wshrpc.RpcContext, commandHandlerFn CommandHandlerFnType) *WshRpc {
rtn := &WshRpc{ rtn := &WshRpc{
Lock: &sync.Mutex{}, Lock: &sync.Mutex{},
clientId: uuid.New().String(),
InputCh: inputCh, InputCh: inputCh,
OutputCh: outputCh, OutputCh: outputCh,
RpcMap: make(map[string]*rpcData), RpcMap: make(map[string]*rpcData),
RpcContext: &atomic.Pointer[RpcContext]{}, RpcContext: &atomic.Pointer[wshrpc.RpcContext]{},
HandlerFn: commandHandlerFn, HandlerFn: commandHandlerFn,
ResponseHandlerMap: make(map[string]*RpcResponseHandler), ResponseHandlerMap: make(map[string]*RpcResponseHandler),
} }
@ -154,12 +144,30 @@ func MakeWshRpc(inputCh chan []byte, outputCh chan []byte, rpcCtx RpcContext, co
return rtn return rtn
} }
func (w *WshRpc) GetRpcContext() RpcContext { func (w *WshRpc) ClientId() string {
return w.clientId
}
func (w *WshRpc) SendEvent(event wshrpc.WaveEvent) {
// for wps compatibility
msg := &RpcMessage{
Command: wshrpc.Command_EventPublish,
Data: event,
}
barr, err := json.Marshal(msg)
if err != nil {
log.Printf("error marshalling event: %v\n", err)
return
}
w.OutputCh <- barr
}
func (w *WshRpc) GetRpcContext() wshrpc.RpcContext {
rtnPtr := w.RpcContext.Load() rtnPtr := w.RpcContext.Load()
return *rtnPtr return *rtnPtr
} }
func (w *WshRpc) SetRpcContext(ctx RpcContext) { func (w *WshRpc) SetRpcContext(ctx wshrpc.RpcContext) {
w.RpcContext.Store(&ctx) w.RpcContext.Store(&ctx)
} }
@ -399,7 +407,7 @@ type RpcResponseHandler struct {
reqId string reqId string
command string command string
commandData any commandData any
rpcCtx RpcContext rpcCtx wshrpc.RpcContext
canceled *atomic.Bool // canceled by requestor canceled *atomic.Bool // canceled by requestor
done *atomic.Bool done *atomic.Bool
} }
@ -416,10 +424,25 @@ func (handler *RpcResponseHandler) GetCommandRawData() any {
return handler.commandData return handler.commandData
} }
func (handler *RpcResponseHandler) GetRpcContext() RpcContext { func (handler *RpcResponseHandler) GetRpcContext() wshrpc.RpcContext {
return handler.rpcCtx return handler.rpcCtx
} }
func (handler *RpcResponseHandler) NeedsResponse() bool {
return handler.reqId != ""
}
func (handler *RpcResponseHandler) SendMessage(msg string) {
rpcMsg := &RpcMessage{
Command: wshrpc.Command_Message,
Data: wshrpc.CommandMessageData{
Message: msg,
},
}
msgBytes, _ := json.Marshal(rpcMsg) // will never fail
handler.w.OutputCh <- msgBytes
}
func (handler *RpcResponseHandler) SendResponse(data any, done bool) error { func (handler *RpcResponseHandler) SendResponse(data any, done bool) error {
if handler.reqId == "" { if handler.reqId == "" {
return nil // no response expected return nil // no response expected

View File

@ -14,7 +14,11 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
"golang.org/x/term" "golang.org/x/term"
) )
@ -180,7 +184,7 @@ func SetupTerminalRpcClient(handlerFn func(*RpcResponseHandler) bool) (*WshRpc,
messageCh := make(chan []byte, 32) messageCh := make(chan []byte, 32)
outputCh := make(chan []byte, 32) outputCh := make(chan []byte, 32)
ptyBuf := MakePtyBuffer(WaveServerOSCPrefix, os.Stdin, messageCh) ptyBuf := MakePtyBuffer(WaveServerOSCPrefix, os.Stdin, messageCh)
rpcClient := MakeWshRpc(messageCh, outputCh, RpcContext{}, handlerFn) rpcClient := MakeWshRpc(messageCh, outputCh, wshrpc.RpcContext{}, handlerFn)
go func() { go func() {
for msg := range outputCh { for msg := range outputCh {
barr := EncodeWaveOSCBytes(WaveOSC, msg) barr := EncodeWaveOSCBytes(WaveOSC, msg)
@ -189,3 +193,79 @@ func SetupTerminalRpcClient(handlerFn func(*RpcResponseHandler) bool) (*WshRpc,
}() }()
return rpcClient, ptyBuf return rpcClient, ptyBuf
} }
func MakeClientJWTToken(rpcCtx wshrpc.RpcContext, sockName string) (string, error) {
claims := jwt.MapClaims{}
claims["iat"] = time.Now().Unix()
claims["iss"] = "waveterm"
claims["sock"] = sockName
claims["exp"] = time.Now().Add(time.Hour * 24 * 365).Unix()
if rpcCtx.BlockId != "" {
claims["blockid"] = rpcCtx.BlockId
}
if rpcCtx.TabId != "" {
claims["tabid"] = rpcCtx.TabId
}
if rpcCtx.WindowId != "" {
claims["windowid"] = rpcCtx.WindowId
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(wavebase.JwtSecret))
if err != nil {
return "", fmt.Errorf("error signing token: %w", err)
}
return tokenStr, nil
}
func ValidateAndExtractRpcContextFromToken(tokenStr string) (*wshrpc.RpcContext, error) {
parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
token, err := parser.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return []byte(wavebase.JwtSecret), nil
})
if err != nil {
return nil, fmt.Errorf("error parsing token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("error getting claims from token")
}
// validate "exp" claim
if exp, ok := claims["exp"].(float64); ok {
if int64(exp) < time.Now().Unix() {
return nil, fmt.Errorf("token has expired")
}
} else {
return nil, fmt.Errorf("exp claim is missing or invalid")
}
// validate "iss" claim
if iss, ok := claims["iss"].(string); ok {
if iss != "waveterm" {
return nil, fmt.Errorf("unexpected issuer: %s", iss)
}
} else {
return nil, fmt.Errorf("iss claim is missing or invalid")
}
rpcCtx := &wshrpc.RpcContext{}
rpcCtx.BlockId = claims["blockid"].(string)
rpcCtx.TabId = claims["tabid"].(string)
rpcCtx.WindowId = claims["windowid"].(string)
return rpcCtx, nil
}
func ExtractUnverifiedSocketName(tokenStr string) (string, error) {
// this happens on the client who does not have access to the secret key
// we want to read the claims without validating the signature
token, _, err := new(jwt.Parser).ParseUnverified(tokenStr, jwt.MapClaims{})
if err != nil {
return "", fmt.Errorf("error parsing token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", fmt.Errorf("error getting claims from token")
}
sockName, ok := claims["sock"].(string)
if !ok {
return "", fmt.Errorf("sock claim is missing or invalid")
}
return sockName, nil
}

View File

@ -3546,6 +3546,31 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/color-convert@npm:*":
version: 2.0.3
resolution: "@types/color-convert@npm:2.0.3"
dependencies:
"@types/color-name": "npm:*"
checksum: 10c0/a5870547660f426cddd76b54e942703e29c3b43fc26b1ba567e10b9707d144b7d8863e0af7affd9c3391815c06582571f43835c71ede270a6c58949155d18b77
languageName: node
linkType: hard
"@types/color-name@npm:*":
version: 1.1.4
resolution: "@types/color-name@npm:1.1.4"
checksum: 10c0/11a5b67408a53a972fa98e4bbe2b0ff4cb74a3b3abb5f250cb5ec7b055a45aa8e00ddaf39b8327ef683ede9b2ff9b3ee9d25cd708d12b1b6a9aee5e8e6002920
languageName: node
linkType: hard
"@types/color@npm:^3.0.6":
version: 3.0.6
resolution: "@types/color@npm:3.0.6"
dependencies:
"@types/color-convert": "npm:*"
checksum: 10c0/79267eeb67f9d11761aecee36bb1503fb8daa699b9ae7e036fc23a74380e5b130c5c0f6d7adafabba89256e46f36ee4d3e28e0ac7e107e8258550eae7d091acf
languageName: node
linkType: hard
"@types/connect@npm:*": "@types/connect@npm:*":
version: 3.4.38 version: 3.4.38
resolution: "@types/connect@npm:3.4.38" resolution: "@types/connect@npm:3.4.38"
@ -5258,7 +5283,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"color-string@npm:^1.6.0": "color-string@npm:^1.6.0, color-string@npm:^1.9.0":
version: 1.9.1 version: 1.9.1
resolution: "color-string@npm:1.9.1" resolution: "color-string@npm:1.9.1"
dependencies: dependencies:
@ -5278,6 +5303,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"color@npm:^4.2.3":
version: 4.2.3
resolution: "color@npm:4.2.3"
dependencies:
color-convert: "npm:^2.0.1"
color-string: "npm:^1.9.0"
checksum: 10c0/7fbe7cfb811054c808349de19fb380252e5e34e61d7d168ec3353e9e9aacb1802674bddc657682e4e9730c2786592a4de6f8283e7e0d3870b829bb0b7b2f6118
languageName: node
linkType: hard
"colorspace@npm:1.1.x": "colorspace@npm:1.1.x":
version: 1.1.4 version: 1.1.4
resolution: "colorspace@npm:1.1.4" resolution: "colorspace@npm:1.1.4"
@ -11768,6 +11803,7 @@ __metadata:
"@table-nav/core": "npm:^0.0.7" "@table-nav/core": "npm:^0.0.7"
"@table-nav/react": "npm:^0.0.7" "@table-nav/react": "npm:^0.0.7"
"@tanstack/react-table": "npm:^8.19.3" "@tanstack/react-table": "npm:^8.19.3"
"@types/color": "npm:^3.0.6"
"@types/electron": "npm:^1.6.10" "@types/electron": "npm:^1.6.10"
"@types/node": "npm:^20.14.12" "@types/node": "npm:^20.14.12"
"@types/papaparse": "npm:^5" "@types/papaparse": "npm:^5"
@ -11783,6 +11819,7 @@ __metadata:
"@xterm/xterm": "npm:^5.5.0" "@xterm/xterm": "npm:^5.5.0"
base64-js: "npm:^1.5.1" base64-js: "npm:^1.5.1"
clsx: "npm:^2.1.1" clsx: "npm:^2.1.1"
color: "npm:^4.2.3"
dayjs: "npm:^1.11.12" dayjs: "npm:^1.11.12"
electron: "npm:^31.3.0" electron: "npm:^31.3.0"
electron-builder: "npm:^24.13.3" electron-builder: "npm:^24.13.3"