mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
connection handling / block controller handling (#326)
This commit is contained in:
parent
b796ec9729
commit
3e0ca6b41e
@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/authkey"
|
"github.com/wavetermdev/thenextwave/pkg/authkey"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/telemetry"
|
"github.com/wavetermdev/thenextwave/pkg/telemetry"
|
||||||
@ -53,6 +54,7 @@ func doShutdown(reason string) {
|
|||||||
log.Printf("shutting down: %s\n", reason)
|
log.Printf("shutting down: %s\n", reason)
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
go blockcontroller.StopAllBlockControllers()
|
||||||
shutdownActivityUpdate()
|
shutdownActivityUpdate()
|
||||||
sendTelemetryWrapper()
|
sendTelemetryWrapper()
|
||||||
// TODO deal with flush in progress
|
// TODO deal with flush in progress
|
||||||
@ -61,7 +63,7 @@ func doShutdown(reason string) {
|
|||||||
if watcher != nil {
|
if watcher != nil {
|
||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -276,6 +276,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connstatus-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--header-height);
|
||||||
|
left: 2px;
|
||||||
|
right: 2px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: var(--zindex-block-mask-inner);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.connstatus-mainelem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(60, 60, 60, 0.65);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
font: var(--base-font);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
|
||||||
|
.connstatus-error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connstatus-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.block-mask {
|
.block-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -288,12 +327,12 @@
|
|||||||
border-radius: calc(var(--block-border-radius) + 2px);
|
border-radius: calc(var(--block-border-radius) + 2px);
|
||||||
z-index: var(--zindex-block-mask-inner);
|
z-index: var(--zindex-block-mask-inner);
|
||||||
|
|
||||||
&.is-layoutmode {
|
&.show-block-mask {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-layoutmode .block-mask-inner {
|
&.show-block-mask .block-mask-inner {
|
||||||
margin-top: var(--header-height); // TODO fix this magic
|
margin-top: var(--header-height); // TODO fix this magic
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
height: calc(100% - var(--header-height));
|
height: calc(100% - var(--header-height));
|
||||||
|
@ -7,7 +7,12 @@ import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/
|
|||||||
import { ErrorBoundary } from "@/element/errorboundary";
|
import { ErrorBoundary } from "@/element/errorboundary";
|
||||||
import { CenteredDiv } from "@/element/quickelems";
|
import { CenteredDiv } from "@/element/quickelems";
|
||||||
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
||||||
import { counterInc, getViewModel, registerViewModel, unregisterViewModel } from "@/store/global";
|
import {
|
||||||
|
counterInc,
|
||||||
|
getBlockComponentModel,
|
||||||
|
registerBlockComponentModel,
|
||||||
|
unregisterBlockComponentModel,
|
||||||
|
} from "@/store/global";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { getElemAsStr } from "@/util/focusutil";
|
import { getElemAsStr } from "@/util/focusutil";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
@ -251,14 +256,15 @@ const Block = React.memo((props: BlockProps) => {
|
|||||||
counterInc("render-Block");
|
counterInc("render-Block");
|
||||||
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
|
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
|
||||||
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.nodeModel.blockId));
|
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.nodeModel.blockId));
|
||||||
let viewModel = getViewModel(props.nodeModel.blockId);
|
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
||||||
|
let viewModel = bcm?.viewModel;
|
||||||
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
||||||
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
|
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
|
||||||
registerViewModel(props.nodeModel.blockId, viewModel);
|
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
|
||||||
}
|
}
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
unregisterViewModel(props.nodeModel.blockId);
|
unregisterBlockComponentModel(props.nodeModel.blockId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) {
|
if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) {
|
||||||
|
@ -13,7 +13,15 @@ import {
|
|||||||
import { Button } from "@/app/element/button";
|
import { Button } from "@/app/element/button";
|
||||||
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
|
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
|
||||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||||
import { atoms, globalStore, useBlockAtom, useSettingsKeyAtom, WOS } from "@/app/store/global";
|
import {
|
||||||
|
atoms,
|
||||||
|
getBlockComponentModel,
|
||||||
|
getConnStatusAtom,
|
||||||
|
globalStore,
|
||||||
|
useBlockAtom,
|
||||||
|
useSettingsKeyAtom,
|
||||||
|
WOS,
|
||||||
|
} from "@/app/store/global";
|
||||||
import * as services from "@/app/store/services";
|
import * as services from "@/app/store/services";
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { MagnifyIcon } from "@/element/magnify";
|
import { MagnifyIcon } from "@/element/magnify";
|
||||||
@ -61,14 +69,6 @@ function handleHeaderContextMenu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const blockController = blockData?.meta?.controller;
|
|
||||||
if (!util.isBlank(blockController)) {
|
|
||||||
menu.push({ type: "separator" });
|
|
||||||
menu.push({
|
|
||||||
label: "Restart Controller",
|
|
||||||
click: () => WshServer.ControllerRestartCommand({ blockid: blockData.oid }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const extraItems = viewModel?.getSettingsMenuItems?.();
|
const extraItems = viewModel?.getSettingsMenuItems?.();
|
||||||
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
|
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
|
||||||
menu.push(
|
menu.push(
|
||||||
@ -256,13 +256,70 @@ function renderHeaderElements(headerTextUnion: HeaderElem[], preview: boolean):
|
|||||||
return headerTextElems;
|
return headerTextElems;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
const ConnStatusOverlay = React.memo(
|
||||||
|
({
|
||||||
|
nodeModel,
|
||||||
|
viewModel,
|
||||||
|
changeConnModalAtom,
|
||||||
|
}: {
|
||||||
|
nodeModel: NodeModel;
|
||||||
|
viewModel: ViewModel;
|
||||||
|
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
||||||
|
}) => {
|
||||||
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
|
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
|
||||||
|
const connName = blockData.meta?.connection;
|
||||||
|
const connStatus = jotai.useAtomValue(getConnStatusAtom(connName));
|
||||||
|
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
||||||
|
const handleTryReconnect = React.useCallback(() => {
|
||||||
|
const prtn = WshServer.ConnConnectCommand(connName, { timeout: 60000 });
|
||||||
|
prtn.catch((e) => console.log("error reconnecting", connName, e));
|
||||||
|
}, [connName]);
|
||||||
|
const handleSwitchConnection = React.useCallback(() => {
|
||||||
|
setConnModalOpen(true);
|
||||||
|
}, [setConnModalOpen]);
|
||||||
|
if (isLayoutMode || connStatus.status == "connected" || connModalOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let statusText = `Disconnected from "${connName}"`;
|
||||||
|
let showReconnect = true;
|
||||||
|
if (connStatus.status == "connecting") {
|
||||||
|
statusText = `Connecting to "${connName}"...`;
|
||||||
|
showReconnect = false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="connstatus-overlay">
|
||||||
|
<div className="connstatus-mainelem">
|
||||||
|
<div style={{ marginBottom: 5 }}>{statusText}</div>
|
||||||
|
{!util.isBlank(connStatus.error) ? (
|
||||||
|
<div className="connstatus-error">error: {connStatus.error}</div>
|
||||||
|
) : null}
|
||||||
|
{showReconnect ? (
|
||||||
|
<div className="connstatus-actions">
|
||||||
|
<Button className="secondary" onClick={handleTryReconnect}>
|
||||||
|
<i className="fa-sharp fa-solid fa-arrow-right-arrow-left" style={{ marginRight: 5 }} />
|
||||||
|
Reconnect Now
|
||||||
|
</Button>
|
||||||
|
<Button className="secondary" onClick={handleSwitchConnection}>
|
||||||
|
<i className="fa-sharp fa-solid fa-arrow-right-arrow-left" style={{ marginRight: 5 }} />
|
||||||
|
Switch Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||||
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
|
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
|
||||||
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
|
|
||||||
const style: React.CSSProperties = {};
|
const style: React.CSSProperties = {};
|
||||||
|
let showBlockMask = false;
|
||||||
|
|
||||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
||||||
style.borderColor = blockData.meta["frame:bordercolor"];
|
style.borderColor = blockData.meta["frame:bordercolor"];
|
||||||
}
|
}
|
||||||
@ -271,6 +328,7 @@ const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
|||||||
}
|
}
|
||||||
let innerElem = null;
|
let innerElem = null;
|
||||||
if (isLayoutMode) {
|
if (isLayoutMode) {
|
||||||
|
showBlockMask = true;
|
||||||
innerElem = (
|
innerElem = (
|
||||||
<div className="block-mask-inner">
|
<div className="block-mask-inner">
|
||||||
<div className="bignum">{blockNum}</div>
|
<div className="bignum">{blockNum}</div>
|
||||||
@ -278,11 +336,11 @@ const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={clsx("block-mask", { "is-layoutmode": isLayoutMode })} style={style}>
|
<div className={clsx("block-mask", { "show-block-mask": showBlockMask })} style={style}>
|
||||||
{innerElem}
|
{innerElem}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||||
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
|
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
|
||||||
@ -290,10 +348,42 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
|||||||
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
||||||
const customBg = util.useAtomValueSafe(viewModel.blockBg);
|
const customBg = util.useAtomValueSafe(viewModel.blockBg);
|
||||||
|
const manageConnection = util.useAtomValueSafe(viewModel.manageConnection);
|
||||||
const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => {
|
const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => {
|
||||||
return jotai.atom(false);
|
return jotai.atom(false);
|
||||||
}) as jotai.PrimitiveAtom<boolean>;
|
}) as jotai.PrimitiveAtom<boolean>;
|
||||||
const connBtnRef = React.useRef<HTMLDivElement>();
|
const connBtnRef = React.useRef<HTMLDivElement>();
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!manageConnection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bcm = getBlockComponentModel(nodeModel.blockId);
|
||||||
|
if (bcm != null) {
|
||||||
|
bcm.openSwitchConnection = () => {
|
||||||
|
globalStore.set(changeConnModalAtom, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const bcm = getBlockComponentModel(nodeModel.blockId);
|
||||||
|
if (bcm != null) {
|
||||||
|
bcm.openSwitchConnection = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [manageConnection]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
// on mount, if manageConnection, call ConnEnsure
|
||||||
|
if (!manageConnection || blockData == null || preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connName = blockData?.meta?.connection;
|
||||||
|
if (!util.isBlank(connName)) {
|
||||||
|
console.log("ensure conn", nodeModel.blockId, connName);
|
||||||
|
WshServer.ConnEnsureCommand(connName, { timeout: 60000 }).catch((e) => {
|
||||||
|
console.log("error ensuring connection", nodeModel.blockId, connName, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [manageConnection, blockData]);
|
||||||
|
|
||||||
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
||||||
const innerStyle: React.CSSProperties = {};
|
const innerStyle: React.CSSProperties = {};
|
||||||
if (!preview && customBg?.bg != null) {
|
if (!preview && customBg?.bg != null) {
|
||||||
@ -319,6 +409,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
|||||||
ref={blockModel?.blockRef}
|
ref={blockModel?.blockRef}
|
||||||
>
|
>
|
||||||
<BlockMask nodeModel={nodeModel} />
|
<BlockMask nodeModel={nodeModel} />
|
||||||
|
<ConnStatusOverlay nodeModel={nodeModel} viewModel={viewModel} changeConnModalAtom={changeConnModalAtom} />
|
||||||
<div className="block-frame-default-inner" style={innerStyle}>
|
<div className="block-frame-default-inner" style={innerStyle}>
|
||||||
<BlockFrame_Header {...props} connBtnRef={connBtnRef} changeConnModalAtom={changeConnModalAtom} />
|
<BlockFrame_Header {...props} connBtnRef={connBtnRef} changeConnModalAtom={changeConnModalAtom} />
|
||||||
{preview ? previewElem : children}
|
{preview ? previewElem : children}
|
||||||
@ -359,6 +450,12 @@ const ChangeConnectionBlockModal = React.memo(
|
|||||||
const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused);
|
const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
const changeConnection = React.useCallback(
|
const changeConnection = React.useCallback(
|
||||||
async (connName: string) => {
|
async (connName: string) => {
|
||||||
|
if (connName == "") {
|
||||||
|
connName = null;
|
||||||
|
}
|
||||||
|
if (connName == blockData?.meta?.connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const oldCwd = blockData?.meta?.file ?? "";
|
const oldCwd = blockData?.meta?.file ?? "";
|
||||||
let newCwd: string;
|
let newCwd: string;
|
||||||
if (oldCwd == "") {
|
if (oldCwd == "") {
|
||||||
@ -370,10 +467,14 @@ const ChangeConnectionBlockModal = React.memo(
|
|||||||
oref: WOS.makeORef("block", blockId),
|
oref: WOS.makeORef("block", blockId),
|
||||||
meta: { connection: connName, file: newCwd },
|
meta: { connection: connName, file: newCwd },
|
||||||
});
|
});
|
||||||
await services.BlockService.EnsureConnection(blockId).catch((e) => console.log(e));
|
const tabId = globalStore.get(atoms.activeTabId);
|
||||||
await WshServer.ControllerRestartCommand({ blockid: blockId });
|
try {
|
||||||
|
await WshServer.ConnEnsureCommand(connName, { timeout: 60000 });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error connecting", blockId, connName, e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[blockId]
|
[blockId, blockData]
|
||||||
);
|
);
|
||||||
const handleTypeAheadKeyDown = React.useCallback(
|
const handleTypeAheadKeyDown = React.useCallback(
|
||||||
(waveEvent: WaveKeyboardEvent): boolean => {
|
(waveEvent: WaveKeyboardEvent): boolean => {
|
||||||
|
@ -160,6 +160,7 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
|||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
const hasController = !util.isBlank(blockData?.meta?.controller);
|
const hasController = !util.isBlank(blockData?.meta?.controller);
|
||||||
const [controllerStatus, setControllerStatus] = React.useState<BlockControllerRuntimeStatus>(null);
|
const [controllerStatus, setControllerStatus] = React.useState<BlockControllerRuntimeStatus>(null);
|
||||||
|
const [gotInitialStatus, setGotInitialStatus] = React.useState(false);
|
||||||
const connection = blockData?.meta?.connection ?? "local";
|
const connection = blockData?.meta?.connection ?? "local";
|
||||||
const connStatusAtom = getConnStatusAtom(connection);
|
const connStatusAtom = getConnStatusAtom(connection);
|
||||||
const connStatus = jotai.useAtomValue(connStatusAtom);
|
const connStatus = jotai.useAtomValue(connStatusAtom);
|
||||||
@ -169,6 +170,7 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
|||||||
}
|
}
|
||||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
||||||
initialRTStatus.then((rts) => {
|
initialRTStatus.then((rts) => {
|
||||||
|
setGotInitialStatus(true);
|
||||||
setControllerStatus(rts);
|
setControllerStatus(rts);
|
||||||
});
|
});
|
||||||
const unsubFn = waveEventSubscribe("controllerstatus", makeORef("block", blockId), (event) => {
|
const unsubFn = waveEventSubscribe("controllerstatus", makeORef("block", blockId), (event) => {
|
||||||
@ -179,25 +181,19 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
|||||||
unsubFn();
|
unsubFn();
|
||||||
};
|
};
|
||||||
}, [hasController]);
|
}, [hasController]);
|
||||||
if (!hasController) {
|
if (!hasController || !gotInitialStatus) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (controllerStatus?.shellprocstatus == "running") {
|
||||||
controllerStatus == null ||
|
|
||||||
(controllerStatus?.status == "running" && controllerStatus?.shellprocstatus == "running")
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (connStatus?.status != "connected") {
|
if (connStatus?.status != "connected") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const controllerStatusElem = (
|
const controllerStatusElem = (
|
||||||
<i
|
<div className="iconbutton disabled" key="controller-status">
|
||||||
key="controller-status"
|
<i className="fa-sharp fa-solid fa-triangle-exclamation" title="Shell Process Is Not Running" />
|
||||||
className="fa-sharp fa-solid fa-triangle-exclamation"
|
</div>
|
||||||
title="Controller Is Not Running"
|
|
||||||
style={{ color: "var(--error-color)" }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
return controllerStatusElem;
|
return controllerStatusElem;
|
||||||
});
|
});
|
||||||
@ -206,7 +202,7 @@ export const ConnectionButton = React.memo(
|
|||||||
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
|
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
|
||||||
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
|
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
|
||||||
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
|
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
|
||||||
const isLocal = util.isBlank(connection) || connection == "local";
|
const isLocal = util.isBlank(connection);
|
||||||
const connStatusAtom = getConnStatusAtom(connection);
|
const connStatusAtom = getConnStatusAtom(connection);
|
||||||
const connStatus = jotai.useAtomValue(connStatusAtom);
|
const connStatus = jotai.useAtomValue(connStatusAtom);
|
||||||
let showDisconnectedSlash = false;
|
let showDisconnectedSlash = false;
|
||||||
|
@ -23,7 +23,7 @@ let PLATFORM: NodeJS.Platform = "darwin";
|
|||||||
const globalStore = jotai.createStore();
|
const globalStore = jotai.createStore();
|
||||||
let atoms: GlobalAtomsType;
|
let atoms: GlobalAtomsType;
|
||||||
let globalEnvironment: "electron" | "renderer";
|
let globalEnvironment: "electron" | "renderer";
|
||||||
const blockViewModelMap = new Map<string, ViewModel>();
|
const blockComponentModelMap = new Map<string, BlockComponentModel>();
|
||||||
const Counters = new Map<string, number>();
|
const Counters = new Map<string, number>();
|
||||||
const ConnStatusMap = new Map<string, jotai.PrimitiveAtom<ConnStatus>>();
|
const ConnStatusMap = new Map<string, jotai.PrimitiveAtom<ConnStatus>>();
|
||||||
|
|
||||||
@ -526,16 +526,16 @@ async function openLink(uri: string, forceOpenInternally = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerViewModel(blockId: string, viewModel: ViewModel) {
|
function registerBlockComponentModel(blockId: string, bcm: BlockComponentModel) {
|
||||||
blockViewModelMap.set(blockId, viewModel);
|
blockComponentModelMap.set(blockId, bcm);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterViewModel(blockId: string) {
|
function unregisterBlockComponentModel(blockId: string) {
|
||||||
blockViewModelMap.delete(blockId);
|
blockComponentModelMap.delete(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewModel(blockId: string): ViewModel {
|
function getBlockComponentModel(blockId: string): BlockComponentModel {
|
||||||
return blockViewModelMap.get(blockId);
|
return blockComponentModelMap.get(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refocusNode(blockId: string) {
|
function refocusNode(blockId: string) {
|
||||||
@ -548,8 +548,8 @@ function refocusNode(blockId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
layoutModel.focusNode(layoutNodeId.id);
|
layoutModel.focusNode(layoutNodeId.id);
|
||||||
const viewModel = getViewModel(blockId);
|
const bcm = getBlockComponentModel(blockId);
|
||||||
const ok = viewModel?.giveFocus?.();
|
const ok = bcm?.viewModel?.giveFocus?.();
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
const inputElem = document.getElementById(`${blockId}-dummy-focus`);
|
const inputElem = document.getElementById(`${blockId}-dummy-focus`);
|
||||||
inputElem?.focus();
|
inputElem?.focus();
|
||||||
@ -604,14 +604,26 @@ function subscribeToConnEvents() {
|
|||||||
function getConnStatusAtom(conn: string): jotai.PrimitiveAtom<ConnStatus> {
|
function getConnStatusAtom(conn: string): jotai.PrimitiveAtom<ConnStatus> {
|
||||||
let rtn = ConnStatusMap.get(conn);
|
let rtn = ConnStatusMap.get(conn);
|
||||||
if (rtn == null) {
|
if (rtn == null) {
|
||||||
const connStatus: ConnStatus = {
|
if (util.isBlank(conn)) {
|
||||||
connection: conn,
|
// create a fake "local" status atom that's always connected
|
||||||
connected: false,
|
const connStatus: ConnStatus = {
|
||||||
error: null,
|
connection: conn,
|
||||||
status: "disconnected",
|
connected: true,
|
||||||
hasconnected: false,
|
error: null,
|
||||||
};
|
status: "connected",
|
||||||
rtn = jotai.atom(connStatus);
|
hasconnected: true,
|
||||||
|
};
|
||||||
|
rtn = jotai.atom(connStatus);
|
||||||
|
} else {
|
||||||
|
const connStatus: ConnStatus = {
|
||||||
|
connection: conn,
|
||||||
|
connected: false,
|
||||||
|
error: null,
|
||||||
|
status: "disconnected",
|
||||||
|
hasconnected: false,
|
||||||
|
};
|
||||||
|
rtn = jotai.atom(connStatus);
|
||||||
|
}
|
||||||
ConnStatusMap.set(conn, rtn);
|
ConnStatusMap.set(conn, rtn);
|
||||||
}
|
}
|
||||||
return rtn;
|
return rtn;
|
||||||
@ -625,13 +637,13 @@ export {
|
|||||||
createBlock,
|
createBlock,
|
||||||
fetchWaveFile,
|
fetchWaveFile,
|
||||||
getApi,
|
getApi,
|
||||||
|
getBlockComponentModel,
|
||||||
getConnStatusAtom,
|
getConnStatusAtom,
|
||||||
getEventORefSubject,
|
getEventORefSubject,
|
||||||
getEventSubject,
|
getEventSubject,
|
||||||
getFileSubject,
|
getFileSubject,
|
||||||
getObjectId,
|
getObjectId,
|
||||||
getUserName,
|
getUserName,
|
||||||
getViewModel,
|
|
||||||
globalStore,
|
globalStore,
|
||||||
globalWS,
|
globalWS,
|
||||||
initGlobal,
|
initGlobal,
|
||||||
@ -641,12 +653,12 @@ export {
|
|||||||
openLink,
|
openLink,
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
refocusNode,
|
refocusNode,
|
||||||
registerViewModel,
|
registerBlockComponentModel,
|
||||||
sendWSCommand,
|
sendWSCommand,
|
||||||
setNodeFocus,
|
setNodeFocus,
|
||||||
setPlatform,
|
setPlatform,
|
||||||
subscribeToConnEvents,
|
subscribeToConnEvents,
|
||||||
unregisterViewModel,
|
unregisterBlockComponentModel,
|
||||||
useBlockAtom,
|
useBlockAtom,
|
||||||
useBlockCache,
|
useBlockCache,
|
||||||
useBlockDataLoaded,
|
useBlockDataLoaded,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { atoms, createBlock, getApi, getViewModel, globalStore, refocusNode, WOS } from "@/app/store/global";
|
import { atoms, createBlock, getApi, getBlockComponentModel, globalStore, refocusNode, WOS } from "@/app/store/global";
|
||||||
import * as services from "@/app/store/services";
|
import * as services from "@/app/store/services";
|
||||||
import {
|
import {
|
||||||
deleteLayoutModelForTab,
|
deleteLayoutModelForTab,
|
||||||
@ -160,7 +160,14 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
|||||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||||
const blockId = focusedNode?.data?.blockId;
|
const blockId = focusedNode?.data?.blockId;
|
||||||
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
||||||
const viewModel = getViewModel(blockId);
|
const bcm = getBlockComponentModel(blockId);
|
||||||
|
if (bcm.openSwitchConnection != null) {
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:g")) {
|
||||||
|
bcm.openSwitchConnection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const viewModel = bcm?.viewModel;
|
||||||
if (viewModel?.keyDownHandler) {
|
if (viewModel?.keyDownHandler) {
|
||||||
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
||||||
if (handledByBlock) {
|
if (handledByBlock) {
|
||||||
|
@ -7,9 +7,6 @@ import * as WOS from "./wos";
|
|||||||
|
|
||||||
// blockservice.BlockService (block)
|
// blockservice.BlockService (block)
|
||||||
class BlockServiceType {
|
class BlockServiceType {
|
||||||
EnsureConnection(arg2: string): Promise<void> {
|
|
||||||
return WOS.callBackendService("block", "EnsureConnection", Array.from(arguments))
|
|
||||||
}
|
|
||||||
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
|
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
|
||||||
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
|
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,14 @@ class WshServerType {
|
|||||||
return WOS.wshServerRpcHelper_call("controllerinput", data, opts);
|
return WOS.wshServerRpcHelper_call("controllerinput", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// command "controllerrestart" [call]
|
// command "controllerresync" [call]
|
||||||
ControllerRestartCommand(data: CommandBlockRestartData, opts?: RpcOpts): Promise<void> {
|
ControllerResyncCommand(data: CommandControllerResyncData, opts?: RpcOpts): Promise<void> {
|
||||||
return WOS.wshServerRpcHelper_call("controllerrestart", data, opts);
|
return WOS.wshServerRpcHelper_call("controllerresync", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "controllerstop" [call]
|
||||||
|
ControllerStopCommand(data: string, opts?: RpcOpts): Promise<void> {
|
||||||
|
return WOS.wshServerRpcHelper_call("controllerstop", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// command "createblock" [call]
|
// command "createblock" [call]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { useHeight } from "@/app/hook/useHeight";
|
import { useHeight } from "@/app/hook/useHeight";
|
||||||
import { useWidth } from "@/app/hook/useWidth";
|
import { useWidth } from "@/app/hook/useWidth";
|
||||||
import { globalStore, waveEventSubscribe, WOS } from "@/store/global";
|
import { getConnStatusAtom, globalStore, waveEventSubscribe, WOS } from "@/store/global";
|
||||||
import { WshServer } from "@/store/wshserver";
|
import { WshServer } from "@/store/wshserver";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import * as Plot from "@observablehq/plot";
|
import * as Plot from "@observablehq/plot";
|
||||||
@ -61,6 +61,7 @@ class CpuPlotViewModel {
|
|||||||
metrics: jotai.Atom<string[]>;
|
metrics: jotai.Atom<string[]>;
|
||||||
connection: jotai.Atom<string>;
|
connection: jotai.Atom<string>;
|
||||||
manageConnection: jotai.Atom<boolean>;
|
manageConnection: jotai.Atom<boolean>;
|
||||||
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
|
|
||||||
constructor(blockId: string) {
|
constructor(blockId: string) {
|
||||||
this.viewType = "cpuplot";
|
this.viewType = "cpuplot";
|
||||||
@ -122,6 +123,12 @@ class CpuPlotViewModel {
|
|||||||
});
|
});
|
||||||
this.dataAtom = jotai.atom(this.getDefaultData());
|
this.dataAtom = jotai.atom(this.getDefaultData());
|
||||||
this.loadInitialData();
|
this.loadInitialData();
|
||||||
|
this.connStatus = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const connName = blockData?.meta?.connection;
|
||||||
|
const connAtom = getConnStatusAtom(connName);
|
||||||
|
return get(connAtom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
@ -165,18 +172,24 @@ function makeCpuPlotViewModel(blockId: string): CpuPlotViewModel {
|
|||||||
|
|
||||||
const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"];
|
const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"];
|
||||||
|
|
||||||
function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
type CpuPlotViewProps = {
|
||||||
const containerRef = React.useRef<HTMLInputElement>();
|
blockId: string;
|
||||||
const plotData = jotai.useAtomValue(model.dataAtom);
|
model: CpuPlotViewModel;
|
||||||
const addPlotData = jotai.useSetAtom(model.addDataAtom);
|
};
|
||||||
const parentHeight = useHeight(containerRef);
|
|
||||||
const parentWidth = useWidth(containerRef);
|
function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
|
||||||
const yvals = jotai.useAtomValue(model.metrics);
|
|
||||||
const connName = jotai.useAtomValue(model.connection);
|
const connName = jotai.useAtomValue(model.connection);
|
||||||
const lastConnName = React.useRef(connName);
|
const lastConnName = React.useRef(connName);
|
||||||
|
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||||
|
const addPlotData = jotai.useSetAtom(model.addDataAtom);
|
||||||
|
const loading = jotai.useAtomValue(model.loadingAtom);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (lastConnName.current !== connName) {
|
if (lastConnName.current !== connName) {
|
||||||
|
lastConnName.current = connName;
|
||||||
model.loadInitialData();
|
model.loadInitialData();
|
||||||
}
|
}
|
||||||
const unsubFn = waveEventSubscribe("sysinfo", connName, (event: WaveEvent) => {
|
const unsubFn = waveEventSubscribe("sysinfo", connName, (event: WaveEvent) => {
|
||||||
@ -191,6 +204,22 @@ function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
|||||||
unsubFn();
|
unsubFn();
|
||||||
};
|
};
|
||||||
}, [connName]);
|
}, [connName]);
|
||||||
|
React.useEffect(() => {}, [connName]);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <CpuPlotViewInner key={connStatus?.connection ?? "local"} blockId={blockId} model={model} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
|
||||||
|
const containerRef = React.useRef<HTMLInputElement>();
|
||||||
|
const plotData = jotai.useAtomValue(model.dataAtom);
|
||||||
|
const parentHeight = useHeight(containerRef);
|
||||||
|
const parentWidth = useWidth(containerRef);
|
||||||
|
const yvals = jotai.useAtomValue(model.metrics);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const marks: Plot.Markish[] = [];
|
const marks: Plot.Markish[] = [];
|
||||||
@ -254,6 +283,6 @@ function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
|||||||
}, [plotData, parentHeight, parentWidth]);
|
}, [plotData, parentHeight, parentWidth]);
|
||||||
|
|
||||||
return <div className="plot-view" ref={containerRef} />;
|
return <div className="plot-view" ref={containerRef} />;
|
||||||
}
|
});
|
||||||
|
|
||||||
export { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel };
|
export { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel };
|
||||||
|
@ -7,7 +7,7 @@ import { tryReinjectKey } from "@/app/store/keymodel";
|
|||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { Markdown } from "@/element/markdown";
|
import { Markdown } from "@/element/markdown";
|
||||||
import { NodeModel } from "@/layout/index";
|
import { NodeModel } from "@/layout/index";
|
||||||
import { createBlock, globalStore, refocusNode } from "@/store/global";
|
import { createBlock, getConnStatusAtom, globalStore, refocusNode } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
@ -98,6 +98,7 @@ export class PreviewModel implements ViewModel {
|
|||||||
specializedView: jotai.Atom<Promise<{ specializedView?: string; errorStr?: string }>>;
|
specializedView: jotai.Atom<Promise<{ specializedView?: string; errorStr?: string }>>;
|
||||||
loadableSpecializedView: jotai.Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
|
loadableSpecializedView: jotai.Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
|
||||||
manageConnection: jotai.Atom<boolean>;
|
manageConnection: jotai.Atom<boolean>;
|
||||||
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
|
|
||||||
metaFilePath: jotai.Atom<string>;
|
metaFilePath: jotai.Atom<string>;
|
||||||
statFilePath: jotai.Atom<Promise<string>>;
|
statFilePath: jotai.Atom<Promise<string>>;
|
||||||
@ -146,10 +147,15 @@ export class PreviewModel implements ViewModel {
|
|||||||
this.monacoRef = createRef();
|
this.monacoRef = createRef();
|
||||||
this.viewIcon = jotai.atom((get) => {
|
this.viewIcon = jotai.atom((get) => {
|
||||||
const blockData = get(this.blockAtom);
|
const blockData = get(this.blockAtom);
|
||||||
const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
|
|
||||||
if (blockData?.meta?.icon) {
|
if (blockData?.meta?.icon) {
|
||||||
return blockData.meta.icon;
|
return blockData.meta.icon;
|
||||||
}
|
}
|
||||||
|
const connStatus = get(this.connStatus);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fileName = get(this.metaFilePath);
|
||||||
|
const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
|
||||||
const mimeType = util.jotaiLoadableValue(mimeTypeLoadable, "");
|
const mimeType = util.jotaiLoadableValue(mimeTypeLoadable, "");
|
||||||
if (mimeType == "directory") {
|
if (mimeType == "directory") {
|
||||||
return {
|
return {
|
||||||
@ -189,9 +195,19 @@ export class PreviewModel implements ViewModel {
|
|||||||
});
|
});
|
||||||
this.viewName = jotai.atom("Preview");
|
this.viewName = jotai.atom("Preview");
|
||||||
this.viewText = jotai.atom((get) => {
|
this.viewText = jotai.atom((get) => {
|
||||||
|
let headerPath = get(this.metaFilePath);
|
||||||
|
const connStatus = get(this.connStatus);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
elemtype: "text",
|
||||||
|
text: headerPath,
|
||||||
|
className: "preview-filename",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
const loadableSV = get(this.loadableSpecializedView);
|
const loadableSV = get(this.loadableSpecializedView);
|
||||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||||
let headerPath = get(this.metaFilePath);
|
|
||||||
const loadableFileInfo = get(this.loadableFileInfo);
|
const loadableFileInfo = get(this.loadableFileInfo);
|
||||||
if (loadableFileInfo.state == "hasData") {
|
if (loadableFileInfo.state == "hasData") {
|
||||||
headerPath = loadableFileInfo.data?.path;
|
headerPath = loadableFileInfo.data?.path;
|
||||||
@ -248,6 +264,10 @@ export class PreviewModel implements ViewModel {
|
|||||||
] as HeaderElem[];
|
] as HeaderElem[];
|
||||||
});
|
});
|
||||||
this.preIconButton = jotai.atom((get) => {
|
this.preIconButton = jotai.atom((get) => {
|
||||||
|
const connStatus = get(this.connStatus);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||||
if (mimeType == "directory") {
|
if (mimeType == "directory") {
|
||||||
return null;
|
return null;
|
||||||
@ -259,6 +279,10 @@ export class PreviewModel implements ViewModel {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.endIconButtons = jotai.atom((get) => {
|
this.endIconButtons = jotai.atom((get) => {
|
||||||
|
const connStatus = get(this.connStatus);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||||
const loadableSV = get(this.loadableSpecializedView);
|
const loadableSV = get(this.loadableSpecializedView);
|
||||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||||
@ -356,6 +380,12 @@ export class PreviewModel implements ViewModel {
|
|||||||
this.loadableSpecializedView = loadable(this.specializedView);
|
this.loadableSpecializedView = loadable(this.specializedView);
|
||||||
this.canPreview = jotai.atom(false);
|
this.canPreview = jotai.atom(false);
|
||||||
this.loadableFileInfo = loadable(this.statFile);
|
this.loadableFileInfo = loadable(this.statFile);
|
||||||
|
this.connStatus = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const connName = blockData?.meta?.connection;
|
||||||
|
const connAtom = getConnStatusAtom(connName);
|
||||||
|
return get(connAtom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
markdownShowTocToggle() {
|
markdownShowTocToggle() {
|
||||||
@ -831,6 +861,10 @@ function PreviewView({
|
|||||||
contentRef: React.RefObject<HTMLDivElement>;
|
contentRef: React.RefObject<HTMLDivElement>;
|
||||||
model: PreviewModel;
|
model: PreviewModel;
|
||||||
}) {
|
}) {
|
||||||
|
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenFileModal blockId={blockId} model={model} blockRef={blockRef} />
|
<OpenFileModal blockId={blockId} model={model} blockRef={blockRef} />
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { VDomView } from "@/app/view/term/vdom";
|
import { VDomView } from "@/app/view/term/vdom";
|
||||||
import { WOS, atoms, getEventORefSubject, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
import { WOS, atoms, getConnStatusAtom, getEventORefSubject, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as keyutil from "@/util/keyutil";
|
import * as keyutil from "@/util/keyutil";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
@ -108,6 +108,7 @@ class TermViewModel {
|
|||||||
viewName: jotai.Atom<string>;
|
viewName: jotai.Atom<string>;
|
||||||
blockBg: jotai.Atom<MetaType>;
|
blockBg: jotai.Atom<MetaType>;
|
||||||
manageConnection: jotai.Atom<boolean>;
|
manageConnection: jotai.Atom<boolean>;
|
||||||
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
|
|
||||||
constructor(blockId: string) {
|
constructor(blockId: string) {
|
||||||
this.viewType = "term";
|
this.viewType = "term";
|
||||||
@ -142,10 +143,12 @@ class TermViewModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
this.connStatus = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
resetConnection() {
|
const connName = blockData?.meta?.connection;
|
||||||
WshServer.ControllerRestartCommand({ blockid: this.blockId });
|
const connAtom = getConnStatusAtom(connName);
|
||||||
|
return get(connAtom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
giveFocus(): boolean {
|
giveFocus(): boolean {
|
||||||
@ -172,21 +175,39 @@ class TermViewModel {
|
|||||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||||
const termThemes = fullConfig?.termthemes ?? {};
|
const termThemes = fullConfig?.termthemes ?? {};
|
||||||
const termThemeKeys = Object.keys(termThemes);
|
const termThemeKeys = Object.keys(termThemes);
|
||||||
|
|
||||||
termThemeKeys.sort((a, b) => {
|
termThemeKeys.sort((a, b) => {
|
||||||
return termThemes[a]["display:order"] - termThemes[b]["display:order"];
|
return termThemes[a]["display:order"] - termThemes[b]["display:order"];
|
||||||
});
|
});
|
||||||
|
const fullMenu: ContextMenuItem[] = [];
|
||||||
const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
|
const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
|
||||||
return {
|
return {
|
||||||
label: termThemes[themeName]["display:name"] ?? themeName,
|
label: termThemes[themeName]["display:name"] ?? themeName,
|
||||||
click: () => this.setTerminalTheme(themeName),
|
click: () => this.setTerminalTheme(themeName),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return [
|
fullMenu.push({
|
||||||
{
|
label: "Themes",
|
||||||
label: "Themes",
|
submenu: submenu,
|
||||||
submenu: submenu,
|
});
|
||||||
|
fullMenu.push({ type: "separator" });
|
||||||
|
fullMenu.push({
|
||||||
|
label: "Force Restart Controller",
|
||||||
|
click: () => {
|
||||||
|
const termsize = {
|
||||||
|
rows: this.termRef.current?.terminal?.rows,
|
||||||
|
cols: this.termRef.current?.terminal?.cols,
|
||||||
|
};
|
||||||
|
const prtn = WshServer.ControllerResyncCommand({
|
||||||
|
tabid: globalStore.get(atoms.activeTabId),
|
||||||
|
blockid: this.blockId,
|
||||||
|
forcerestart: true,
|
||||||
|
rtopts: { termsize: termsize },
|
||||||
|
});
|
||||||
|
prtn.catch((e) => console.log("error controller resync (force restart)", e));
|
||||||
},
|
},
|
||||||
];
|
});
|
||||||
|
return fullMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +220,28 @@ interface TerminalViewProps {
|
|||||||
model: TermViewModel;
|
model: TermViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) => {
|
||||||
|
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||||
|
const [lastConnStatus, setLastConnStatus] = React.useState<ConnStatus>(connStatus);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!model.termRef.current?.hasResized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isConnected = connStatus?.status == "connected";
|
||||||
|
const wasConnected = lastConnStatus?.status == "connected";
|
||||||
|
const curConnName = connStatus?.connection;
|
||||||
|
const lastConnName = lastConnStatus?.connection;
|
||||||
|
if (isConnected == wasConnected && curConnName == lastConnName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.termRef.current?.resyncController("resync handler");
|
||||||
|
setLastConnStatus(connStatus);
|
||||||
|
}, [connStatus]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||||
const viewRef = React.createRef<HTMLDivElement>();
|
const viewRef = React.createRef<HTMLDivElement>();
|
||||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
@ -257,7 +300,9 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
}
|
}
|
||||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||||
// restart
|
// restart
|
||||||
WshServer.ControllerRestartCommand({ blockid: blockId });
|
const tabId = globalStore.get(atoms.activeTabId);
|
||||||
|
const prtn = WshServer.ControllerResyncCommand({ tabid: tabId, blockid: blockId });
|
||||||
|
prtn.catch((e) => console.log("error controller resync (enter)", blockId, e));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -352,6 +397,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
|
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
|
||||||
|
<TermResyncHandler blockId={blockId} model={model} />
|
||||||
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||||
<TermStickers config={stickerConfig} />
|
<TermStickers config={stickerConfig} />
|
||||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||||
|
@ -2,7 +2,16 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { PLATFORM, WOS, fetchWaveFile, getFileSubject, openLink, sendWSCommand } from "@/store/global";
|
import {
|
||||||
|
PLATFORM,
|
||||||
|
WOS,
|
||||||
|
atoms,
|
||||||
|
fetchWaveFile,
|
||||||
|
getFileSubject,
|
||||||
|
globalStore,
|
||||||
|
openLink,
|
||||||
|
sendWSCommand,
|
||||||
|
} from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import { base64ToArray, fireAndForget } from "@/util/util";
|
import { base64ToArray, fireAndForget } from "@/util/util";
|
||||||
@ -11,9 +20,12 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
|
|||||||
import { WebglAddon } from "@xterm/addon-webgl";
|
import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
import * as TermTypes from "@xterm/xterm";
|
import * as TermTypes from "@xterm/xterm";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import debug from "debug";
|
||||||
import { debounce } from "throttle-debounce";
|
import { debounce } from "throttle-debounce";
|
||||||
import { FitAddon } from "./fitaddon";
|
import { FitAddon } from "./fitaddon";
|
||||||
|
|
||||||
|
const dlog = debug("wave:termwrap");
|
||||||
|
|
||||||
const TermFileName = "term";
|
const TermFileName = "term";
|
||||||
const TermCacheFileName = "cache:term:full";
|
const TermCacheFileName = "cache:term:full";
|
||||||
|
|
||||||
@ -49,6 +61,7 @@ export class TermWrap {
|
|||||||
heldData: Uint8Array[];
|
heldData: Uint8Array[];
|
||||||
handleResize_debounced: () => void;
|
handleResize_debounced: () => void;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
|
hasResized: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
blockId: string,
|
blockId: string,
|
||||||
@ -60,13 +73,13 @@ export class TermWrap {
|
|||||||
this.blockId = blockId;
|
this.blockId = blockId;
|
||||||
this.ptyOffset = 0;
|
this.ptyOffset = 0;
|
||||||
this.dataBytesProcessed = 0;
|
this.dataBytesProcessed = 0;
|
||||||
|
this.hasResized = false;
|
||||||
this.terminal = new Terminal(options);
|
this.terminal = new Terminal(options);
|
||||||
this.fitAddon = new FitAddon();
|
this.fitAddon = new FitAddon();
|
||||||
this.fitAddon.noScrollbar = PLATFORM == "darwin";
|
this.fitAddon.noScrollbar = PLATFORM == "darwin";
|
||||||
this.serializeAddon = new SerializeAddon();
|
this.serializeAddon = new SerializeAddon();
|
||||||
this.terminal.loadAddon(this.fitAddon);
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
this.terminal.loadAddon(this.serializeAddon);
|
this.terminal.loadAddon(this.serializeAddon);
|
||||||
|
|
||||||
this.terminal.loadAddon(
|
this.terminal.loadAddon(
|
||||||
new WebLinksAddon((e, uri) => {
|
new WebLinksAddon((e, uri) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -208,18 +221,35 @@ export class TermWrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resyncController(reason: string) {
|
||||||
|
dlog("resync controller", this.blockId, reason);
|
||||||
|
const tabId = globalStore.get(atoms.activeTabId);
|
||||||
|
const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } };
|
||||||
|
try {
|
||||||
|
await WshServer.ControllerResyncCommand({ tabid: tabId, blockid: this.blockId, rtopts: rtOpts });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`error controller resync (${reason})`, this.blockId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
const oldRows = this.terminal.rows;
|
const oldRows = this.terminal.rows;
|
||||||
const oldCols = this.terminal.cols;
|
const oldCols = this.terminal.cols;
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
||||||
|
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
|
||||||
const wsCommand: SetBlockTermSizeWSCommand = {
|
const wsCommand: SetBlockTermSizeWSCommand = {
|
||||||
wscommand: "setblocktermsize",
|
wscommand: "setblocktermsize",
|
||||||
blockid: this.blockId,
|
blockid: this.blockId,
|
||||||
termsize: { rows: this.terminal.rows, cols: this.terminal.cols },
|
termsize: termSize,
|
||||||
};
|
};
|
||||||
sendWSCommand(wsCommand);
|
sendWSCommand(wsCommand);
|
||||||
}
|
}
|
||||||
|
dlog("resize", `${this.terminal.rows}x${this.terminal.cols}`, `${oldRows}x${oldCols}`, this.hasResized);
|
||||||
|
if (!this.hasResized) {
|
||||||
|
this.hasResized = true;
|
||||||
|
this.resyncController("initial resize");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processAndCacheData() {
|
processAndCacheData() {
|
||||||
|
5
frontend/types/custom.d.ts
vendored
5
frontend/types/custom.d.ts
vendored
@ -240,6 +240,11 @@ declare global {
|
|||||||
version: string;
|
version: string;
|
||||||
buildTime: number;
|
buildTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BlockComponentModel = {
|
||||||
|
openSwitchConnection?: () => void;
|
||||||
|
viewModel: ViewModel;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
15
frontend/types/gotypes.d.ts
vendored
15
frontend/types/gotypes.d.ts
vendored
@ -15,8 +15,8 @@ declare global {
|
|||||||
// blockcontroller.BlockControllerRuntimeStatus
|
// blockcontroller.BlockControllerRuntimeStatus
|
||||||
type BlockControllerRuntimeStatus = {
|
type BlockControllerRuntimeStatus = {
|
||||||
blockid: string;
|
blockid: string;
|
||||||
status: string;
|
|
||||||
shellprocstatus?: string;
|
shellprocstatus?: string;
|
||||||
|
shellprocconnname?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// waveobj.BlockDef
|
// waveobj.BlockDef
|
||||||
@ -58,17 +58,20 @@ declare global {
|
|||||||
termsize?: TermSize;
|
termsize?: TermSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wshrpc.CommandBlockRestartData
|
|
||||||
type CommandBlockRestartData = {
|
|
||||||
blockid: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// wshrpc.CommandBlockSetViewData
|
// wshrpc.CommandBlockSetViewData
|
||||||
type CommandBlockSetViewData = {
|
type CommandBlockSetViewData = {
|
||||||
blockid: string;
|
blockid: string;
|
||||||
view: string;
|
view: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wshrpc.CommandControllerResyncData
|
||||||
|
type CommandControllerResyncData = {
|
||||||
|
forcerestart?: boolean;
|
||||||
|
tabid: string;
|
||||||
|
blockid: string;
|
||||||
|
rtopts?: RuntimeOpts;
|
||||||
|
};
|
||||||
|
|
||||||
// wshrpc.CommandCreateBlockData
|
// wshrpc.CommandCreateBlockData
|
||||||
type CommandCreateBlockData = {
|
type CommandCreateBlockData = {
|
||||||
tabid: string;
|
tabid: string;
|
||||||
|
@ -72,7 +72,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const fullConfig = await services.FileService.GetFullConfig();
|
const fullConfig = await services.FileService.GetFullConfig();
|
||||||
console.log("fullconfig", fullConfig);
|
console.log("fullconfig", fullConfig);
|
||||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||||
services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
const prtn = services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||||
|
prtn.catch((e) => {
|
||||||
|
console.log("error on initial SetActiveTab", e);
|
||||||
|
});
|
||||||
const reactElem = React.createElement(App, null, null);
|
const reactElem = React.createElement(App, null, null);
|
||||||
const elem = document.getElementById("main");
|
const elem = document.getElementById("main");
|
||||||
const root = createRoot(elem);
|
const root = createRoot(elem);
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@ -39,7 +38,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Status_Init = "init"
|
|
||||||
Status_Running = "running"
|
Status_Running = "running"
|
||||||
Status_Done = "done"
|
Status_Done = "done"
|
||||||
)
|
)
|
||||||
@ -66,18 +64,16 @@ type BlockController struct {
|
|||||||
TabId string
|
TabId string
|
||||||
BlockId string
|
BlockId string
|
||||||
BlockDef *waveobj.BlockDef
|
BlockDef *waveobj.BlockDef
|
||||||
Status string
|
|
||||||
CreatedHtmlFile bool
|
CreatedHtmlFile bool
|
||||||
ShellProc *shellexec.ShellProc
|
ShellProc *shellexec.ShellProc
|
||||||
ShellInputCh chan *BlockInputUnion
|
ShellInputCh chan *BlockInputUnion
|
||||||
ShellProcStatus string
|
ShellProcStatus string
|
||||||
StopCh chan bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockControllerRuntimeStatus struct {
|
type BlockControllerRuntimeStatus struct {
|
||||||
BlockId string `json:"blockid"`
|
BlockId string `json:"blockid"`
|
||||||
Status string `json:"status"`
|
ShellProcStatus string `json:"shellprocstatus,omitempty"`
|
||||||
ShellProcStatus string `json:"shellprocstatus,omitempty"`
|
ShellProcConnName string `json:"shellprocconnname,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) WithLock(f func()) {
|
func (bc *BlockController) WithLock(f func()) {
|
||||||
@ -90,25 +86,14 @@ func (bc *BlockController) GetRuntimeStatus() *BlockControllerRuntimeStatus {
|
|||||||
var rtn BlockControllerRuntimeStatus
|
var rtn BlockControllerRuntimeStatus
|
||||||
bc.WithLock(func() {
|
bc.WithLock(func() {
|
||||||
rtn.BlockId = bc.BlockId
|
rtn.BlockId = bc.BlockId
|
||||||
rtn.Status = bc.Status
|
|
||||||
rtn.ShellProcStatus = bc.ShellProcStatus
|
rtn.ShellProcStatus = bc.ShellProcStatus
|
||||||
|
if bc.ShellProc != nil {
|
||||||
|
rtn.ShellProcConnName = bc.ShellProc.ConnName
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return &rtn
|
return &rtn
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonDeepCopy(val map[string]any) (map[string]any, error) {
|
|
||||||
barr, err := json.Marshal(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var rtn map[string]any
|
|
||||||
err = json.Unmarshal(barr, &rtn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return rtn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BlockController) getShellProc() *shellexec.ShellProc {
|
func (bc *BlockController) getShellProc() *shellexec.ShellProc {
|
||||||
bc.Lock.Lock()
|
bc.Lock.Lock()
|
||||||
defer bc.Lock.Unlock()
|
defer bc.Lock.Unlock()
|
||||||
@ -136,6 +121,7 @@ func (bc *BlockController) UpdateControllerAndSendUpdate(updateFn func() bool) {
|
|||||||
Event: wshrpc.Event_ControllerStatus,
|
Event: wshrpc.Event_ControllerStatus,
|
||||||
Scopes: []string{
|
Scopes: []string{
|
||||||
waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String(),
|
waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String(),
|
||||||
|
waveobj.MakeORef(waveobj.OType_Block, bc.BlockId).String(),
|
||||||
},
|
},
|
||||||
Data: rtStatus,
|
Data: rtStatus,
|
||||||
}
|
}
|
||||||
@ -228,19 +214,11 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
bc.resetTerminalState()
|
bc.resetTerminalState()
|
||||||
}
|
}
|
||||||
err = nil
|
err = nil
|
||||||
if bc.getShellProc() != nil {
|
bcInitStatus := bc.GetRuntimeStatus()
|
||||||
|
if bcInitStatus.ShellProcStatus == Status_Running {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var shellProcErr error
|
// TODO better sync here (don't let two starts happen at the same times)
|
||||||
bc.WithLock(func() {
|
|
||||||
if bc.ShellProc != nil {
|
|
||||||
shellProcErr = fmt.Errorf("shell process already running")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if shellProcErr != nil {
|
|
||||||
return shellProcErr
|
|
||||||
}
|
|
||||||
remoteName := blockMeta.GetString(waveobj.MetaKey_Connection, "")
|
remoteName := blockMeta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
var cmdStr string
|
var cmdStr string
|
||||||
cmdOpts := shellexec.CommandOptsType{
|
cmdOpts := shellexec.CommandOptsType{
|
||||||
@ -288,10 +266,10 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn := conncontroller.GetConn(credentialCtx, opts, true)
|
conn := conncontroller.GetConn(credentialCtx, opts, false)
|
||||||
connStatus := conn.DeriveConnStatus()
|
connStatus := conn.DeriveConnStatus()
|
||||||
if connStatus.Error != "" {
|
if connStatus.Status != conncontroller.Status_Connected {
|
||||||
return fmt.Errorf("error connecting to remote: %s", connStatus.Error)
|
return fmt.Errorf("not connected, cannot start shellproc")
|
||||||
}
|
}
|
||||||
if !blockMeta.GetBool(waveobj.MetaKey_CmdNoWsh, false) {
|
if !blockMeta.GetBool(waveobj.MetaKey_CmdNoWsh, false) {
|
||||||
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.GetDomainSocketName())
|
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.GetDomainSocketName())
|
||||||
@ -300,7 +278,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
}
|
}
|
||||||
cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr
|
cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr
|
||||||
}
|
}
|
||||||
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn.Client)
|
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -335,12 +313,14 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
// handles regular output from the pty (goes to the blockfile and xterm)
|
// handles regular output from the pty (goes to the blockfile and xterm)
|
||||||
defer func() {
|
defer func() {
|
||||||
log.Printf("[shellproc] pty-read loop done\n")
|
log.Printf("[shellproc] pty-read loop done\n")
|
||||||
|
|
||||||
// needs synchronization
|
|
||||||
bc.ShellProc.Close()
|
bc.ShellProc.Close()
|
||||||
close(bc.ShellInputCh)
|
bc.WithLock(func() {
|
||||||
bc.ShellProc = nil
|
// so no other events are sent
|
||||||
bc.ShellInputCh = nil
|
bc.ShellInputCh = nil
|
||||||
|
})
|
||||||
|
// to stop the inputCh loop
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
close(shellInputCh) // don't use bc.ShellInputCh (it's nil)
|
||||||
}()
|
}()
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
@ -365,6 +345,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
log.Printf("[shellproc] shellInputCh loop done\n")
|
log.Printf("[shellproc] shellInputCh loop done\n")
|
||||||
}()
|
}()
|
||||||
// handles input from the shellInputCh, sent to pty
|
// handles input from the shellInputCh, sent to pty
|
||||||
|
// use shellInputCh instead of bc.ShellInputCh (because we want to be attached to *this* ch. bc.ShellInputCh can be updated)
|
||||||
for ic := range shellInputCh {
|
for ic := range shellInputCh {
|
||||||
if len(ic.InputData) > 0 {
|
if len(ic.InputData) > 0 {
|
||||||
bc.ShellProc.Cmd.Write(ic.InputData)
|
bc.ShellProc.Cmd.Write(ic.InputData)
|
||||||
@ -446,23 +427,7 @@ func setTermSize(ctx context.Context, blockId string, termSize waveobj.TermSize)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) run(bdata *waveobj.Block, blockMeta map[string]any) {
|
func (bc *BlockController) run(bdata *waveobj.Block, blockMeta map[string]any, rtOpts *waveobj.RuntimeOpts) {
|
||||||
defer func() {
|
|
||||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
|
||||||
if bc.Status == Status_Running {
|
|
||||||
bc.Status = Status_Done
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
globalLock.Lock()
|
|
||||||
defer globalLock.Unlock()
|
|
||||||
delete(blockControllerMap, bc.BlockId)
|
|
||||||
}()
|
|
||||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
|
||||||
bc.Status = Status_Running
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
controllerName := bdata.Meta.GetString(waveobj.MetaKey_Controller, "")
|
controllerName := bdata.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||||
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
||||||
log.Printf("unknown controller %q\n", controllerName)
|
log.Printf("unknown controller %q\n", controllerName)
|
||||||
@ -477,51 +442,136 @@ func (bc *BlockController) run(bdata *waveobj.Block, blockMeta map[string]any) {
|
|||||||
runOnStart := getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdRunOnStart, true)
|
runOnStart := getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdRunOnStart, true)
|
||||||
if runOnStart {
|
if runOnStart {
|
||||||
go func() {
|
go func() {
|
||||||
err := bc.DoRunShellCommand(&RunShellOpts{TermSize: getTermSize(bdata)}, bdata.Meta)
|
var termSize waveobj.TermSize
|
||||||
|
if rtOpts != nil {
|
||||||
|
termSize = rtOpts.TermSize
|
||||||
|
} else {
|
||||||
|
termSize = getTermSize(bdata)
|
||||||
|
}
|
||||||
|
err := bc.DoRunShellCommand(&RunShellOpts{TermSize: termSize}, bdata.Meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error running shell: %v\n", err)
|
log.Printf("error running shell: %v\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
<-bc.StopCh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) SendInput(inputUnion *BlockInputUnion) error {
|
func (bc *BlockController) SendInput(inputUnion *BlockInputUnion) error {
|
||||||
if bc.ShellInputCh == nil {
|
var shellInputCh chan *BlockInputUnion
|
||||||
|
bc.WithLock(func() {
|
||||||
|
shellInputCh = bc.ShellInputCh
|
||||||
|
})
|
||||||
|
if shellInputCh == nil {
|
||||||
return fmt.Errorf("no shell input chan")
|
return fmt.Errorf("no shell input chan")
|
||||||
}
|
}
|
||||||
bc.ShellInputCh <- inputUnion
|
shellInputCh <- inputUnion
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) RestartController() error {
|
func CheckConnStatus(blockId string) error {
|
||||||
// kill the command if it's running
|
bdata, err := wstore.DBMustGet[*waveobj.Block](context.Background(), blockId)
|
||||||
bc.Lock.Lock()
|
|
||||||
if bc.ShellProc != nil {
|
|
||||||
bc.ShellProc.Close()
|
|
||||||
}
|
|
||||||
bc.Lock.Unlock()
|
|
||||||
|
|
||||||
// wait for process to complete
|
|
||||||
if bc.ShellProc != nil {
|
|
||||||
doneCh := bc.ShellProc.DoneCh
|
|
||||||
<-doneCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// restart controller
|
|
||||||
bdata, err := wstore.DBMustGet[*waveobj.Block](context.Background(), bc.BlockId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
}
|
}
|
||||||
err = bc.DoRunShellCommand(&RunShellOpts{TermSize: getTermSize(bdata)}, bdata.Meta)
|
connName := bdata.Meta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
|
if connName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
opts, err := remote.ParseOpts(connName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error running shell command: %v\n", err)
|
return fmt.Errorf("error parsing connection name: %w", err)
|
||||||
|
}
|
||||||
|
conn := conncontroller.GetConn(context.Background(), opts, false)
|
||||||
|
connStatus := conn.DeriveConnStatus()
|
||||||
|
if connStatus.Status != conncontroller.Status_Connected {
|
||||||
|
return fmt.Errorf("not connected: %s", connStatus.Status)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartBlockController(ctx context.Context, tabId string, blockId string) error {
|
func (bc *BlockController) StopShellProc(shouldWait bool) {
|
||||||
log.Printf("start blockcontroller %q\n", blockId)
|
bc.Lock.Lock()
|
||||||
|
defer bc.Lock.Unlock()
|
||||||
|
if bc.ShellProc == nil || bc.ShellProcStatus == Status_Done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bc.ShellProc.Close()
|
||||||
|
if shouldWait {
|
||||||
|
doneCh := bc.ShellProc.DoneCh
|
||||||
|
<-doneCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrCreateBlockController(tabId string, blockId string, controllerName string) *BlockController {
|
||||||
|
var createdController bool
|
||||||
|
var bc *BlockController
|
||||||
|
defer func() {
|
||||||
|
if !createdController || bc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
globalLock.Lock()
|
||||||
|
defer globalLock.Unlock()
|
||||||
|
bc = blockControllerMap[blockId]
|
||||||
|
if bc == nil {
|
||||||
|
bc = &BlockController{
|
||||||
|
Lock: &sync.Mutex{},
|
||||||
|
ControllerType: controllerName,
|
||||||
|
TabId: tabId,
|
||||||
|
BlockId: blockId,
|
||||||
|
ShellProcStatus: Status_Done,
|
||||||
|
}
|
||||||
|
blockControllerMap[blockId] = bc
|
||||||
|
createdController = true
|
||||||
|
}
|
||||||
|
return bc
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts) error {
|
||||||
|
if tabId == "" || blockId == "" {
|
||||||
|
return fmt.Errorf("invalid tabId or blockId passed to ResyncController")
|
||||||
|
}
|
||||||
|
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
|
}
|
||||||
|
connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
|
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||||
|
curBc := GetBlockController(blockId)
|
||||||
|
if controllerName == "" {
|
||||||
|
if curBc != nil {
|
||||||
|
StopBlockController(blockId)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// check if conn is different, if so, stop the current controller
|
||||||
|
if curBc != nil {
|
||||||
|
bcStatus := curBc.GetRuntimeStatus()
|
||||||
|
if bcStatus.ShellProcStatus == Status_Running && bcStatus.ShellProcConnName != connName {
|
||||||
|
StopBlockController(blockId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now if there is a conn, ensure it is connected
|
||||||
|
if connName != "" {
|
||||||
|
err = CheckConnStatus(blockId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot start shellproc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if curBc == nil {
|
||||||
|
return startBlockController(ctx, tabId, blockId, rtOpts)
|
||||||
|
}
|
||||||
|
bcStatus := curBc.GetRuntimeStatus()
|
||||||
|
if bcStatus.ShellProcStatus != Status_Running {
|
||||||
|
return startBlockController(ctx, tabId, blockId, rtOpts)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startBlockController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts) error {
|
||||||
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
@ -534,23 +584,17 @@ func StartBlockController(ctx context.Context, tabId string, blockId string) err
|
|||||||
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
||||||
return fmt.Errorf("unknown controller %q", controllerName)
|
return fmt.Errorf("unknown controller %q", controllerName)
|
||||||
}
|
}
|
||||||
globalLock.Lock()
|
connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
defer globalLock.Unlock()
|
log.Printf("start blockcontroller %s %q (%q)\n", blockId, controllerName, connName)
|
||||||
if _, ok := blockControllerMap[blockId]; ok {
|
err = CheckConnStatus(blockId)
|
||||||
// already running
|
if err != nil {
|
||||||
return nil
|
return fmt.Errorf("cannot start shellproc: %w", err)
|
||||||
}
|
}
|
||||||
bc := &BlockController{
|
bc := getOrCreateBlockController(tabId, blockId, controllerName)
|
||||||
Lock: &sync.Mutex{},
|
bcStatus := bc.GetRuntimeStatus()
|
||||||
ControllerType: controllerName,
|
if bcStatus.ShellProcStatus == Status_Done {
|
||||||
TabId: tabId,
|
go bc.run(blockData, blockData.Meta, rtOpts)
|
||||||
BlockId: blockId,
|
|
||||||
Status: Status_Init,
|
|
||||||
ShellProcStatus: Status_Init,
|
|
||||||
StopCh: make(chan bool),
|
|
||||||
}
|
}
|
||||||
blockControllerMap[blockId] = bc
|
|
||||||
go bc.run(blockData, blockData.Meta)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,8 +605,32 @@ func StopBlockController(blockId string) {
|
|||||||
}
|
}
|
||||||
if bc.getShellProc() != nil {
|
if bc.getShellProc() != nil {
|
||||||
bc.ShellProc.Close()
|
bc.ShellProc.Close()
|
||||||
|
<-bc.ShellProc.DoneCh
|
||||||
|
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||||
|
bc.ShellProcStatus = Status_Done
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getControllerList() []*BlockController {
|
||||||
|
globalLock.Lock()
|
||||||
|
defer globalLock.Unlock()
|
||||||
|
var rtn []*BlockController
|
||||||
|
for _, bc := range blockControllerMap {
|
||||||
|
rtn = append(rtn, bc)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopAllBlockControllers() {
|
||||||
|
clist := getControllerList()
|
||||||
|
for _, bc := range clist {
|
||||||
|
if bc.ShellProcStatus == Status_Running {
|
||||||
|
go StopBlockController(bc.BlockId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
close(bc.StopCh)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetBlockController(blockId string) *BlockController {
|
func GetBlockController(blockId string) *BlockController {
|
||||||
|
@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
|
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wps"
|
"github.com/wavetermdev/thenextwave/pkg/wps"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
@ -40,6 +39,8 @@ const (
|
|||||||
Status_Error = "error"
|
Status_Error = "error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultConnectionTimeout = 60 * time.Second
|
||||||
|
|
||||||
var globalLock = &sync.Mutex{}
|
var globalLock = &sync.Mutex{}
|
||||||
var clientControllerMap = make(map[remote.SSHOpts]*SSHConn)
|
var clientControllerMap = make(map[remote.SSHOpts]*SSHConn)
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ func (conn *SSHConn) OpenDomainSocketListener() error {
|
|||||||
return fmt.Errorf("error generating random string: %w", err)
|
return fmt.Errorf("error generating random string: %w", err)
|
||||||
}
|
}
|
||||||
sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr)
|
sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr)
|
||||||
log.Printf("remote domain socket %s %q\n", conn.GetName(), sockName)
|
log.Printf("remote domain socket %s %q\n", conn.GetName(), conn.GetDomainSocketName())
|
||||||
listener, err := client.ListenUnix(sockName)
|
listener, err := client.ListenUnix(sockName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to request connection domain socket: %v", err)
|
return fmt.Errorf("unable to request connection domain socket: %v", err)
|
||||||
@ -251,6 +252,13 @@ func (conn *SSHConn) StartConnServer() error {
|
|||||||
log.Printf("[conncontroller:%s] error reading output: %v\n", conn.GetName(), readErr)
|
log.Printf("[conncontroller:%s] error reading output: %v\n", conn.GetName(), readErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
regCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("timeout waiting for connserver to register")
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,6 +381,7 @@ func (conn *SSHConn) Connect(ctx context.Context) error {
|
|||||||
connectAllowed = true
|
connectAllowed = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
log.Printf("Connect %s\n", conn.GetName())
|
||||||
if !connectAllowed {
|
if !connectAllowed {
|
||||||
return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus())
|
return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus())
|
||||||
}
|
}
|
||||||
@ -467,45 +476,31 @@ func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool) *SSH
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience function for ensuring a connection is established
|
// Convenience function for ensuring a connection is established
|
||||||
func EnsureConnection(ctx context.Context, blockData *waveobj.Block) error {
|
func EnsureConnection(ctx context.Context, connName string) error {
|
||||||
connectionName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
|
if connName == "" {
|
||||||
if connectionName == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
credentialCtx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second)
|
connOpts, err := remote.ParseOpts(connName)
|
||||||
defer cancelFunc()
|
|
||||||
|
|
||||||
opts, err := remote.ParseOpts(connectionName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("error parsing connection name: %w", err)
|
||||||
}
|
}
|
||||||
conn := GetConn(credentialCtx, opts, true)
|
conn := GetConn(ctx, connOpts, false)
|
||||||
statusChan := make(chan string, 1)
|
if conn == nil {
|
||||||
go func() {
|
return fmt.Errorf("connection not found: %s", connName)
|
||||||
// we need to wait for connected/disconnected/error
|
}
|
||||||
// to ensure the connection has been established before
|
connStatus := conn.DeriveConnStatus()
|
||||||
// continuing in the original thread
|
switch connStatus.Status {
|
||||||
for {
|
case Status_Connected:
|
||||||
// GetStatus has a lock which makes this reasonable to loop over
|
return nil
|
||||||
status := conn.GetStatus()
|
case Status_Connecting:
|
||||||
if credentialCtx.Err() != nil {
|
return conn.WaitForConnect(ctx)
|
||||||
// prevent infinite loop from context
|
case Status_Init, Status_Disconnected:
|
||||||
statusChan <- Status_Error
|
return conn.Connect(ctx)
|
||||||
return
|
case Status_Error:
|
||||||
}
|
return fmt.Errorf("connection error: %s", connStatus.Error)
|
||||||
if status == Status_Connected || status == Status_Disconnected || status == Status_Error {
|
default:
|
||||||
statusChan <- status
|
return fmt.Errorf("unknown connection status %q", connStatus.Status)
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
status := <-statusChan
|
|
||||||
if status == Status_Error {
|
|
||||||
return fmt.Errorf("connection error: %v", conn.Error)
|
|
||||||
} else if status == Status_Disconnected {
|
|
||||||
return fmt.Errorf("disconnected: %v", conn.Error)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisconnectClient(opts *remote.SSHOpts) error {
|
func DisconnectClient(opts *remote.SSHOpts) error {
|
||||||
|
@ -292,9 +292,6 @@ func CpHostToRemote(client *ssh.Client, sourcePath string, destPath string) erro
|
|||||||
func InstallClientRcFiles(client *ssh.Client) error {
|
func InstallClientRcFiles(client *ssh.Client) error {
|
||||||
path := GetWshPath(client)
|
path := GetWshPath(client)
|
||||||
log.Printf("path to wsh searched is: %s", path)
|
log.Printf("path to wsh searched is: %s", path)
|
||||||
log.Printf("in bytes is: %v", []byte(path))
|
|
||||||
log.Printf("in bytes expected would be: %v", []byte("~/.waveterm/bin/wsh"))
|
|
||||||
|
|
||||||
session, err := client.NewSession()
|
session, err := client.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// this is a true error that should stop further progress
|
// this is a true error that should stop further progress
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
@ -34,10 +33,7 @@ func (bs *BlockService) SendCommand_Meta() tsgenmeta.MethodMeta {
|
|||||||
func (bs *BlockService) GetControllerStatus(ctx context.Context, blockId string) (*blockcontroller.BlockControllerRuntimeStatus, error) {
|
func (bs *BlockService) GetControllerStatus(ctx context.Context, blockId string) (*blockcontroller.BlockControllerRuntimeStatus, error) {
|
||||||
bc := blockcontroller.GetBlockController(blockId)
|
bc := blockcontroller.GetBlockController(blockId)
|
||||||
if bc == nil {
|
if bc == nil {
|
||||||
return &blockcontroller.BlockControllerRuntimeStatus{
|
return nil, nil
|
||||||
BlockId: blockId,
|
|
||||||
Status: "stopped",
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
return bc.GetRuntimeStatus(), nil
|
return bc.GetRuntimeStatus(), nil
|
||||||
}
|
}
|
||||||
@ -84,11 +80,3 @@ func (bs *BlockService) SaveWaveAiData(ctx context.Context, blockId string, hist
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *BlockService) EnsureConnection(ctx context.Context, blockId string) error {
|
|
||||||
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return conncontroller.EnsureConnection(ctx, block)
|
|
||||||
}
|
|
||||||
|
@ -6,12 +6,9 @@ package objectservice
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wcore"
|
"github.com/wavetermdev/thenextwave/pkg/wcore"
|
||||||
@ -22,6 +19,7 @@ import (
|
|||||||
type ObjectService struct{}
|
type ObjectService struct{}
|
||||||
|
|
||||||
const DefaultTimeout = 2 * time.Second
|
const DefaultTimeout = 2 * time.Second
|
||||||
|
const ConnContextTimeout = 60 * time.Second
|
||||||
|
|
||||||
func parseORef(oref string) (*waveobj.ORef, error) {
|
func parseORef(oref string) (*waveobj.ORef, error) {
|
||||||
fields := strings.Split(oref, ":")
|
fields := strings.Split(oref, ":")
|
||||||
@ -133,22 +131,6 @@ func (svc *ObjectService) SetActiveTab(uiContext waveobj.UIContext, tabId string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||||
}
|
}
|
||||||
for _, blockId := range tab.BlockIds {
|
|
||||||
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting block: %w", err)
|
|
||||||
}
|
|
||||||
err = conncontroller.EnsureConnection(ctx, blockData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to ensure connection: %v", err)
|
|
||||||
}
|
|
||||||
blockErr := blockcontroller.StartBlockController(ctx, tabId, blockId)
|
|
||||||
if blockErr != nil {
|
|
||||||
// we don't want to fail the set active tab operation if a block controller fails to start
|
|
||||||
log.Printf("error starting block controller (blockid:%s): %v", blockId, blockErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blockORefs := tab.GetBlockORefs()
|
blockORefs := tab.GetBlockORefs()
|
||||||
blocks, err := wstore.DBSelectORefs(ctx, blockORefs)
|
blocks, err := wstore.DBSelectORefs(ctx, blockORefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2,7 +2,9 @@ package shellexec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
@ -10,6 +12,7 @@ import (
|
|||||||
|
|
||||||
type ConnInterface interface {
|
type ConnInterface interface {
|
||||||
Kill()
|
Kill()
|
||||||
|
KillGraceful(time.Duration)
|
||||||
Wait() error
|
Wait() error
|
||||||
Start() error
|
Start() error
|
||||||
StdinPipe() (io.WriteCloser, error)
|
StdinPipe() (io.WriteCloser, error)
|
||||||
@ -32,6 +35,22 @@ func (cw CmdWrap) Wait() error {
|
|||||||
return cw.Cmd.Wait()
|
return cw.Cmd.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cw CmdWrap) KillGraceful(timeout time.Duration) {
|
||||||
|
if cw.Cmd.Process == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cw.Cmd.ProcessState != nil && cw.Cmd.ProcessState.Exited() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cw.Cmd.Process.Signal(os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(timeout)
|
||||||
|
if cw.Cmd.ProcessState == nil || !cw.Cmd.ProcessState.Exited() {
|
||||||
|
cw.Cmd.Process.Kill() // force kill if it is already not exited
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (cw CmdWrap) Start() error {
|
func (cw CmdWrap) Start() error {
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, extraFile := range cw.Cmd.ExtraFiles {
|
for _, extraFile := range cw.Cmd.ExtraFiles {
|
||||||
@ -75,6 +94,10 @@ func (sw SessionWrap) Kill() {
|
|||||||
sw.Session.Close()
|
sw.Session.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sw SessionWrap) KillGraceful(timeout time.Duration) {
|
||||||
|
sw.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
func (sw SessionWrap) Wait() error {
|
func (sw SessionWrap) Wait() error {
|
||||||
return sw.Session.Wait()
|
return sw.Session.Wait()
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/remote"
|
"github.com/wavetermdev/thenextwave/pkg/remote"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
|
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultGracefulKillWait = 400 * time.Millisecond
|
||||||
|
|
||||||
type CommandOptsType struct {
|
type CommandOptsType struct {
|
||||||
Interactive bool `json:"interactive,omitempty"`
|
Interactive bool `json:"interactive,omitempty"`
|
||||||
Login bool `json:"login,omitempty"`
|
Login bool `json:"login,omitempty"`
|
||||||
@ -33,6 +36,7 @@ type CommandOptsType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShellProc struct {
|
type ShellProc struct {
|
||||||
|
ConnName string
|
||||||
Cmd ConnInterface
|
Cmd ConnInterface
|
||||||
CloseOnce *sync.Once
|
CloseOnce *sync.Once
|
||||||
DoneCh chan any // closed after proc.Wait() returns
|
DoneCh chan any // closed after proc.Wait() returns
|
||||||
@ -40,7 +44,7 @@ type ShellProc struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sp *ShellProc) Close() {
|
func (sp *ShellProc) Close() {
|
||||||
sp.Cmd.Kill()
|
sp.Cmd.KillGraceful(DefaultGracefulKillWait)
|
||||||
go func() {
|
go func() {
|
||||||
waitErr := sp.Cmd.Wait()
|
waitErr := sp.Cmd.Wait()
|
||||||
sp.SetWaitErrorAndSignalDone(waitErr)
|
sp.SetWaitErrorAndSignalDone(waitErr)
|
||||||
@ -134,7 +138,8 @@ func (pp *PipePty) WriteString(s string) (n int, err error) {
|
|||||||
return pp.Write([]byte(s))
|
return pp.Write([]byte(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, client *ssh.Client) (*ShellProc, error) {
|
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
|
||||||
|
client := conn.GetClient()
|
||||||
shellPath, err := remote.DetectShell(client)
|
shellPath, err := remote.DetectShell(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -244,7 +249,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm
|
|||||||
pipePty.Close()
|
pipePty.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ShellProc{Cmd: sessionWrap, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isZshShell(shellPath string) bool {
|
func isZshShell(shellPath string) bool {
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wps"
|
"github.com/wavetermdev/thenextwave/pkg/wps"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
@ -24,6 +23,7 @@ import (
|
|||||||
// TODO bring Tx infra into wcore
|
// TODO bring Tx infra into wcore
|
||||||
|
|
||||||
const DefaultTimeout = 2 * time.Second
|
const DefaultTimeout = 2 * time.Second
|
||||||
|
const DefaultActivateBlockTimeout = 60 * time.Second
|
||||||
|
|
||||||
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
||||||
err := wstore.DeleteBlock(ctx, tabId, blockId)
|
err := wstore.DeleteBlock(ctx, tabId, blockId)
|
||||||
@ -174,17 +174,5 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating block: %w", err)
|
return nil, fmt.Errorf("error creating block: %w", err)
|
||||||
}
|
}
|
||||||
err = conncontroller.EnsureConnection(ctx, blockData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to ensure connection: %v", err)
|
|
||||||
}
|
|
||||||
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
|
|
||||||
if controllerName != "" {
|
|
||||||
err = blockcontroller.StartBlockController(ctx, tabId, blockData.OID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error starting block controller: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return blockData, nil
|
return blockData, nil
|
||||||
}
|
}
|
||||||
|
@ -60,9 +60,15 @@ func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// command "controllerrestart", wshserver.ControllerRestartCommand
|
// command "controllerresync", wshserver.ControllerResyncCommand
|
||||||
func ControllerRestartCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockRestartData, opts *wshrpc.RpcOpts) error {
|
func ControllerResyncCommand(w *wshutil.WshRpc, data wshrpc.CommandControllerResyncData, opts *wshrpc.RpcOpts) error {
|
||||||
_, err := sendRpcRequestCallHelper[any](w, "controllerrestart", data, opts)
|
_, err := sendRpcRequestCallHelper[any](w, "controllerresync", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "controllerstop", wshserver.ControllerStopCommand
|
||||||
|
func ControllerStopCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "controllerstop", data, opts)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ const (
|
|||||||
Command_SetView = "setview"
|
Command_SetView = "setview"
|
||||||
Command_ControllerInput = "controllerinput"
|
Command_ControllerInput = "controllerinput"
|
||||||
Command_ControllerRestart = "controllerrestart"
|
Command_ControllerRestart = "controllerrestart"
|
||||||
|
Command_ControllerStop = "controllerstop"
|
||||||
|
Command_ControllerResync = "controllerresync"
|
||||||
Command_FileAppend = "fileappend"
|
Command_FileAppend = "fileappend"
|
||||||
Command_FileAppendIJson = "fileappendijson"
|
Command_FileAppendIJson = "fileappendijson"
|
||||||
Command_ResolveIds = "resolveids"
|
Command_ResolveIds = "resolveids"
|
||||||
@ -84,7 +86,8 @@ type WshRpcInterface interface {
|
|||||||
SetMetaCommand(ctx context.Context, data CommandSetMetaData) error
|
SetMetaCommand(ctx context.Context, data CommandSetMetaData) error
|
||||||
SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error
|
SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error
|
||||||
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
|
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
|
||||||
ControllerRestartCommand(ctx context.Context, data CommandBlockRestartData) error
|
ControllerStopCommand(ctx context.Context, blockId string) error
|
||||||
|
ControllerResyncCommand(ctx context.Context, data CommandControllerResyncData) error
|
||||||
FileAppendCommand(ctx context.Context, data CommandFileData) error
|
FileAppendCommand(ctx context.Context, data CommandFileData) error
|
||||||
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
|
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
|
||||||
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
|
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
|
||||||
@ -217,8 +220,11 @@ type CommandBlockSetViewData struct {
|
|||||||
View string `json:"view"`
|
View string `json:"view"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandBlockRestartData struct {
|
type CommandControllerResyncData struct {
|
||||||
BlockId string `json:"blockid" wshcontext:"BlockId"`
|
ForceRestart bool `json:"forcerestart,omitempty"`
|
||||||
|
TabId string `json:"tabid" wshcontext:"TabId"`
|
||||||
|
BlockId string `json:"blockid" wshcontext:"BlockId"`
|
||||||
|
RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandBlockInputData struct {
|
type CommandBlockInputData struct {
|
||||||
|
@ -284,12 +284,20 @@ func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBloc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ControllerRestartCommand(ctx context.Context, data wshrpc.CommandBlockRestartData) error {
|
func (ws *WshServer) ControllerStopCommand(ctx context.Context, blockId string) error {
|
||||||
bc := blockcontroller.GetBlockController(data.BlockId)
|
bc := blockcontroller.GetBlockController(blockId)
|
||||||
if bc == nil {
|
if bc == nil {
|
||||||
return fmt.Errorf("block controller not found for block %q", data.BlockId)
|
return nil
|
||||||
}
|
}
|
||||||
return bc.RestartController()
|
bc.StopShellProc(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error {
|
||||||
|
if data.ForceRestart {
|
||||||
|
blockcontroller.StopBlockController(data.BlockId)
|
||||||
|
}
|
||||||
|
return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
|
func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
|
||||||
@ -476,27 +484,7 @@ func (ws *WshServer) ConnStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ConnEnsureCommand(ctx context.Context, connName string) error {
|
func (ws *WshServer) ConnEnsureCommand(ctx context.Context, connName string) error {
|
||||||
connOpts, err := remote.ParseOpts(connName)
|
return conncontroller.EnsureConnection(ctx, connName)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error parsing connection name: %w", err)
|
|
||||||
}
|
|
||||||
conn := conncontroller.GetConn(ctx, connOpts, false)
|
|
||||||
if conn == nil {
|
|
||||||
return fmt.Errorf("connection not found: %s", connName)
|
|
||||||
}
|
|
||||||
connStatus := conn.DeriveConnStatus()
|
|
||||||
switch connStatus.Status {
|
|
||||||
case conncontroller.Status_Connected:
|
|
||||||
return nil
|
|
||||||
case conncontroller.Status_Connecting:
|
|
||||||
return conn.WaitForConnect(ctx)
|
|
||||||
case conncontroller.Status_Init, conncontroller.Status_Disconnected:
|
|
||||||
return conn.Connect(ctx)
|
|
||||||
case conncontroller.Status_Error:
|
|
||||||
return fmt.Errorf("connection error: %s", connStatus.Error)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown connection status %q", connStatus.Status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error {
|
func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error {
|
||||||
|
@ -4,11 +4,13 @@
|
|||||||
package wshutil
|
package wshutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wps"
|
"github.com/wavetermdev/thenextwave/pkg/wps"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
@ -237,6 +239,20 @@ func (router *WshRouter) runServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (router *WshRouter) WaitForRegister(ctx context.Context, routeId string) error {
|
||||||
|
for {
|
||||||
|
if router.GetRpc(routeId) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(30 * time.Millisecond):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// this will also consume the output channel of the abstract client
|
// this will also consume the output channel of the abstract client
|
||||||
func (router *WshRouter) RegisterRoute(routeId string, rpc AbstractRpcClient) {
|
func (router *WshRouter) RegisterRoute(routeId string, rpc AbstractRpcClient) {
|
||||||
if routeId == SysRoute {
|
if routeId == SysRoute {
|
||||||
|
Loading…
Reference in New Issue
Block a user