diff --git a/src/app/line/linecomps.tsx b/src/app/line/linecomps.tsx index 5cd1b618a..b7ef72319 100644 --- a/src/app/line/linecomps.tsx +++ b/src/app/line/linecomps.tsx @@ -360,6 +360,12 @@ class LineCmd extends React.Component< GlobalCommandRunner.lineDelete(line.lineid, true); } + @boundMethod + clickRestart() { + let { line } = this.props; + GlobalCommandRunner.lineRestart(line.lineid, true); + } + @boundMethod clickMinimize() { mobx.action(() => { @@ -467,12 +473,21 @@ class LineCmd extends React.Component< renderMeta1(cmd: Cmd) { let { line } = this.props; let termOpts = cmd.getTermOpts(); - let formattedTime = lineutil.getLineDateTimeStr(line.ts); + let formattedTime: string = ""; + let restartTs = cmd.getRestartTs(); + let timeTitle: string = null; + if (restartTs != null && restartTs > 0) { + formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs); + timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts); + } + else { + formattedTime = lineutil.getLineDateTimeStr(line.ts); + } let renderer = line.renderer; return (
-
{formattedTime}
+
{formattedTime}
 
@@ -665,6 +680,9 @@ class LineCmd extends React.Component< {this.renderMeta1(cmd)} {this.renderCmdText(cmd)}
+
+ +
diff --git a/src/electron/emain.ts b/src/electron/emain.ts index a10d157d7..4f6f8eb23 100644 --- a/src/electron/emain.ts +++ b/src/electron/emain.ts @@ -36,7 +36,7 @@ ensureDir(waveHome); // these are either "darwin/amd64" or "darwin/arm64" // normalize darwin/x64 to darwin/amd64 for GOARCH compatibility let unamePlatform = process.platform; -let unameArch = process.arch; +let unameArch: string = process.arch; if (unameArch == "x64") { unameArch = "amd64"; } @@ -189,15 +189,21 @@ let menuTemplate = [ { role: "quit" }, ], }, - { - label: "File", - submenu: [{ role: "close" }], - }, { role: "editMenu", }, { role: "viewMenu", + submenu: [ + { role: "reload", accelerator: "Option+R" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], }, { role: "windowMenu", @@ -284,14 +290,11 @@ function createMainWindow(clientData) { } else { win.webContents.toggleDevTools(); } - return; } if (checkKeyPressed(waveEvent, "Cmd:r")) { - if (input.shift) { - e.preventDefault(); - win.reload(); - } + e.preventDefault(); + win.webContents.send("r-cmd", mods); return; } if (checkKeyPressed(waveEvent, "Cmd:l")) { diff --git a/src/electron/preload.js b/src/electron/preload.js index c58eafc11..229a76e09 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld("api", { onHCmd: (callback) => ipcRenderer.on("h-cmd", callback), onWCmd: (callback) => ipcRenderer.on("w-cmd", callback), onPCmd: (callback) => ipcRenderer.on("p-cmd", callback), + onRCmd: (callback) => ipcRenderer.on("r-cmd", callback), onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback), onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback), onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback), diff --git a/src/model/model.ts b/src/model/model.ts index 641b5dd98..c03b14fc9 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -200,6 +200,7 @@ type ElectronApi = { onLCmd: (callback: (mods: KeyModsType) => void) => void; onHCmd: (callback: (mods: KeyModsType) => void) => void; onPCmd: (callback: (mods: KeyModsType) => void) => void; + onRCmd: (callback: (mods: KeyModsType) => void) => void; onWCmd: (callback: (mods: KeyModsType) => void) => void; onMenuItemAbout: (callback: () => void) => void; onMetaArrowUp: (callback: () => void) => void; @@ -249,6 +250,10 @@ class Cmd { })(); } + getRestartTs(): number { + return this.data.get().restartts; + } + getAsWebCmd(lineid: string): WebCmd { let cmd = this.data.get(); let remote = GlobalModel.getRemote(this.remote.remoteid); @@ -3403,6 +3408,7 @@ class Model { getApi().onHCmd(this.onHCmd.bind(this)); getApi().onPCmd(this.onPCmd.bind(this)); getApi().onWCmd(this.onWCmd.bind(this)); + getApi().onRCmd(this.onRCmd.bind(this)); getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this)); getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this)); getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this)); @@ -3674,6 +3680,9 @@ class Model { } onWCmd(e: any, mods: KeyModsType) { + if (this.activeMainView.get() != "session") { + return; + } let activeScreen = this.getActiveScreen(); if (activeScreen == null) { return; @@ -3690,6 +3699,27 @@ class Model { }); } + onRCmd(e: any, mods: KeyModsType) { + if (this.activeMainView.get() != "session") { + return; + } + let activeScreen = this.getActiveScreen(); + if (activeScreen == null) { + return; + } + if (mods.shift) { + // restart last line + GlobalCommandRunner.lineRestart("E", true); + } else { + // restart selected line + let selectedLine = activeScreen.selectedLine.get(); + if (selectedLine == null || selectedLine == 0) { + return; + } + GlobalCommandRunner.lineRestart(String(selectedLine), true); + } + } + clearModals(): boolean { let didSomething = false; mobx.action(() => { @@ -4151,12 +4181,28 @@ class Model { return session.getActiveScreen(); } + handleCmdRestart(cmd: CmdDataType) { + if (cmd == null || !cmd.restarted) { + return; + } + let screen = this.screenMap.get(cmd.screenid); + if (screen == null) { + return; + } + let termWrap = screen.getTermWrap(cmd.lineid); + if (termWrap == null) { + return; + } + termWrap.reload(0); + } + addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) { let slines = this.getScreenLinesById(line.screenid); if (slines == null) { return; } slines.addLineCmd(line, cmd, interactive); + this.handleCmdRestart(cmd); } updateCmd(cmd: CmdDataType) { @@ -4164,6 +4210,7 @@ class Model { if (slines != null) { slines.updateCmd(cmd); } + this.handleCmdRestart(cmd); } isInfoUpdate(update: UpdateMessage): boolean { @@ -4601,6 +4648,10 @@ class CommandRunner { return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive); } + lineRestart(lineArg: string, interactive: boolean): Promise { + return GlobalModel.submitCommand("line", "restart", [lineArg], { nohist: "1" }, interactive); + } + lineSet(lineArg: string, opts: { renderer?: string }): Promise { let kwargs = { nohist: "1" }; if ("renderer" in opts) { diff --git a/src/types/types.ts b/src/types/types.ts index d80f8a934..54ca3311a 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -245,12 +245,14 @@ type CmdDataType = { status: string; cmdpid: number; remotepid: number; + restartts: number; donets: number; exitcode: number; durationms: number; runout: any[]; rtnstate: boolean; remove?: boolean; + restarted?: boolean; }; type PtyDataUpdateType = { diff --git a/src/util/keyutil.ts b/src/util/keyutil.ts index 242d755f7..347640afb 100644 --- a/src/util/keyutil.ts +++ b/src/util/keyutil.ts @@ -7,6 +7,8 @@ type KeyPressDecl = { Option?: boolean; Shift?: boolean; Ctrl?: boolean; + Alt?: boolean; + Meta?: boolean; }; key: string; }; @@ -30,6 +32,10 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { rtn.mods.Ctrl = true; } else if (key == "Option") { rtn.mods.Option = true; + } else if (key == "Alt") { + rtn.mods.Alt = true; + } else if (key == "Meta") { + rtn.mods.Meta = true; } else { rtn.key = key; if (key.length == 1) { @@ -49,7 +55,7 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean { let keyPress = parseKeyDescription(description); - if (keyPress.mods.Option && !event.alt) { + if (keyPress.mods.Option && !event.option) { return false; } if (keyPress.mods.Cmd && !event.cmd) { @@ -61,6 +67,12 @@ function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean if (keyPress.mods.Ctrl && !event.control) { return false; } + if (keyPress.mods.Alt && !event.alt) { + return false; + } + if (keyPress.mods.Meta && !event.meta) { + return false; + } let eventKey = event.key; let descKey = keyPress.key; if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) { @@ -78,7 +90,8 @@ function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean return true; } -type ModKeyStrs = "Cmd" | "Option" | "Shift" | "Ctrl"; +// Cmd and Option are portable between Mac and Linux/Windows +type ModKeyStrs = "Cmd" | "Option" | "Shift" | "Ctrl" | "Alt" | "Meta"; interface WaveKeyboardEvent { type: string; @@ -105,7 +118,16 @@ interface WaveKeyboardEvent { /** * Equivalent to KeyboardEvent.metaKey. */ + meta: boolean; + /** + * cmd is special, on mac it is meta, on windows it is alt + */ cmd: boolean; + /** + * option is special, on mac it is alt, on windows it is meta + */ + option: boolean; + repeat: boolean; /** * Equivalent to KeyboardEvent.location. @@ -117,12 +139,10 @@ function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEve let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; rtn.control = event.ctrlKey; rtn.shift = event.shiftKey; - if (PLATFORM == PlatformMacOS) { - rtn.cmd = event.metaKey; - rtn.alt = event.altKey; - } else { - rtn.cmd = event.altKey; - } + rtn.cmd = (PLATFORM == PlatformMacOS ? event.metaKey : event.altKey); + rtn.option = (PLATFORM == PlatformMacOS ? event.altKey : event.metaKey); + rtn.meta = event.metaKey; + rtn.alt = event.altKey; rtn.code = event.code; rtn.key = event.key; rtn.location = event.location; @@ -135,12 +155,10 @@ function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent { let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent; rtn.type = event.type; rtn.control = event.control; - if (PLATFORM == PlatformMacOS) { - rtn.cmd = event.meta; - rtn.alt = event.alt; - } else { - rtn.cmd = event.alt; - } + rtn.cmd = (PLATFORM == PlatformMacOS ? event.meta : event.alt) + rtn.option = (PLATFORM == PlatformMacOS ? event.alt : event.meta); + rtn.meta = event.meta; + rtn.alt = event.alt; rtn.shift = event.shift; rtn.repeat = event.isAutoRepeat; rtn.location = event.location; diff --git a/waveshell/pkg/utilfn/syncmap.go b/waveshell/pkg/utilfn/syncmap.go new file mode 100644 index 000000000..3e04ae61e --- /dev/null +++ b/waveshell/pkg/utilfn/syncmap.go @@ -0,0 +1,84 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package utilfn + +import ( + "sync" +) + +type SyncMap[K comparable, V any] struct { + lock *sync.Mutex + m map[K]V +} + +func MakeSyncMap[K comparable, V any]() *SyncMap[K, V] { + return &SyncMap[K, V]{ + lock: &sync.Mutex{}, + m: make(map[K]V), + } +} + +func (sm *SyncMap[K, V]) Set(k K, v V) { + sm.lock.Lock() + defer sm.lock.Unlock() + sm.m[k] = v +} + +func (sm *SyncMap[K, V]) Get(k K) V { + sm.lock.Lock() + defer sm.lock.Unlock() + return sm.m[k] +} + +func (sm *SyncMap[K, V]) GetEx(k K) (V, bool) { + sm.lock.Lock() + defer sm.lock.Unlock() + v, ok := sm.m[k] + return v, ok +} + +func (sm *SyncMap[K, V]) Delete(k K) { + sm.lock.Lock() + defer sm.lock.Unlock() + delete(sm.m, k) +} + +func (sm *SyncMap[K, V]) Clear() { + sm.lock.Lock() + defer sm.lock.Unlock() + sm.m = make(map[K]V) +} + +func (sm *SyncMap[K, V]) Len() int { + sm.lock.Lock() + defer sm.lock.Unlock() + return len(sm.m) +} + +func (sm *SyncMap[K, V]) Keys() []K { + sm.lock.Lock() + defer sm.lock.Unlock() + keys := make([]K, len(sm.m)) + i := 0 + for k := range sm.m { + keys[i] = k + i++ + } + return keys +} + +func (sm *SyncMap[K, V]) Replace(newMap map[K]V) { + sm.lock.Lock() + defer sm.lock.Unlock() + sm.m = make(map[K]V, len(newMap)) + for k, v := range newMap { + sm.m[k] = v + } +} + +func IncSyncMap[K comparable, V int | int64](sm *SyncMap[K, V], key K, incAmt V) { + sm.lock.Lock() + defer sm.lock.Unlock() + sm.m[key] += incAmt +} diff --git a/wavesrv/db/migrations/000031_restart_cmd.down.sql b/wavesrv/db/migrations/000031_restart_cmd.down.sql new file mode 100644 index 000000000..11ae9230c --- /dev/null +++ b/wavesrv/db/migrations/000031_restart_cmd.down.sql @@ -0,0 +1 @@ +ALTER TABLE cmd DROP COLUMN restartts; diff --git a/wavesrv/db/migrations/000031_restart_cmd.up.sql b/wavesrv/db/migrations/000031_restart_cmd.up.sql new file mode 100644 index 000000000..7a0316c4a --- /dev/null +++ b/wavesrv/db/migrations/000031_restart_cmd.up.sql @@ -0,0 +1 @@ +ALTER TABLE cmd ADD COLUMN restartts bigint NOT NULL DEFAULT 0; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 3a0eafa4b..2065c1edb 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -210,6 +210,7 @@ func init() { registerCmdFn("line:setheight", LineSetHeightCommand) registerCmdFn("line:view", LineViewCommand) registerCmdFn("line:set", LineSetCommand) + registerCmdFn("line:restart", LineRestartCommand) registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) @@ -491,7 +492,12 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } runPacket.Command = ":" runPacket.ReturnState = true - cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket) + rcOpts := remote.RunCommandOpts{ + SessionId: ids.SessionId, + ScreenId: ids.ScreenId, + RemotePtr: ids.Remote.RemotePtr, + } + cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket) if callback != nil { defer callback() } @@ -587,7 +593,12 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } runPacket.Command = strings.TrimSpace(cmdStr) runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd) - cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket) + rcOpts := remote.RunCommandOpts{ + SessionId: ids.SessionId, + ScreenId: ids.ScreenId, + RemotePtr: ids.Remote.RemotePtr, + } + cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket) if callback != nil { defer callback() } @@ -3337,6 +3348,121 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, nil } +func LineRestartCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + var lineId string + if len(pk.Args) >= 1 { + lineArg := pk.Args[0] + resolvedLineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + lineId = resolvedLineId + } else { + selectedLineId, err := sstore.GetScreenSelectedLineId(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("error getting selected lineid: %v", err) + } + lineId = selectedLineId + } + if lineId == "" { + return nil, fmt.Errorf("%s requires a lineid to operate on", GetCmdStr(pk)) + } + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) + if err != nil { + return nil, fmt.Errorf("error getting line: %v", err) + } + if line == nil { + return nil, fmt.Errorf("line not found") + } + if cmd == nil { + return nil, fmt.Errorf("cannot restart line (no cmd found)") + } + if cmd.Status == sstore.CmdStatusRunning || cmd.Status == sstore.CmdStatusDetached { + killCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + err = ids.Remote.MShell.KillRunningCommandAndWait(killCtx, base.MakeCommandKey(ids.ScreenId, lineId)) + if err != nil { + return nil, err + } + } + ids.Remote.MShell.ResetDataPos(base.MakeCommandKey(ids.ScreenId, lineId)) + err = sstore.ClearCmdPtyFile(ctx, ids.ScreenId, lineId) + if err != nil { + return nil, fmt.Errorf("error clearing existing pty file: %v", err) + } + runPacket := packet.MakeRunPacket() + runPacket.ReqId = uuid.New().String() + runPacket.CK = base.MakeCommandKey(ids.ScreenId, lineId) + runPacket.UsePty = true + // TODO how can we preseve the original termopts? + runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, DefaultPTERM) + if err != nil { + return nil, fmt.Errorf("error getting creating termopts for command: %w", err) + } + runPacket.Command = cmd.CmdStr + runPacket.ReturnState = false + rcOpts := remote.RunCommandOpts{ + SessionId: ids.SessionId, + ScreenId: ids.ScreenId, + RemotePtr: ids.Remote.RemotePtr, + StatePtr: &cmd.StatePtr, + NoCreateCmdPtyFile: true, + } + cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket) + if callback != nil { + defer callback() + } + if err != nil { + return nil, err + } + newTs := time.Now().UnixMilli() + err = sstore.UpdateCmdForRestart(ctx, runPacket.CK, newTs, cmd.CmdPid, cmd.RemotePid, convertTermOpts(runPacket.TermOpts)) + if err != nil { + return nil, fmt.Errorf("error updating cmd for restart: %w", err) + } + line, cmd, err = sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) + if err != nil { + return nil, fmt.Errorf("error getting updated line/cmd: %w", err) + } + cmd.Restarted = true + update := &sstore.ModelUpdate{ + Line: line, + Cmd: cmd, + Interactive: pk.Interactive, + } + screen, focusErr := focusScreenLine(ctx, ids.ScreenId, line.LineNum) + if focusErr != nil { + // not a fatal error, so just log + log.Printf("error focusing screen line: %v\n", focusErr) + } + if screen != nil { + update.Screens = []*sstore.ScreenType{screen} + } + return update, nil +} + +func focusScreenLine(ctx context.Context, screenId string, lineNum int64) (*sstore.ScreenType, error) { + screen, err := sstore.GetScreenById(ctx, screenId) + if err != nil { + return nil, fmt.Errorf("error getting screen: %v", err) + } + if screen == nil { + return nil, fmt.Errorf("screen not found") + } + updateMap := make(map[string]interface{}) + updateMap[sstore.ScreenField_SelectedLine] = lineNum + updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmd + screen, err = sstore.UpdateScreen(ctx, screenId, updateMap) + if err != nil { + return nil, fmt.Errorf("error updating screen: %v", err) + } + return screen, nil +} + func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { @@ -3768,6 +3894,10 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr)) } + if cmd.RestartTs > 0 { + restartTs := time.UnixMilli(cmd.RestartTs) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "restartts", restartTs.Format(TsFormatStr))) + } if cmd.DoneTs != 0 { doneTs := time.UnixMilli(cmd.DoneTs) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "donets", doneTs.Format(TsFormatStr))) diff --git a/wavesrv/pkg/cmdrunner/termopts.go b/wavesrv/pkg/cmdrunner/termopts.go index 4d43ced6f..0892f20c5 100644 --- a/wavesrv/pkg/cmdrunner/termopts.go +++ b/wavesrv/pkg/cmdrunner/termopts.go @@ -123,3 +123,12 @@ func convertTermOpts(pkto *packet.TermOpts) *sstore.TermOpts { MaxPtySize: pkto.MaxPtySize, } } + +func convertToPacketTermOpts(sto sstore.TermOpts) *packet.TermOpts { + return &packet.TermOpts{ + Rows: int(sto.Rows), + Cols: int(sto.Cols), + FlexRows: sto.FlexRows, + MaxPtySize: sto.MaxPtySize, + } +} diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index a5dd9c2f0..c509fe4b2 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -161,6 +161,7 @@ type MShellProc struct { StateMap *server.ShellStateMap NumTryConnect int InitPkShellType string + DataPosMap *utilfn.SyncMap[base.CommandKey, int64] // install InstallStatus string @@ -169,7 +170,6 @@ type MShellProc struct { InstallErr error RunningCmds map[base.CommandKey]RunCmdType - WaitingCmds []RunCmdType PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name] launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized } @@ -209,6 +209,18 @@ func (msh *MShellProc) GetDefaultState(shellType string) *packet.ShellState { return state } +func (msh *MShellProc) EnsureShellType(ctx context.Context, shellType string) error { + if msh.StateMap.HasShell(shellType) { + return nil + } + // try to reinit the shell + _, err := msh.ReInit(ctx, shellType) + if err != nil { + return fmt.Errorf("error trying to initialize shell %q: %v", shellType, err) + } + return nil +} + func (msh *MShellProc) GetDefaultStatePtr(shellType string) *sstore.ShellStatePtr { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -692,6 +704,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { PendingStateCmds: make(map[pendingStateKey]base.CommandKey), StateMap: server.MakeShellStateMap(), launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized + DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](), } // for conditional launch method based on ssh library in use // remove once ssh library is stabilized @@ -1615,12 +1628,8 @@ func replaceHomePath(pathStr string, homeDir string) string { func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { msh.Lock.Lock() defer msh.Lock.Unlock() - for runningCk := range msh.RunningCmds { - if runningCk == ck { - return true - } - } - return false + _, ok := msh.RunningCmds[ck] + return ok } func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error { @@ -1633,6 +1642,30 @@ func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error { return msh.ServerProc.Input.SendPacket(dataPk) } +func (msh *MShellProc) KillRunningCommandAndWait(ctx context.Context, ck base.CommandKey) error { + if !msh.IsCmdRunning(ck) { + return nil + } + siPk := packet.MakeSpecialInputPacket() + siPk.CK = ck + siPk.SigName = "SIGTERM" + err := msh.SendSpecialInput(siPk) + if err != nil { + return fmt.Errorf("error trying to kill running cmd: %w", err) + } + for { + if ctx.Err() != nil { + return ctx.Err() + } + if !msh.IsCmdRunning(ck) { + return nil + } + // TODO fix busy wait (sync with msh.RunningCmds) + // not a huge deal though since this is not processor intensive and not widely used + time.Sleep(100 * time.Millisecond) + } +} + func (msh *MShellProc) SendSpecialInput(siPk *packet.SpecialInputPacketType) error { if !msh.IsConnected() { return fmt.Errorf("remote is not connected, cannot send input") @@ -1682,14 +1715,25 @@ func (msh *MShellProc) removePendingStateCmd(screenId string, rptr sstore.Remote } } -// returns (cmdtype, allow-updates-callback, err) -func RunCommand(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { - rct := RunCmdType{ - SessionId: sessionId, - ScreenId: screenId, - RemotePtr: remotePtr, - RunPacket: runPacket, - } +type RunCommandOpts struct { + SessionId string + ScreenId string + RemotePtr sstore.RemotePtrType + + // optional, if not provided shellstate will look up state from remote instance + // ReturnState cannot be used with StatePtr + // this will also cause this command to bypass the pending state cmd logic + StatePtr *sstore.ShellStatePtr + + // set to true to skip creating the pty file (for restarted commands) + NoCreateCmdPtyFile bool +} + +// returns (CmdType, allow-updates-callback, err) +// we must persist the CmdType to the DB before calling the callback to allow updates +// otherwise an early CmdDone packet might not get processed (since cmd will not exist in DB) +func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { + sessionId, screenId, remotePtr := rcOpts.SessionId, rcOpts.ScreenId, rcOpts.RemotePtr if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } @@ -1706,56 +1750,85 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt if runPacket.State != nil { return nil, nil, fmt.Errorf("runPacket.State should not be set, it is set in RunCommand") } - var newPSC *base.CommandKey - if runPacket.ReturnState { - newPSC = &runPacket.CK + if rcOpts.StatePtr != nil && runPacket.ReturnState { + return nil, nil, fmt.Errorf("RunCommand: cannot use ReturnState with StatePtr") } - ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC) - if !ok { - line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId()) - if err != nil { - return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) + + // pending state command logic + // if we are currently running a command that can change the state, we need to wait for it to finish + if rcOpts.StatePtr == nil { + var newPSC *base.CommandKey + if runPacket.ReturnState { + newPSC = &runPacket.CK } - if line == nil { - return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC) - } - return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) - } - startCmdWait(runPacket.CK) - defer func() { - if rtnErr != nil { - removeCmdWait(runPacket.CK) - if newPSC != nil { - msh.removePendingStateCmd(screenId, remotePtr, *newPSC) + ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC) + if !ok { + line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId()) + if err != nil { + return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) } + if line == nil { + return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC) + } + return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) } - }() + if newPSC != nil { + defer func() { + // if we get an error, remove the pending state cmd + // if no error, PSC will get removed when we see a CmdDone or CmdFinal packet + if rtnErr != nil { + msh.removePendingStateCmd(screenId, remotePtr, *newPSC) + } + }() + } + } + // get current remote-instance state - statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr) - if err != nil { - return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err) + var statePtr *sstore.ShellStatePtr + if rcOpts.StatePtr != nil { + statePtr = rcOpts.StatePtr + } else { + var err error + statePtr, err = sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr) + if err != nil { + return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err) + } } - if statePtr == nil { + if statePtr == nil { // can be null if there is no remote-instance (screen has unchanged state from default) + err := msh.EnsureShellType(ctx, msh.GetShellPref()) // make sure shellType is initialized + if err != nil { + return nil, nil, err + } statePtr = msh.GetDefaultStatePtr(msh.GetShellPref()) - } - if statePtr == nil { - return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr") + if statePtr == nil { + return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr") + } } currentState, err := sstore.GetFullState(ctx, *statePtr) if err != nil || currentState == nil { - return nil, nil, fmt.Errorf("cannot get current remote state: %w", err) + return nil, nil, fmt.Errorf("cannot load current remote state: %w", err) } runPacket.State = addScVarsToState(currentState) runPacket.StateComplete = true runPacket.ShellType = currentState.GetShellType() - // check to see if shellType is initialized - if !msh.StateMap.HasShell(runPacket.ShellType) { - // try to reinit the shell - _, err := msh.ReInit(ctx, runPacket.ShellType) - if err != nil { - return nil, nil, fmt.Errorf("error trying to initialize shell %q: %v", runPacket.ShellType, err) - } + err = msh.EnsureShellType(ctx, runPacket.ShellType) // make sure shellType is initialized + if err != nil { + return nil, nil, err } + + // start cmdwait. must be started before sending the run packet + // this ensures that we don't process output, or cmddone packets until we set up the line, cmd, and ptyout file + startCmdWait(runPacket.CK) + defer func() { + // if we get an error, remove the cmdwait + // if no error, cmdwait will get removed by the caller w/ the callback fn that's returned on success + if rtnErr != nil { + removeCmdWait(runPacket.CK) + } + }() + + // RegisterRpc + WaitForResponse is used to get any waveshell side errors + // waveshell will either return an error (in a ResponsePacketType) or a CmdStartPacketType msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { @@ -1776,6 +1849,8 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt } return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } + + // command is now successfully runnning status := sstore.CmdStatusRunning if runPacket.Detached { status = sstore.CmdStatusDetached @@ -1797,44 +1872,20 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt RunOut: nil, RtnState: runPacket.ReturnState, } - err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) - if err != nil { - // TODO the cmd is running, so this is a tricky error to handle - return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) - } - msh.AddRunningCmd(rct) - return cmd, func() { removeCmdWait(runPacket.CK) }, nil -} - -func (msh *MShellProc) AddWaitingCmd(rct RunCmdType) { - msh.Lock.Lock() - defer msh.Lock.Unlock() - msh.WaitingCmds = append(msh.WaitingCmds, rct) -} - -func (msh *MShellProc) reExecSingle(rct RunCmdType) { - // TODO fixme - ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second) - defer cancelFn() - _, callback, _ := RunCommand(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, rct.RunPacket) - if callback != nil { - defer callback() - } -} - -func (msh *MShellProc) ReExecWaitingCmds() { - msh.Lock.Lock() - defer msh.Lock.Unlock() - for len(msh.WaitingCmds) > 0 { - rct := msh.WaitingCmds[0] - go msh.reExecSingle(rct) - if rct.RunPacket.ReturnState { - break + if !rcOpts.NoCreateCmdPtyFile { + err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) + if err != nil { + // TODO the cmd is running, so this is a tricky error to handle + return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) } } - if len(msh.WaitingCmds) == 0 { - msh.WaitingCmds = nil - } + msh.AddRunningCmd(RunCmdType{ + SessionId: sessionId, + ScreenId: screenId, + RemotePtr: remotePtr, + RunPacket: runPacket, + }) + return cmd, func() { removeCmdWait(runPacket.CK) }, nil } func (msh *MShellProc) AddRunningCmd(rct RunCmdType) { @@ -1940,7 +1991,6 @@ func (msh *MShellProc) notifyHangups_nolock() { } msh.RunningCmds = make(map[base.CommandKey]RunCmdType) msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey) - msh.WaitingCmds = nil } func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { @@ -2057,7 +2107,11 @@ func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { return } -func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) { +func (msh *MShellProc) ResetDataPos(ck base.CommandKey) { + msh.DataPosMap.Delete(ck) +} + +func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) { realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) if err != nil { ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) @@ -2066,7 +2120,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa } var ack *packet.DataAckPacketType if len(realData) > 0 { - dataPos := dataPosMap[dataPk.CK] + dataPos := dataPosMap.Get(dataPk.CK) rcmd := msh.GetRunningCmd(dataPk.CK) update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { @@ -2074,7 +2128,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa } else { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) } - dataPosMap[dataPk.CK] += int64(len(realData)) + utilfn.IncSyncMap(dataPosMap, dataPk.CK, int64(len(realData))) if update != nil { sstore.MainBus.SendScreenUpdate(dataPk.CK.GetGroupId(), update) } @@ -2085,7 +2139,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa // log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) } -func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) func() { +func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) func() { return func() { msh.handleDataPacket(dataPk, dataPosMap) } @@ -2124,12 +2178,10 @@ func (msh *MShellProc) ProcessPackets() { go sendScreenUpdates(screens) } }) - // TODO need to clean dataPosMap - dataPosMap := make(map[base.CommandKey]int64) for pk := range msh.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) - runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, dataPosMap)) + runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, msh.DataPosMap)) go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output) continue } diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index aa5d8a3a0..3ebe8f9c9 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -763,29 +763,37 @@ func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { }) } +// special "E" returns last unarchived line, "EA" returns last line (even if archived) func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (string, error) { - var lineId string - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { + if lineArg == "E" { + query := `SELECT lineid FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum DESC LIMIT 1` + lineId := tx.GetString(query, screenId) + return lineId, nil + } + if lineArg == "EA" { + query := `SELECT lineid FROM line WHERE screenid = ? ORDER BY linenum DESC LIMIT 1` + lineId := tx.GetString(query, screenId) + return lineId, nil + } lineNum, err := strconv.Atoi(lineArg) if err == nil { // valid linenum query := `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?` - lineId = tx.GetString(query, screenId, lineNum) + lineId := tx.GetString(query, screenId, lineNum) + return lineId, nil } else if len(lineArg) == 8 { // prefix id string match query := `SELECT lineid FROM line WHERE screenid = ? AND substr(lineid, 1, 8) = ?` - lineId = tx.GetString(query, screenId, lineArg) + lineId := tx.GetString(query, screenId, lineArg) + return lineId, nil } else { // id match query := `SELECT lineid FROM line WHERE screenid = ? AND lineid = ?` - lineId = tx.GetString(query, screenId, lineArg) + lineId := tx.GetString(query, screenId, lineArg) + return lineId, nil } - return nil }) - if txErr != nil { - return "", txErr - } - return lineId, nil } func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) { @@ -836,8 +844,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, cmdpid, remotepid, donets, exitcode, durationms, rtnstate, runout, rtnbasehash, rtndiffhasharr) - VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:cmdpid,:remotepid,:donets,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) +INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, cmdpid, remotepid, donets, restartts, exitcode, durationms, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:cmdpid,:remotepid,:donets,:restartts,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExec(query, cmdMap) } @@ -879,6 +887,20 @@ func UpdateWithUpdateOpenAICmdInfoPacket(ctx context.Context, screenId string, m return UpdateWithCurrentOpenAICmdInfoChat(screenId) } +func UpdateCmdForRestart(ctx context.Context, ck base.CommandKey, ts int64, cmdPid int, remotePid int, termOpts *TermOpts) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd + SET restartts = ?, status = ?, exitcode = ?, cmdpid = ?, remotepid = ?, durationms = ?, termopts = ?, origtermopts = ? + WHERE screenid = ? AND lineid = ?` + tx.Exec(query, ts, CmdStatusRunning, 0, cmdPid, remotePid, 0, quickJson(termOpts), quickJson(termOpts), ck.GetGroupId(), lineIdFromCK(ck)) + query = `UPDATE history + SET ts = ?, status = ?, exitcode = ?, durationms = ? + WHERE screenid = ? AND lineid = ?` + tx.Exec(query, ts, CmdStatusRunning, 0, 0, ck.GetGroupId(), lineIdFromCK(ck)) + return nil + }) +} + func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) { if donePk == nil { return nil, fmt.Errorf("invalid cmddone packet") @@ -1490,12 +1512,16 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err func DeleteScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { var lineIds []string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT lineid FROM line WHERE screenid = ?` - lineIds = tx.SelectStrings(query, screenId) - query = `DELETE FROM line WHERE screenid = ?` - tx.Exec(query, screenId) - query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ?` - tx.Exec(query, screenId) + query := `SELECT lineid FROM line + WHERE screenid = ? + AND NOT EXISTS (SELECT lineid FROM cmd c WHERE c.screenid = ? AND c.lineid = line.lineid AND c.status IN ('running', 'detached'))` + lineIds = tx.SelectStrings(query, screenId, screenId) + query = `DELETE FROM line + WHERE screenid = ? AND lineid IN (SELECT value FROM json_each(?))` + tx.Exec(query, screenId, quickJsonArr(lineIds)) + query = `UPDATE history SET lineid = '', linenum = 0 + WHERE screenid = ? AND lineid IN (SELECT value FROM json_each(?))` + tx.Exec(query, screenId, quickJsonArr(lineIds)) return nil }) if txErr != nil { @@ -2091,6 +2117,19 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar return txErr } +func GetScreenSelectedLineId(ctx context.Context, screenId string) (string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { + query := `SELECT selectedline FROM screen WHERE screenid = ?` + sline := tx.GetInt(query, screenId) + if sline <= 0 { + return "", nil + } + query = `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?` + lineId := tx.GetString(query, screenId, sline) + return lineId, nil + }) +} + // returns updated screen (only if updated) func FixupScreenSelectedLine(ctx context.Context, screenId string) (*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { diff --git a/wavesrv/pkg/sstore/fileops.go b/wavesrv/pkg/sstore/fileops.go index 082b369b5..7b1b9ab17 100644 --- a/wavesrv/pkg/sstore/fileops.go +++ b/wavesrv/pkg/sstore/fileops.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/wavetermdev/waveterm/waveshell/pkg/cirfile" + "github.com/wavetermdev/waveterm/waveshell/pkg/shexec" "github.com/wavetermdev/waveterm/wavesrv/pkg/scbase" ) @@ -39,6 +40,27 @@ func StatCmdPtyFile(ctx context.Context, screenId string, lineId string) (*cirfi return cirfile.StatCirFile(ctx, ptyOutFileName) } +func ClearCmdPtyFile(ctx context.Context, screenId string, lineId string) error { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) + if err != nil { + return err + } + stat, err := cirfile.StatCirFile(ctx, ptyOutFileName) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + os.Remove(ptyOutFileName) // ignore error + var maxSize int64 = shexec.DefaultMaxPtySize + if stat != nil { + maxSize = stat.MaxSize + } + err = CreateCmdPtyFile(ctx, screenId, lineId, maxSize) + if err != nil { + return err + } + return nil +} + func AppendToCmdPtyBlob(ctx context.Context, screenId string, lineId string, data []byte, pos int64) (*PtyDataUpdate, error) { if screenId == "" { return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set") diff --git a/wavesrv/pkg/sstore/migrate.go b/wavesrv/pkg/sstore/migrate.go index 4d0a7c6e4..dcd36d9af 100644 --- a/wavesrv/pkg/sstore/migrate.go +++ b/wavesrv/pkg/sstore/migrate.go @@ -22,7 +22,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 30 +const MaxMigration = 31 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 const CmdLineSpecialMigration = 20 diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index 937f5acec..a689f8021 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -1118,13 +1118,15 @@ type CmdType struct { Status string `json:"status"` CmdPid int `json:"cmdpid"` RemotePid int `json:"remotepid"` + RestartTs int64 `json:"restartts,omitempty"` DoneTs int64 `json:"donets"` ExitCode int `json:"exitcode"` DurationMs int `json:"durationms"` RunOut []packet.PacketType `json:"runout,omitempty"` RtnState bool `json:"rtnstate,omitempty"` RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"` - Remove bool `json:"remove,omitempty"` + Remove bool `json:"remove,omitempty"` // not persisted to DB + Restarted bool `json:"restarted,omitempty"` // not persisted to DB } func (r *RemoteType) ToMap() map[string]interface{} { @@ -1189,6 +1191,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["status"] = cmd.Status rtn["cmdpid"] = cmd.CmdPid rtn["remotepid"] = cmd.RemotePid + rtn["restartts"] = cmd.RestartTs rtn["donets"] = cmd.DoneTs rtn["exitcode"] = cmd.ExitCode rtn["durationms"] = cmd.DurationMs @@ -1216,6 +1219,7 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetInt(&cmd.CmdPid, m, "cmdpid") quickSetInt(&cmd.RemotePid, m, "remotepid") quickSetInt64(&cmd.DoneTs, m, "donets") + quickSetInt64(&cmd.RestartTs, m, "restartts") quickSetInt(&cmd.ExitCode, m, "exitcode") quickSetInt(&cmd.DurationMs, m, "durationms") quickSetJson(&cmd.RunOut, m, "runout")