From f87cc42ab933003d0c309af61c7dd72e244b259f Mon Sep 17 00:00:00 2001 From: Cole Lashley Date: Wed, 13 Mar 2024 18:47:16 -0700 Subject: [PATCH 01/15] Slash Commands for keybindings (#441) * first pass of slash commands * added mainview slashcommand * added focus cmd input, added sleep * addressed review comments * addressed feedback * demo idea of changing hide do cmd+m, can remove if we decide we don't like it * added new keybinding for minimize * addressed feedback * added hide label * fix hide (use app.hid(), not window.hide() * fix history keybinding, make mainview command consistent --- assets/default-keybindings.json | 47 ++++++++++++++------- src/electron/emain.ts | 14 ++++++- src/electron/preload.js | 1 + src/models/model.ts | 41 +++++++----------- src/types/custom.d.ts | 1 + src/util/keyutil.ts | 67 ++++++++++++++++++++++++++---- wavesrv/pkg/cmdrunner/cmdrunner.go | 43 +++++++++++++++++++ wavesrv/pkg/sstore/sstore.go | 8 ++-- 8 files changed, 170 insertions(+), 52 deletions(-) diff --git a/assets/default-keybindings.json b/assets/default-keybindings.json index c942ec4c4..fff2891d5 100644 --- a/assets/default-keybindings.json +++ b/assets/default-keybindings.json @@ -3,6 +3,10 @@ "command": "system:toggleDeveloperTools", "keys": ["Cmd:Option:i"] }, + { + "command": "system:hideWindow", + "keys": ["Cmd:m"] + }, { "command": "generic:cancel", "keys": ["Escape"] @@ -32,7 +36,7 @@ "keys": ["PageDown"] }, { - "command": "app:openHistory", + "command": "app:openHistoryView", "keys": ["Cmd:h"] }, { @@ -41,11 +45,13 @@ }, { "command": "app:openConnectionsView", - "keys": [] + "keys": [], + "commandStr": "/mainview connections" }, { "command": "app:openSettingsView", - "keys": [] + "keys": [], + "commandStr": "/mainview clientsettings" }, { "command": "app:newTab", @@ -125,39 +131,48 @@ }, { "command": "app:selectWorkspace-1", - "keys": ["Cmd:Ctrl:1"] + "keys": ["Cmd:Ctrl:1"], + "commandStr": "/session 1" }, { "command": "app:selectWorkspace-2", - "keys": ["Cmd:Ctrl:2"] + "keys": ["Cmd:Ctrl:2"], + "commandStr": "/session 2" }, { "command": "app:selectWorkspace-3", - "keys": ["Cmd:Ctrl:3"] + "keys": ["Cmd:Ctrl:3"], + "commandStr": "/session 3" }, { "command": "app:selectWorkspace-4", - "keys": ["Cmd:Ctrl:4"] + "keys": ["Cmd:Ctrl:4"], + "commandStr": "/session 4" }, { "command": "app:selectWorkspace-5", - "keys": ["Cmd:Ctrl:5"] + "keys": ["Cmd:Ctrl:5"], + "commandStr": "/session 5" }, { "command": "app:selectWorkspace-6", - "keys": ["Cmd:Ctrl:6"] + "keys": ["Cmd:Ctrl:6"], + "commandStr": "/session 6" }, { "command": "app:selectWorkspace-7", - "keys": ["Cmd:Ctrl:7"] + "keys": ["Cmd:Ctrl:7"], + "commandStr": "/session 7" }, { "command": "app:selectWorkspace-8", - "keys": ["Cmd:Ctrl:8"] + "keys": ["Cmd:Ctrl:8"], + "commandStr": "/session 8" }, { "command": "app:selectWorkspace-9", - "keys": ["Cmd:Ctrl:9"] + "keys": ["Cmd:Ctrl:9"], + "commandStr": "/session 9" }, { "command": "app:toggleSidebar", @@ -168,8 +183,9 @@ "keys": ["Cmd:d"] }, { - "command": "app:bookmarkActiveLine", - "keys": ["Cmd:b"] + "command": "app:openBookmarksView", + "keys": ["Cmd:b"], + "commandStr": "/bookmarks:show" }, { "command": "bookmarks:edit", @@ -213,7 +229,8 @@ }, { "command": "cmdinput:openHistory", - "keys": ["Ctrl:r"] + "keys": ["Ctrl:r"], + "commandStr": "/history" }, { "command": "cmdinput:openAIChat", diff --git a/src/electron/emain.ts b/src/electron/emain.ts index 5aff80c44..be4f429f6 100644 --- a/src/electron/emain.ts +++ b/src/electron/emain.ts @@ -263,7 +263,12 @@ const menuTemplate: Electron.MenuItemConstructorOptions[] = [ { type: "separator" }, { role: "services" }, { type: "separator" }, - { role: "hide" }, + { + label: "Hide", + click: () => { + app.hide(); + }, + }, { role: "hideOthers" }, { type: "separator" }, { role: "quit" }, @@ -513,6 +518,13 @@ electron.ipcMain.on("toggle-developer-tools", (event) => { event.returnValue = true; }); +electron.ipcMain.on("hide-window", (event) => { + if (MainWindow != null) { + MainWindow.hide(); + } + event.returnValue = true; +}); + electron.ipcMain.on("get-id", (event) => { event.returnValue = instanceId + ":" + event.processId; }); diff --git a/src/electron/preload.js b/src/electron/preload.js index 14be4eb10..1b5f1a637 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -1,6 +1,7 @@ let { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { + hideWindow: () => ipc.Renderer.send("hide-window"), toggleDeveloperTools: () => ipcRenderer.send("toggle-developer-tools"), getId: () => ipcRenderer.sendSync("get-id"), getPlatform: () => ipcRenderer.sendSync("get-platform"), diff --git a/src/models/model.ts b/src/models/model.ts index 15af1ae58..a1d0069a6 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -143,7 +143,7 @@ class Model { this.runUpdate(message, interactive); }); this.ws.reconnect(); - this.keybindManager = new KeybindManager(); + this.keybindManager = new KeybindManager(this); this.readConfigKeybindings(); this.initSystemKeybindings(); this.initAppKeybindings(); @@ -222,46 +222,31 @@ class Model { getApi().toggleDeveloperTools(); return true; }); + this.keybindManager.registerKeybinding("system", "electron", "system:minimizeWindow", (waveEvent) => { + getApi().hideWindow(); + return true; + }); } initAppKeybindings() { for (let index = 1; index <= 9; index++) { - this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, (waveEvent) => { - this.onSwitchSessionCmd(index); - return true; - }); + this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, null); } - this.keybindManager.registerKeybinding("app", "model", "app:focusCmdInput", (waveEvent) => { - console.log("focus cmd input callback"); this.onFocusCmdInputPressed(); return true; }); - - this.keybindManager.registerKeybinding("app", "model", "app:bookmarkActiveLine", (waveEvent) => { - this.onBookmarkViewPressed(); - return true; - }); - - this.keybindManager.registerKeybinding("app", "model", "app:openHistory", (waveEvent) => { + this.keybindManager.registerKeybinding("app", "model", "app:openBookmarksView", null); + this.keybindManager.registerKeybinding("app", "model", "app:openHistoryView", (waveEvent) => { this.onOpenHistoryPressed(); return true; }); - this.keybindManager.registerKeybinding("app", "model", "app:openTabSearchModal", (waveEvent) => { this.onOpenTabSearchModalPressed(); return true; }); - - this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", (waveEvent) => { - this.onOpenConnectionsViewPressed(); - return true; - }); - - this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", (waveEvent) => { - this.onOpenSettingsViewPressed(); - return true; - }); + this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", null); + this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", null); } static getInstance(): Model { @@ -1023,6 +1008,12 @@ class Model { console.warn("invalid bookmarksview in update:", update.mainview); } break; + case "clientsettings": + this.activeMainView.set("clientsettings"); + break; + case "connections": + this.activeMainView.set("connections"); + break; case "plugins": this.pluginsModel.showPluginsView(); break; diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 20630fdfd..08e424ce7 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -880,6 +880,7 @@ declare global { }; type ElectronApi = { + hideWindow: () => void; toggleDeveloperTools: () => void; getId: () => string; getIsDev: () => boolean; diff --git a/src/util/keyutil.ts b/src/util/keyutil.ts index 705335b9f..4da4fecf1 100644 --- a/src/util/keyutil.ts +++ b/src/util/keyutil.ts @@ -4,7 +4,7 @@ import * as electron from "electron"; import { parse } from "node:path"; import { v4 as uuidv4 } from "uuid"; import defaultKeybindingsFile from "../../assets/default-keybindings.json"; -const defaultKeybindings: KeybindConfig = defaultKeybindingsFile; +const defaultKeybindings: KeybindConfigArray = defaultKeybindingsFile; type KeyPressDecl = { mods: { @@ -24,12 +24,18 @@ const KeyTypeKey = "key"; const KeyTypeCode = "code"; type KeybindCallback = (event: WaveKeyboardEvent) => boolean; -type KeybindConfig = Array<{ command: string; keys: Array }>; +type KeybindConfigArray = Array; +type KeybindConfig = { command: string; keys: Array; commandStr?: string }; + +const Callback = "callback"; +const Command = "command"; type Keybind = { domain: string; keybinding: string; + action: string; callback: KeybindCallback; + commandStr: string; }; const KeybindLevels = ["system", "modal", "app", "pane", "plugin"]; @@ -38,11 +44,12 @@ class KeybindManager { domainCallbacks: Map; levelMap: Map>; levelArray: Array; - keyDescriptionsMap: Map>; - userKeybindings: KeybindConfig; + keyDescriptionsMap: Map; + userKeybindings: KeybindConfigArray; userKeybindingError: OV; + globalModel: any; - constructor() { + constructor(GlobalModel: any) { this.levelMap = new Map(); this.domainCallbacks = new Map(); this.levelArray = KeybindLevels; @@ -53,6 +60,7 @@ class KeybindManager { this.userKeybindingError = mobx.observable.box(null, { name: "keyutil-userKeybindingError", }); + this.globalModel = GlobalModel; this.initKeyDescriptionsMap(); } @@ -63,7 +71,7 @@ class KeybindManager { let newKeyDescriptions = new Map(); for (let index = 0; index < defaultKeybindings.length; index++) { let curKeybind = defaultKeybindings[index]; - newKeyDescriptions.set(curKeybind.command, curKeybind.keys); + newKeyDescriptions.set(curKeybind.command, curKeybind); } let curUserCommand = ""; if (this.userKeybindings != null && this.userKeybindings instanceof Array) { @@ -85,7 +93,15 @@ class KeybindManager { throw new Error("invalid keybind key"); } } - newKeyDescriptions.set(curKeybind.command, curKeybind.keys); + let defaultCmd = this.keyDescriptionsMap.get(curKeybind.command); + if ( + defaultCmd != null && + defaultCmd.commandStr != null && + (curKeybind.commandStr == null || curKeybind.commandStr == "") + ) { + curKeybind.commandStr = this.keyDescriptionsMap.get(curKeybind.command).commandStr; + } + newKeyDescriptions.set(curKeybind.command, curKeybind); } } catch (e) { let userError = `${curUserCommand} is invalid: error: ${e}`; @@ -98,16 +114,48 @@ class KeybindManager { this.keyDescriptionsMap = newKeyDescriptions; } + runSlashCommand(curKeybind: Keybind): boolean { + let curConfigKeybind = this.keyDescriptionsMap.get(curKeybind.keybinding); + if (curConfigKeybind == null || curConfigKeybind.commandStr == null || curKeybind.commandStr == "") { + return false; + } + let commandsList = curConfigKeybind.commandStr.trim().split(";"); + this.runIndividualSlashCommand(commandsList); + return true; + } + + runIndividualSlashCommand(commandsList: Array): boolean { + if (commandsList.length == 0) { + return true; + } + let curCommand = commandsList.shift(); + console.log("running: ", curCommand); + let prtn = this.globalModel.submitRawCommand(curCommand, false, false); + prtn.then((rtn) => { + if (!rtn.success) { + console.log("error running command ", curCommand); + return false; + } + return this.runIndividualSlashCommand(commandsList); + }).catch((error) => { + console.log("caught error running command ", curCommand, ": ", error); + return false; + }); + } + processLevel(nativeEvent: any, event: WaveKeyboardEvent, keybindsArray: Array): boolean { // iterate through keybinds in backwards order for (let index = keybindsArray.length - 1; index >= 0; index--) { let curKeybind = keybindsArray[index]; if (this.checkKeyPressed(event, curKeybind.keybinding)) { let shouldReturn = false; + let shouldRunCommand = true; if (curKeybind.callback != null) { shouldReturn = curKeybind.callback(event); + shouldRunCommand = false; } if (!shouldReturn && this.domainCallbacks.has(curKeybind.domain)) { + shouldRunCommand = false; let curDomainCallback = this.domainCallbacks.get(curKeybind.domain); if (curDomainCallback != null) { shouldReturn = curDomainCallback(event); @@ -115,6 +163,9 @@ class KeybindManager { console.log("domain callback for ", curKeybind.domain, " is null. This should never happen"); } } + if (shouldRunCommand) { + shouldReturn = this.runSlashCommand(curKeybind); + } if (shouldReturn) { nativeEvent.preventDefault(); nativeEvent.stopPropagation(); @@ -269,7 +320,7 @@ class KeybindManager { if (!this.keyDescriptionsMap.has(keyDescription)) { return false; } - let keyPressArray = this.keyDescriptionsMap.get(keyDescription); + let keyPressArray = this.keyDescriptionsMap.get(keyDescription).keys; for (let index = 0; index < keyPressArray.length; index++) { let curKeyPress = keyPressArray[index]; let pressed = checkKeyPressed(event, curKeyPress); diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 5d4bd4711..0d674efb7 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -171,6 +171,9 @@ func init() { registerCmdFn("reset:cwd", ResetCwdCommand) registerCmdFn("signal", SignalCommand) registerCmdFn("sync", SyncCommand) + registerCmdFn("sleep", SleepCommand) + + registerCmdFn("mainview", MainViewCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -3560,6 +3563,46 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return update, nil } +func SleepCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { + sleepTimeLimit := 10000 + if len(pk.Args) < 1 { + return nil, fmt.Errorf("no argument found - usage: /sleep [ms]") + } + sleepArg := pk.Args[0] + sleepArgInt, err := strconv.Atoi(sleepArg) + if err != nil { + return nil, fmt.Errorf("couldn't parse sleep arg: %v", err) + } + if sleepArgInt > sleepTimeLimit { + return nil, fmt.Errorf("sleep arg is too long, max value is %v", sleepTimeLimit) + } + time.Sleep(time.Duration(sleepArgInt) * time.Millisecond) + update := scbus.MakeUpdatePacket() + return update, nil +} + +func MainViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { + if len(pk.Args) < 1 { + return nil, fmt.Errorf("no argument found - usage: /mainview [view]") + } + update := scbus.MakeUpdatePacket() + mainViewArg := pk.Args[0] + if mainViewArg == sstore.MainViewSession { + update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSession}) + } else if mainViewArg == sstore.MainViewConnections { + update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewConnections}) + } else if mainViewArg == sstore.MainViewSettings { + update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSettings}) + } else if mainViewArg == sstore.MainViewHistory { + return nil, fmt.Errorf("use /history instead") + } else if mainViewArg == sstore.MainViewBookmarks { + return nil, fmt.Errorf("use /bookmarks instead") + } else { + return nil, fmt.Errorf("unrecognized main view") + } + return update, nil +} + func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, 0) if err != nil { diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index ba07baa44..294594357 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -71,9 +71,11 @@ const ( ) const ( - MainViewSession = "session" - MainViewBookmarks = "bookmarks" - MainViewHistory = "history" + MainViewSession = "session" + MainViewBookmarks = "bookmarks" + MainViewHistory = "history" + MainViewConnections = "connections" + MainViewSettings = "clientsettings" ) const ( From 0241e47f835b35e6624c9b7b0918970f94dd88f9 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Thu, 14 Mar 2024 09:48:46 +0800 Subject: [PATCH 02/15] fix broken bookmarks view (#446) --- src/app/bookmarks/bookmarks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bookmarks/bookmarks.tsx b/src/app/bookmarks/bookmarks.tsx index e3c21d2ca..9e675ca61 100644 --- a/src/app/bookmarks/bookmarks.tsx +++ b/src/app/bookmarks/bookmarks.tsx @@ -193,7 +193,7 @@ class BookmarksView extends React.Component<{}, {}> { let bookmarks = GlobalModel.bookmarksModel.bookmarks; let bookmark: BookmarkType = null; return ( - +
From 9eb746196432d4e9b0f163c25023ad4d37ab9b05 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Thu, 14 Mar 2024 09:51:16 +0800 Subject: [PATCH 03/15] Close modals when pressing Escape (#433) * remove clearmodals mothed as it's no longer needed * close modals on ECS --- src/app/common/modals/alert.tsx | 2 +- src/models/bookmarks.ts | 2 +- src/models/clientsettingsview.ts | 10 ++++++++ src/models/connectionsview.ts | 10 ++++++++ src/models/historyview.ts | 2 +- src/models/modals.ts | 3 ++- src/models/model.ts | 41 ++++++-------------------------- 7 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/app/common/modals/alert.tsx b/src/app/common/modals/alert.tsx index f128f26d8..714869aec 100644 --- a/src/app/common/modals/alert.tsx +++ b/src/app/common/modals/alert.tsx @@ -44,7 +44,7 @@ class AlertModal extends React.Component<{}, {}> { {message?.message} - + void) { mobx.action(() => { this.store.pop(); })(); + callback && callback(); } } diff --git a/src/models/model.ts b/src/models/model.ts index a1d0069a6..ea0128bcf 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -357,7 +357,6 @@ class Model { cancelAlert(): void { mobx.action(() => { this.alertMessage.set(null); - this.modalsModel.popModal(); })(); if (this.alertPromiseResolver != null) { this.alertPromiseResolver(false); @@ -478,7 +477,7 @@ class Model { if (this.alertMessage.get() != null) { if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); - this.cancelAlert(); + this.modalsModel.popModal(() => this.cancelAlert()); return; } if (checkKeyPressed(waveEvent, "Enter")) { @@ -488,6 +487,10 @@ class Model { } return; } + if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) { + this.modalsModel.popModal(); + return; + } if (this.activeMainView.get() == "bookmarks") { this.bookmarksModel.handleDocKeyDown(e); } @@ -495,10 +498,10 @@ class Model { this.historyViewModel.handleDocKeyDown(e); } if (this.activeMainView.get() == "connections") { - this.historyViewModel.handleDocKeyDown(e); + this.connectionViewModel.handleDocKeyDown(e); } if (this.activeMainView.get() == "clientsettings") { - this.historyViewModel.handleDocKeyDown(e); + this.clientSettingsViewModel.handleDocKeyDown(e); } else { if (checkKeyPressed(waveEvent, "Escape")) { e.preventDefault(); @@ -506,9 +509,6 @@ class Model { this.showSessionView(); return; } - if (this.clearModals()) { - return; - } const inputModel = this.inputModel; inputModel.toggleInfoMsg(); if (inputModel.inputMode.get() != null) { @@ -628,33 +628,6 @@ class Model { return screen.getTermWrap(line.lineid); } - clearModals(): boolean { - let didSomething = false; - mobx.action(() => { - if (this.screenSettingsModal.get()) { - this.screenSettingsModal.set(null); - didSomething = true; - } - if (this.sessionSettingsModal.get()) { - this.sessionSettingsModal.set(null); - didSomething = true; - } - if (this.screenSettingsModal.get()) { - this.screenSettingsModal.set(null); - didSomething = true; - } - if (this.clientSettingsModal.get()) { - this.clientSettingsModal.set(false); - didSomething = true; - } - if (this.lineSettingsModal.get()) { - this.lineSettingsModal.set(null); - didSomething = true; - } - })(); - return didSomething; - } - restartWaveSrv(): void { getApi().restartWaveSrv(); } From 550d9c97161a9b1dde1a81536fe306487f89481d Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Wed, 13 Mar 2024 18:52:02 -0700 Subject: [PATCH 04/15] fix: split the apple locale on the region for LANG (#447) We use AppleLocale to get the desired language when running on macos. Unfortunately, if a region is set, the locale is not equivalent to the LANG environment variable. It appends an extra region field that we did not previously filter out. This change ensures that it will be filtered out when present. --- wavesrv/pkg/scbase/scbase.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wavesrv/pkg/scbase/scbase.go b/wavesrv/pkg/scbase/scbase.go index 9ba375991..795f02833 100644 --- a/wavesrv/pkg/scbase/scbase.go +++ b/wavesrv/pkg/scbase/scbase.go @@ -393,7 +393,9 @@ func determineLang() string { log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err) return "" } - return strings.TrimSpace(string(out)) + ".UTF-8" + strOut := string(out) + truncOut := strings.Split(strOut, "@")[0] + return strings.TrimSpace(truncOut) + ".UTF-8" } else { // this is specifically to get the wavesrv LANG so waveshell // on a remote uses the same LANG From bff51c851afd0614e27599647df1ddfeedfbf490 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Wed, 13 Mar 2024 18:52:41 -0700 Subject: [PATCH 05/15] sync command to synchronize shell state with wave prompt (#444) * remove two unused packet types, remove unused detatched command code * CmdStart is invalid in this command loop * slight refactor, remove closure funcs * pass rct through to 'handle' funcs * deal with rct (running command), update handler funcs accordingly * update for runningcmdtype to be a pointer in the map (for updates) * lots of changes related to ephemeral commands (for sync), checkpoint * fix ephemeral setting * sync shell state when you switch to a new tab --- src/models/commandrunner.ts | 4 + src/models/model.ts | 3 + waveshell/main-waveshell.go | 10 +- waveshell/pkg/cmdtail/cmdtail.go | 473 ----------------------------- waveshell/pkg/packet/packet.go | 73 +---- waveshell/pkg/server/server.go | 2 +- waveshell/pkg/shexec/shexec.go | 100 ------ wavesrv/pkg/cmdrunner/cmdrunner.go | 28 +- wavesrv/pkg/remote/remote.go | 311 ++++++++++--------- wavesrv/pkg/sstore/dbops.go | 29 +- 10 files changed, 194 insertions(+), 839 deletions(-) delete mode 100644 waveshell/pkg/cmdtail/cmdtail.go diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts index 98150ccee..5b57a0c4d 100644 --- a/src/models/commandrunner.ts +++ b/src/models/commandrunner.ts @@ -304,6 +304,10 @@ class CommandRunner { GlobalModel.clientSettingsViewModel.showClientSettingsView(); } + syncShellState() { + GlobalModel.submitCommand("sync", null, null, { nohist: "1" }, false); + } + historyView(params: HistorySearchParams) { let kwargs = { nohist: "1" }; kwargs["offset"] = String(params.offset); diff --git a/src/models/model.ts b/src/models/model.ts index ea0128bcf..ad65aa55f 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -1050,6 +1050,9 @@ class Model { this.activeMainView.set("session"); this.deactivateScreenLines(); this.ws.watchScreen(newActiveSessionId, newActiveScreenId); + setTimeout(() => { + GlobalCommandRunner.syncShellState(); + }, 100); } } else { console.warn("unknown update", genUpdate); diff --git a/waveshell/main-waveshell.go b/waveshell/main-waveshell.go index 77f1511d0..d032b194a 100644 --- a/waveshell/main-waveshell.go +++ b/waveshell/main-waveshell.go @@ -61,15 +61,7 @@ func handleSingle() { sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("run packets from server must have a CK: %v", err)) } if runPacket.Detached { - cmd, startPk, err := shexec.RunCommandDetached(runPacket, sender) - if err != nil { - sender.SendErrorResponse(runPacket.ReqId, err) - return - } - sender.SendPacket(startPk) - sender.Close() - sender.WaitForDone() - cmd.DetachedWait(startPk) + sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("detached mode not supported")) return } else { shexec.IgnoreSigPipe() diff --git a/waveshell/pkg/cmdtail/cmdtail.go b/waveshell/pkg/cmdtail/cmdtail.go deleted file mode 100644 index fed754ad4..000000000 --- a/waveshell/pkg/cmdtail/cmdtail.go +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright 2023, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -package cmdtail - -import ( - "encoding/base64" - "fmt" - "io" - "os" - "regexp" - "sync" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/wavetermdev/waveterm/waveshell/pkg/base" - "github.com/wavetermdev/waveterm/waveshell/pkg/packet" -) - -const MaxDataBytes = 4096 -const FileTypePty = "ptyout" -const FileTypeRun = "runout" - -type Tailer struct { - Lock *sync.Mutex - WatchList map[base.CommandKey]CmdWatchEntry - Watcher *fsnotify.Watcher - Sender *packet.PacketSender - Gen FileNameGenerator - Sessions map[string]bool -} - -type TailPos struct { - ReqId string - Running bool // an active tailer sending data - TailPtyPos int64 - TailRunPos int64 - Follow bool -} - -type CmdWatchEntry struct { - CmdKey base.CommandKey - FilePtyLen int64 - FileRunLen int64 - Tails []TailPos - Done bool -} - -type FileNameGenerator interface { - PtyOutFile(ck base.CommandKey) string - RunOutFile(ck base.CommandKey) string - SessionDir(sessionId string) string -} - -func (w CmdWatchEntry) getTailPos(reqId string) (TailPos, bool) { - for _, pos := range w.Tails { - if pos.ReqId == reqId { - return pos, true - } - } - return TailPos{}, false -} - -func (w *CmdWatchEntry) updateTailPos(reqId string, newPos TailPos) { - for idx, pos := range w.Tails { - if pos.ReqId == reqId { - w.Tails[idx] = newPos - return - } - } - w.Tails = append(w.Tails, newPos) -} - -func (w *CmdWatchEntry) removeTailPos(reqId string) { - var newTails []TailPos - for _, pos := range w.Tails { - if pos.ReqId == reqId { - continue - } - newTails = append(newTails, pos) - } - w.Tails = newTails -} - -func (pos TailPos) IsCurrent(entry CmdWatchEntry) bool { - return pos.TailPtyPos >= entry.FilePtyLen && pos.TailRunPos >= entry.FileRunLen -} - -func (t *Tailer) updateTailPos_nolock(cmdKey base.CommandKey, reqId string, pos TailPos) { - entry, found := t.WatchList[cmdKey] - if !found { - return - } - entry.updateTailPos(reqId, pos) - t.WatchList[cmdKey] = entry -} - -func (t *Tailer) removeTailPos(cmdKey base.CommandKey, reqId string) { - t.Lock.Lock() - defer t.Lock.Unlock() - t.removeTailPos_nolock(cmdKey, reqId) -} - -func (t *Tailer) removeTailPos_nolock(cmdKey base.CommandKey, reqId string) { - entry, found := t.WatchList[cmdKey] - if !found { - return - } - entry.removeTailPos(reqId) - t.WatchList[cmdKey] = entry - if len(entry.Tails) == 0 { - t.removeWatch_nolock(cmdKey) - } -} - -func (t *Tailer) removeWatch_nolock(cmdKey base.CommandKey) { - // delete from watchlist, remove watches - delete(t.WatchList, cmdKey) - t.Watcher.Remove(t.Gen.PtyOutFile(cmdKey)) - t.Watcher.Remove(t.Gen.RunOutFile(cmdKey)) -} - -func (t *Tailer) getEntryAndPos_nolock(cmdKey base.CommandKey, reqId string) (CmdWatchEntry, TailPos, bool) { - entry, found := t.WatchList[cmdKey] - if !found { - return CmdWatchEntry{}, TailPos{}, false - } - pos, found := entry.getTailPos(reqId) - if !found { - return CmdWatchEntry{}, TailPos{}, false - } - return entry, pos, true -} - -func (t *Tailer) addSessionWatcher(sessionId string) error { - t.Lock.Lock() - defer t.Lock.Unlock() - - if t.Sessions[sessionId] { - return nil - } - sdir := t.Gen.SessionDir(sessionId) - err := t.Watcher.Add(sdir) - if err != nil { - return err - } - t.Sessions[sessionId] = true - return nil -} - -func (t *Tailer) removeSessionWatcher(sessionId string) { - t.Lock.Lock() - defer t.Lock.Unlock() - - if !t.Sessions[sessionId] { - return - } - sdir := t.Gen.SessionDir(sessionId) - t.Watcher.Remove(sdir) -} - -func MakeTailer(sender *packet.PacketSender, gen FileNameGenerator) (*Tailer, error) { - rtn := &Tailer{ - Lock: &sync.Mutex{}, - WatchList: make(map[base.CommandKey]CmdWatchEntry), - Sessions: make(map[string]bool), - Sender: sender, - Gen: gen, - } - var err error - rtn.Watcher, err = fsnotify.NewWatcher() - if err != nil { - return nil, err - } - return rtn, nil -} - -func (t *Tailer) readDataFromFile(fileName string, pos int64, maxBytes int) ([]byte, error) { - fd, err := os.Open(fileName) - defer fd.Close() - if err != nil { - return nil, err - } - buf := make([]byte, maxBytes) - nr, err := fd.ReadAt(buf, pos) - if err != nil && err != io.EOF { // ignore EOF error - return nil, err - } - return buf[0:nr], nil -} - -func (t *Tailer) makeCmdDataPacket(entry CmdWatchEntry, pos TailPos) (*packet.CmdDataPacketType, error) { - dataPacket := packet.MakeCmdDataPacket(pos.ReqId) - dataPacket.CK = entry.CmdKey - dataPacket.PtyPos = pos.TailPtyPos - dataPacket.RunPos = pos.TailRunPos - if entry.FilePtyLen > pos.TailPtyPos { - ptyData, err := t.readDataFromFile(t.Gen.PtyOutFile(entry.CmdKey), pos.TailPtyPos, MaxDataBytes) - if err != nil { - return nil, err - } - dataPacket.PtyData64 = base64.StdEncoding.EncodeToString(ptyData) - dataPacket.PtyDataLen = len(ptyData) - } - if entry.FileRunLen > pos.TailRunPos { - runData, err := t.readDataFromFile(t.Gen.RunOutFile(entry.CmdKey), pos.TailRunPos, MaxDataBytes) - if err != nil { - return nil, err - } - dataPacket.RunData64 = base64.StdEncoding.EncodeToString(runData) - dataPacket.RunDataLen = len(runData) - } - return dataPacket, nil -} - -// returns (data-packet, keepRunning) -func (t *Tailer) runSingleDataTransfer(key base.CommandKey, reqId string) (*packet.CmdDataPacketType, bool, error) { - t.Lock.Lock() - entry, pos, foundPos := t.getEntryAndPos_nolock(key, reqId) - t.Lock.Unlock() - if !foundPos { - return nil, false, nil - } - dataPacket, dataErr := t.makeCmdDataPacket(entry, pos) - - t.Lock.Lock() - defer t.Lock.Unlock() - entry, pos, foundPos = t.getEntryAndPos_nolock(key, reqId) - if !foundPos { - return nil, false, nil - } - // pos was updated between first and second get, throw out data-packet and re-run - if pos.TailPtyPos != dataPacket.PtyPos || pos.TailRunPos != dataPacket.RunPos { - return nil, true, nil - } - if dataErr != nil { - // error, so return error packet, and stop running - pos.Running = false - t.updateTailPos_nolock(key, reqId, pos) - return nil, false, dataErr - } - pos.TailPtyPos += int64(dataPacket.PtyDataLen) - pos.TailRunPos += int64(dataPacket.RunDataLen) - if pos.IsCurrent(entry) { - // we caught up, tail position equals file length - pos.Running = false - } - t.updateTailPos_nolock(key, reqId, pos) - return dataPacket, pos.Running, nil -} - -// returns (removed) -func (t *Tailer) checkRemove(cmdKey base.CommandKey, reqId string) bool { - t.Lock.Lock() - defer t.Lock.Unlock() - entry, pos, foundPos := t.getEntryAndPos_nolock(cmdKey, reqId) - if !foundPos { - return false - } - if !pos.IsCurrent(entry) { - return false - } - if !pos.Follow || entry.Done { - t.removeTailPos_nolock(cmdKey, reqId) - return true - } - return false -} - -func (t *Tailer) RunDataTransfer(key base.CommandKey, reqId string) { - for { - dataPacket, keepRunning, err := t.runSingleDataTransfer(key, reqId) - if dataPacket != nil { - t.Sender.SendPacket(dataPacket) - } - if err != nil { - t.removeTailPos(key, reqId) - t.Sender.SendErrorResponse(reqId, err) - break - } - if !keepRunning { - removed := t.checkRemove(key, reqId) - if removed { - t.Sender.SendResponse(reqId, true) - } - break - } - time.Sleep(10 * time.Millisecond) - } -} - -func (t *Tailer) tryStartRun_nolock(entry CmdWatchEntry, pos TailPos) { - if pos.Running { - return - } - if pos.IsCurrent(entry) { - return - } - pos.Running = true - t.updateTailPos_nolock(entry.CmdKey, pos.ReqId, pos) - go t.RunDataTransfer(entry.CmdKey, pos.ReqId) -} - -var updateFileRe = regexp.MustCompile("/([a-z0-9-]+)/([a-z0-9-]+)\\.(ptyout|runout)$") - -func (t *Tailer) updateFile(relFileName string) { - m := updateFileRe.FindStringSubmatch(relFileName) - if m == nil { - return - } - finfo, err := os.Stat(relFileName) - if err != nil { - t.Sender.SendPacket(packet.FmtMessagePacket("error trying to stat file '%s': %v", relFileName, err)) - return - } - cmdKey := base.MakeCommandKey(m[1], m[2]) - t.Lock.Lock() - defer t.Lock.Unlock() - entry, foundEntry := t.WatchList[cmdKey] - if !foundEntry { - return - } - fileType := m[3] - if fileType == FileTypePty { - entry.FilePtyLen = finfo.Size() - } else if fileType == FileTypeRun { - entry.FileRunLen = finfo.Size() - } - t.WatchList[cmdKey] = entry - for _, pos := range entry.Tails { - t.tryStartRun_nolock(entry, pos) - } -} - -func (t *Tailer) Run() { - for { - select { - case event, ok := <-t.Watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write == fsnotify.Write { - t.updateFile(event.Name) - } - - case err, ok := <-t.Watcher.Errors: - if !ok { - return - } - // what to do with this error? just send a message - t.Sender.SendPacket(packet.FmtMessagePacket("error in tailer: %v", err)) - } - } -} - -func (t *Tailer) Close() error { - return t.Watcher.Close() -} - -func max(v1 int64, v2 int64) int64 { - if v1 > v2 { - return v1 - } - return v2 -} - -func (entry *CmdWatchEntry) fillFilePos(gen FileNameGenerator) { - ptyInfo, _ := os.Stat(gen.PtyOutFile(entry.CmdKey)) - if ptyInfo != nil { - entry.FilePtyLen = ptyInfo.Size() - } - runoutInfo, _ := os.Stat(gen.RunOutFile(entry.CmdKey)) - if runoutInfo != nil { - entry.FileRunLen = runoutInfo.Size() - } -} - -func (t *Tailer) KeyDone(key base.CommandKey) { - t.Lock.Lock() - defer t.Lock.Unlock() - entry, foundEntry := t.WatchList[key] - if !foundEntry { - return - } - entry.Done = true - var newTails []TailPos - for _, pos := range entry.Tails { - if pos.IsCurrent(entry) { - continue - } - newTails = append(newTails, pos) - } - entry.Tails = newTails - t.WatchList[key] = entry - if len(entry.Tails) == 0 { - t.removeWatch_nolock(key) - } - t.WatchList[key] = entry -} - -func (t *Tailer) RemoveWatch(pk *packet.UntailCmdPacketType) { - t.Lock.Lock() - defer t.Lock.Unlock() - t.removeTailPos_nolock(pk.CK, pk.ReqId) -} - -func (t *Tailer) AddFileWatches_nolock(key base.CommandKey, ptyOnly bool) error { - ptyName := t.Gen.PtyOutFile(key) - runName := t.Gen.RunOutFile(key) - fmt.Printf("WATCH> add %s\n", ptyName) - err := t.Watcher.Add(ptyName) - if err != nil { - return err - } - if ptyOnly { - return nil - } - err = t.Watcher.Add(runName) - if err != nil { - t.Watcher.Remove(ptyName) // best effort clean up - return err - } - return nil -} - -// returns (up-to-date/done, error) -func (t *Tailer) AddWatch(getPacket *packet.GetCmdPacketType) (bool, error) { - if err := getPacket.CK.Validate("getcmd"); err != nil { - return false, err - } - if getPacket.ReqId == "" { - return false, fmt.Errorf("getcmd, no reqid specified") - } - t.Lock.Lock() - defer t.Lock.Unlock() - key := getPacket.CK - entry, foundEntry := t.WatchList[key] - if !foundEntry { - // initialize entry, add watches - entry = CmdWatchEntry{CmdKey: key} - entry.fillFilePos(t.Gen) - } - pos, foundPos := entry.getTailPos(getPacket.ReqId) - if !foundPos { - // initialize a new tailpos - pos = TailPos{ReqId: getPacket.ReqId} - } - // update tailpos with new values from getpacket - pos.TailPtyPos = getPacket.PtyPos - pos.TailRunPos = getPacket.RunPos - pos.Follow = getPacket.Tail - // convert negative pos to positive - if pos.TailPtyPos < 0 { - pos.TailPtyPos = max(0, entry.FilePtyLen+pos.TailPtyPos) // + because negative - } - if pos.TailRunPos < 0 { - pos.TailRunPos = max(0, entry.FileRunLen+pos.TailRunPos) // + because negative - } - entry.updateTailPos(pos.ReqId, pos) - if !pos.Follow && pos.IsCurrent(entry) { - // don't add to t.WatchList, don't t.AddFileWatches_nolock, send rpc response - return true, nil - } - if !foundEntry { - err := t.AddFileWatches_nolock(key, getPacket.PtyOnly) - if err != nil { - return false, err - } - } - t.WatchList[key] = entry - t.tryStartRun_nolock(entry, pos) - return false, nil -} diff --git a/waveshell/pkg/packet/packet.go b/waveshell/pkg/packet/packet.go index 077e9e884..ff89de4d4 100644 --- a/waveshell/pkg/packet/packet.go +++ b/waveshell/pkg/packet/packet.go @@ -43,12 +43,10 @@ const ( DataEndPacketStr = "dataend" ResponsePacketStr = "resp" // rpc-response DonePacketStr = "done" - CmdErrorPacketStr = "cmderror" // command MessagePacketStr = "message" GetCmdPacketStr = "getcmd" // rpc UntailCmdPacketStr = "untailcmd" // rpc CdPacketStr = "cd" // rpc - CmdDataPacketStr = "cmddata" // rpc-response RawPacketStr = "raw" SpecialInputPacketStr = "sinput" // command CompGenPacketStr = "compgen" // rpc @@ -90,7 +88,6 @@ func init() { TypeStrToFactory[PingPacketStr] = reflect.TypeOf(PingPacketType{}) TypeStrToFactory[ResponsePacketStr] = reflect.TypeOf(ResponsePacketType{}) TypeStrToFactory[DonePacketStr] = reflect.TypeOf(DonePacketType{}) - TypeStrToFactory[CmdErrorPacketStr] = reflect.TypeOf(CmdErrorPacketType{}) TypeStrToFactory[MessagePacketStr] = reflect.TypeOf(MessagePacketType{}) TypeStrToFactory[CmdStartPacketStr] = reflect.TypeOf(CmdStartPacketType{}) TypeStrToFactory[CmdDonePacketStr] = reflect.TypeOf(CmdDonePacketType{}) @@ -98,7 +95,6 @@ func init() { TypeStrToFactory[UntailCmdPacketStr] = reflect.TypeOf(UntailCmdPacketType{}) TypeStrToFactory[InitPacketStr] = reflect.TypeOf(InitPacketType{}) TypeStrToFactory[CdPacketStr] = reflect.TypeOf(CdPacketType{}) - TypeStrToFactory[CmdDataPacketStr] = reflect.TypeOf(CmdDataPacketType{}) TypeStrToFactory[RawPacketStr] = reflect.TypeOf(RawPacketType{}) TypeStrToFactory[SpecialInputPacketStr] = reflect.TypeOf(SpecialInputPacketType{}) TypeStrToFactory[DataPacketStr] = reflect.TypeOf(DataPacketType{}) @@ -128,7 +124,6 @@ func init() { var _ RpcResponsePacketType = (*CmdStartPacketType)(nil) var _ RpcResponsePacketType = (*ResponsePacketType)(nil) - var _ RpcResponsePacketType = (*CmdDataPacketType)(nil) var _ RpcResponsePacketType = (*StreamFileResponseType)(nil) var _ RpcResponsePacketType = (*FileDataPacketType)(nil) var _ RpcResponsePacketType = (*WriteFileReadyPacketType)(nil) @@ -155,36 +150,6 @@ func MakePacket(packetType string) (PacketType, error) { return rtn.Interface().(PacketType), nil } -type CmdDataPacketType struct { - Type string `json:"type"` - RespId string `json:"respid"` - CK base.CommandKey `json:"ck"` - PtyPos int64 `json:"ptypos"` - PtyLen int64 `json:"ptylen"` - RunPos int64 `json:"runpos"` - RunLen int64 `json:"runlen"` - PtyData64 string `json:"ptydata64"` - PtyDataLen int `json:"ptydatalen"` - RunData64 string `json:"rundata64"` - RunDataLen int `json:"rundatalen"` -} - -func (*CmdDataPacketType) GetType() string { - return CmdDataPacketStr -} - -func (p *CmdDataPacketType) GetResponseId() string { - return p.RespId -} - -func (*CmdDataPacketType) GetResponseDone() bool { - return false -} - -func MakeCmdDataPacket(reqId string) *CmdDataPacketType { - return &CmdDataPacketType{Type: CmdDataPacketStr, RespId: reqId} -} - type PingPacketType struct { Type string `json:"type"` } @@ -830,28 +795,6 @@ type BarePacketType struct { Type string `json:"type"` } -type CmdErrorPacketType struct { - Type string `json:"type"` - CK base.CommandKey `json:"ck"` - Error string `json:"error"` -} - -func (*CmdErrorPacketType) GetType() string { - return CmdErrorPacketStr -} - -func (p *CmdErrorPacketType) GetCK() base.CommandKey { - return p.CK -} - -func (p *CmdErrorPacketType) String() string { - return fmt.Sprintf("error[%s]", p.Error) -} - -func MakeCmdErrorPacket(ck base.CommandKey, err error) *CmdErrorPacketType { - return &CmdErrorPacketType{Type: CmdErrorPacketStr, CK: ck, Error: err.Error()} -} - type WriteFilePacketType struct { Type string `json:"type"` ReqId string `json:"reqid"` @@ -1074,10 +1017,6 @@ func SendPacket(w io.Writer, packet PacketType) error { return nil } -func SendCmdError(w io.Writer, ck base.CommandKey, err error) error { - return SendPacket(w, MakeCmdErrorPacket(ck, err)) -} - type PacketSender struct { Lock *sync.Mutex SendCh chan PacketType @@ -1197,10 +1136,6 @@ func (sender *PacketSender) SendPacket(pk PacketType) error { return nil } -func (sender *PacketSender) SendCmdError(ck base.CommandKey, err error) error { - return sender.SendPacket(MakeCmdErrorPacket(ck, err)) -} - func (sender *PacketSender) SendErrorResponse(reqId string, err error) error { pk := MakeErrorResponsePacket(reqId, err) return sender.SendPacket(pk) @@ -1222,17 +1157,13 @@ type UnknownPacketReporter interface { type DefaultUPR struct{} func (DefaultUPR) UnknownPacket(pk PacketType) { - if pk.GetType() == CmdErrorPacketStr { - errPacket := pk.(*CmdErrorPacketType) - // at this point, just send the error packet to stderr rather than try to do something special - fmt.Fprintf(os.Stderr, "[error] %s\n", errPacket.Error) - } else if pk.GetType() == RawPacketStr { + if pk.GetType() == RawPacketStr { rawPacket := pk.(*RawPacketType) fmt.Fprintf(os.Stderr, "%s\n", rawPacket.Data) } else if pk.GetType() == CmdStartPacketStr { return // do nothing } else { - fmt.Fprintf(os.Stderr, "[error] invalid packet received '%s'", AsExtType(pk)) + wlog.Logf("[upr] invalid packet received '%s'", AsExtType(pk)) } } diff --git a/waveshell/pkg/server/server.go b/waveshell/pkg/server/server.go index d3729eb16..9fc098ce0 100644 --- a/waveshell/pkg/server/server.go +++ b/waveshell/pkg/server/server.go @@ -151,7 +151,7 @@ func (m *MServer) ProcessCommandPacket(pk packet.CommandPacketType) { cproc := m.ClientMap[ck] m.Lock.Unlock() if cproc == nil { - m.Sender.SendCmdError(ck, fmt.Errorf("no client proc for ck '%s', pk=%s", ck, packet.AsString(pk))) + wlog.Logf("no client proc for ck %q, pk=%s", ck, packet.AsString(pk)) return } cproc.Input.SendPacket(pk) diff --git a/waveshell/pkg/shexec/shexec.go b/waveshell/pkg/shexec/shexec.go index 93446f5d5..817692bbe 100644 --- a/waveshell/pkg/shexec/shexec.go +++ b/waveshell/pkg/shexec/shexec.go @@ -1069,106 +1069,6 @@ func copyToCirFile(dest *cirfile.File, src io.Reader) error { } } -func (cmd *ShExecType) DetachedWait(startPacket *packet.CmdStartPacketType) { - // after Start(), any output/errors must go to DetachedOutput - // close stdin, redirect stdout/stderr to /dev/null, but wait for cmdstart packet to get sent - cmd.DetachedOutput.SendPacket(startPacket) - err := os.Stdin.Close() - if err != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot close stdin: %w", err)) - } - err = unix.Dup2(int(cmd.RunnerOutFd.Fd()), int(os.Stdout.Fd())) - if err != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot dup2 stdin to runout: %w", err)) - } - err = unix.Dup2(int(cmd.RunnerOutFd.Fd()), int(os.Stderr.Fd())) - if err != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot dup2 stdin to runout: %w", err)) - } - ptyOutFile, err := cirfile.CreateCirFile(cmd.FileNames.PtyOutFile, cmd.MaxPtySize) - if err != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("cannot open ptyout file '%s': %w", cmd.FileNames.PtyOutFile, err)) - // don't return (command is already running) - } - ptyCopyDone := make(chan bool) - go func() { - // copy pty output to .ptyout file - defer close(ptyCopyDone) - defer ptyOutFile.Close() - copyErr := copyToCirFile(ptyOutFile, cmd.CmdPty) - if copyErr != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("copying pty output to ptyout file: %w", copyErr)) - } - }() - go func() { - // copy .stdin fifo contents to pty input - copyFifoErr := MakeAndCopyStdinFifo(cmd.CmdPty, cmd.FileNames.StdinFifo) - if copyFifoErr != nil { - cmd.DetachedOutput.SendCmdError(cmd.CK, fmt.Errorf("reading from stdin fifo: %w", copyFifoErr)) - } - }() - donePacket := cmd.WaitForCommand() - cmd.DetachedOutput.SendPacket(donePacket) - <-ptyCopyDone - cmd.Close() -} - -func RunCommandDetached(pk *packet.RunPacketType, sender *packet.PacketSender) (*ShExecType, *packet.CmdStartPacketType, error) { - sapi, err := shellapi.MakeShellApi(pk.ShellType) - if err != nil { - return nil, nil, err - } - fileNames, err := base.GetCommandFileNames(pk.CK) - if err != nil { - return nil, nil, err - } - runOutInfo, err := os.Stat(fileNames.RunnerOutFile) - if err == nil { // non-nil error will be caught by regular OpenFile below - // must have size 0 - if runOutInfo.Size() != 0 { - return nil, nil, fmt.Errorf("cmdkey '%s' was already used (runout len=%d)", pk.CK, runOutInfo.Size()) - } - } - cmdPty, cmdTty, err := pty.Open() - if err != nil { - return nil, nil, fmt.Errorf("opening new pty: %w", err) - } - pty.Setsize(cmdPty, GetWinsize(pk)) - defer func() { - cmdTty.Close() - }() - cmd := MakeShExec(pk.CK, nil, sapi) - cmd.FileNames = fileNames - cmd.CmdPty = cmdPty - cmd.Detached = true - cmd.MaxPtySize = DefaultMaxPtySize - if pk.TermOpts != nil && pk.TermOpts.MaxPtySize > 0 { - cmd.MaxPtySize = base.BoundInt64(pk.TermOpts.MaxPtySize, MinMaxPtySize, MaxMaxPtySize) - } - cmd.RunnerOutFd, err = os.OpenFile(fileNames.RunnerOutFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) - if err != nil { - return nil, nil, fmt.Errorf("cannot open runout file '%s': %w", fileNames.RunnerOutFile, err) - } - cmd.DetachedOutput = packet.MakePacketSender(cmd.RunnerOutFd, nil) - ecmd, err := MakeDetachedExecCmd(pk, cmdTty) - if err != nil { - return nil, nil, err - } - cmd.Cmd = ecmd - SetupSignalsForDetach() - err = ecmd.Start() - if err != nil { - return nil, nil, fmt.Errorf("starting command: %w", err) - } - for _, fd := range ecmd.ExtraFiles { - if fd != cmdTty { - fd.Close() - } - } - startPacket := cmd.MakeCmdStartPacket(pk.ReqId) - return cmd, startPacket, nil -} - func GetExitCode(err error) int { if err == nil { return 0 diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 0d674efb7..2d66c5ae2 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -496,7 +496,7 @@ func getEvalDepth(ctx context.Context) int { func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { - return nil, fmt.Errorf("/run error: %w", err) + return nil, fmt.Errorf("/sync error: %w", err) } runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() @@ -513,22 +513,21 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U SessionId: ids.SessionId, ScreenId: ids.ScreenId, RemotePtr: ids.Remote.RemotePtr, + Ephemeral: true, } - cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket) + _, callback, err := remote.RunCommand(ctx, rcOpts, runPacket) if callback != nil { defer callback() } if err != nil { return nil, err } - cmd.RawCmdStr = pk.GetRawStr() - update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal", nil) - if err != nil { - return nil, err - } - update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive)) - scbus.MainUpdateBus.DoScreenUpdate(ids.ScreenId, update) - return nil, nil + update := scbus.MakeUpdatePacket() + update.AddUpdate(sstore.InfoMsgType{ + InfoMsg: "syncing state", + TimeoutMs: 2000, + }) + return update, nil } func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) { @@ -1178,7 +1177,8 @@ func deferWriteCmdStatus(ctx context.Context, cmd *sstore.CmdType, startTime tim donePk.Ts = time.Now().UnixMilli() donePk.ExitCode = exitCode donePk.DurationMs = duration.Milliseconds() - update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus) + update := scbus.MakeUpdatePacket() + err := sstore.UpdateCmdDoneInfo(context.Background(), update, ck, donePk, cmdStatus) if err != nil { // nothing to do log.Printf("error updating cmddoneinfo (in openai): %v\n", err) @@ -2554,7 +2554,8 @@ func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt donePk.Ts = time.Now().UnixMilli() donePk.ExitCode = exitCode donePk.DurationMs = duration.Milliseconds() - update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus) + update := scbus.MakeUpdatePacket() + err := sstore.UpdateCmdDoneInfo(context.Background(), update, ck, donePk, cmdStatus) if err != nil { // nothing to do log.Printf("error updating cmddoneinfo (in openai): %v\n", err) @@ -2713,7 +2714,8 @@ func doOpenAIStreamCompletion(cmd *sstore.CmdType, clientId string, opts *sstore donePk.Ts = time.Now().UnixMilli() donePk.ExitCode = exitCode donePk.DurationMs = duration.Milliseconds() - update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus) + update := scbus.MakeUpdatePacket() + err := sstore.UpdateCmdDoneInfo(context.Background(), update, ck, donePk, cmdStatus) if err != nil { // nothing to do log.Printf("error updating cmddoneinfo (in openai): %v\n", err) diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index 07f61dec9..3304f3412 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -18,6 +18,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -160,17 +161,20 @@ type MShellProc struct { InstallCancelFn context.CancelFunc InstallErr error - RunningCmds map[base.CommandKey]RunCmdType + RunningCmds map[base.CommandKey]*RunCmdType PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name] launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized Client *ssh.Client } type RunCmdType struct { - SessionId string - ScreenId string - RemotePtr sstore.RemotePtrType - RunPacket *packet.RunPacketType + CK base.CommandKey + SessionId string + ScreenId string + RemotePtr sstore.RemotePtrType + RunPacket *packet.RunPacketType + Ephemeral bool + EphCancled atomic.Bool // only for Ephemeral commands, if true, then the command result should be discarded } type RemoteRuntimeState = sstore.RemoteRuntimeState @@ -704,7 +708,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { Status: StatusDisconnected, PtyBuffer: buf, InstallStatus: StatusDisconnected, - RunningCmds: make(map[base.CommandKey]RunCmdType), + RunningCmds: make(map[base.CommandKey]*RunCmdType), PendingStateCmds: make(map[pendingStateKey]base.CommandKey), StateMap: server.MakeShellStateMap(), launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized @@ -1928,6 +1932,8 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { } // returns (ok, currentPSC) +// if ok is true, currentPSC will be nil +// if ok is false, currentPSC will be the existing pending state command (not nil) func (msh *MShellProc) testAndSetPendingStateCmd(screenId string, rptr sstore.RemotePtrType, newCK *base.CommandKey) (bool, *base.CommandKey) { key := pendingStateKey{ScreenId: screenId, RemotePtr: rptr} msh.Lock.Lock() @@ -1986,6 +1992,10 @@ type RunCommandOpts struct { // set to true to skip creating the pty file (for restarted commands) NoCreateCmdPtyFile bool + + // this command will not go into the DB, and will not have a ptyout file created + // forces special packet handling (sets RunCommandType.Ephemeral) + Ephemeral bool } // returns (CmdType, allow-updates-callback, err) @@ -2022,14 +2032,14 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru } ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC) if !ok { - line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId()) - if err != nil { - return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) + rct := msh.GetRunningCmd(*existingPSC) + if rct.Ephemeral { + // if the existing command is ephemeral, we cancel it and continue + rct.EphCancled.Store(true) + } else { + line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId()) + return nil, nil, makePSCLineError(*existingPSC, line, err) } - if line == nil { - return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC) - } - return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) } if newPSC != nil { defer func() { @@ -2121,24 +2131,37 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru RunOut: nil, RtnState: runPacket.ReturnState, } - if !rcOpts.NoCreateCmdPtyFile { + if !rcOpts.NoCreateCmdPtyFile && !rcOpts.Ephemeral { err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO the cmd is running, so this is a tricky error to handle return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) } } - msh.AddRunningCmd(RunCmdType{ + msh.AddRunningCmd(&RunCmdType{ + CK: runPacket.CK, SessionId: sessionId, ScreenId: screenId, RemotePtr: remotePtr, RunPacket: runPacket, + Ephemeral: rcOpts.Ephemeral, }) return cmd, func() { removeCmdWait(runPacket.CK) }, nil } -func (msh *MShellProc) AddRunningCmd(rct RunCmdType) { +// helper func to construct the proper error given what information we have +func makePSCLineError(existingPSC base.CommandKey, line *sstore.LineType, lineErr error) error { + if lineErr != nil { + return fmt.Errorf("cannot run command while a stateful command is still running: %v", lineErr) + } + if line == nil { + return fmt.Errorf("cannot run command while a stateful command is still running %s", existingPSC) + } + return fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) +} + +func (msh *MShellProc) AddRunningCmd(rct *RunCmdType) { msh.Lock.Lock() defer msh.Lock.Unlock() msh.RunningCmds[rct.RunPacket.CK] = rct @@ -2147,11 +2170,7 @@ func (msh *MShellProc) AddRunningCmd(rct RunCmdType) { func (msh *MShellProc) GetRunningCmd(ck base.CommandKey) *RunCmdType { msh.Lock.Lock() defer msh.Lock.Unlock() - rct, found := msh.RunningCmds[ck] - if !found { - return nil - } - return &rct + return msh.RunningCmds[ck] } func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { @@ -2241,39 +2260,74 @@ func (msh *MShellProc) notifyHangups_nolock() { scbus.MainUpdateBus.DoScreenUpdate(ck.GetGroupId(), update) go pushNumRunningCmdsUpdate(&ck, -1) } - msh.RunningCmds = make(map[base.CommandKey]RunCmdType) + msh.RunningCmds = make(map[base.CommandKey]*RunCmdType) msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey) } -func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { - ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelFn() - // this will remove from RunningCmds and from PendingStateCmds - defer msh.RemoveRunningCmd(donePk.CK) +// either fullstate or statediff will be set (not both) <- this is so the result is compatible with the sstore.UpdateRemoteState function +// note that this function *does* touch the DB, if FinalStateDiff is set, will ensure that StateBase is written to DB +func (msh *MShellProc) makeStatePtrFromFinalState(ctx context.Context, donePk *packet.CmdDonePacketType) (*sstore.ShellStatePtr, map[string]string, *packet.ShellState, *packet.ShellStateDiff, error) { if donePk.FinalState != nil { - donePk.FinalState = stripScVarsFromState(donePk.FinalState) + finalState := stripScVarsFromState(donePk.FinalState) + feState := sstore.FeStateFromShellState(finalState) + statePtr := &sstore.ShellStatePtr{BaseHash: finalState.GetHashVal(false)} + return statePtr, feState, finalState, nil, nil } if donePk.FinalStateDiff != nil { - donePk.FinalStateDiff = stripScVarsFromStateDiff(donePk.FinalStateDiff) + stateDiff := stripScVarsFromStateDiff(donePk.FinalStateDiff) + feState, err := msh.getFeStateFromDiff(stateDiff) + if err != nil { + return nil, nil, nil, nil, err + } + fullState := msh.StateMap.GetStateByHash(stateDiff.GetShellType(), stateDiff.BaseHash) + if fullState != nil { + sstore.StoreStateBase(ctx, fullState) + } + diffHashArr := append(([]string)(nil), donePk.FinalStateDiff.DiffHashArr...) + diffHashArr = append(diffHashArr, donePk.FinalStateDiff.GetHashVal(false)) + statePtr := &sstore.ShellStatePtr{BaseHash: donePk.FinalStateDiff.BaseHash, DiffHashArr: diffHashArr} + return statePtr, feState, nil, stateDiff, nil } - update, err := sstore.UpdateCmdDoneInfo(ctx, donePk.CK, donePk, sstore.CmdStatusDone) - if err != nil { - msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) + return nil, nil, nil, nil, nil +} + +func (msh *MShellProc) handleCmdDonePacket(rct *RunCmdType, donePk *packet.CmdDonePacketType) { + if rct == nil { + log.Printf("cmddone packet received, but no running command found for it %q\n", donePk.CK) return } - screen, err := sstore.UpdateScreenFocusForDoneCmd(ctx, donePk.CK.GetGroupId(), donePk.CK.GetCmdId()) - if err != nil { - msh.WriteToPtyBuffer("*error trying to update screen focus type: %v\n", err) - // fall-through (nothing to do) + // this will remove from RunningCmds and from PendingStateCmds + defer msh.RemoveRunningCmd(donePk.CK) + if rct.Ephemeral && rct.EphCancled.Load() { + // do nothing when an ephemeral command is canceled + return } - if screen != nil { - update.AddUpdate(*screen) + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + update := scbus.MakeUpdatePacket() + if !rct.Ephemeral { + // only update DB for non-ephemeral commands + err := sstore.UpdateCmdDoneInfo(ctx, update, donePk.CK, donePk, sstore.CmdStatusDone) + if err != nil { + msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) + return + } + screen, err := sstore.UpdateScreenFocusForDoneCmd(ctx, donePk.CK.GetGroupId(), donePk.CK.GetCmdId()) + if err != nil { + msh.WriteToPtyBuffer("*error trying to update screen focus type: %v\n", err) + // fall-through (nothing to do) + } + if screen != nil { + update.AddUpdate(*screen) + } } - rct := msh.GetRunningCmd(donePk.CK) - var statePtr *sstore.ShellStatePtr - if donePk.FinalState != nil && rct != nil { - feState := sstore.FeStateFromShellState(donePk.FinalState) - remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, donePk.FinalState, nil) + // ephemeral commands *do* update the remote state + if donePk.FinalState != nil || donePk.FinalStateDiff != nil { + statePtr, feState, finalState, finalStateDiff, err := msh.makeStatePtrFromFinalState(ctx, donePk) + if err != nil { + msh.WriteToPtyBuffer("*error trying to read final command state: %v\n", err) + } + remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, finalState, finalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) @@ -2281,43 +2335,28 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { if remoteInst != nil { update.AddUpdate(sstore.MakeSessionUpdateForRemote(rct.SessionId, remoteInst)) } - statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalState.GetHashVal(false)} - } else if donePk.FinalStateDiff != nil && rct != nil { - feState, err := msh.getFeStateFromDiff(donePk.FinalStateDiff) - if err != nil { - msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) - // fall-through (nothing to do) - } else { - stateDiff := donePk.FinalStateDiff - fullState := msh.StateMap.GetStateByHash(stateDiff.GetShellType(), stateDiff.BaseHash) - if fullState != nil { - sstore.StoreStateBase(ctx, fullState) - } - remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, nil, stateDiff) + // ephemeral commands *do not* update cmd state (there is no command) + if statePtr != nil && !rct.Ephemeral { + err = sstore.UpdateCmdRtnState(ctx, donePk.CK, *statePtr) if err != nil { - msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) + msh.WriteToPtyBuffer("*error trying to update cmd rtnstate: %v\n", err) // fall-through (nothing to do) } - if remoteInst != nil { - update.AddUpdate(sstore.MakeSessionUpdateForRemote(rct.SessionId, remoteInst)) - } - diffHashArr := append(([]string)(nil), donePk.FinalStateDiff.DiffHashArr...) - diffHashArr = append(diffHashArr, donePk.FinalStateDiff.GetHashVal(false)) - statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalStateDiff.BaseHash, DiffHashArr: diffHashArr} - } - } - if statePtr != nil { - err = sstore.UpdateCmdRtnState(ctx, donePk.CK, *statePtr) - if err != nil { - msh.WriteToPtyBuffer("*error trying to update cmd rtnstate: %v\n", err) - // fall-through (nothing to do) } } scbus.MainUpdateBus.DoUpdate(update) } -func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) { +func (msh *MShellProc) handleCmdFinalPacket(rct *RunCmdType, finalPk *packet.CmdFinalPacketType) { + if rct == nil { + // this is somewhat expected, since cmddone should have removed the running command + return + } defer msh.RemoveRunningCmd(finalPk.CK) + if rct.Ephemeral { + // just remove the running command, but there is no DB state to update in this case + return + } rtnCmd, err := sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId()) if err != nil { log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err) @@ -2350,31 +2389,31 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) scbus.MainUpdateBus.DoUpdate(update) } -// TODO notify FE about cmd errors -func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { - err := sstore.AppendCmdErrorPk(context.Background(), errPk) - if err != nil { - msh.WriteToPtyBuffer("cmderr> [remote %s] [error] adding cmderr: %v\n", msh.GetRemoteName(), err) - return - } -} - func (msh *MShellProc) ResetDataPos(ck base.CommandKey) { msh.DataPosMap.Delete(ck) } -func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) { +func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) { + if rct == nil { + ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, fmt.Errorf("no running cmd found")) + msh.ServerProc.Input.SendPacket(ack) + return + } realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) if err != nil { ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) msh.ServerProc.Input.SendPacket(ack) return } + if rct.Ephemeral { + ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) + msh.ServerProc.Input.SendPacket(ack) + return + } var ack *packet.DataAckPacketType if len(realData) > 0 { dataPos := dataPosMap.Get(dataPk.CK) - rcmd := msh.GetRunningCmd(dataPk.CK) - update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) + update, err := sstore.AppendToCmdPtyBlob(context.Background(), rct.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { @@ -2388,25 +2427,6 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa if ack != nil { msh.ServerProc.Input.SendPacket(ack) } - // log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) -} - -func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) func() { - return func() { - msh.handleDataPacket(dataPk, dataPosMap) - } -} - -func (msh *MShellProc) makeHandleCmdDonePacketClosure(donePk *packet.CmdDonePacketType) func() { - return func() { - msh.handleCmdDonePacket(donePk) - } -} - -func (msh *MShellProc) makeHandleCmdFinalPacketClosure(finalPk *packet.CmdFinalPacketType) func() { - return func() { - msh.handleCmdFinalPacket(finalPk) - } } func sendScreenUpdates(screens []*sstore.ScreenType) { @@ -2417,6 +2437,45 @@ func sendScreenUpdates(screens []*sstore.ScreenType) { } } +func (msh *MShellProc) processSinglePacket(pk packet.PacketType) { + if _, ok := pk.(*packet.DataAckPacketType); ok { + // TODO process ack (need to keep track of buffer size for sending) + // this is low priority though since most input is coming from keyboard and won't overflow this buffer + return + } + if dataPk, ok := pk.(*packet.DataPacketType); ok { + runCmdUpdateFn(dataPk.CK, func() { + rct := msh.GetRunningCmd(dataPk.CK) + msh.handleDataPacket(rct, dataPk, msh.DataPosMap) + }) + go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output) + return + } + if donePk, ok := pk.(*packet.CmdDonePacketType); ok { + runCmdUpdateFn(donePk.CK, func() { + rct := msh.GetRunningCmd(donePk.CK) + msh.handleCmdDonePacket(rct, donePk) + }) + return + } + if finalPk, ok := pk.(*packet.CmdFinalPacketType); ok { + runCmdUpdateFn(finalPk.CK, func() { + rct := msh.GetRunningCmd(finalPk.CK) + msh.handleCmdFinalPacket(rct, finalPk) + }) + return + } + if msgPk, ok := pk.(*packet.MessagePacketType); ok { + msh.WriteToPtyBuffer("msg> [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPk.CK, msgPk.Message) + return + } + if rawPk, ok := pk.(*packet.RawPacketType); ok { + msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPk.Data) + return + } + msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk)) +} + func (msh *MShellProc) ProcessPackets() { defer msh.WithLock(func() { if msh.Status == StatusConnected { @@ -2433,53 +2492,7 @@ func (msh *MShellProc) ProcessPackets() { } }) for pk := range msh.ServerProc.Output.MainCh { - if pk.GetType() == packet.DataPacketStr { - dataPk := pk.(*packet.DataPacketType) - runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, msh.DataPosMap)) - go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output) - continue - } - if pk.GetType() == packet.DataAckPacketStr { - // TODO process ack (need to keep track of buffer size for sending) - // this is low priority though since most input is coming from keyboard and won't overflow this buffer - continue - } - if pk.GetType() == packet.CmdDataPacketStr { - dataPacket := pk.(*packet.CmdDataPacketType) - go msh.WriteToPtyBuffer("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) - go pushStatusIndicatorUpdate(&dataPacket.CK, sstore.StatusIndicatorLevel_Output) - continue - } - if pk.GetType() == packet.CmdDonePacketStr { - donePk := pk.(*packet.CmdDonePacketType) - runCmdUpdateFn(donePk.CK, msh.makeHandleCmdDonePacketClosure(donePk)) - continue - } - if pk.GetType() == packet.CmdFinalPacketStr { - finalPk := pk.(*packet.CmdFinalPacketType) - runCmdUpdateFn(finalPk.CK, msh.makeHandleCmdFinalPacketClosure(finalPk)) - continue - } - if pk.GetType() == packet.CmdErrorPacketStr { - msh.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType)) - continue - } - if pk.GetType() == packet.MessagePacketStr { - msgPacket := pk.(*packet.MessagePacketType) - msh.WriteToPtyBuffer("msg> [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPacket.CK, msgPacket.Message) - continue - } - if pk.GetType() == packet.RawPacketStr { - rawPacket := pk.(*packet.RawPacketType) - msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPacket.Data) - continue - } - if pk.GetType() == packet.CmdStartPacketStr { - startPk := pk.(*packet.CmdStartPacketType) - msh.WriteToPtyBuffer("start> [remote %s] reqid=%s (%p)\n", msh.GetRemoteName(), startPk.RespId, msh.ServerProc.Output) - continue - } - msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk)) + msh.processSinglePacket(pk) } } diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index e4d6685dd..8accfbf82 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -916,12 +916,12 @@ func UpdateCmdForRestart(ctx context.Context, ck base.CommandKey, ts int64, cmdP }) } -func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*scbus.ModelUpdatePacketType, error) { +func UpdateCmdDoneInfo(ctx context.Context, update *scbus.ModelUpdatePacketType, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) error { if donePk == nil { - return nil, fmt.Errorf("invalid cmddone packet") + return fmt.Errorf("invalid cmddone packet") } if ck.IsEmpty() { - return nil, fmt.Errorf("cannot update cmddoneinfo, empty ck") + return fmt.Errorf("cannot update cmddoneinfo, empty ck") } screenId := ck.GetGroupId() var rtnCmd *CmdType @@ -944,15 +944,12 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C return nil }) if txErr != nil { - return nil, txErr + return txErr } if rtnCmd == nil { - return nil, fmt.Errorf("cmd data not found for ck[%s]", ck) + return fmt.Errorf("cmd data not found for ck[%s]", ck) } - - update := scbus.MakeUpdatePacket() update.AddUpdate(*rtnCmd) - // Update in-memory screen indicator status var indicator StatusIndicatorLevel if rtnCmd.ExitCode == 0 { @@ -960,15 +957,13 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C } else { indicator = StatusIndicatorLevel_Error } - err := SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false) if err != nil { // This is not a fatal error, so just log it log.Printf("error setting status indicator level after done packet: %v\n", err) } IncrementNumRunningCmds_Update(update, screenId, -1) - - return update, nil + return nil } func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellStatePtr) error { @@ -991,18 +986,6 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt return nil } -func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { - if errPk == nil || errPk.CK.IsEmpty() { - return fmt.Errorf("invalid cmderror packet (no ck)") - } - screenId := errPk.CK.GetGroupId() - return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND lineid = ?` - tx.Exec(query, quickJson(errPk), screenId, lineIdFromCK(errPk.CK)) - return nil - }) -} - func ReInitFocus(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE screen SET focustype = 'input'` From 4c68fc4cebbd97d9f830dad91a256a6673884922 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Wed, 13 Mar 2024 23:25:11 -0700 Subject: [PATCH 06/15] pdf viewer (#448) * checkpoint on pdf viewer * implement a pdf renderer (fix emain shFrameNavHandler to allow it) --- src/app/line/renderer/basicrenderer.tsx | 276 ------------------------ src/electron/emain.ts | 7 +- src/models/model.ts | 7 +- src/plugins/pdf/pdf.less | 7 + src/plugins/pdf/pdf.tsx | 49 +++++ src/plugins/plugins.ts | 11 + wavesrv/pkg/cmdrunner/cmdrunner.go | 32 +++ wavesrv/pkg/cmdrunner/shparse.go | 1 + 8 files changed, 111 insertions(+), 279 deletions(-) delete mode 100644 src/app/line/renderer/basicrenderer.tsx create mode 100644 src/plugins/pdf/pdf.less create mode 100644 src/plugins/pdf/pdf.tsx diff --git a/src/app/line/renderer/basicrenderer.tsx b/src/app/line/renderer/basicrenderer.tsx deleted file mode 100644 index 2862b0a12..000000000 --- a/src/app/line/renderer/basicrenderer.tsx +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2023, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import * as React from "react"; -import * as mobxReact from "mobx-react"; -import * as mobx from "mobx"; - -import { debounce } from "throttle-debounce"; -import * as util from "@/util/util"; -import { GlobalModel } from "@/models"; - -class SimpleBlobRendererModel { - context: RendererContext; - opts: RendererOpts; - isDone: OV; - api: RendererModelContainerApi; - savedHeight: number; - loading: OV; - loadError: OV = mobx.observable.box(null, { - name: "renderer-loadError", - }); - lineState: LineStateType; - ptyData: PtyDataType; - ptyDataSource: (termContext: TermContextUnion) => Promise; - dataBlob: Blob; - readOnly: boolean; - notFound: boolean; - - initialize(params: RendererModelInitializeParams): void { - this.loading = mobx.observable.box(true, { name: "renderer-loading" }); - this.isDone = mobx.observable.box(params.isDone, { - name: "renderer-isDone", - }); - this.context = params.context; - this.opts = params.opts; - this.api = params.api; - this.lineState = params.lineState; - this.savedHeight = params.savedHeight; - this.ptyDataSource = params.ptyDataSource; - if (this.isDone.get()) { - setTimeout(() => this.reload(0), 10); - } - } - - dispose(): void { - return; - } - - giveFocus(): void { - return; - } - - updateOpts(update: RendererOptsUpdate): void { - Object.assign(this.opts, update); - } - - updateHeight(newHeight: number): void { - if (this.savedHeight != newHeight) { - this.savedHeight = newHeight; - this.api.saveHeight(newHeight); - } - } - - setIsDone(): void { - if (this.isDone.get()) { - return; - } - mobx.action(() => { - this.isDone.set(true); - })(); - this.reload(0); - } - - reload(delayMs: number): void { - mobx.action(() => { - this.loading.set(true); - })(); - if (delayMs == 0) { - this.reload_noDelay(); - } else { - setTimeout(() => { - this.reload_noDelay(); - }, delayMs); - } - } - - reload_noDelay(): void { - let source = this.lineState["prompt:source"] || "pty"; - if (source == "pty") { - this.reloadPtyData(); - } else if (source == "file") { - this.reloadFileData(); - } else { - mobx.action(() => { - this.loadError.set("error: invalid load source: " + source); - })(); - } - } - - reloadFileData(): void { - // todo add file methods to API, so we don't have a GlobalModel dependency here! - let path = this.lineState["prompt:file"]; - if (util.isBlank(path)) { - mobx.action(() => { - this.loadError.set("renderer has file source, but no prompt:file specified"); - })(); - return; - } - let rtnp = GlobalModel.readRemoteFile(this.context.screenId, this.context.lineId, path); - rtnp.then((file) => { - this.notFound = (file as any).notFound; - this.readOnly = (file as any).readOnly; - this.dataBlob = file; - mobx.action(() => { - this.loading.set(false); - this.loadError.set(null); - })(); - }).catch((e) => { - mobx.action(() => { - this.loadError.set("error loading file data: " + e); - })(); - }); - } - - reloadPtyData(): void { - this.readOnly = true; - let rtnp = this.ptyDataSource(this.context); - if (rtnp == null) { - console.log("no promise returned from ptyDataSource (simplerenderer)", this.context); - return; - } - rtnp.then((ptydata) => { - this.ptyData = ptydata; - this.dataBlob = new Blob([this.ptyData.data]); - mobx.action(() => { - this.loading.set(false); - this.loadError.set(null); - })(); - }).catch((e) => { - mobx.action(() => { - this.loadError.set("error loading data: " + e); - })(); - }); - } - - receiveData(pos: number, data: Uint8Array, reason?: string): void { - // this.dataBuf.receiveData(pos, data, reason); - } -} - -@mobxReact.observer -class SimpleBlobRenderer extends React.Component< - { - rendererContainer: RendererContainerType; - lineId: string; - plugin: RendererPluginType; - onHeightChange: () => void; - initParams: RendererModelInitializeParams; - scrollToBringIntoViewport: () => void; - isSelected: boolean; - shouldFocus: boolean; - }, - {} -> { - model: SimpleBlobRendererModel; - wrapperDivRef: React.RefObject = React.createRef(); - rszObs: ResizeObserver; - updateHeight_debounced: (newHeight: number) => void; - - constructor(props: any) { - super(props); - let { rendererContainer, lineId, plugin, initParams } = this.props; - this.model = new SimpleBlobRendererModel(); - this.model.initialize(initParams); - rendererContainer.registerRenderer(lineId, this.model); - this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this)); - } - - updateHeight(newHeight: number): void { - this.model.updateHeight(newHeight); - } - - handleResize(entries: ResizeObserverEntry[]): void { - if (this.model.loading.get()) { - return; - } - if (this.props.onHeightChange) { - this.props.onHeightChange(); - } - if (!this.model.loading.get() && this.wrapperDivRef.current != null) { - let height = this.wrapperDivRef.current.offsetHeight; - this.updateHeight_debounced(height); - } - } - - checkRszObs() { - if (this.rszObs != null) { - return; - } - if (this.wrapperDivRef.current == null) { - return; - } - this.rszObs = new ResizeObserver(this.handleResize.bind(this)); - this.rszObs.observe(this.wrapperDivRef.current); - } - - componentDidMount() { - this.checkRszObs(); - } - - componentWillUnmount() { - let { rendererContainer, lineId } = this.props; - rendererContainer.unloadRenderer(lineId); - if (this.rszObs != null) { - this.rszObs.disconnect(); - this.rszObs = null; - } - } - - componentDidUpdate() { - this.checkRszObs(); - } - - render() { - let { plugin } = this.props; - let model = this.model; - if (model.loadError.get() != null) { - let errorText = model.loadError.get(); - let height = this.model.savedHeight; - return ( -
-
ERROR: {errorText}
-
- ); - } - if (model.loading.get()) { - let height = this.model.savedHeight; - return ( -
- loading content -
- ); - } - let Comp = plugin.simpleComponent; - if (Comp == null) { -
(no component found in plugin)
; - } - let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd; - return ( -
- -
- ); - } -} - -export { SimpleBlobRendererModel, SimpleBlobRenderer }; diff --git a/src/electron/emain.ts b/src/electron/emain.ts index be4f429f6..2b6114775 100644 --- a/src/electron/emain.ts +++ b/src/electron/emain.ts @@ -308,16 +308,21 @@ function shFrameNavHandler(event: Electron.Event { - const urlParams = { + readRemoteFile(screenId: string, lineId: string, path: string, mimetype?: string): Promise { + const urlParams: Record = { screenid: screenId, lineid: lineId, path: path, }; + if (mimetype != null) { + urlParams["mimetype"] = mimetype; + } const usp = new URLSearchParams(urlParams); const url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString()); const fetchHeaders = this.getFetchHeaders(); diff --git a/src/plugins/pdf/pdf.less b/src/plugins/pdf/pdf.less new file mode 100644 index 000000000..4f58b90f7 --- /dev/null +++ b/src/plugins/pdf/pdf.less @@ -0,0 +1,7 @@ +.pdf-renderer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: var(--termpad); +} diff --git a/src/plugins/pdf/pdf.tsx b/src/plugins/pdf/pdf.tsx new file mode 100644 index 000000000..dda6208e9 --- /dev/null +++ b/src/plugins/pdf/pdf.tsx @@ -0,0 +1,49 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobx from "mobx"; +import * as mobxReact from "mobx-react"; + +import "./pdf.less"; + +@mobxReact.observer +class SimplePdfRenderer extends React.Component< + { data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number }, + {} +> { + objUrl: string = null; + + componentWillUnmount() { + if (this.objUrl != null) { + URL.revokeObjectURL(this.objUrl); + } + } + + render() { + let dataBlob = this.props.data; + if (dataBlob == null || dataBlob.notFound) { + return ( +
+
+ ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found +
+
+ ); + } + if (this.objUrl == null) { + const pdfBlob = new File([dataBlob], "test.pdf", { type: "application/pdf" }); + this.objUrl = URL.createObjectURL(pdfBlob); + } + const opts = this.props.opts; + const maxHeight = opts.maxSize.height - 10; + const maxWidth = opts.maxSize.width - 10; + return ( +
+