Compare commits

..

8 Commits

Author SHA1 Message Date
sawka 14b10d0848 fix hmac implementation/typo. read WaveAuthKey and store in scbase instead of main-server 2024-03-14 00:22:30 -07:00
Mike Sawka 865658aa92 allow hmac authentication for read-file (#449) 2024-03-13 23:59:20 -07:00
Mike Sawka 4c68fc4ceb pdf viewer (#448)
* checkpoint on pdf viewer

* implement a pdf renderer (fix emain shFrameNavHandler to allow it)
2024-03-13 23:25:11 -07:00
Mike Sawka bff51c851a 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
2024-03-13 18:52:41 -07:00
Sylvie Crowe 550d9c9716 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.
2024-03-13 18:52:02 -07:00
Red J Adaya 9eb7461964 Close modals when pressing Escape (#433)
* remove clearmodals mothed as it's no longer needed

* close modals on ECS
2024-03-13 18:51:16 -07:00
Red J Adaya 0241e47f83 fix broken bookmarks view (#446) 2024-03-13 18:48:46 -07:00
Cole Lashley f87cc42ab9 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
2024-03-13 18:47:16 -07:00
25 changed files with 585 additions and 1190 deletions
+32 -15
View File
@@ -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",
+1 -1
View File
@@ -193,7 +193,7 @@ class BookmarksView extends React.Component<{}, {}> {
let bookmarks = GlobalModel.bookmarksModel.bookmarks;
let bookmark: BookmarkType = null;
return (
<MainView viewName="bookmarks" title="Bookmarks" onClose={this.handleClose}>
<MainView className="bookmarks-view" title="Bookmarks" onClose={this.handleClose}>
<div className="bookmarks-list">
<For index="idx" each="bookmark" of={bookmarks}>
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark} />
-276
View File
@@ -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<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
loadError: OV<string> = mobx.observable.box(null, {
name: "renderer-loadError",
});
lineState: LineStateType;
ptyData: PtyDataType;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
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<any> = 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 (
<div ref={this.wrapperDivRef} style={{ minHeight: height, fontSize: model.opts.termFontSize }}>
<div className="load-error-text">ERROR: {errorText}</div>
</div>
);
}
if (model.loading.get()) {
let height = this.model.savedHeight;
return (
<div
ref={this.wrapperDivRef}
className="renderer-loading"
style={{ minHeight: height, fontSize: model.opts.termFontSize }}
>
loading content <i className="fa fa-ellipsis fa-fade" />
</div>
);
}
let Comp = plugin.simpleComponent;
if (Comp == null) {
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
}
let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd;
return (
<div ref={this.wrapperDivRef} className="sr-wrapper">
<Comp
cwd={festate.cwd}
cmdstr={cmdstr}
exitcode={exitcode}
data={model.dataBlob as ExtBlob}
readOnly={model.readOnly}
notFound={model.notFound}
lineState={model.lineState}
context={model.context}
opts={model.opts}
savedHeight={model.savedHeight}
scrollToBringIntoViewport={this.props.scrollToBringIntoViewport}
isSelected={this.props.isSelected}
shouldFocus={this.props.shouldFocus}
rendererApi={model.api}
/>
</div>
);
}
}
export { SimpleBlobRendererModel, SimpleBlobRenderer };
+19 -2
View File
@@ -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" },
@@ -303,16 +308,21 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
return;
}
event.preventDefault();
const url = event.url;
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
if (event.frame.name == "webview") {
// "webview" links always open in new window
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
console.log("open external, frameNav", url);
event.preventDefault();
electron.shell.openExternal(url);
return;
}
if (event.frame.name == "pdfview" && url.startsWith("blob:file:///")) {
// allowed
return;
}
event.preventDefault();
console.log("frame navigation canceled");
}
@@ -513,6 +523,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;
});
+1
View File
@@ -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"),
+4
View File
@@ -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);
+24 -27
View File
@@ -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 {
@@ -996,6 +981,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;
@@ -1059,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);
@@ -1556,12 +1550,15 @@ class Model {
return remote.remotecanonicalname;
}
readRemoteFile(screenId: string, lineId: string, path: string): Promise<ExtFile> {
const urlParams = {
readRemoteFile(screenId: string, lineId: string, path: string, mimetype?: string): Promise<ExtFile> {
const urlParams: Record<string, string> = {
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();
+7
View File
@@ -0,0 +1,7 @@
.pdf-renderer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--termpad);
}
+49
View File
@@ -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 (
<div className="pdf-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
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 (
<div className="pdf-renderer">
<iframe src={this.objUrl} width={maxWidth} height={maxHeight} name="pdfview" />
</div>
);
}
}
export { SimplePdfRenderer };
+11
View File
@@ -7,6 +7,7 @@ import { SourceCodeRenderer } from "./code/code";
import { SimpleMustacheRenderer } from "./mustache/mustache";
import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -78,6 +79,16 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["image/*"],
simpleComponent: SimpleImageRenderer,
},
{
name: "pdf",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["application/pdf"],
simpleComponent: SimplePdfRenderer,
},
];
class PluginModelClass {
+1
View File
@@ -880,6 +880,7 @@ declare global {
};
type ElectronApi = {
hideWindow: () => void;
toggleDeveloperTools: () => void;
getId: () => string;
getIsDev: () => boolean;
+59 -8
View File
@@ -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<string> }>;
type KeybindConfigArray = Array<KeybindConfig>;
type KeybindConfig = { command: string; keys: Array<string>; 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<string, KeybindCallback>;
levelMap: Map<string, Array<Keybind>>;
levelArray: Array<string>;
keyDescriptionsMap: Map<string, Array<string>>;
userKeybindings: KeybindConfig;
keyDescriptionsMap: Map<string, KeybindConfig>;
userKeybindings: KeybindConfigArray;
userKeybindingError: OV<string>;
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<string>): 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<Keybind>): 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);
+1 -9
View File
@@ -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()
-473
View File
@@ -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
}
+2 -71
View File
@@ -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))
}
}
+1 -1
View File
@@ -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)
-100
View File
@@ -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
+35 -7
View File
@@ -37,6 +37,7 @@ import (
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
@@ -73,7 +74,6 @@ var BuildTime = "0"
var GlobalLock = &sync.Mutex{}
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
var GlobalAuthKey string
var shutdownOnce sync.Once
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
@@ -139,7 +139,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
}
state := getWSState(clientId)
if state == nil {
state = scws.MakeWSState(clientId, GlobalAuthKey)
state = scws.MakeWSState(clientId, scbase.WaveAuthKey)
state.ReplaceShell(shell)
setWSState(state)
} else {
@@ -686,7 +686,7 @@ func AuthKeyMiddleWare(next http.Handler) http.Handler {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -695,6 +695,35 @@ func AuthKeyMiddleWare(next http.Handler) http.Handler {
})
}
func AuthKeyWrapAllowHmac(fn WebFnType) WebFnType {
return func(w http.ResponseWriter, r *http.Request) {
reqAuthKey := r.Header.Get("X-AuthKey")
if reqAuthKey == "" {
// try hmac
qvals := r.URL.Query()
if !qvals.Has("hmac") {
w.WriteHeader(500)
w.Write([]byte("no x-authkey header"))
return
}
hmacOk, err := promptenc.ValidateUrlHmac([]byte(scbase.WaveAuthKey), r.URL.Path, qvals)
if err != nil || !hmacOk {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error validating hmac")))
return
}
// fallthrough (hmac is valid)
} else if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
}
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
fn(w, r)
}
}
func AuthKeyWrap(fn WebFnType) WebFnType {
return func(w http.ResponseWriter, r *http.Request) {
reqAuthKey := r.Header.Get("X-AuthKey")
@@ -703,7 +732,7 @@ func AuthKeyWrap(fn WebFnType) WebFnType {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -860,12 +889,11 @@ func main() {
}
return
}
authKey, err := scbase.ReadWaveAuthKey()
err = scbase.InitializeWaveAuthKey()
if err != nil {
log.Printf("[error] %v\n", err)
return
}
GlobalAuthKey = authKey
err = sstore.TryMigrateUp()
if err != nil {
log.Printf("[error] migrate up: %v\n", err)
@@ -921,7 +949,7 @@ func main() {
gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData))
gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize))
gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState))
gr.HandleFunc("/api/read-file", AuthKeyWrap(HandleReadFile))
gr.HandleFunc("/api/read-file", AuthKeyWrapAllowHmac(HandleReadFile))
gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST")
configPath := path.Join(scbase.GetWaveHomeDir(), "config") + "/"
log.Printf("[wave] config path: %q\n", configPath)
+92 -14
View File
@@ -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)
@@ -276,6 +279,7 @@ func init() {
registerCmdFn("imageview", ImageViewCommand)
registerCmdFn("mdview", MarkdownViewCommand)
registerCmdFn("markdownview", MarkdownViewCommand)
registerCmdFn("pdfview", PdfViewCommand)
registerCmdFn("csvview", CSVViewCommand)
}
@@ -493,7 +497,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()
@@ -510,22 +514,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) {
@@ -1175,7 +1178,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)
@@ -2551,7 +2555,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)
@@ -2710,7 +2715,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)
@@ -3560,6 +3566,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 {
@@ -4808,6 +4854,38 @@ func CSVViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
}
func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
}
// TODO more error checking on filename format?
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
filePath := pk.Args[0]
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), filePath)
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_Source] = "file"
lineState[sstore.LineState_File] = filePath
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
return update, nil
}
func PdfViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
}
@@ -4829,7 +4907,7 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
lineState := make(map[string]any)
lineState[sstore.LineState_Source] = "file"
lineState[sstore.LineState_File] = pk.Args[0]
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState)
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "pdf", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
+1
View File
@@ -35,6 +35,7 @@ var BareMetaCmds = []BareMetaCmdDecl{
{"markdownview", "markdownview"},
{"mdview", "markdownview"},
{"csvview", "csvview"},
{"pdfview", "pdfview"},
}
const (

Some files were not shown because too many files have changed in this diff Show More