mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
cmd blocks (#74)
This commit is contained in:
@@ -32,7 +32,7 @@ function processTitleString(titleString: string): React.ReactNode[] {
|
||||
if (titleString == null) {
|
||||
return null;
|
||||
}
|
||||
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9-]+))?>/g;
|
||||
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g;
|
||||
let lastIdx = 0;
|
||||
let match;
|
||||
let partsStack = [[]];
|
||||
@@ -46,10 +46,11 @@ function processTitleString(titleString: string): React.ReactNode[] {
|
||||
if (tagParam == null) {
|
||||
continue;
|
||||
}
|
||||
if (!tagParam.match(/^[a-z0-9-]+$/)) {
|
||||
const iconClass = util.makeIconClass(tagParam, false);
|
||||
if (iconClass == null) {
|
||||
continue;
|
||||
}
|
||||
lastPart.push(<i key={match.index} className={`fa fa-solid fa-${tagParam}`} />);
|
||||
lastPart.push(<i key={match.index} className={iconClass} />);
|
||||
continue;
|
||||
}
|
||||
if (tagName == "c" || tagName == "color") {
|
||||
@@ -105,10 +106,9 @@ function getBlockHeaderText(blockIcon: string, blockData: Block): React.ReactNod
|
||||
if (!util.isBlank(iconColor)) {
|
||||
iconStyle = { color: iconColor };
|
||||
}
|
||||
if (blockIcon.match(/^[a-z0-9-]+$/)) {
|
||||
blockIconElem = (
|
||||
<i key="icon" style={iconStyle} className={`block-frame-icon fa fa-solid fa-${blockIcon}`} />
|
||||
);
|
||||
const iconClass = util.makeIconClass(blockIcon, false);
|
||||
if (iconClass != null) {
|
||||
blockIconElem = <i key="icon" style={iconStyle} className={clsx(`block-frame-icon`, iconClass)} />;
|
||||
}
|
||||
}
|
||||
if (!util.isBlank(blockData?.meta?.title)) {
|
||||
@@ -123,7 +123,11 @@ function getBlockHeaderText(blockIcon: string, blockData: Block): React.ReactNod
|
||||
return [blockIconElem, blockData.meta.title];
|
||||
}
|
||||
}
|
||||
return [blockIconElem, `${blockData?.view} [${blockData.oid.substring(0, 8)}]`];
|
||||
let viewString = blockData?.view;
|
||||
if (blockData.controller == "cmd") {
|
||||
viewString = "cmd";
|
||||
}
|
||||
return [blockIconElem, `${viewString} [${blockData.oid.substring(0, 8)}]`];
|
||||
}
|
||||
|
||||
interface FramelessBlockHeaderProps {
|
||||
|
||||
@@ -216,7 +216,6 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// we send to two subjects just eventType and eventType|oref
|
||||
// we don't use getORefSubject here because we don't want to create a new subject
|
||||
const eventSubject = eventSubjects.get(msg.eventtype);
|
||||
|
||||
@@ -7,6 +7,9 @@ import * as WOS from "./wos";
|
||||
|
||||
// blockservice.BlockService (block)
|
||||
class BlockServiceType {
|
||||
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
|
||||
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
|
||||
}
|
||||
SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise<void> {
|
||||
return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WOS, atoms, globalStore, sendWSCommand, useBlockAtom, useSettingsAtom } from "@/store/global";
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getEventORefSubject,
|
||||
globalStore,
|
||||
sendWSCommand,
|
||||
useBlockAtom,
|
||||
useSettingsAtom,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
@@ -142,7 +151,10 @@ function setBlockFocus(blockId: string) {
|
||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const termRef = React.useRef<TermWrap>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
const shellProcStatusRef = React.useRef<string>(null);
|
||||
const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
|
||||
return jotai.atom<string>(null);
|
||||
}) as jotai.PrimitiveAtom<string>;
|
||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
||||
@@ -157,14 +169,38 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
React.useEffect(() => {
|
||||
const termWrap = new TermWrap(blockId, connectElemRef.current, {
|
||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||
fontSize: termSettings?.fontsize ?? 12,
|
||||
fontFamily: termSettings?.fontfamily ?? "Hack",
|
||||
drawBoldTextInBrightColors: false,
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
});
|
||||
function handleTerminalKeydown(event: KeyboardEvent) {
|
||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||
services.BlockService.SendCommand(this.blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
// restart
|
||||
const restartCmd: BlockRestartCommand = { command: "controller:restart", blockid: blockId };
|
||||
services.BlockService.SendCommand(blockId, restartCmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const termWrap = new TermWrap(
|
||||
blockId,
|
||||
connectElemRef.current,
|
||||
{
|
||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||
fontSize: termSettings?.fontsize ?? 12,
|
||||
fontFamily: termSettings?.fontfamily ?? "Hack",
|
||||
drawBoldTextInBrightColors: false,
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
},
|
||||
{
|
||||
keydownHandler: handleTerminalKeydown,
|
||||
}
|
||||
);
|
||||
(window as any).term = termWrap;
|
||||
termRef.current = termWrap;
|
||||
termWrap.addFocusListener(() => {
|
||||
@@ -181,7 +217,8 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
}, []);
|
||||
|
||||
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code === "Escape" && event.metaKey) {
|
||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
// reset term:mode
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
||||
services.BlockService.SendCommand(blockId, metaCmd);
|
||||
@@ -211,6 +248,32 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
function updateShellProcStatus(status: string) {
|
||||
if (status == null) {
|
||||
return;
|
||||
}
|
||||
shellProcStatusRef.current = status;
|
||||
if (status == "running") {
|
||||
termRef.current?.setIsRunning(true);
|
||||
globalStore.set(blockIconOverrideAtom, "square-terminal");
|
||||
} else {
|
||||
termRef.current?.setIsRunning(false);
|
||||
globalStore.set(blockIconOverrideAtom, "regular@square-terminal");
|
||||
}
|
||||
}
|
||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
||||
initialRTStatus.then((rts) => {
|
||||
updateShellProcStatus(rts?.shellprocstatus);
|
||||
});
|
||||
const bcSubject = getEventORefSubject("blockcontroller:status", WOS.makeORef("block", blockId));
|
||||
bcSubject.subscribe((data: WSEventType) => {
|
||||
let bcRTS: BlockControllerRuntimeStatus = data.data;
|
||||
updateShellProcStatus(bcRTS?.shellprocstatus);
|
||||
});
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let stickerConfig = {
|
||||
charWidth: 8,
|
||||
charHeight: 16,
|
||||
|
||||
@@ -22,11 +22,14 @@ export class TermWrap {
|
||||
loaded: boolean;
|
||||
heldData: Uint8Array[];
|
||||
handleResize_debounced: () => void;
|
||||
isRunning: boolean;
|
||||
keydownHandler: (e: KeyboardEvent) => void;
|
||||
|
||||
constructor(
|
||||
blockId: string,
|
||||
connectElem: HTMLDivElement,
|
||||
options?: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions
|
||||
options: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions,
|
||||
waveOptions: { keydownHandler?: (e: KeyboardEvent) => void }
|
||||
) {
|
||||
this.blockId = blockId;
|
||||
this.ptyOffset = 0;
|
||||
@@ -43,10 +46,12 @@ export class TermWrap {
|
||||
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
|
||||
this.terminal.open(this.connectElem);
|
||||
this.handleResize();
|
||||
this.isRunning = true;
|
||||
this.keydownHandler = waveOptions.keydownHandler;
|
||||
}
|
||||
|
||||
async initTerminal() {
|
||||
this.connectElem.addEventListener("keydown", this.keydownListener.bind(this), true);
|
||||
this.connectElem.addEventListener("keydown", this.keydownHandler, true);
|
||||
this.terminal.onData(this.handleTermData.bind(this));
|
||||
this.mainFileSubject = getFileSubject(this.blockId, "main");
|
||||
this.mainFileSubject.subscribe(this.handleNewFileSubjectData.bind(this));
|
||||
@@ -58,6 +63,10 @@ export class TermWrap {
|
||||
this.runProcessIdleTimeout();
|
||||
}
|
||||
|
||||
setIsRunning(isRunning: boolean) {
|
||||
this.isRunning = isRunning;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.terminal.dispose();
|
||||
this.mainFileSubject.release();
|
||||
@@ -69,7 +78,11 @@ export class TermWrap {
|
||||
const wsCmd: BlockInputWSCommand = { wscommand: "blockinput", blockid: this.blockId, inputdata64: b64data };
|
||||
sendWSCommand(wsCmd);
|
||||
} else {
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
const inputCmd: BlockInputCommand = {
|
||||
command: "controller:input",
|
||||
blockid: this.blockId,
|
||||
inputdata64: b64data,
|
||||
};
|
||||
services.BlockService.SendCommand(this.blockId, inputCmd);
|
||||
}
|
||||
}
|
||||
@@ -78,27 +91,21 @@ export class TermWrap {
|
||||
this.terminal.textarea.addEventListener("focus", focusFn);
|
||||
}
|
||||
|
||||
keydownListener(ev: KeyboardEvent) {
|
||||
if (ev.code == "Escape" && ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||
services.BlockService.SendCommand(this.blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handleNewFileSubjectData(msg: WSFileEventData) {
|
||||
if (msg.fileop != "append") {
|
||||
if (msg.fileop == "truncate") {
|
||||
this.terminal.clear();
|
||||
this.heldData = [];
|
||||
} else if (msg.fileop == "append") {
|
||||
const decodedData = base64ToArray(msg.data64);
|
||||
if (this.loaded) {
|
||||
this.doTerminalWrite(decodedData, null);
|
||||
} else {
|
||||
this.heldData.push(decodedData);
|
||||
}
|
||||
} else {
|
||||
console.log("bad fileop for terminal", msg);
|
||||
return;
|
||||
}
|
||||
const decodedData = base64ToArray(msg.data64);
|
||||
if (this.loaded) {
|
||||
this.doTerminalWrite(decodedData, null);
|
||||
} else {
|
||||
this.heldData.push(decodedData);
|
||||
}
|
||||
}
|
||||
|
||||
doTerminalWrite(data: string | Uint8Array, setPtyOffset?: number): Promise<void> {
|
||||
|
||||
Vendored
+33
-18
@@ -31,7 +31,14 @@ declare global {
|
||||
|
||||
type BlockCommand = {
|
||||
command: string;
|
||||
} & ( BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | CreateBlockCommand | BlockGetMetaCommand | BlockMessageCommand | ResolveIdsCommand | BlockSetMetaCommand | BlockSetViewCommand );
|
||||
} & ( BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | BlockRestartCommand | CreateBlockCommand | BlockGetMetaCommand | BlockMessageCommand | ResolveIdsCommand | BlockSetMetaCommand | BlockSetViewCommand );
|
||||
|
||||
// blockcontroller.BlockControllerRuntimeStatus
|
||||
type BlockControllerRuntimeStatus = {
|
||||
blockid: string;
|
||||
status: string;
|
||||
shellprocstatus?: string;
|
||||
};
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
@@ -69,6 +76,12 @@ declare global {
|
||||
message: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockRestartCommand
|
||||
type BlockRestartCommand = {
|
||||
command: "controller:restart";
|
||||
blockid: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockSetMetaCommand
|
||||
type BlockSetMetaCommand = {
|
||||
command: "setmeta";
|
||||
@@ -97,15 +110,17 @@ declare global {
|
||||
rtopts?: RuntimeOpts;
|
||||
};
|
||||
|
||||
type DateTimeConfigType = {
|
||||
locale: string;
|
||||
format: DateTimeFormatConfigType;
|
||||
}
|
||||
// wconfig.DateTimeConfigType
|
||||
type DateTimeConfigType = {
|
||||
locale: string;
|
||||
format: DateTimeFormatConfigType;
|
||||
};
|
||||
|
||||
type DateTimeFormatConfigType = {
|
||||
dateStyle: "full" | "long" | "medium" | "short";
|
||||
timeStyle: "full" | "long" | "medium" | "short";
|
||||
}
|
||||
// wconfig.DateTimeFormatConfigType
|
||||
type DateTimeFormatConfigType = {
|
||||
dateStyle: number;
|
||||
timeStyle: number;
|
||||
};
|
||||
|
||||
// wstore.FileDef
|
||||
type FileDef = {
|
||||
@@ -122,7 +137,7 @@ declare global {
|
||||
notfound?: boolean;
|
||||
size: number;
|
||||
mode: number;
|
||||
modestr: string;
|
||||
modestr: string;
|
||||
modtime: number;
|
||||
isdir?: boolean;
|
||||
mimetype?: string;
|
||||
@@ -157,10 +172,10 @@ declare global {
|
||||
ReturnDesc: string;
|
||||
};
|
||||
|
||||
//wconfig.MimeTypeConfigType
|
||||
type MimeTypeConfigType = {
|
||||
icon: string;
|
||||
}
|
||||
// wconfig.MimeTypeConfigType
|
||||
type MimeTypeConfigType = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
// waveobj.ORef
|
||||
type ORef = {
|
||||
@@ -195,10 +210,10 @@ declare global {
|
||||
|
||||
// wconfig.SettingsConfigType
|
||||
type SettingsConfigType = {
|
||||
datetime: DateTimeConfigType;
|
||||
mimetypes: {[key:string]: MimeTypeConfigType}
|
||||
widgets: WidgetsConfigType[];
|
||||
mimetypes: {[key: string]: MimeTypeConfigType};
|
||||
datetime: DateTimeConfigType;
|
||||
term: TerminalConfigType;
|
||||
widgets: WidgetsConfigType[];
|
||||
};
|
||||
|
||||
// wstore.StickerClickOptsType
|
||||
@@ -352,4 +367,4 @@ declare global {
|
||||
|
||||
}
|
||||
|
||||
export {}
|
||||
export {}
|
||||
|
||||
+19
-1
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0s
|
||||
|
||||
import base64 from "base64-js";
|
||||
import clsx from "clsx";
|
||||
|
||||
function isBlank(str: string): boolean {
|
||||
return str == null || str == "";
|
||||
@@ -71,4 +72,21 @@ function jsonDeepEqual(v1: any, v2: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, isBlank, jsonDeepEqual, stringToBase64 };
|
||||
function makeIconClass(icon: string, fw: boolean): string {
|
||||
if (icon == null) {
|
||||
return null;
|
||||
}
|
||||
if (icon.match(/^(solid@)?[a-z0-9-]+$/)) {
|
||||
// strip off "solid@" prefix if it exists
|
||||
icon = icon.replace(/^solid@/, "");
|
||||
return clsx(`fa fa-sharp fa-solid fa-${icon}`, fw ? "fa-fw" : null);
|
||||
}
|
||||
if (icon.match(/^regular@[a-z0-9-]+$/)) {
|
||||
// strip off the "regular@" prefix if it exists
|
||||
icon = icon.replace(/^regular@/, "");
|
||||
return clsx(`fa fa-sharp fa-regular fa-${icon}`, fw ? "fa-fw" : null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, isBlank, jsonDeepEqual, makeIconClass, stringToBase64 };
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
@@ -34,6 +36,12 @@ const (
|
||||
BlockFile_Html = "html" // used for alt html layout
|
||||
)
|
||||
|
||||
const (
|
||||
Status_Init = "init"
|
||||
Status_Running = "running"
|
||||
Status_Done = "done"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTermMaxFileSize = 256 * 1024
|
||||
DefaultHtmlMaxFileSize = 256 * 1024
|
||||
@@ -63,15 +71,32 @@ type BlockController struct {
|
||||
CreatedHtmlFile bool
|
||||
ShellProc *shellexec.ShellProc
|
||||
ShellInputCh chan *BlockInputUnion
|
||||
ShellProcStatus string
|
||||
RunCmdFn RunCmdFnType
|
||||
}
|
||||
|
||||
type BlockControllerRuntimeStatus struct {
|
||||
BlockId string `json:"blockid"`
|
||||
Status string `json:"status"`
|
||||
ShellProcStatus string `json:"shellprocstatus,omitempty"`
|
||||
}
|
||||
|
||||
func (bc *BlockController) WithLock(f func()) {
|
||||
bc.Lock.Lock()
|
||||
defer bc.Lock.Unlock()
|
||||
f()
|
||||
}
|
||||
|
||||
func (bc *BlockController) GetRuntimeStatus() *BlockControllerRuntimeStatus {
|
||||
var rtn BlockControllerRuntimeStatus
|
||||
bc.WithLock(func() {
|
||||
rtn.BlockId = bc.BlockId
|
||||
rtn.Status = bc.Status
|
||||
rtn.ShellProcStatus = bc.ShellProcStatus
|
||||
})
|
||||
return &rtn
|
||||
}
|
||||
|
||||
func jsonDeepCopy(val map[string]any) (map[string]any, error) {
|
||||
barr, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
@@ -85,16 +110,6 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) {
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error {
|
||||
bc.Lock.Lock()
|
||||
defer bc.Lock.Unlock()
|
||||
if bc.ShellProc != nil {
|
||||
return fmt.Errorf("shell process already running")
|
||||
}
|
||||
bc.ShellProc = shellProc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *BlockController) getShellProc() *shellexec.ShellProc {
|
||||
bc.Lock.Lock()
|
||||
defer bc.Lock.Unlock()
|
||||
@@ -105,12 +120,44 @@ type RunShellOpts struct {
|
||||
TermSize shellexec.TermSize `json:"termsize,omitempty"`
|
||||
}
|
||||
|
||||
func (bc *BlockController) Close() {
|
||||
if bc.getShellProc() != nil {
|
||||
bc.ShellProc.Close()
|
||||
func (bc *BlockController) UpdateControllerAndSendUpdate(updateFn func() bool) {
|
||||
var sendUpdate bool
|
||||
bc.WithLock(func() {
|
||||
sendUpdate = updateFn()
|
||||
})
|
||||
if sendUpdate {
|
||||
log.Printf("sending blockcontroller update %#v\n", bc.GetRuntimeStatus())
|
||||
go eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "blockcontroller:status",
|
||||
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
|
||||
Data: bc.GetRuntimeStatus(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func HandleTruncateBlockFile(blockId string, blockFile string) error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
err := filestore.WFS.WriteFile(ctx, blockId, blockFile, nil)
|
||||
if err == fs.ErrNotExist {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error truncating blockfile: %w", err)
|
||||
}
|
||||
eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "blockfile",
|
||||
ORef: waveobj.MakeORef(wstore.OType_Block, blockId).String(),
|
||||
Data: &eventbus.WSFileEventData{
|
||||
ZoneId: blockId,
|
||||
FileName: blockFile,
|
||||
FileOp: eventbus.FileOp_Truncate,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func HandleAppendBlockFile(blockId string, blockFile string, data []byte) error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
@@ -134,6 +181,19 @@ func HandleAppendBlockFile(blockId string, blockFile string, data []byte) error
|
||||
func (bc *BlockController) resetTerminalState() {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
var shouldTruncate bool
|
||||
blockData, getBlockDataErr := wstore.DBMustGet[*wstore.Block](ctx, bc.BlockId)
|
||||
if getBlockDataErr == nil {
|
||||
shouldTruncate = getBoolFromMeta(blockData.Meta, wstore.MetaKey_CmdClearOnRestart, false)
|
||||
}
|
||||
if shouldTruncate {
|
||||
err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Main)
|
||||
if err != nil {
|
||||
log.Printf("error truncating main blockfile: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// controller type = "shell"
|
||||
var buf bytes.Buffer
|
||||
// buf.WriteString("\x1b[?1049l") // disable alternative buffer
|
||||
buf.WriteString("\x1b[0m") // reset attributes
|
||||
@@ -154,30 +214,86 @@ func (bc *BlockController) waveOSCMessageHandler(ctx context.Context, cmd wshuti
|
||||
return bc.RunCmdFn(ctx, cmd, wshutil.CmdContextType{BlockId: bc.BlockId, TabId: bc.TabId})
|
||||
}
|
||||
|
||||
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
|
||||
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[string]any) error {
|
||||
// create a circular blockfile for the output
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
err := filestore.WFS.MakeFile(ctx, bc.BlockId, "main", nil, filestore.FileOptsType{MaxSize: DefaultTermMaxFileSize, Circular: true})
|
||||
if err != nil && err != filestore.ErrAlreadyExists {
|
||||
if err != nil && err != fs.ErrExist {
|
||||
err = fs.ErrExist
|
||||
return fmt.Errorf("error creating blockfile: %w", err)
|
||||
}
|
||||
if err == filestore.ErrAlreadyExists {
|
||||
if err == fs.ErrExist {
|
||||
// reset the terminal state
|
||||
bc.resetTerminalState()
|
||||
}
|
||||
err = nil
|
||||
if bc.getShellProc() != nil {
|
||||
return nil
|
||||
}
|
||||
shellProc, err := shellexec.StartShellProc(rc.TermSize)
|
||||
var shellProcErr error
|
||||
bc.WithLock(func() {
|
||||
if bc.ShellProc != nil {
|
||||
shellProcErr = fmt.Errorf("shell process already running")
|
||||
return
|
||||
}
|
||||
})
|
||||
if shellProcErr != nil {
|
||||
return shellProcErr
|
||||
}
|
||||
var cmdStr string
|
||||
var cmdOpts shellexec.CommandOptsType
|
||||
if bc.ControllerType == BlockController_Shell {
|
||||
cmdOpts = shellexec.CommandOptsType{Interactive: true, Login: true}
|
||||
} else if bc.ControllerType == BlockController_Cmd {
|
||||
if _, ok := blockMeta["cmd"].(string); ok {
|
||||
cmdStr = blockMeta["cmd"].(string)
|
||||
} else {
|
||||
return fmt.Errorf("missing cmd in block meta")
|
||||
}
|
||||
if _, ok := blockMeta["cwd"].(string); ok {
|
||||
cmdOpts.Cwd = blockMeta["cwd"].(string)
|
||||
if cmdOpts.Cwd != "" {
|
||||
cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
|
||||
}
|
||||
}
|
||||
if _, ok := blockMeta["cmd:interactive"]; ok {
|
||||
if blockMeta["cmd:interactive"].(bool) {
|
||||
cmdOpts.Interactive = true
|
||||
}
|
||||
}
|
||||
if _, ok := blockMeta["cmd:login"]; ok {
|
||||
if blockMeta["cmd:login"].(bool) {
|
||||
cmdOpts.Login = true
|
||||
}
|
||||
}
|
||||
if _, ok := blockMeta["cmd:env"].(map[string]any); ok {
|
||||
cmdEnv := blockMeta["cmd:env"].(map[string]any)
|
||||
cmdOpts.Env = make(map[string]string)
|
||||
for k, v := range cmdEnv {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.(string); ok {
|
||||
cmdOpts.Env[k] = v.(string)
|
||||
}
|
||||
if _, ok := v.(float64); ok {
|
||||
cmdOpts.Env[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unknown controller type %q", bc.ControllerType)
|
||||
}
|
||||
shellProc, err := shellexec.StartShellProc(rc.TermSize, cmdStr, cmdOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bc.setShellProc(shellProc)
|
||||
if err != nil {
|
||||
bc.ShellProc.Close()
|
||||
return err
|
||||
}
|
||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||
bc.ShellProc = shellProc
|
||||
bc.ShellProcStatus = Status_Running
|
||||
return true
|
||||
})
|
||||
shellInputCh := make(chan *BlockInputUnion, 32)
|
||||
bc.ShellInputCh = shellInputCh
|
||||
messageCh := make(chan wshutil.RpcMessage, 32)
|
||||
@@ -186,6 +302,8 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
|
||||
go func() {
|
||||
// handles regular output from the pty (goes to the blockfile and xterm)
|
||||
defer func() {
|
||||
log.Printf("[shellproc] pty-read loop done\n")
|
||||
|
||||
// needs synchronization
|
||||
bc.ShellProc.Close()
|
||||
close(bc.ShellInputCh)
|
||||
@@ -211,6 +329,9 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer func() {
|
||||
log.Printf("[shellproc] shellInputCh loop done\n")
|
||||
}()
|
||||
// handles input from the shellInputCh, sent to pty
|
||||
for ic := range shellInputCh {
|
||||
if len(ic.InputData) > 0 {
|
||||
@@ -232,37 +353,71 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
|
||||
shellInputCh <- &BlockInputUnion{InputData: out}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
// wait for the shell to finish
|
||||
defer func() {
|
||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||
bc.ShellProcStatus = Status_Done
|
||||
return true
|
||||
})
|
||||
log.Printf("[shellproc] shell process wait loop done\n")
|
||||
}()
|
||||
waitErr := shellProc.Cmd.Wait()
|
||||
shellProc.SetWaitErrorAndSignalDone(waitErr)
|
||||
exitCode := shellexec.ExitCodeFromWaitErr(waitErr)
|
||||
termMsg := fmt.Sprintf("\r\nprocess finished with exit code = %d\r\n\r\n", exitCode)
|
||||
HandleAppendBlockFile(bc.BlockId, BlockFile_Main, []byte(termMsg))
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *BlockController) Run(bdata *wstore.Block) {
|
||||
func getBoolFromMeta(meta map[string]any, key string, def bool) bool {
|
||||
ival, found := meta[key]
|
||||
if !found || ival == nil {
|
||||
return def
|
||||
}
|
||||
if val, ok := ival.(bool); ok {
|
||||
return val
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (bc *BlockController) run(bdata *wstore.Block, blockMeta map[string]any) {
|
||||
defer func() {
|
||||
bc.WithLock(func() {
|
||||
// if the controller had an error status, don't change it
|
||||
if bc.Status == "running" {
|
||||
bc.Status = "done"
|
||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||
if bc.Status == Status_Running {
|
||||
bc.Status = Status_Done
|
||||
return true
|
||||
}
|
||||
})
|
||||
eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "block:done",
|
||||
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
|
||||
Data: nil,
|
||||
return false
|
||||
})
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
delete(blockControllerMap, bc.BlockId)
|
||||
}()
|
||||
bc.WithLock(func() {
|
||||
bc.Status = "running"
|
||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||
bc.Status = Status_Running
|
||||
return true
|
||||
})
|
||||
|
||||
// only controller is "shell" for now
|
||||
go func() {
|
||||
err := bc.DoRunShellCommand(&RunShellOpts{TermSize: bdata.RuntimeOpts.TermSize})
|
||||
if bdata.Controller != BlockController_Shell && bdata.Controller != BlockController_Cmd {
|
||||
log.Printf("unknown controller %q\n", bdata.Controller)
|
||||
return
|
||||
}
|
||||
if getBoolFromMeta(blockMeta, wstore.MetaKey_CmdClearOnStart, false) {
|
||||
err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Main)
|
||||
if err != nil {
|
||||
log.Printf("error running shell: %v\n", err)
|
||||
log.Printf("error truncating main blockfile: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
runOnStart := getBoolFromMeta(blockMeta, wstore.MetaKey_CmdRunOnStart, true)
|
||||
if runOnStart {
|
||||
go func() {
|
||||
err := bc.DoRunShellCommand(&RunShellOpts{TermSize: bdata.RuntimeOpts.TermSize}, bdata.Meta)
|
||||
if err != nil {
|
||||
log.Printf("error running shell: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for genCmd := range bc.InputCh {
|
||||
switch cmd := genCmd.(type) {
|
||||
@@ -284,6 +439,14 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
|
||||
inputUnion.InputData = inputBuf[:nw]
|
||||
}
|
||||
bc.ShellInputCh <- inputUnion
|
||||
case *wshutil.BlockRestartCommand:
|
||||
// TODO: if shell command is already running
|
||||
// we probably want to kill it off, wait, and then restart it
|
||||
err := bc.DoRunShellCommand(&RunShellOpts{TermSize: bdata.RuntimeOpts.TermSize}, bdata.Meta)
|
||||
if err != nil {
|
||||
log.Printf("error running shell command: %v\n", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("unknown command type %T\n", cmd)
|
||||
}
|
||||
@@ -299,7 +462,7 @@ func StartBlockController(ctx context.Context, tabId string, blockId string, run
|
||||
// nothing to start
|
||||
return nil
|
||||
}
|
||||
if blockData.Controller != BlockController_Shell {
|
||||
if blockData.Controller != BlockController_Shell && blockData.Controller != BlockController_Cmd {
|
||||
return fmt.Errorf("unknown controller %q", blockData.Controller)
|
||||
}
|
||||
globalLock.Lock()
|
||||
@@ -309,16 +472,17 @@ func StartBlockController(ctx context.Context, tabId string, blockId string, run
|
||||
return nil
|
||||
}
|
||||
bc := &BlockController{
|
||||
Lock: &sync.Mutex{},
|
||||
ControllerType: blockData.Controller,
|
||||
TabId: tabId,
|
||||
BlockId: blockId,
|
||||
Status: "init",
|
||||
InputCh: make(chan wshutil.BlockCommand),
|
||||
RunCmdFn: runCmdFn,
|
||||
Lock: &sync.Mutex{},
|
||||
ControllerType: blockData.Controller,
|
||||
TabId: tabId,
|
||||
BlockId: blockId,
|
||||
Status: Status_Init,
|
||||
InputCh: make(chan wshutil.BlockCommand),
|
||||
RunCmdFn: runCmdFn,
|
||||
ShellProcStatus: Status_Init,
|
||||
}
|
||||
blockControllerMap[blockId] = bc
|
||||
go bc.Run(blockData)
|
||||
go bc.run(blockData, blockData.Meta)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -327,7 +491,9 @@ func StopBlockController(blockId string) {
|
||||
if bc == nil {
|
||||
return
|
||||
}
|
||||
bc.Close()
|
||||
if bc.getShellProc() != nil {
|
||||
bc.ShellProc.Close()
|
||||
}
|
||||
close(bc.InputCh)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -226,7 +227,7 @@ func handleAppendIJsonFile(blockId string, blockFile string, cmd map[string]any,
|
||||
defer cancelFn()
|
||||
if blockFile == blockcontroller.BlockFile_Html && tryCreate {
|
||||
err := filestore.WFS.MakeFile(ctx, blockId, blockFile, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
|
||||
if err != nil && err != filestore.ErrAlreadyExists {
|
||||
if err != nil && err != fs.ErrExist {
|
||||
return fmt.Errorf("error creating blockfile[html]: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ type WSEventType struct {
|
||||
}
|
||||
|
||||
const (
|
||||
FileOp_Append = "append"
|
||||
FileOp_Append = "append"
|
||||
FileOp_Truncate = "truncate"
|
||||
)
|
||||
|
||||
type WSFileEventData struct {
|
||||
|
||||
@@ -6,19 +6,19 @@ package filestore
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
)
|
||||
|
||||
var ErrAlreadyExists = fmt.Errorf("file already exists")
|
||||
|
||||
// can return fs.ErrExist
|
||||
func dbInsertFile(ctx context.Context, file *WaveFile) error {
|
||||
// will fail if file already exists
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
query := "SELECT zoneid FROM db_wave_file WHERE zoneid = ? AND name = ?"
|
||||
if tx.Exists(query, file.ZoneId, file.Name) {
|
||||
return ErrAlreadyExists
|
||||
return fs.ErrExist
|
||||
}
|
||||
query = "INSERT INTO db_wave_file (zoneid, name, size, createdts, modts, opts, meta) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
tx.Exec(query, file.ZoneId, file.Name, file.Size, file.CreatedTs, file.ModTs, dbutil.QuickJson(file.Opts), dbutil.QuickJson(file.Meta))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/cmdqueue"
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||
@@ -28,6 +29,17 @@ func (bs *BlockService) SendCommand_Meta() tsgenmeta.MethodMeta {
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BlockService) GetControllerStatus(ctx context.Context, blockId string) (*blockcontroller.BlockControllerRuntimeStatus, error) {
|
||||
bc := blockcontroller.GetBlockController(blockId)
|
||||
if bc == nil {
|
||||
return &blockcontroller.BlockControllerRuntimeStatus{
|
||||
BlockId: blockId,
|
||||
Status: "stopped",
|
||||
}, nil
|
||||
}
|
||||
return bc.GetRuntimeStatus(), nil
|
||||
}
|
||||
|
||||
func (bs *BlockService) SendCommand(uiContext wstore.UIContext, blockId string, cmd wshutil.BlockCommand) error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
|
||||
@@ -76,9 +76,8 @@ func (fs *FileService) ReadFile(path string) (*FullFile, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse directory %s", finfo.Path)
|
||||
}
|
||||
|
||||
if len(innerFilesEntries) > 1000 {
|
||||
innerFilesEntries = innerFilesEntries[:1001]
|
||||
innerFilesEntries = innerFilesEntries[:1000]
|
||||
}
|
||||
var innerFilesInfo []FileInfo
|
||||
for _, innerFileEntry := range innerFilesEntries {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/creack/pty"
|
||||
@@ -22,19 +23,65 @@ type TermSize struct {
|
||||
Cols int `json:"cols"`
|
||||
}
|
||||
|
||||
type CommandOptsType struct {
|
||||
Interactive bool `json:"interactive,omitempty"`
|
||||
Login bool `json:"login,omitempty"`
|
||||
Cwd string `json:"cwd,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
type ShellProc struct {
|
||||
Cmd *exec.Cmd
|
||||
Pty *os.File
|
||||
Cmd *exec.Cmd
|
||||
Pty *os.File
|
||||
CloseOnce *sync.Once
|
||||
DoneCh chan any // closed after proc.Wait() returns
|
||||
WaitErr error // WaitErr is synchronized by DoneCh (written before DoneCh is closed) and CloseOnce
|
||||
}
|
||||
|
||||
func (sp *ShellProc) Close() {
|
||||
sp.Cmd.Process.Kill()
|
||||
go func() {
|
||||
sp.Cmd.Process.Wait()
|
||||
_, waitErr := sp.Cmd.Process.Wait()
|
||||
sp.SetWaitErrorAndSignalDone(waitErr)
|
||||
sp.Pty.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
func (sp *ShellProc) SetWaitErrorAndSignalDone(waitErr error) {
|
||||
sp.CloseOnce.Do(func() {
|
||||
sp.WaitErr = waitErr
|
||||
close(sp.DoneCh)
|
||||
})
|
||||
}
|
||||
|
||||
func (sp *ShellProc) Wait() error {
|
||||
<-sp.DoneCh
|
||||
return sp.WaitErr
|
||||
}
|
||||
|
||||
// returns (done, waitError)
|
||||
func (sp *ShellProc) WaitNB() (bool, error) {
|
||||
select {
|
||||
case <-sp.DoneCh:
|
||||
return true, sp.WaitErr
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ExitCodeFromWaitErr(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||
return status.ExitStatus()
|
||||
}
|
||||
}
|
||||
return -1
|
||||
|
||||
}
|
||||
|
||||
func setBoolConditionally(rval reflect.Value, field string, value bool) {
|
||||
if rval.Elem().FieldByName(field).IsValid() {
|
||||
rval.Elem().FieldByName(field).SetBool(value)
|
||||
@@ -47,11 +94,40 @@ func setSysProcAttrs(cmd *exec.Cmd) {
|
||||
setBoolConditionally(rval, "Setctty", true)
|
||||
}
|
||||
|
||||
func StartShellProc(termSize TermSize) (*ShellProc, error) {
|
||||
shellPath := shellutil.DetectLocalShellPath()
|
||||
ecmd := exec.Command(shellPath, "-i", "-l")
|
||||
func checkCwd(cwd string) error {
|
||||
if cwd == "" {
|
||||
return fmt.Errorf("cwd is empty")
|
||||
}
|
||||
if _, err := os.Stat(cwd); err != nil {
|
||||
return fmt.Errorf("error statting cwd %q: %w", cwd, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (*ShellProc, error) {
|
||||
var ecmd *exec.Cmd
|
||||
var shellOpts []string
|
||||
if cmdOpts.Login {
|
||||
shellOpts = append(shellOpts, "-l")
|
||||
}
|
||||
if cmdOpts.Interactive {
|
||||
shellOpts = append(shellOpts, "-i")
|
||||
}
|
||||
if cmdStr == "" {
|
||||
shellPath := shellutil.DetectLocalShellPath()
|
||||
ecmd = exec.Command(shellPath, shellOpts...)
|
||||
} else {
|
||||
shellPath := shellutil.DetectLocalShellPath()
|
||||
shellOpts = append(shellOpts, "-c", cmdStr)
|
||||
ecmd = exec.Command(shellPath, shellOpts...)
|
||||
}
|
||||
ecmd.Env = os.Environ()
|
||||
ecmd.Dir = wavebase.GetHomeDir()
|
||||
if cmdOpts.Cwd != "" {
|
||||
ecmd.Dir = cmdOpts.Cwd
|
||||
}
|
||||
if cwdErr := checkCwd(ecmd.Dir); cwdErr != nil {
|
||||
ecmd.Dir = wavebase.GetHomeDir()
|
||||
}
|
||||
envToAdd := shellutil.WaveshellEnvVars(shellutil.DefaultTermType)
|
||||
if os.Getenv("LANG") == "" {
|
||||
envToAdd["LANG"] = wavebase.DetermineLang()
|
||||
@@ -80,7 +156,7 @@ func StartShellProc(termSize TermSize) (*ShellProc, error) {
|
||||
cmdPty.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &ShellProc{Cmd: ecmd, Pty: cmdPty}, nil
|
||||
return &ShellProc{Cmd: ecmd, Pty: cmdPty, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
||||
}
|
||||
|
||||
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
BlockCommand_SetMeta = "setmeta"
|
||||
BlockCommand_GetMeta = "getmeta"
|
||||
BlockCommand_Input = "controller:input"
|
||||
BlockCommand_Restart = "controller:restart"
|
||||
BlockCommand_AppendBlockFile = "blockfile:append"
|
||||
BlockCommand_AppendIJson = "blockfile:appendijson"
|
||||
Command_ResolveIds = "resolveids"
|
||||
@@ -31,6 +32,7 @@ const (
|
||||
|
||||
var CommandToTypeMap = map[string]reflect.Type{
|
||||
BlockCommand_Input: reflect.TypeOf(BlockInputCommand{}),
|
||||
BlockCommand_Restart: reflect.TypeOf(BlockRestartCommand{}),
|
||||
BlockCommand_SetView: reflect.TypeOf(BlockSetViewCommand{}),
|
||||
BlockCommand_SetMeta: reflect.TypeOf(BlockSetMetaCommand{}),
|
||||
BlockCommand_GetMeta: reflect.TypeOf(BlockGetMetaCommand{}),
|
||||
@@ -97,6 +99,19 @@ func ParseCmdMap(cmdMap map[string]any) (BlockCommand, error) {
|
||||
return cmd.(BlockCommand), nil
|
||||
}
|
||||
|
||||
type BlockRestartCommand struct {
|
||||
Command string `json:"command" tstype:"\"controller:restart\""`
|
||||
BlockId string `json:"blockid"`
|
||||
}
|
||||
|
||||
func (rc *BlockRestartCommand) GetCommand() string {
|
||||
return BlockCommand_Restart
|
||||
}
|
||||
|
||||
func (rc *BlockRestartCommand) GetBlockId() string {
|
||||
return rc.BlockId
|
||||
}
|
||||
|
||||
type BlockInputCommand struct {
|
||||
BlockId string `json:"blockid"`
|
||||
Command string `json:"command" tstype:"\"controller:input\""`
|
||||
|
||||
@@ -12,6 +12,26 @@ import (
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
)
|
||||
|
||||
// well known meta keys
|
||||
const (
|
||||
MetaKey_Title = "title"
|
||||
MetaKey_File = "file"
|
||||
MetaKey_Url = "url"
|
||||
MetaKey_Icon = "icon"
|
||||
MetaKey_IconColor = "icon:color"
|
||||
MetaKey_Frame = "frame"
|
||||
MetaKey_FrameBorderColor = "frame:bordercolor"
|
||||
MetaKey_FrameBorderColor_Focused = "frame:bordercolor:focused"
|
||||
MetaKey_Cmd = "cmd"
|
||||
MetaKey_CmdInteractive = "cmd:interactive"
|
||||
MetaKey_CmdLogin = "cmd:login"
|
||||
MetaKey_CmdRunOnStart = "cmd:runonstart"
|
||||
MetaKey_CmdClearOnStart = "cmd:clearonstart"
|
||||
MetaKey_CmdClearOnRestart = "cmd:clearonrestart"
|
||||
MetaKey_CmdEnv = "env"
|
||||
MetaKey_CmdCwd = "cwd"
|
||||
)
|
||||
|
||||
type UIContext struct {
|
||||
WindowId string `json:"windowid"`
|
||||
ActiveTabId string `json:"activetabid"`
|
||||
|
||||
Reference in New Issue
Block a user