mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
merge branch 'main' into ssh-extra-fixes
This commit is contained in:
commit
00c2ab9fc4
3
.github/workflows/build-helper.yml
vendored
3
.github/workflows/build-helper.yml
vendored
@ -41,6 +41,7 @@ jobs:
|
|||||||
cd scripthaus;
|
cd scripthaus;
|
||||||
go get ./...;
|
go get ./...;
|
||||||
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
|
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
|
||||||
|
echo $PWD >> $GITHUB_PATH
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{env.NODE_VERSION}}
|
node-version: ${{env.NODE_VERSION}}
|
||||||
@ -53,7 +54,7 @@ jobs:
|
|||||||
- name: Install Yarn Dependencies
|
- name: Install Yarn Dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
|
- name: Build ${{ matrix.platform }}/${{ matrix.arch }}
|
||||||
run: ./scripthaus/scripthaus run ${{ matrix.scripthaus }}
|
run: scripthaus run ${{ matrix.scripthaus }}
|
||||||
env:
|
env:
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
|
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
|
||||||
|
33
.github/workflows/codeql.yml
vendored
33
.github/workflows/codeql.yml
vendored
@ -47,6 +47,32 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout Scripthaus (Go only)
|
||||||
|
if: matrix.language == 'go'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: scripthaus-dev/scripthaus
|
||||||
|
path: scripthaus
|
||||||
|
|
||||||
|
- name: Setup Go (Go only)
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
if: matrix.language == 'go'
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
cache-dependency-path: |
|
||||||
|
wavesrv/go.sum
|
||||||
|
waveshell/go.sum
|
||||||
|
scripthaus/go.sum
|
||||||
|
|
||||||
|
- name: Install Scripthaus (Go only)
|
||||||
|
if: matrix.language == 'go'
|
||||||
|
run: |
|
||||||
|
go work use ./scripthaus;
|
||||||
|
cd scripthaus;
|
||||||
|
go get ./...;
|
||||||
|
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
|
||||||
|
echo $PWD >> $GITHUB_PATH
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
@ -61,9 +87,14 @@ jobs:
|
|||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild (not Go)
|
||||||
|
if: matrix.language != 'go'
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Build (Go only)
|
||||||
|
if: matrix.language == 'go'
|
||||||
|
run: scripthaus run build-backend
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
"command": "system:toggleDeveloperTools",
|
"command": "system:toggleDeveloperTools",
|
||||||
"keys": ["Cmd:Option:i"]
|
"keys": ["Cmd:Option:i"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "system:hideWindow",
|
||||||
|
"keys": ["Cmd:m"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "generic:cancel",
|
"command": "generic:cancel",
|
||||||
"keys": ["Escape"]
|
"keys": ["Escape"]
|
||||||
@ -32,7 +36,7 @@
|
|||||||
"keys": ["PageDown"]
|
"keys": ["PageDown"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:openHistory",
|
"command": "app:openHistoryView",
|
||||||
"keys": ["Cmd:h"]
|
"keys": ["Cmd:h"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,11 +45,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:openConnectionsView",
|
"command": "app:openConnectionsView",
|
||||||
"keys": []
|
"keys": [],
|
||||||
|
"commandStr": "/mainview connections"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:openSettingsView",
|
"command": "app:openSettingsView",
|
||||||
"keys": []
|
"keys": [],
|
||||||
|
"commandStr": "/mainview clientsettings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:newTab",
|
"command": "app:newTab",
|
||||||
@ -125,39 +131,48 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-1",
|
"command": "app:selectWorkspace-1",
|
||||||
"keys": ["Cmd:Ctrl:1"]
|
"keys": ["Cmd:Ctrl:1"],
|
||||||
|
"commandStr": "/session 1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-2",
|
"command": "app:selectWorkspace-2",
|
||||||
"keys": ["Cmd:Ctrl:2"]
|
"keys": ["Cmd:Ctrl:2"],
|
||||||
|
"commandStr": "/session 2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-3",
|
"command": "app:selectWorkspace-3",
|
||||||
"keys": ["Cmd:Ctrl:3"]
|
"keys": ["Cmd:Ctrl:3"],
|
||||||
|
"commandStr": "/session 3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-4",
|
"command": "app:selectWorkspace-4",
|
||||||
"keys": ["Cmd:Ctrl:4"]
|
"keys": ["Cmd:Ctrl:4"],
|
||||||
|
"commandStr": "/session 4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-5",
|
"command": "app:selectWorkspace-5",
|
||||||
"keys": ["Cmd:Ctrl:5"]
|
"keys": ["Cmd:Ctrl:5"],
|
||||||
|
"commandStr": "/session 5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-6",
|
"command": "app:selectWorkspace-6",
|
||||||
"keys": ["Cmd:Ctrl:6"]
|
"keys": ["Cmd:Ctrl:6"],
|
||||||
|
"commandStr": "/session 6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-7",
|
"command": "app:selectWorkspace-7",
|
||||||
"keys": ["Cmd:Ctrl:7"]
|
"keys": ["Cmd:Ctrl:7"],
|
||||||
|
"commandStr": "/session 7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-8",
|
"command": "app:selectWorkspace-8",
|
||||||
"keys": ["Cmd:Ctrl:8"]
|
"keys": ["Cmd:Ctrl:8"],
|
||||||
|
"commandStr": "/session 8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:selectWorkspace-9",
|
"command": "app:selectWorkspace-9",
|
||||||
"keys": ["Cmd:Ctrl:9"]
|
"keys": ["Cmd:Ctrl:9"],
|
||||||
|
"commandStr": "/session 9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:toggleSidebar",
|
"command": "app:toggleSidebar",
|
||||||
@ -168,8 +183,9 @@
|
|||||||
"keys": ["Cmd:d"]
|
"keys": ["Cmd:d"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "app:bookmarkActiveLine",
|
"command": "app:openBookmarksView",
|
||||||
"keys": ["Cmd:b"]
|
"keys": ["Cmd:b"],
|
||||||
|
"commandStr": "/bookmarks:show"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "bookmarks:edit",
|
"command": "bookmarks:edit",
|
||||||
@ -213,7 +229,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cmdinput:openHistory",
|
"command": "cmdinput:openHistory",
|
||||||
"keys": ["Ctrl:r"]
|
"keys": ["Ctrl:r"],
|
||||||
|
"commandStr": "/history"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cmdinput:openAIChat",
|
"command": "cmdinput:openAIChat",
|
||||||
|
@ -193,7 +193,7 @@ class BookmarksView extends React.Component<{}, {}> {
|
|||||||
let bookmarks = GlobalModel.bookmarksModel.bookmarks;
|
let bookmarks = GlobalModel.bookmarksModel.bookmarks;
|
||||||
let bookmark: BookmarkType = null;
|
let bookmark: BookmarkType = null;
|
||||||
return (
|
return (
|
||||||
<MainView viewName="bookmarks" title="Bookmarks" onClose={this.handleClose}>
|
<MainView className="bookmarks-view" title="Bookmarks" onClose={this.handleClose}>
|
||||||
<div className="bookmarks-list">
|
<div className="bookmarks-list">
|
||||||
<For index="idx" each="bookmark" of={bookmarks}>
|
<For index="idx" each="bookmark" of={bookmarks}>
|
||||||
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark} />
|
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark} />
|
||||||
|
@ -44,7 +44,7 @@ class AlertModal extends React.Component<{}, {}> {
|
|||||||
<Markdown text={message?.message ?? ""} extraClassName="bottom-margin" />
|
<Markdown text={message?.message ?? ""} extraClassName="bottom-margin" />
|
||||||
</If>
|
</If>
|
||||||
<If condition={!message?.markdown}>{message?.message}</If>
|
<If condition={!message?.markdown}>{message?.message}</If>
|
||||||
<If condition={message.confirmflag}>
|
<If condition={message?.confirmflag}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={this.handleDontShowAgain}
|
onChange={this.handleDontShowAgain}
|
||||||
label={"Don't show me this again"}
|
label={"Don't show me this again"}
|
||||||
|
@ -126,6 +126,7 @@
|
|||||||
|
|
||||||
&:hover .line-actions {
|
&:hover .line-actions {
|
||||||
background-color: var(--line-actions-bg-color);
|
background-color: var(--line-actions-bg-color);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
.line-icon {
|
.line-icon {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
|
@ -263,7 +263,12 @@ const menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
|||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "services" },
|
{ role: "services" },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "hide" },
|
{
|
||||||
|
label: "Hide",
|
||||||
|
click: () => {
|
||||||
|
app.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
{ role: "hideOthers" },
|
{ role: "hideOthers" },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "quit" },
|
{ 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)
|
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
const url = event.url;
|
const url = event.url;
|
||||||
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
||||||
if (event.frame.name == "webview") {
|
if (event.frame.name == "webview") {
|
||||||
// "webview" links always open in new window
|
// "webview" links always open in new window
|
||||||
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
||||||
console.log("open external, frameNav", url);
|
console.log("open external, frameNav", url);
|
||||||
|
event.preventDefault();
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.frame.name == "pdfview" && url.startsWith("blob:file:///")) {
|
||||||
|
// allowed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
console.log("frame navigation canceled");
|
console.log("frame navigation canceled");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,6 +523,13 @@ electron.ipcMain.on("toggle-developer-tools", (event) => {
|
|||||||
event.returnValue = true;
|
event.returnValue = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.on("hide-window", (event) => {
|
||||||
|
if (MainWindow != null) {
|
||||||
|
MainWindow.hide();
|
||||||
|
}
|
||||||
|
event.returnValue = true;
|
||||||
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-id", (event) => {
|
electron.ipcMain.on("get-id", (event) => {
|
||||||
event.returnValue = instanceId + ":" + event.processId;
|
event.returnValue = instanceId + ":" + event.processId;
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
let { contextBridge, ipcRenderer } = require("electron");
|
let { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("api", {
|
contextBridge.exposeInMainWorld("api", {
|
||||||
|
hideWindow: () => ipc.Renderer.send("hide-window"),
|
||||||
toggleDeveloperTools: () => ipcRenderer.send("toggle-developer-tools"),
|
toggleDeveloperTools: () => ipcRenderer.send("toggle-developer-tools"),
|
||||||
getId: () => ipcRenderer.sendSync("get-id"),
|
getId: () => ipcRenderer.sendSync("get-id"),
|
||||||
getPlatform: () => ipcRenderer.sendSync("get-platform"),
|
getPlatform: () => ipcRenderer.sendSync("get-platform"),
|
||||||
|
@ -206,7 +206,7 @@ class BookmarksModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocKeyDown(e: any): void {
|
handleDocKeyDown(e: any): void {
|
||||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.editingBookmark.get() != null) {
|
if (this.editingBookmark.get() != null) {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { Model } from "./model";
|
import { Model } from "./model";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||||
|
|
||||||
class ClientSettingsViewModel {
|
class ClientSettingsViewModel {
|
||||||
globalModel: Model;
|
globalModel: Model;
|
||||||
@ -21,6 +22,15 @@ class ClientSettingsViewModel {
|
|||||||
this.globalModel.activeMainView.set("clientsettings");
|
this.globalModel.activeMainView.set("clientsettings");
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDocKeyDown(e: any): void {
|
||||||
|
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.closeView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ClientSettingsViewModel };
|
export { ClientSettingsViewModel };
|
||||||
|
@ -304,6 +304,10 @@ class CommandRunner {
|
|||||||
GlobalModel.clientSettingsViewModel.showClientSettingsView();
|
GlobalModel.clientSettingsViewModel.showClientSettingsView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncShellState() {
|
||||||
|
GlobalModel.submitCommand("sync", null, null, { nohist: "1" }, false);
|
||||||
|
}
|
||||||
|
|
||||||
historyView(params: HistorySearchParams) {
|
historyView(params: HistorySearchParams) {
|
||||||
let kwargs = { nohist: "1" };
|
let kwargs = { nohist: "1" };
|
||||||
kwargs["offset"] = String(params.offset);
|
kwargs["offset"] = String(params.offset);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { Model } from "./model";
|
import { Model } from "./model";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||||
|
|
||||||
class ConnectionsViewModel {
|
class ConnectionsViewModel {
|
||||||
globalModel: Model;
|
globalModel: Model;
|
||||||
@ -21,6 +22,15 @@ class ConnectionsViewModel {
|
|||||||
this.globalModel.activeMainView.set("connections");
|
this.globalModel.activeMainView.set("connections");
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDocKeyDown(e: any): void {
|
||||||
|
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.closeView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ConnectionsViewModel };
|
export { ConnectionsViewModel };
|
||||||
|
@ -291,7 +291,7 @@ class HistoryViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocKeyDown(e: any): void {
|
handleDocKeyDown(e: any): void {
|
||||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.closeView();
|
this.closeView();
|
||||||
|
@ -18,10 +18,11 @@ class ModalsModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popModal() {
|
popModal(callback?: () => void) {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.store.pop();
|
this.store.pop();
|
||||||
})();
|
})();
|
||||||
|
callback && callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ class Model {
|
|||||||
this.runUpdate(message, interactive);
|
this.runUpdate(message, interactive);
|
||||||
});
|
});
|
||||||
this.ws.reconnect();
|
this.ws.reconnect();
|
||||||
this.keybindManager = new KeybindManager();
|
this.keybindManager = new KeybindManager(this);
|
||||||
this.readConfigKeybindings();
|
this.readConfigKeybindings();
|
||||||
this.initSystemKeybindings();
|
this.initSystemKeybindings();
|
||||||
this.initAppKeybindings();
|
this.initAppKeybindings();
|
||||||
@ -222,46 +222,31 @@ class Model {
|
|||||||
getApi().toggleDeveloperTools();
|
getApi().toggleDeveloperTools();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
this.keybindManager.registerKeybinding("system", "electron", "system:minimizeWindow", (waveEvent) => {
|
||||||
|
getApi().hideWindow();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initAppKeybindings() {
|
initAppKeybindings() {
|
||||||
for (let index = 1; index <= 9; index++) {
|
for (let index = 1; index <= 9; index++) {
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, (waveEvent) => {
|
this.keybindManager.registerKeybinding("app", "model", "app:selectWorkspace-" + index, null);
|
||||||
this.onSwitchSessionCmd(index);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:focusCmdInput", (waveEvent) => {
|
this.keybindManager.registerKeybinding("app", "model", "app:focusCmdInput", (waveEvent) => {
|
||||||
console.log("focus cmd input callback");
|
|
||||||
this.onFocusCmdInputPressed();
|
this.onFocusCmdInputPressed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
this.keybindManager.registerKeybinding("app", "model", "app:openBookmarksView", null);
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:bookmarkActiveLine", (waveEvent) => {
|
this.keybindManager.registerKeybinding("app", "model", "app:openHistoryView", (waveEvent) => {
|
||||||
this.onBookmarkViewPressed();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:openHistory", (waveEvent) => {
|
|
||||||
this.onOpenHistoryPressed();
|
this.onOpenHistoryPressed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:openTabSearchModal", (waveEvent) => {
|
this.keybindManager.registerKeybinding("app", "model", "app:openTabSearchModal", (waveEvent) => {
|
||||||
this.onOpenTabSearchModalPressed();
|
this.onOpenTabSearchModalPressed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", null);
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:openConnectionsView", (waveEvent) => {
|
this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", null);
|
||||||
this.onOpenConnectionsViewPressed();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.keybindManager.registerKeybinding("app", "model", "app:openSettingsView", (waveEvent) => {
|
|
||||||
this.onOpenSettingsViewPressed();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): Model {
|
static getInstance(): Model {
|
||||||
@ -372,7 +357,6 @@ class Model {
|
|||||||
cancelAlert(): void {
|
cancelAlert(): void {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.alertMessage.set(null);
|
this.alertMessage.set(null);
|
||||||
this.modalsModel.popModal();
|
|
||||||
})();
|
})();
|
||||||
if (this.alertPromiseResolver != null) {
|
if (this.alertPromiseResolver != null) {
|
||||||
this.alertPromiseResolver(false);
|
this.alertPromiseResolver(false);
|
||||||
@ -493,7 +477,7 @@ class Model {
|
|||||||
if (this.alertMessage.get() != null) {
|
if (this.alertMessage.get() != null) {
|
||||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.cancelAlert();
|
this.modalsModel.popModal(() => this.cancelAlert());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
@ -503,6 +487,10 @@ class Model {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) {
|
||||||
|
this.modalsModel.popModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.activeMainView.get() == "bookmarks") {
|
if (this.activeMainView.get() == "bookmarks") {
|
||||||
this.bookmarksModel.handleDocKeyDown(e);
|
this.bookmarksModel.handleDocKeyDown(e);
|
||||||
}
|
}
|
||||||
@ -510,10 +498,10 @@ class Model {
|
|||||||
this.historyViewModel.handleDocKeyDown(e);
|
this.historyViewModel.handleDocKeyDown(e);
|
||||||
}
|
}
|
||||||
if (this.activeMainView.get() == "connections") {
|
if (this.activeMainView.get() == "connections") {
|
||||||
this.historyViewModel.handleDocKeyDown(e);
|
this.connectionViewModel.handleDocKeyDown(e);
|
||||||
}
|
}
|
||||||
if (this.activeMainView.get() == "clientsettings") {
|
if (this.activeMainView.get() == "clientsettings") {
|
||||||
this.historyViewModel.handleDocKeyDown(e);
|
this.clientSettingsViewModel.handleDocKeyDown(e);
|
||||||
} else {
|
} else {
|
||||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -521,9 +509,6 @@ class Model {
|
|||||||
this.showSessionView();
|
this.showSessionView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.clearModals()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const inputModel = this.inputModel;
|
const inputModel = this.inputModel;
|
||||||
inputModel.toggleInfoMsg();
|
inputModel.toggleInfoMsg();
|
||||||
if (inputModel.inputMode.get() != null) {
|
if (inputModel.inputMode.get() != null) {
|
||||||
@ -643,33 +628,6 @@ class Model {
|
|||||||
return screen.getTermWrap(line.lineid);
|
return screen.getTermWrap(line.lineid);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearModals(): boolean {
|
|
||||||
let didSomething = false;
|
|
||||||
mobx.action(() => {
|
|
||||||
if (this.screenSettingsModal.get()) {
|
|
||||||
this.screenSettingsModal.set(null);
|
|
||||||
didSomething = true;
|
|
||||||
}
|
|
||||||
if (this.sessionSettingsModal.get()) {
|
|
||||||
this.sessionSettingsModal.set(null);
|
|
||||||
didSomething = true;
|
|
||||||
}
|
|
||||||
if (this.screenSettingsModal.get()) {
|
|
||||||
this.screenSettingsModal.set(null);
|
|
||||||
didSomething = true;
|
|
||||||
}
|
|
||||||
if (this.clientSettingsModal.get()) {
|
|
||||||
this.clientSettingsModal.set(false);
|
|
||||||
didSomething = true;
|
|
||||||
}
|
|
||||||
if (this.lineSettingsModal.get()) {
|
|
||||||
this.lineSettingsModal.set(null);
|
|
||||||
didSomething = true;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return didSomething;
|
|
||||||
}
|
|
||||||
|
|
||||||
restartWaveSrv(): void {
|
restartWaveSrv(): void {
|
||||||
getApi().restartWaveSrv();
|
getApi().restartWaveSrv();
|
||||||
}
|
}
|
||||||
@ -1023,6 +981,12 @@ class Model {
|
|||||||
console.warn("invalid bookmarksview in update:", update.mainview);
|
console.warn("invalid bookmarksview in update:", update.mainview);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "clientsettings":
|
||||||
|
this.activeMainView.set("clientsettings");
|
||||||
|
break;
|
||||||
|
case "connections":
|
||||||
|
this.activeMainView.set("connections");
|
||||||
|
break;
|
||||||
case "plugins":
|
case "plugins":
|
||||||
this.pluginsModel.showPluginsView();
|
this.pluginsModel.showPluginsView();
|
||||||
break;
|
break;
|
||||||
@ -1086,6 +1050,9 @@ class Model {
|
|||||||
this.activeMainView.set("session");
|
this.activeMainView.set("session");
|
||||||
this.deactivateScreenLines();
|
this.deactivateScreenLines();
|
||||||
this.ws.watchScreen(newActiveSessionId, newActiveScreenId);
|
this.ws.watchScreen(newActiveSessionId, newActiveScreenId);
|
||||||
|
setTimeout(() => {
|
||||||
|
GlobalCommandRunner.syncShellState();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("unknown update", genUpdate);
|
console.warn("unknown update", genUpdate);
|
||||||
@ -1583,12 +1550,15 @@ class Model {
|
|||||||
return remote.remotecanonicalname;
|
return remote.remotecanonicalname;
|
||||||
}
|
}
|
||||||
|
|
||||||
readRemoteFile(screenId: string, lineId: string, path: string): Promise<ExtFile> {
|
readRemoteFile(screenId: string, lineId: string, path: string, mimetype?: string): Promise<ExtFile> {
|
||||||
const urlParams = {
|
const urlParams: Record<string, string> = {
|
||||||
screenid: screenId,
|
screenid: screenId,
|
||||||
lineid: lineId,
|
lineid: lineId,
|
||||||
path: path,
|
path: path,
|
||||||
};
|
};
|
||||||
|
if (mimetype != null) {
|
||||||
|
urlParams["mimetype"] = mimetype;
|
||||||
|
}
|
||||||
const usp = new URLSearchParams(urlParams);
|
const usp = new URLSearchParams(urlParams);
|
||||||
const url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
const url = new URL(this.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
||||||
const fetchHeaders = this.getFetchHeaders();
|
const fetchHeaders = this.getFetchHeaders();
|
||||||
|
@ -48,12 +48,15 @@ class SimpleImageRenderer extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<div className="image-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
|
<div className="image-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
|
||||||
<div className="load-error-text">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.objUrl == null) {
|
if (this.objUrl == null) {
|
||||||
|
if (dataBlob.name?.endsWith(".svg")) {
|
||||||
|
dataBlob = new Blob([dataBlob], { type: "image/svg+xml" }) as ExtBlob;
|
||||||
|
}
|
||||||
this.objUrl = URL.createObjectURL(dataBlob);
|
this.objUrl = URL.createObjectURL(dataBlob);
|
||||||
}
|
}
|
||||||
let opts = this.props.opts;
|
let opts = this.props.opts;
|
||||||
|
14
src/plugins/media/media.less
Normal file
14
src/plugins/media/media.less
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
60
src/plugins/media/media.tsx
Normal file
60
src/plugins/media/media.tsx
Normal file
@ -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 };
|
7
src/plugins/pdf/pdf.less
Normal file
7
src/plugins/pdf/pdf.less
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.pdf-renderer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: var(--termpad);
|
||||||
|
}
|
49
src/plugins/pdf/pdf.tsx
Normal file
49
src/plugins/pdf/pdf.tsx
Normal 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], 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 { SimpleMustacheRenderer } from "./mustache/mustache";
|
||||||
import { CSVRenderer } from "./csv/csv";
|
import { CSVRenderer } from "./csv/csv";
|
||||||
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
|
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
|
||||||
|
import { SimplePdfRenderer } from "./pdf/pdf";
|
||||||
|
import { SimpleMediaRenderer } from "./media/media";
|
||||||
import { isBlank } from "@/util/util";
|
import { isBlank } from "@/util/util";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
|
|
||||||
@ -78,6 +80,26 @@ const PluginConfigs: RendererPluginType[] = [
|
|||||||
mimeTypes: ["image/*"],
|
mimeTypes: ["image/*"],
|
||||||
simpleComponent: SimpleImageRenderer,
|
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 {
|
class PluginModelClass {
|
||||||
|
1
src/types/custom.d.ts
vendored
1
src/types/custom.d.ts
vendored
@ -881,6 +881,7 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ElectronApi = {
|
type ElectronApi = {
|
||||||
|
hideWindow: () => void;
|
||||||
toggleDeveloperTools: () => void;
|
toggleDeveloperTools: () => void;
|
||||||
getId: () => string;
|
getId: () => string;
|
||||||
getIsDev: () => boolean;
|
getIsDev: () => boolean;
|
||||||
|
@ -4,7 +4,7 @@ import * as electron from "electron";
|
|||||||
import { parse } from "node:path";
|
import { parse } from "node:path";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import defaultKeybindingsFile from "../../assets/default-keybindings.json";
|
import defaultKeybindingsFile from "../../assets/default-keybindings.json";
|
||||||
const defaultKeybindings: KeybindConfig = defaultKeybindingsFile;
|
const defaultKeybindings: KeybindConfigArray = defaultKeybindingsFile;
|
||||||
|
|
||||||
type KeyPressDecl = {
|
type KeyPressDecl = {
|
||||||
mods: {
|
mods: {
|
||||||
@ -24,12 +24,18 @@ const KeyTypeKey = "key";
|
|||||||
const KeyTypeCode = "code";
|
const KeyTypeCode = "code";
|
||||||
|
|
||||||
type KeybindCallback = (event: WaveKeyboardEvent) => boolean;
|
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 = {
|
type Keybind = {
|
||||||
domain: string;
|
domain: string;
|
||||||
keybinding: string;
|
keybinding: string;
|
||||||
|
action: string;
|
||||||
callback: KeybindCallback;
|
callback: KeybindCallback;
|
||||||
|
commandStr: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeybindLevels = ["system", "modal", "app", "pane", "plugin"];
|
const KeybindLevels = ["system", "modal", "app", "pane", "plugin"];
|
||||||
@ -38,11 +44,12 @@ class KeybindManager {
|
|||||||
domainCallbacks: Map<string, KeybindCallback>;
|
domainCallbacks: Map<string, KeybindCallback>;
|
||||||
levelMap: Map<string, Array<Keybind>>;
|
levelMap: Map<string, Array<Keybind>>;
|
||||||
levelArray: Array<string>;
|
levelArray: Array<string>;
|
||||||
keyDescriptionsMap: Map<string, Array<string>>;
|
keyDescriptionsMap: Map<string, KeybindConfig>;
|
||||||
userKeybindings: KeybindConfig;
|
userKeybindings: KeybindConfigArray;
|
||||||
userKeybindingError: OV<string>;
|
userKeybindingError: OV<string>;
|
||||||
|
globalModel: any;
|
||||||
|
|
||||||
constructor() {
|
constructor(GlobalModel: any) {
|
||||||
this.levelMap = new Map();
|
this.levelMap = new Map();
|
||||||
this.domainCallbacks = new Map();
|
this.domainCallbacks = new Map();
|
||||||
this.levelArray = KeybindLevels;
|
this.levelArray = KeybindLevels;
|
||||||
@ -53,6 +60,7 @@ class KeybindManager {
|
|||||||
this.userKeybindingError = mobx.observable.box(null, {
|
this.userKeybindingError = mobx.observable.box(null, {
|
||||||
name: "keyutil-userKeybindingError",
|
name: "keyutil-userKeybindingError",
|
||||||
});
|
});
|
||||||
|
this.globalModel = GlobalModel;
|
||||||
this.initKeyDescriptionsMap();
|
this.initKeyDescriptionsMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +71,7 @@ class KeybindManager {
|
|||||||
let newKeyDescriptions = new Map();
|
let newKeyDescriptions = new Map();
|
||||||
for (let index = 0; index < defaultKeybindings.length; index++) {
|
for (let index = 0; index < defaultKeybindings.length; index++) {
|
||||||
let curKeybind = defaultKeybindings[index];
|
let curKeybind = defaultKeybindings[index];
|
||||||
newKeyDescriptions.set(curKeybind.command, curKeybind.keys);
|
newKeyDescriptions.set(curKeybind.command, curKeybind);
|
||||||
}
|
}
|
||||||
let curUserCommand = "";
|
let curUserCommand = "";
|
||||||
if (this.userKeybindings != null && this.userKeybindings instanceof Array) {
|
if (this.userKeybindings != null && this.userKeybindings instanceof Array) {
|
||||||
@ -85,7 +93,15 @@ class KeybindManager {
|
|||||||
throw new Error("invalid keybind key");
|
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) {
|
} catch (e) {
|
||||||
let userError = `${curUserCommand} is invalid: error: ${e}`;
|
let userError = `${curUserCommand} is invalid: error: ${e}`;
|
||||||
@ -98,16 +114,48 @@ class KeybindManager {
|
|||||||
this.keyDescriptionsMap = newKeyDescriptions;
|
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 {
|
processLevel(nativeEvent: any, event: WaveKeyboardEvent, keybindsArray: Array<Keybind>): boolean {
|
||||||
// iterate through keybinds in backwards order
|
// iterate through keybinds in backwards order
|
||||||
for (let index = keybindsArray.length - 1; index >= 0; index--) {
|
for (let index = keybindsArray.length - 1; index >= 0; index--) {
|
||||||
let curKeybind = keybindsArray[index];
|
let curKeybind = keybindsArray[index];
|
||||||
if (this.checkKeyPressed(event, curKeybind.keybinding)) {
|
if (this.checkKeyPressed(event, curKeybind.keybinding)) {
|
||||||
let shouldReturn = false;
|
let shouldReturn = false;
|
||||||
|
let shouldRunCommand = true;
|
||||||
if (curKeybind.callback != null) {
|
if (curKeybind.callback != null) {
|
||||||
shouldReturn = curKeybind.callback(event);
|
shouldReturn = curKeybind.callback(event);
|
||||||
|
shouldRunCommand = false;
|
||||||
}
|
}
|
||||||
if (!shouldReturn && this.domainCallbacks.has(curKeybind.domain)) {
|
if (!shouldReturn && this.domainCallbacks.has(curKeybind.domain)) {
|
||||||
|
shouldRunCommand = false;
|
||||||
let curDomainCallback = this.domainCallbacks.get(curKeybind.domain);
|
let curDomainCallback = this.domainCallbacks.get(curKeybind.domain);
|
||||||
if (curDomainCallback != null) {
|
if (curDomainCallback != null) {
|
||||||
shouldReturn = curDomainCallback(event);
|
shouldReturn = curDomainCallback(event);
|
||||||
@ -115,6 +163,9 @@ class KeybindManager {
|
|||||||
console.log("domain callback for ", curKeybind.domain, " is null. This should never happen");
|
console.log("domain callback for ", curKeybind.domain, " is null. This should never happen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (shouldRunCommand) {
|
||||||
|
shouldReturn = this.runSlashCommand(curKeybind);
|
||||||
|
}
|
||||||
if (shouldReturn) {
|
if (shouldReturn) {
|
||||||
nativeEvent.preventDefault();
|
nativeEvent.preventDefault();
|
||||||
nativeEvent.stopPropagation();
|
nativeEvent.stopPropagation();
|
||||||
@ -269,7 +320,7 @@ class KeybindManager {
|
|||||||
if (!this.keyDescriptionsMap.has(keyDescription)) {
|
if (!this.keyDescriptionsMap.has(keyDescription)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let keyPressArray = this.keyDescriptionsMap.get(keyDescription);
|
let keyPressArray = this.keyDescriptionsMap.get(keyDescription).keys;
|
||||||
for (let index = 0; index < keyPressArray.length; index++) {
|
for (let index = 0; index < keyPressArray.length; index++) {
|
||||||
let curKeyPress = keyPressArray[index];
|
let curKeyPress = keyPressArray[index];
|
||||||
let pressed = checkKeyPressed(event, curKeyPress);
|
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))
|
sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("run packets from server must have a CK: %v", err))
|
||||||
}
|
}
|
||||||
if runPacket.Detached {
|
if runPacket.Detached {
|
||||||
cmd, startPk, err := shexec.RunCommandDetached(runPacket, sender)
|
sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("detached mode not supported"))
|
||||||
if err != nil {
|
|
||||||
sender.SendErrorResponse(runPacket.ReqId, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sender.SendPacket(startPk)
|
|
||||||
sender.Close()
|
|
||||||
sender.WaitForDone()
|
|
||||||
cmd.DetachedWait(startPk)
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
shexec.IgnoreSigPipe()
|
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"
|
DataEndPacketStr = "dataend"
|
||||||
ResponsePacketStr = "resp" // rpc-response
|
ResponsePacketStr = "resp" // rpc-response
|
||||||
DonePacketStr = "done"
|
DonePacketStr = "done"
|
||||||
CmdErrorPacketStr = "cmderror" // command
|
|
||||||
MessagePacketStr = "message"
|
MessagePacketStr = "message"
|
||||||
GetCmdPacketStr = "getcmd" // rpc
|
GetCmdPacketStr = "getcmd" // rpc
|
||||||
UntailCmdPacketStr = "untailcmd" // rpc
|
UntailCmdPacketStr = "untailcmd" // rpc
|
||||||
CdPacketStr = "cd" // rpc
|
CdPacketStr = "cd" // rpc
|
||||||
CmdDataPacketStr = "cmddata" // rpc-response
|
|
||||||
RawPacketStr = "raw"
|
RawPacketStr = "raw"
|
||||||
SpecialInputPacketStr = "sinput" // command
|
SpecialInputPacketStr = "sinput" // command
|
||||||
CompGenPacketStr = "compgen" // rpc
|
CompGenPacketStr = "compgen" // rpc
|
||||||
@ -90,7 +88,6 @@ func init() {
|
|||||||
TypeStrToFactory[PingPacketStr] = reflect.TypeOf(PingPacketType{})
|
TypeStrToFactory[PingPacketStr] = reflect.TypeOf(PingPacketType{})
|
||||||
TypeStrToFactory[ResponsePacketStr] = reflect.TypeOf(ResponsePacketType{})
|
TypeStrToFactory[ResponsePacketStr] = reflect.TypeOf(ResponsePacketType{})
|
||||||
TypeStrToFactory[DonePacketStr] = reflect.TypeOf(DonePacketType{})
|
TypeStrToFactory[DonePacketStr] = reflect.TypeOf(DonePacketType{})
|
||||||
TypeStrToFactory[CmdErrorPacketStr] = reflect.TypeOf(CmdErrorPacketType{})
|
|
||||||
TypeStrToFactory[MessagePacketStr] = reflect.TypeOf(MessagePacketType{})
|
TypeStrToFactory[MessagePacketStr] = reflect.TypeOf(MessagePacketType{})
|
||||||
TypeStrToFactory[CmdStartPacketStr] = reflect.TypeOf(CmdStartPacketType{})
|
TypeStrToFactory[CmdStartPacketStr] = reflect.TypeOf(CmdStartPacketType{})
|
||||||
TypeStrToFactory[CmdDonePacketStr] = reflect.TypeOf(CmdDonePacketType{})
|
TypeStrToFactory[CmdDonePacketStr] = reflect.TypeOf(CmdDonePacketType{})
|
||||||
@ -98,7 +95,6 @@ func init() {
|
|||||||
TypeStrToFactory[UntailCmdPacketStr] = reflect.TypeOf(UntailCmdPacketType{})
|
TypeStrToFactory[UntailCmdPacketStr] = reflect.TypeOf(UntailCmdPacketType{})
|
||||||
TypeStrToFactory[InitPacketStr] = reflect.TypeOf(InitPacketType{})
|
TypeStrToFactory[InitPacketStr] = reflect.TypeOf(InitPacketType{})
|
||||||
TypeStrToFactory[CdPacketStr] = reflect.TypeOf(CdPacketType{})
|
TypeStrToFactory[CdPacketStr] = reflect.TypeOf(CdPacketType{})
|
||||||
TypeStrToFactory[CmdDataPacketStr] = reflect.TypeOf(CmdDataPacketType{})
|
|
||||||
TypeStrToFactory[RawPacketStr] = reflect.TypeOf(RawPacketType{})
|
TypeStrToFactory[RawPacketStr] = reflect.TypeOf(RawPacketType{})
|
||||||
TypeStrToFactory[SpecialInputPacketStr] = reflect.TypeOf(SpecialInputPacketType{})
|
TypeStrToFactory[SpecialInputPacketStr] = reflect.TypeOf(SpecialInputPacketType{})
|
||||||
TypeStrToFactory[DataPacketStr] = reflect.TypeOf(DataPacketType{})
|
TypeStrToFactory[DataPacketStr] = reflect.TypeOf(DataPacketType{})
|
||||||
@ -128,7 +124,6 @@ func init() {
|
|||||||
|
|
||||||
var _ RpcResponsePacketType = (*CmdStartPacketType)(nil)
|
var _ RpcResponsePacketType = (*CmdStartPacketType)(nil)
|
||||||
var _ RpcResponsePacketType = (*ResponsePacketType)(nil)
|
var _ RpcResponsePacketType = (*ResponsePacketType)(nil)
|
||||||
var _ RpcResponsePacketType = (*CmdDataPacketType)(nil)
|
|
||||||
var _ RpcResponsePacketType = (*StreamFileResponseType)(nil)
|
var _ RpcResponsePacketType = (*StreamFileResponseType)(nil)
|
||||||
var _ RpcResponsePacketType = (*FileDataPacketType)(nil)
|
var _ RpcResponsePacketType = (*FileDataPacketType)(nil)
|
||||||
var _ RpcResponsePacketType = (*WriteFileReadyPacketType)(nil)
|
var _ RpcResponsePacketType = (*WriteFileReadyPacketType)(nil)
|
||||||
@ -155,36 +150,6 @@ func MakePacket(packetType string) (PacketType, error) {
|
|||||||
return rtn.Interface().(PacketType), nil
|
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 PingPacketType struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
@ -830,28 +795,6 @@ type BarePacketType struct {
|
|||||||
Type string `json:"type"`
|
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 WriteFilePacketType struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ReqId string `json:"reqid"`
|
ReqId string `json:"reqid"`
|
||||||
@ -1074,10 +1017,6 @@ func SendPacket(w io.Writer, packet PacketType) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendCmdError(w io.Writer, ck base.CommandKey, err error) error {
|
|
||||||
return SendPacket(w, MakeCmdErrorPacket(ck, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketSender struct {
|
type PacketSender struct {
|
||||||
Lock *sync.Mutex
|
Lock *sync.Mutex
|
||||||
SendCh chan PacketType
|
SendCh chan PacketType
|
||||||
@ -1197,10 +1136,6 @@ func (sender *PacketSender) SendPacket(pk PacketType) error {
|
|||||||
return nil
|
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 {
|
func (sender *PacketSender) SendErrorResponse(reqId string, err error) error {
|
||||||
pk := MakeErrorResponsePacket(reqId, err)
|
pk := MakeErrorResponsePacket(reqId, err)
|
||||||
return sender.SendPacket(pk)
|
return sender.SendPacket(pk)
|
||||||
@ -1222,17 +1157,13 @@ type UnknownPacketReporter interface {
|
|||||||
type DefaultUPR struct{}
|
type DefaultUPR struct{}
|
||||||
|
|
||||||
func (DefaultUPR) UnknownPacket(pk PacketType) {
|
func (DefaultUPR) UnknownPacket(pk PacketType) {
|
||||||
if pk.GetType() == CmdErrorPacketStr {
|
if pk.GetType() == RawPacketStr {
|
||||||
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 {
|
|
||||||
rawPacket := pk.(*RawPacketType)
|
rawPacket := pk.(*RawPacketType)
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", rawPacket.Data)
|
fmt.Fprintf(os.Stderr, "%s\n", rawPacket.Data)
|
||||||
} else if pk.GetType() == CmdStartPacketStr {
|
} else if pk.GetType() == CmdStartPacketStr {
|
||||||
return // do nothing
|
return // do nothing
|
||||||
} else {
|
} 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]
|
cproc := m.ClientMap[ck]
|
||||||
m.Lock.Unlock()
|
m.Lock.Unlock()
|
||||||
if cproc == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
cproc.Input.SendPacket(pk)
|
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 {
|
func GetExitCode(err error) int {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 0
|
return 0
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/cmdrunner"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
"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/releasechecker"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
|
||||||
@ -73,7 +74,6 @@ var BuildTime = "0"
|
|||||||
|
|
||||||
var GlobalLock = &sync.Mutex{}
|
var GlobalLock = &sync.Mutex{}
|
||||||
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
|
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
|
||||||
var GlobalAuthKey string
|
|
||||||
var shutdownOnce sync.Once
|
var shutdownOnce sync.Once
|
||||||
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
|
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
state := getWSState(clientId)
|
state := getWSState(clientId)
|
||||||
if state == nil {
|
if state == nil {
|
||||||
state = scws.MakeWSState(clientId, GlobalAuthKey)
|
state = scws.MakeWSState(clientId, scbase.WaveAuthKey)
|
||||||
state.ReplaceShell(shell)
|
state.ReplaceShell(shell)
|
||||||
setWSState(state)
|
setWSState(state)
|
||||||
} else {
|
} else {
|
||||||
@ -686,7 +686,7 @@ func AuthKeyMiddleWare(next http.Handler) http.Handler {
|
|||||||
w.Write([]byte("no x-authkey header"))
|
w.Write([]byte("no x-authkey header"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if reqAuthKey != GlobalAuthKey {
|
if reqAuthKey != scbase.WaveAuthKey {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
w.Write([]byte("x-authkey header is invalid"))
|
w.Write([]byte("x-authkey header is invalid"))
|
||||||
return
|
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 {
|
func AuthKeyWrap(fn WebFnType) WebFnType {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
reqAuthKey := r.Header.Get("X-AuthKey")
|
reqAuthKey := r.Header.Get("X-AuthKey")
|
||||||
@ -703,7 +732,7 @@ func AuthKeyWrap(fn WebFnType) WebFnType {
|
|||||||
w.Write([]byte("no x-authkey header"))
|
w.Write([]byte("no x-authkey header"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if reqAuthKey != GlobalAuthKey {
|
if reqAuthKey != scbase.WaveAuthKey {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
w.Write([]byte("x-authkey header is invalid"))
|
w.Write([]byte("x-authkey header is invalid"))
|
||||||
return
|
return
|
||||||
@ -860,12 +889,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authKey, err := scbase.ReadWaveAuthKey()
|
err = scbase.InitializeWaveAuthKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[error] %v\n", err)
|
log.Printf("[error] %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GlobalAuthKey = authKey
|
|
||||||
err = sstore.TryMigrateUp()
|
err = sstore.TryMigrateUp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[error] migrate up: %v\n", err)
|
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/get-client-data", AuthKeyWrap(HandleGetClientData))
|
||||||
gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize))
|
gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize))
|
||||||
gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState))
|
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")
|
gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST")
|
||||||
configPath := path.Join(scbase.GetWaveHomeDir(), "config") + "/"
|
configPath := path.Join(scbase.GetWaveHomeDir(), "config") + "/"
|
||||||
log.Printf("[wave] config path: %q\n", configPath)
|
log.Printf("[wave] config path: %q\n", configPath)
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
"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/releasechecker"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
|
||||||
@ -171,6 +172,9 @@ func init() {
|
|||||||
registerCmdFn("reset:cwd", ResetCwdCommand)
|
registerCmdFn("reset:cwd", ResetCwdCommand)
|
||||||
registerCmdFn("signal", SignalCommand)
|
registerCmdFn("signal", SignalCommand)
|
||||||
registerCmdFn("sync", SyncCommand)
|
registerCmdFn("sync", SyncCommand)
|
||||||
|
registerCmdFn("sleep", SleepCommand)
|
||||||
|
|
||||||
|
registerCmdFn("mainview", MainViewCommand)
|
||||||
|
|
||||||
registerCmdFn("session", SessionCommand)
|
registerCmdFn("session", SessionCommand)
|
||||||
registerCmdFn("session:open", SessionOpenCommand)
|
registerCmdFn("session:open", SessionOpenCommand)
|
||||||
@ -276,6 +280,8 @@ func init() {
|
|||||||
registerCmdFn("imageview", ImageViewCommand)
|
registerCmdFn("imageview", ImageViewCommand)
|
||||||
registerCmdFn("mdview", MarkdownViewCommand)
|
registerCmdFn("mdview", MarkdownViewCommand)
|
||||||
registerCmdFn("markdownview", MarkdownViewCommand)
|
registerCmdFn("markdownview", MarkdownViewCommand)
|
||||||
|
registerCmdFn("pdfview", PdfViewCommand)
|
||||||
|
registerCmdFn("mediaview", MediaViewCommand)
|
||||||
|
|
||||||
registerCmdFn("csvview", CSVViewCommand)
|
registerCmdFn("csvview", CSVViewCommand)
|
||||||
}
|
}
|
||||||
@ -493,7 +499,7 @@ func getEvalDepth(ctx context.Context) int {
|
|||||||
func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("/run error: %w", err)
|
return nil, fmt.Errorf("/sync error: %w", err)
|
||||||
}
|
}
|
||||||
runPacket := packet.MakeRunPacket()
|
runPacket := packet.MakeRunPacket()
|
||||||
runPacket.ReqId = uuid.New().String()
|
runPacket.ReqId = uuid.New().String()
|
||||||
@ -510,22 +516,21 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
|
|||||||
SessionId: ids.SessionId,
|
SessionId: ids.SessionId,
|
||||||
ScreenId: ids.ScreenId,
|
ScreenId: ids.ScreenId,
|
||||||
RemotePtr: ids.Remote.RemotePtr,
|
RemotePtr: ids.Remote.RemotePtr,
|
||||||
|
Ephemeral: true,
|
||||||
}
|
}
|
||||||
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
_, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
defer callback()
|
defer callback()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cmd.RawCmdStr = pk.GetRawStr()
|
update := scbus.MakeUpdatePacket()
|
||||||
update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal", nil)
|
update.AddUpdate(sstore.InfoMsgType{
|
||||||
if err != nil {
|
InfoMsg: "syncing state",
|
||||||
return nil, err
|
TimeoutMs: 2000,
|
||||||
}
|
})
|
||||||
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
|
return update, nil
|
||||||
scbus.MainUpdateBus.DoScreenUpdate(ids.ScreenId, update)
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) {
|
func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) {
|
||||||
@ -1175,7 +1180,8 @@ func deferWriteCmdStatus(ctx context.Context, cmd *sstore.CmdType, startTime tim
|
|||||||
donePk.Ts = time.Now().UnixMilli()
|
donePk.Ts = time.Now().UnixMilli()
|
||||||
donePk.ExitCode = exitCode
|
donePk.ExitCode = exitCode
|
||||||
donePk.DurationMs = duration.Milliseconds()
|
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 {
|
if err != nil {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
||||||
@ -2556,7 +2562,8 @@ func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt
|
|||||||
donePk.Ts = time.Now().UnixMilli()
|
donePk.Ts = time.Now().UnixMilli()
|
||||||
donePk.ExitCode = exitCode
|
donePk.ExitCode = exitCode
|
||||||
donePk.DurationMs = duration.Milliseconds()
|
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 {
|
if err != nil {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
||||||
@ -2715,7 +2722,8 @@ func doOpenAIStreamCompletion(cmd *sstore.CmdType, clientId string, opts *sstore
|
|||||||
donePk.Ts = time.Now().UnixMilli()
|
donePk.Ts = time.Now().UnixMilli()
|
||||||
donePk.ExitCode = exitCode
|
donePk.ExitCode = exitCode
|
||||||
donePk.DurationMs = duration.Milliseconds()
|
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 {
|
if err != nil {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
||||||
@ -3565,6 +3573,46 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
|||||||
return update, nil
|
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) {
|
func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||||
ids, err := resolveUiIds(ctx, pk, 0)
|
ids, err := resolveUiIds(ctx, pk, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -4813,6 +4861,38 @@ func CSVViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
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 {
|
if len(pk.Args) == 0 {
|
||||||
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
||||||
}
|
}
|
||||||
@ -4834,7 +4914,59 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
|
|||||||
lineState := make(map[string]any)
|
lineState := make(map[string]any)
|
||||||
lineState[sstore.LineState_Source] = "file"
|
lineState[sstore.LineState_Source] = "file"
|
||||||
lineState[sstore.LineState_File] = pk.Args[0]
|
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
|
||||||
|
}
|
||||||
|
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeReadFileUrl(screenId string, lineId string, filePath string) (string, error) {
|
||||||
|
qvals := make(url.Values)
|
||||||
|
qvals.Set("screenid", screenId)
|
||||||
|
qvals.Set("lineid", lineId)
|
||||||
|
qvals.Set("path", filePath)
|
||||||
|
qvals.Set("nonce", uuid.New().String())
|
||||||
|
hmacStr, err := promptenc.ComputeUrlHmac([]byte(scbase.WaveAuthKey), "/api/read-file", qvals)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error computing hmac-url: %v", err)
|
||||||
|
}
|
||||||
|
qvals.Set("hmac", hmacStr)
|
||||||
|
return "/api/read-file?" + qvals.Encode(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MediaViewCommand(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))
|
||||||
|
}
|
||||||
|
fileName := 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), fileName)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// compute hmac read-file URL
|
||||||
|
readFileUrl, err := MakeReadFileUrl(ids.ScreenId, cmd.LineId, fileName)
|
||||||
|
if err != nil {
|
||||||
|
// TODO tricky error since the command was a success, but we can't show the output
|
||||||
|
return nil, fmt.Errorf("error making read-file url: %v", err)
|
||||||
|
}
|
||||||
|
// set the line state
|
||||||
|
lineState := make(map[string]any)
|
||||||
|
lineState[sstore.LineState_FileUrl] = readFileUrl
|
||||||
|
lineState[sstore.LineState_File] = fileName
|
||||||
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "media", lineState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO tricky error since the command was a success, but we can't show the output
|
// TODO tricky error since the command was a success, but we can't show the output
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -35,6 +35,8 @@ var BareMetaCmds = []BareMetaCmdDecl{
|
|||||||
{"markdownview", "markdownview"},
|
{"markdownview", "markdownview"},
|
||||||
{"mdview", "markdownview"},
|
{"mdview", "markdownview"},
|
||||||
{"csvview", "csvview"},
|
{"csvview", "csvview"},
|
||||||
|
{"pdfview", "pdfview"},
|
||||||
|
{"mediaview", "mediaview"},
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
52
wavesrv/pkg/promptenc/hmac.go
Normal file
52
wavesrv/pkg/promptenc/hmac.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package promptenc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ComputeUrlHmac(key []byte, baseUrl string, qvals url.Values) (string, error) {
|
||||||
|
if !qvals.Has("nonce") {
|
||||||
|
return "", fmt.Errorf("nonce is required for hmac")
|
||||||
|
}
|
||||||
|
if qvals.Has("hmac") {
|
||||||
|
return "", fmt.Errorf("hmac is already present")
|
||||||
|
}
|
||||||
|
encStr := baseUrl + "?" + qvals.Encode()
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(encStr))
|
||||||
|
rtn := mac.Sum(nil)
|
||||||
|
return base64.URLEncoding.EncodeToString(rtn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyUrlValues(src url.Values) url.Values {
|
||||||
|
rtn := make(url.Values)
|
||||||
|
for k, v := range src {
|
||||||
|
rtn[k] = v
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateUrlHmac(key []byte, baseUrl string, qvalsOrig url.Values) (bool, error) {
|
||||||
|
qvals := copyUrlValues(qvalsOrig)
|
||||||
|
hmacStr := qvals.Get("hmac")
|
||||||
|
if hmacStr == "" {
|
||||||
|
return false, fmt.Errorf("no hmac key found")
|
||||||
|
}
|
||||||
|
qvals.Del("hmac")
|
||||||
|
encStr := baseUrl + "?" + qvals.Encode()
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(encStr))
|
||||||
|
expected := mac.Sum(nil)
|
||||||
|
actual, err := base64.URLEncoding.DecodeString(hmacStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding hmac: %w", err)
|
||||||
|
}
|
||||||
|
return hmac.Equal(expected, actual), nil
|
||||||
|
}
|
@ -19,6 +19,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -153,16 +154,19 @@ type MShellProc struct {
|
|||||||
InstallCancelFn context.CancelFunc
|
InstallCancelFn context.CancelFunc
|
||||||
InstallErr error
|
InstallErr error
|
||||||
|
|
||||||
RunningCmds map[base.CommandKey]RunCmdType
|
RunningCmds map[base.CommandKey]*RunCmdType
|
||||||
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
|
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
|
||||||
Client *ssh.Client
|
Client *ssh.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunCmdType struct {
|
type RunCmdType struct {
|
||||||
|
CK base.CommandKey
|
||||||
SessionId string
|
SessionId string
|
||||||
ScreenId string
|
ScreenId string
|
||||||
RemotePtr sstore.RemotePtrType
|
RemotePtr sstore.RemotePtrType
|
||||||
RunPacket *packet.RunPacketType
|
RunPacket *packet.RunPacketType
|
||||||
|
Ephemeral bool
|
||||||
|
EphCancled atomic.Bool // only for Ephemeral commands, if true, then the command result should be discarded
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoteRuntimeState = sstore.RemoteRuntimeState
|
type RemoteRuntimeState = sstore.RemoteRuntimeState
|
||||||
@ -690,7 +694,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
|
|||||||
Status: StatusDisconnected,
|
Status: StatusDisconnected,
|
||||||
PtyBuffer: buf,
|
PtyBuffer: buf,
|
||||||
InstallStatus: StatusDisconnected,
|
InstallStatus: StatusDisconnected,
|
||||||
RunningCmds: make(map[base.CommandKey]RunCmdType),
|
RunningCmds: make(map[base.CommandKey]*RunCmdType),
|
||||||
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
|
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
|
||||||
StateMap: server.MakeShellStateMap(),
|
StateMap: server.MakeShellStateMap(),
|
||||||
DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](),
|
DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](),
|
||||||
@ -1781,6 +1785,8 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns (ok, currentPSC)
|
// returns (ok, currentPSC)
|
||||||
|
// if ok is true, currentPSC will be nil
|
||||||
|
// if ok is false, currentPSC will be the existing pending state command (not nil)
|
||||||
func (msh *MShellProc) testAndSetPendingStateCmd(screenId string, rptr sstore.RemotePtrType, newCK *base.CommandKey) (bool, *base.CommandKey) {
|
func (msh *MShellProc) testAndSetPendingStateCmd(screenId string, rptr sstore.RemotePtrType, newCK *base.CommandKey) (bool, *base.CommandKey) {
|
||||||
key := pendingStateKey{ScreenId: screenId, RemotePtr: rptr}
|
key := pendingStateKey{ScreenId: screenId, RemotePtr: rptr}
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
@ -1839,6 +1845,10 @@ type RunCommandOpts struct {
|
|||||||
|
|
||||||
// set to true to skip creating the pty file (for restarted commands)
|
// set to true to skip creating the pty file (for restarted commands)
|
||||||
NoCreateCmdPtyFile bool
|
NoCreateCmdPtyFile bool
|
||||||
|
|
||||||
|
// this command will not go into the DB, and will not have a ptyout file created
|
||||||
|
// forces special packet handling (sets RunCommandType.Ephemeral)
|
||||||
|
Ephemeral bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns (CmdType, allow-updates-callback, err)
|
// returns (CmdType, allow-updates-callback, err)
|
||||||
@ -1875,14 +1885,14 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
|||||||
}
|
}
|
||||||
ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
rct := msh.GetRunningCmd(*existingPSC)
|
||||||
|
if rct.Ephemeral {
|
||||||
|
// if the existing command is ephemeral, we cancel it and continue
|
||||||
|
rct.EphCancled.Store(true)
|
||||||
|
} else {
|
||||||
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId())
|
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId())
|
||||||
if err != nil {
|
return nil, nil, makePSCLineError(*existingPSC, line, err)
|
||||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err)
|
|
||||||
}
|
}
|
||||||
if line == nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC)
|
|
||||||
}
|
|
||||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum)
|
|
||||||
}
|
}
|
||||||
if newPSC != nil {
|
if newPSC != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -1974,24 +1984,37 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
|||||||
RunOut: nil,
|
RunOut: nil,
|
||||||
RtnState: runPacket.ReturnState,
|
RtnState: runPacket.ReturnState,
|
||||||
}
|
}
|
||||||
if !rcOpts.NoCreateCmdPtyFile {
|
if !rcOpts.NoCreateCmdPtyFile && !rcOpts.Ephemeral {
|
||||||
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO the cmd is running, so this is a tricky error to handle
|
// TODO the cmd is running, so this is a tricky error to handle
|
||||||
return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err)
|
return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msh.AddRunningCmd(RunCmdType{
|
msh.AddRunningCmd(&RunCmdType{
|
||||||
|
CK: runPacket.CK,
|
||||||
SessionId: sessionId,
|
SessionId: sessionId,
|
||||||
ScreenId: screenId,
|
ScreenId: screenId,
|
||||||
RemotePtr: remotePtr,
|
RemotePtr: remotePtr,
|
||||||
RunPacket: runPacket,
|
RunPacket: runPacket,
|
||||||
|
Ephemeral: rcOpts.Ephemeral,
|
||||||
})
|
})
|
||||||
|
|
||||||
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) AddRunningCmd(rct RunCmdType) {
|
// helper func to construct the proper error given what information we have
|
||||||
|
func makePSCLineError(existingPSC base.CommandKey, line *sstore.LineType, lineErr error) error {
|
||||||
|
if lineErr != nil {
|
||||||
|
return fmt.Errorf("cannot run command while a stateful command is still running: %v", lineErr)
|
||||||
|
}
|
||||||
|
if line == nil {
|
||||||
|
return fmt.Errorf("cannot run command while a stateful command is still running %s", existingPSC)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) AddRunningCmd(rct *RunCmdType) {
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
defer msh.Lock.Unlock()
|
defer msh.Lock.Unlock()
|
||||||
msh.RunningCmds[rct.RunPacket.CK] = rct
|
msh.RunningCmds[rct.RunPacket.CK] = rct
|
||||||
@ -2000,11 +2023,7 @@ func (msh *MShellProc) AddRunningCmd(rct RunCmdType) {
|
|||||||
func (msh *MShellProc) GetRunningCmd(ck base.CommandKey) *RunCmdType {
|
func (msh *MShellProc) GetRunningCmd(ck base.CommandKey) *RunCmdType {
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
defer msh.Lock.Unlock()
|
defer msh.Lock.Unlock()
|
||||||
rct, found := msh.RunningCmds[ck]
|
return msh.RunningCmds[ck]
|
||||||
if !found {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &rct
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) {
|
func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) {
|
||||||
@ -2094,22 +2113,54 @@ func (msh *MShellProc) notifyHangups_nolock() {
|
|||||||
scbus.MainUpdateBus.DoScreenUpdate(ck.GetGroupId(), update)
|
scbus.MainUpdateBus.DoScreenUpdate(ck.GetGroupId(), update)
|
||||||
go pushNumRunningCmdsUpdate(&ck, -1)
|
go pushNumRunningCmdsUpdate(&ck, -1)
|
||||||
}
|
}
|
||||||
msh.RunningCmds = make(map[base.CommandKey]RunCmdType)
|
msh.RunningCmds = make(map[base.CommandKey]*RunCmdType)
|
||||||
msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey)
|
msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
// either fullstate or statediff will be set (not both) <- this is so the result is compatible with the sstore.UpdateRemoteState function
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
// note that this function *does* touch the DB, if FinalStateDiff is set, will ensure that StateBase is written to DB
|
||||||
defer cancelFn()
|
func (msh *MShellProc) makeStatePtrFromFinalState(ctx context.Context, donePk *packet.CmdDonePacketType) (*sstore.ShellStatePtr, map[string]string, *packet.ShellState, *packet.ShellStateDiff, error) {
|
||||||
// this will remove from RunningCmds and from PendingStateCmds
|
|
||||||
defer msh.RemoveRunningCmd(donePk.CK)
|
|
||||||
if donePk.FinalState != nil {
|
if donePk.FinalState != nil {
|
||||||
donePk.FinalState = stripScVarsFromState(donePk.FinalState)
|
finalState := stripScVarsFromState(donePk.FinalState)
|
||||||
|
feState := sstore.FeStateFromShellState(finalState)
|
||||||
|
statePtr := &sstore.ShellStatePtr{BaseHash: finalState.GetHashVal(false)}
|
||||||
|
return statePtr, feState, finalState, nil, nil
|
||||||
}
|
}
|
||||||
if donePk.FinalStateDiff != nil {
|
if donePk.FinalStateDiff != nil {
|
||||||
donePk.FinalStateDiff = stripScVarsFromStateDiff(donePk.FinalStateDiff)
|
stateDiff := stripScVarsFromStateDiff(donePk.FinalStateDiff)
|
||||||
|
feState, err := msh.getFeStateFromDiff(stateDiff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
update, err := sstore.UpdateCmdDoneInfo(ctx, donePk.CK, donePk, sstore.CmdStatusDone)
|
fullState := msh.StateMap.GetStateByHash(stateDiff.GetShellType(), stateDiff.BaseHash)
|
||||||
|
if fullState != nil {
|
||||||
|
sstore.StoreStateBase(ctx, fullState)
|
||||||
|
}
|
||||||
|
diffHashArr := append(([]string)(nil), donePk.FinalStateDiff.DiffHashArr...)
|
||||||
|
diffHashArr = append(diffHashArr, donePk.FinalStateDiff.GetHashVal(false))
|
||||||
|
statePtr := &sstore.ShellStatePtr{BaseHash: donePk.FinalStateDiff.BaseHash, DiffHashArr: diffHashArr}
|
||||||
|
return statePtr, feState, nil, stateDiff, nil
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) handleCmdDonePacket(rct *RunCmdType, donePk *packet.CmdDonePacketType) {
|
||||||
|
if rct == nil {
|
||||||
|
log.Printf("cmddone packet received, but no running command found for it %q\n", donePk.CK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// this will remove from RunningCmds and from PendingStateCmds
|
||||||
|
defer msh.RemoveRunningCmd(donePk.CK)
|
||||||
|
if rct.Ephemeral && rct.EphCancled.Load() {
|
||||||
|
// do nothing when an ephemeral command is canceled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
update := scbus.MakeUpdatePacket()
|
||||||
|
if !rct.Ephemeral {
|
||||||
|
// only update DB for non-ephemeral commands
|
||||||
|
err := sstore.UpdateCmdDoneInfo(ctx, update, donePk.CK, donePk, sstore.CmdStatusDone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err)
|
msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err)
|
||||||
return
|
return
|
||||||
@ -2122,11 +2173,14 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
|||||||
if screen != nil {
|
if screen != nil {
|
||||||
update.AddUpdate(*screen)
|
update.AddUpdate(*screen)
|
||||||
}
|
}
|
||||||
rct := msh.GetRunningCmd(donePk.CK)
|
}
|
||||||
var statePtr *sstore.ShellStatePtr
|
// ephemeral commands *do* update the remote state
|
||||||
if donePk.FinalState != nil && rct != nil {
|
if donePk.FinalState != nil || donePk.FinalStateDiff != nil {
|
||||||
feState := sstore.FeStateFromShellState(donePk.FinalState)
|
statePtr, feState, finalState, finalStateDiff, err := msh.makeStatePtrFromFinalState(ctx, donePk)
|
||||||
remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, donePk.FinalState, nil)
|
if err != nil {
|
||||||
|
msh.WriteToPtyBuffer("*error trying to read final command state: %v\n", err)
|
||||||
|
}
|
||||||
|
remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, finalState, finalStateDiff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err)
|
msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err)
|
||||||
// fall-through (nothing to do)
|
// fall-through (nothing to do)
|
||||||
@ -2134,43 +2188,28 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
|||||||
if remoteInst != nil {
|
if remoteInst != nil {
|
||||||
update.AddUpdate(sstore.MakeSessionUpdateForRemote(rct.SessionId, remoteInst))
|
update.AddUpdate(sstore.MakeSessionUpdateForRemote(rct.SessionId, remoteInst))
|
||||||
}
|
}
|
||||||
statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalState.GetHashVal(false)}
|
// ephemeral commands *do not* update cmd state (there is no command)
|
||||||
} else if donePk.FinalStateDiff != nil && rct != nil {
|
if statePtr != nil && !rct.Ephemeral {
|
||||||
feState, err := msh.getFeStateFromDiff(donePk.FinalStateDiff)
|
|
||||||
if err != nil {
|
|
||||||
msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err)
|
|
||||||
// fall-through (nothing to do)
|
|
||||||
} else {
|
|
||||||
stateDiff := donePk.FinalStateDiff
|
|
||||||
fullState := msh.StateMap.GetStateByHash(stateDiff.GetShellType(), stateDiff.BaseHash)
|
|
||||||
if fullState != nil {
|
|
||||||
sstore.StoreStateBase(ctx, fullState)
|
|
||||||
}
|
|
||||||
remoteInst, err := sstore.UpdateRemoteState(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, nil, stateDiff)
|
|
||||||
if err != nil {
|
|
||||||
msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err)
|
|
||||||
// fall-through (nothing to do)
|
|
||||||
}
|
|
||||||
if remoteInst != nil {
|
|
||||||
update.AddUpdate(sstore.MakeSessionUpdateForRemote(rct.SessionId, remoteInst))
|
|
||||||
}
|
|
||||||
diffHashArr := append(([]string)(nil), donePk.FinalStateDiff.DiffHashArr...)
|
|
||||||
diffHashArr = append(diffHashArr, donePk.FinalStateDiff.GetHashVal(false))
|
|
||||||
statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalStateDiff.BaseHash, DiffHashArr: diffHashArr}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if statePtr != nil {
|
|
||||||
err = sstore.UpdateCmdRtnState(ctx, donePk.CK, *statePtr)
|
err = sstore.UpdateCmdRtnState(ctx, donePk.CK, *statePtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msh.WriteToPtyBuffer("*error trying to update cmd rtnstate: %v\n", err)
|
msh.WriteToPtyBuffer("*error trying to update cmd rtnstate: %v\n", err)
|
||||||
// fall-through (nothing to do)
|
// fall-through (nothing to do)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
scbus.MainUpdateBus.DoUpdate(update)
|
scbus.MainUpdateBus.DoUpdate(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) {
|
func (msh *MShellProc) handleCmdFinalPacket(rct *RunCmdType, finalPk *packet.CmdFinalPacketType) {
|
||||||
|
if rct == nil {
|
||||||
|
// this is somewhat expected, since cmddone should have removed the running command
|
||||||
|
return
|
||||||
|
}
|
||||||
defer msh.RemoveRunningCmd(finalPk.CK)
|
defer msh.RemoveRunningCmd(finalPk.CK)
|
||||||
|
if rct.Ephemeral {
|
||||||
|
// just remove the running command, but there is no DB state to update in this case
|
||||||
|
return
|
||||||
|
}
|
||||||
rtnCmd, err := sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId())
|
rtnCmd, err := sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err)
|
log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err)
|
||||||
@ -2203,31 +2242,31 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType)
|
|||||||
scbus.MainUpdateBus.DoUpdate(update)
|
scbus.MainUpdateBus.DoUpdate(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO notify FE about cmd errors
|
|
||||||
func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) {
|
|
||||||
err := sstore.AppendCmdErrorPk(context.Background(), errPk)
|
|
||||||
if err != nil {
|
|
||||||
msh.WriteToPtyBuffer("cmderr> [remote %s] [error] adding cmderr: %v\n", msh.GetRemoteName(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) ResetDataPos(ck base.CommandKey) {
|
func (msh *MShellProc) ResetDataPos(ck base.CommandKey) {
|
||||||
msh.DataPosMap.Delete(ck)
|
msh.DataPosMap.Delete(ck)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) {
|
func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) {
|
||||||
|
if rct == nil {
|
||||||
|
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, fmt.Errorf("no running cmd found"))
|
||||||
|
msh.ServerProc.Input.SendPacket(ack)
|
||||||
|
return
|
||||||
|
}
|
||||||
realData, err := base64.StdEncoding.DecodeString(dataPk.Data64)
|
realData, err := base64.StdEncoding.DecodeString(dataPk.Data64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
||||||
msh.ServerProc.Input.SendPacket(ack)
|
msh.ServerProc.Input.SendPacket(ack)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if rct.Ephemeral {
|
||||||
|
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
||||||
|
msh.ServerProc.Input.SendPacket(ack)
|
||||||
|
return
|
||||||
|
}
|
||||||
var ack *packet.DataAckPacketType
|
var ack *packet.DataAckPacketType
|
||||||
if len(realData) > 0 {
|
if len(realData) > 0 {
|
||||||
dataPos := dataPosMap.Get(dataPk.CK)
|
dataPos := dataPosMap.Get(dataPk.CK)
|
||||||
rcmd := msh.GetRunningCmd(dataPk.CK)
|
update, err := sstore.AppendToCmdPtyBlob(context.Background(), rct.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos)
|
||||||
update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
||||||
} else {
|
} else {
|
||||||
@ -2241,25 +2280,6 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
|||||||
if ack != nil {
|
if ack != nil {
|
||||||
msh.ServerProc.Input.SendPacket(ack)
|
msh.ServerProc.Input.SendPacket(ack)
|
||||||
}
|
}
|
||||||
// log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) func() {
|
|
||||||
return func() {
|
|
||||||
msh.handleDataPacket(dataPk, dataPosMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) makeHandleCmdDonePacketClosure(donePk *packet.CmdDonePacketType) func() {
|
|
||||||
return func() {
|
|
||||||
msh.handleCmdDonePacket(donePk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) makeHandleCmdFinalPacketClosure(finalPk *packet.CmdFinalPacketType) func() {
|
|
||||||
return func() {
|
|
||||||
msh.handleCmdFinalPacket(finalPk)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendScreenUpdates(screens []*sstore.ScreenType) {
|
func sendScreenUpdates(screens []*sstore.ScreenType) {
|
||||||
@ -2270,6 +2290,45 @@ func sendScreenUpdates(screens []*sstore.ScreenType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) processSinglePacket(pk packet.PacketType) {
|
||||||
|
if _, ok := pk.(*packet.DataAckPacketType); ok {
|
||||||
|
// TODO process ack (need to keep track of buffer size for sending)
|
||||||
|
// this is low priority though since most input is coming from keyboard and won't overflow this buffer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dataPk, ok := pk.(*packet.DataPacketType); ok {
|
||||||
|
runCmdUpdateFn(dataPk.CK, func() {
|
||||||
|
rct := msh.GetRunningCmd(dataPk.CK)
|
||||||
|
msh.handleDataPacket(rct, dataPk, msh.DataPosMap)
|
||||||
|
})
|
||||||
|
go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if donePk, ok := pk.(*packet.CmdDonePacketType); ok {
|
||||||
|
runCmdUpdateFn(donePk.CK, func() {
|
||||||
|
rct := msh.GetRunningCmd(donePk.CK)
|
||||||
|
msh.handleCmdDonePacket(rct, donePk)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if finalPk, ok := pk.(*packet.CmdFinalPacketType); ok {
|
||||||
|
runCmdUpdateFn(finalPk.CK, func() {
|
||||||
|
rct := msh.GetRunningCmd(finalPk.CK)
|
||||||
|
msh.handleCmdFinalPacket(rct, finalPk)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msgPk, ok := pk.(*packet.MessagePacketType); ok {
|
||||||
|
msh.WriteToPtyBuffer("msg> [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPk.CK, msgPk.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rawPk, ok := pk.(*packet.RawPacketType); ok {
|
||||||
|
msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPk.Data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk))
|
||||||
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) ProcessPackets() {
|
func (msh *MShellProc) ProcessPackets() {
|
||||||
defer msh.WithLock(func() {
|
defer msh.WithLock(func() {
|
||||||
if msh.Status == StatusConnected {
|
if msh.Status == StatusConnected {
|
||||||
@ -2286,53 +2345,7 @@ func (msh *MShellProc) ProcessPackets() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
for pk := range msh.ServerProc.Output.MainCh {
|
for pk := range msh.ServerProc.Output.MainCh {
|
||||||
if pk.GetType() == packet.DataPacketStr {
|
msh.processSinglePacket(pk)
|
||||||
dataPk := pk.(*packet.DataPacketType)
|
|
||||||
runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, msh.DataPosMap))
|
|
||||||
go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.DataAckPacketStr {
|
|
||||||
// TODO process ack (need to keep track of buffer size for sending)
|
|
||||||
// this is low priority though since most input is coming from keyboard and won't overflow this buffer
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.CmdDataPacketStr {
|
|
||||||
dataPacket := pk.(*packet.CmdDataPacketType)
|
|
||||||
go msh.WriteToPtyBuffer("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen)
|
|
||||||
go pushStatusIndicatorUpdate(&dataPacket.CK, sstore.StatusIndicatorLevel_Output)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.CmdDonePacketStr {
|
|
||||||
donePk := pk.(*packet.CmdDonePacketType)
|
|
||||||
runCmdUpdateFn(donePk.CK, msh.makeHandleCmdDonePacketClosure(donePk))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.CmdFinalPacketStr {
|
|
||||||
finalPk := pk.(*packet.CmdFinalPacketType)
|
|
||||||
runCmdUpdateFn(finalPk.CK, msh.makeHandleCmdFinalPacketClosure(finalPk))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.CmdErrorPacketStr {
|
|
||||||
msh.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.MessagePacketStr {
|
|
||||||
msgPacket := pk.(*packet.MessagePacketType)
|
|
||||||
msh.WriteToPtyBuffer("msg> [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPacket.CK, msgPacket.Message)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.RawPacketStr {
|
|
||||||
rawPacket := pk.(*packet.RawPacketType)
|
|
||||||
msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPacket.Data)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == packet.CmdStartPacketStr {
|
|
||||||
startPk := pk.(*packet.CmdStartPacketType)
|
|
||||||
msh.WriteToPtyBuffer("start> [remote %s] reqid=%s (%p)\n", msh.GetRemoteName(), startPk.RespId, msh.ServerProc.Output)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,9 @@ const WaveAppPathVarName = "WAVETERM_APP_PATH"
|
|||||||
const WaveAuthKeyFileName = "waveterm.authkey"
|
const WaveAuthKeyFileName = "waveterm.authkey"
|
||||||
const MShellVersion = "v0.5.0"
|
const MShellVersion = "v0.5.0"
|
||||||
|
|
||||||
|
// initialized by InitialzeWaveAuthKey (called by main-server)
|
||||||
|
var WaveAuthKey string
|
||||||
|
|
||||||
var SessionDirCache = make(map[string]string)
|
var SessionDirCache = make(map[string]string)
|
||||||
var ScreenDirCache = make(map[string]string)
|
var ScreenDirCache = make(map[string]string)
|
||||||
var BaseLock = &sync.Mutex{}
|
var BaseLock = &sync.Mutex{}
|
||||||
@ -108,25 +111,28 @@ func MShellBinaryReader(version string, goos string, goarch string) (io.ReadClos
|
|||||||
return fd, nil
|
return fd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWaveAuthKeyFile(fileName string) (string, error) {
|
// also sets WaveAuthKey
|
||||||
|
func createWaveAuthKeyFile(fileName string) error {
|
||||||
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
keyStr := GenWaveUUID()
|
keyStr := GenWaveUUID()
|
||||||
_, err = fd.Write([]byte(keyStr))
|
_, err = fd.Write([]byte(keyStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
return keyStr, nil
|
WaveAuthKey = keyStr
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadWaveAuthKey() (string, error) {
|
// sets WaveAuthKey
|
||||||
|
func InitializeWaveAuthKey() error {
|
||||||
homeDir := GetWaveHomeDir()
|
homeDir := GetWaveHomeDir()
|
||||||
err := ensureDir(homeDir)
|
err := ensureDir(homeDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
|
return fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
|
||||||
}
|
}
|
||||||
fileName := path.Join(homeDir, WaveAuthKeyFileName)
|
fileName := path.Join(homeDir, WaveAuthKeyFileName)
|
||||||
fd, err := os.Open(fileName)
|
fd, err := os.Open(fileName)
|
||||||
@ -134,19 +140,20 @@ func ReadWaveAuthKey() (string, error) {
|
|||||||
return createWaveAuthKeyFile(fileName)
|
return createWaveAuthKeyFile(fileName)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
|
return fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
|
||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
buf, err := io.ReadAll(fd)
|
buf, err := io.ReadAll(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
|
return fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
|
||||||
}
|
}
|
||||||
keyStr := string(buf)
|
keyStr := string(buf)
|
||||||
_, err = uuid.Parse(keyStr)
|
_, err = uuid.Parse(keyStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
|
return fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
|
||||||
}
|
}
|
||||||
return keyStr, nil
|
WaveAuthKey = keyStr
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcquireWaveLock() (*os.File, error) {
|
func AcquireWaveLock() (*os.File, error) {
|
||||||
@ -393,7 +400,9 @@ func determineLang() string {
|
|||||||
log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err)
|
log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(out)) + ".UTF-8"
|
strOut := string(out)
|
||||||
|
truncOut := strings.Split(strOut, "@")[0]
|
||||||
|
return strings.TrimSpace(truncOut) + ".UTF-8"
|
||||||
} else {
|
} else {
|
||||||
// this is specifically to get the wavesrv LANG so waveshell
|
// this is specifically to get the wavesrv LANG so waveshell
|
||||||
// on a remote uses the same LANG
|
// on a remote uses the same LANG
|
||||||
|
@ -916,12 +916,12 @@ func UpdateCmdForRestart(ctx context.Context, ck base.CommandKey, ts int64, cmdP
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*scbus.ModelUpdatePacketType, error) {
|
func UpdateCmdDoneInfo(ctx context.Context, update *scbus.ModelUpdatePacketType, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) error {
|
||||||
if donePk == nil {
|
if donePk == nil {
|
||||||
return nil, fmt.Errorf("invalid cmddone packet")
|
return fmt.Errorf("invalid cmddone packet")
|
||||||
}
|
}
|
||||||
if ck.IsEmpty() {
|
if ck.IsEmpty() {
|
||||||
return nil, fmt.Errorf("cannot update cmddoneinfo, empty ck")
|
return fmt.Errorf("cannot update cmddoneinfo, empty ck")
|
||||||
}
|
}
|
||||||
screenId := ck.GetGroupId()
|
screenId := ck.GetGroupId()
|
||||||
var rtnCmd *CmdType
|
var rtnCmd *CmdType
|
||||||
@ -944,15 +944,12 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return nil, txErr
|
return txErr
|
||||||
}
|
}
|
||||||
if rtnCmd == nil {
|
if rtnCmd == nil {
|
||||||
return nil, fmt.Errorf("cmd data not found for ck[%s]", ck)
|
return fmt.Errorf("cmd data not found for ck[%s]", ck)
|
||||||
}
|
}
|
||||||
|
|
||||||
update := scbus.MakeUpdatePacket()
|
|
||||||
update.AddUpdate(*rtnCmd)
|
update.AddUpdate(*rtnCmd)
|
||||||
|
|
||||||
// Update in-memory screen indicator status
|
// Update in-memory screen indicator status
|
||||||
var indicator StatusIndicatorLevel
|
var indicator StatusIndicatorLevel
|
||||||
if rtnCmd.ExitCode == 0 {
|
if rtnCmd.ExitCode == 0 {
|
||||||
@ -960,15 +957,13 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
|
|||||||
} else {
|
} else {
|
||||||
indicator = StatusIndicatorLevel_Error
|
indicator = StatusIndicatorLevel_Error
|
||||||
}
|
}
|
||||||
|
|
||||||
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false)
|
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This is not a fatal error, so just log it
|
// This is not a fatal error, so just log it
|
||||||
log.Printf("error setting status indicator level after done packet: %v\n", err)
|
log.Printf("error setting status indicator level after done packet: %v\n", err)
|
||||||
}
|
}
|
||||||
IncrementNumRunningCmds_Update(update, screenId, -1)
|
IncrementNumRunningCmds_Update(update, screenId, -1)
|
||||||
|
return nil
|
||||||
return update, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellStatePtr) error {
|
func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellStatePtr) error {
|
||||||
@ -991,18 +986,6 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error {
|
|
||||||
if errPk == nil || errPk.CK.IsEmpty() {
|
|
||||||
return fmt.Errorf("invalid cmderror packet (no ck)")
|
|
||||||
}
|
|
||||||
screenId := errPk.CK.GetGroupId()
|
|
||||||
return WithTx(ctx, func(tx *TxWrap) error {
|
|
||||||
query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND lineid = ?`
|
|
||||||
tx.Exec(query, quickJson(errPk), screenId, lineIdFromCK(errPk.CK))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReInitFocus(ctx context.Context) error {
|
func ReInitFocus(ctx context.Context) error {
|
||||||
return WithTx(ctx, func(tx *TxWrap) error {
|
return WithTx(ctx, func(tx *TxWrap) error {
|
||||||
query := `UPDATE screen SET focustype = 'input'`
|
query := `UPDATE screen SET focustype = 'input'`
|
||||||
|
@ -63,6 +63,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
LineState_Source = "prompt:source"
|
LineState_Source = "prompt:source"
|
||||||
LineState_File = "prompt:file"
|
LineState_File = "prompt:file"
|
||||||
|
LineState_FileUrl = "wave:fileurl"
|
||||||
LineState_Min = "wave:min"
|
LineState_Min = "wave:min"
|
||||||
LineState_Template = "template"
|
LineState_Template = "template"
|
||||||
LineState_Mode = "mode"
|
LineState_Mode = "mode"
|
||||||
@ -74,6 +75,8 @@ const (
|
|||||||
MainViewSession = "session"
|
MainViewSession = "session"
|
||||||
MainViewBookmarks = "bookmarks"
|
MainViewBookmarks = "bookmarks"
|
||||||
MainViewHistory = "history"
|
MainViewHistory = "history"
|
||||||
|
MainViewConnections = "connections"
|
||||||
|
MainViewSettings = "clientsettings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -4016,9 +4016,9 @@ fn.name@1.x.x:
|
|||||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||||
|
|
||||||
follow-redirects@^1.0.0:
|
follow-redirects@^1.0.0:
|
||||||
version "1.15.4"
|
version "1.15.6"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||||
|
|
||||||
foreground-child@^3.1.0:
|
foreground-child@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user