mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 395a315068 | |||
| e6c41788f5 | |||
| 3f988c0e6b | |||
| 0f2f6c9cc0 | |||
| 697f7c0758 | |||
| 865658aa92 | |||
| 4c68fc4ceb | |||
| bff51c851a | |||
| 550d9c9716 | |||
| 9eb7461964 | |||
| 0241e47f83 | |||
| f87cc42ab9 |
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
@@ -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,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"),
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -48,12 +48,15 @@ class SimpleImageRenderer extends React.Component<
|
||||
return (
|
||||
<div className="image-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
|
||||
<div className="load-error-text">
|
||||
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
|
||||
ERROR: file {dataBlob?.name ? JSON.stringify(dataBlob.name) : ""} not found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.objUrl == null) {
|
||||
if (dataBlob.name?.endsWith(".svg")) {
|
||||
dataBlob = new Blob([dataBlob], { type: "image/svg+xml" }) as ExtBlob;
|
||||
}
|
||||
this.objUrl = URL.createObjectURL(dataBlob);
|
||||
}
|
||||
let opts = this.props.opts;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.media-renderer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--termpad);
|
||||
|
||||
video {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 * as util from "@/util/util";
|
||||
import { GlobalModel } from "@/models";
|
||||
|
||||
import "./media.less";
|
||||
|
||||
@mobxReact.observer
|
||||
class SimpleMediaRenderer extends React.Component<
|
||||
{ data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number; lineState: LineStateType },
|
||||
{}
|
||||
> {
|
||||
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="media-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>
|
||||
);
|
||||
}
|
||||
let fileUrl = this.props.lineState["wave:fileurl"];
|
||||
if (util.isBlank(fileUrl)) {
|
||||
return (
|
||||
<div className="media-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
|
||||
<div className="load-error-text">
|
||||
ERROR: no fileurl found (please use `mediaview` to view media files)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let fullVideoUrl = GlobalModel.getBaseHostPort() + fileUrl;
|
||||
const opts = this.props.opts;
|
||||
const height = opts.idealSize.height - 10;
|
||||
const width = opts.maxSize.width - 10;
|
||||
return (
|
||||
<div className="media-renderer" style={{ height: height, width: width }}>
|
||||
<video controls>
|
||||
<source src={fullVideoUrl} />
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { SimpleMediaRenderer };
|
||||
@@ -0,0 +1,7 @@
|
||||
.pdf-renderer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--termpad);
|
||||
}
|
||||
@@ -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], dataBlob.name ?? "file.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 };
|
||||
@@ -7,6 +7,8 @@ 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 { SimpleMediaRenderer } from "./media/media";
|
||||
import { isBlank } from "@/util/util";
|
||||
import { sprintf } from "sprintf-js";
|
||||
|
||||
@@ -78,6 +80,26 @@ const PluginConfigs: RendererPluginType[] = [
|
||||
mimeTypes: ["image/*"],
|
||||
simpleComponent: SimpleImageRenderer,
|
||||
},
|
||||
{
|
||||
name: "pdf",
|
||||
rendererType: "simple",
|
||||
heightType: "pixels",
|
||||
dataType: "blob",
|
||||
collapseType: "hide",
|
||||
globalCss: null,
|
||||
mimeTypes: ["application/pdf"],
|
||||
simpleComponent: SimplePdfRenderer,
|
||||
},
|
||||
{
|
||||
name: "media",
|
||||
rendererType: "simple",
|
||||
heightType: "pixels",
|
||||
dataType: "blob",
|
||||
collapseType: "hide",
|
||||
globalCss: null,
|
||||
mimeTypes: ["video/*", "audio/*"],
|
||||
simpleComponent: SimpleMediaRenderer,
|
||||
},
|
||||
];
|
||||
|
||||
class PluginModelClass {
|
||||
|
||||
Vendored
+1
@@ -880,6 +880,7 @@ declare global {
|
||||
};
|
||||
|
||||
type ElectronApi = {
|
||||
hideWindow: () => void;
|
||||
toggleDeveloperTools: () => void;
|
||||
getId: () => string;
|
||||
getIsDev: () => boolean;
|
||||
|
||||
+59
-8
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user