big update to screen/session delete, and clear (#199)

* sync schema.sql with running schema

* remove incognito field from history table. also don't add empty FeState vars

* history updates, add festate, durationms, exitcode, status, and tags into history table

* update screen/session delete, and clear to no longer purge history items.  move deleted screens/sessions into a tombstone table.

* update schema

* fix alias -> fn

* quiet the security warning about an unchecked byte conversion. no real security issue here, but add a range check for good measure.
This commit is contained in:
Mike Sawka
2023-12-27 13:11:53 -08:00
committed by GitHub
parent 01074af129
commit 2f7cce294c
13 changed files with 338 additions and 195 deletions
+5 -5
View File
@@ -39,13 +39,13 @@ const BUILD = __WAVETERM_BUILD__;
const ScreenDeleteMessage = `
Are you sure you want to delete this tab?
All commands and output will be deleted, and removed from history. To hide the tab, and retain the commands in history, use 'archive'.
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
`.trim();
const SessionDeleteMessage = `
Are you sure you want to delete this workspace?
All commands and output will be deleted, and removed from history. To hide the workspace, and retain the commands in history, use 'archive'.
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
`.trim();
const WebShareConfirmMarkdown = `
@@ -219,7 +219,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
return;
}
if (this.screen.getScreenLines().lines.length == 0) {
GlobalCommandRunner.screenPurge(this.screenId);
GlobalCommandRunner.screenDelete(this.screenId);
GlobalModel.modalsModel.popModal();
return;
}
@@ -229,7 +229,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenPurge(this.screenId);
let prtn = GlobalCommandRunner.screenDelete(this.screenId);
commandRtnHandler(prtn, this.errorMessage);
GlobalModel.modalsModel.popModal();
});
@@ -439,7 +439,7 @@ class SessionSettingsModal extends React.Component<{}, {}> {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.sessionPurge(this.sessionId);
let prtn = GlobalCommandRunner.sessionDelete(this.sessionId);
commandRtnHandler(prtn, this.errorMessage, () => GlobalModel.modalsModel.popModal());
});
}
+4 -4
View File
@@ -4339,8 +4339,8 @@ class CommandRunner {
);
}
screenPurge(screenId: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("screen", "purge", [screenId], { nohist: "1" }, false);
screenDelete(screenId: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("screen", "delete", [screenId], { nohist: "1" }, false);
}
screenWebShare(screenId: string, shouldShare: boolean): Promise<CommandRtnType> {
@@ -4469,8 +4469,8 @@ class CommandRunner {
);
}
sessionPurge(sessionId: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("session", "purge", [sessionId], { nohist: "1" }, false);
sessionDelete(sessionId: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("session", "delete", [sessionId], { nohist: "1" }, false);
}
sessionSetSettings(sessionId: string, settings: { name?: string }, interactive: boolean): Promise<CommandRtnType> {
@@ -0,0 +1,10 @@
ALTER TABLE history ADD COLUMN incognito boolean NOT NULL DEFAULT false;
ALTER TABLE history DROP COLUMN exitcode;
ALTER TABLE history DROP COLUMN durationms;
ALTER TABLE history DROP COLUMN festate;
ALTER TABLE history DROP COLUMN tags;
ALTER TABLE history DROP COLUMN status;
DROP TABLE session_tombstone;
DROP TABLE screen_tombstone;
@@ -0,0 +1,39 @@
ALTER TABLE history DROP COLUMN incognito;
ALTER TABLE history ADD COLUMN exitcode int NULL DEFAULT NULL;
ALTER TABLE history ADD COLUMN durationms int NULL DEFAULT NULL;
ALTER TABLE history ADD COLUMN festate json NOT NULL DEFAULT '{}';
ALTER TABLE history ADD COLUMN tags json NOT NULL DEFAULT '{}';
ALTER TABLE history ADD COLUMN status varchar(10) NOT NULL DEFAULT 'unknown';
UPDATE cmd
SET festate = json_remove(festate, "$.PROMPTVAR_GITBRANCH")
WHERE festate->>'PROMPTVAR_GITBRANCH' = '';
UPDATE history
SET exitcode = cmd.exitcode,
durationms = cmd.durationms,
festate = cmd.festate,
status = cmd.status
FROM cmd
WHERE history.screenid = cmd.screenid
AND history.lineid = cmd.lineid;
UPDATE history
SET status = 'done'
WHERE lineid = '';
CREATE TABLE session_tombstone (
sessionid varchar(36) PRIMARY KEY,
deletedts bigint NOT NULL,
name varchar(50) NOT NULL
);
CREATE TABLE screen_tombstone (
screenid varchar(36) PRIMARY KEY,
sessionid varchar(36) NOT NULL,
deletedts bigint NOT NULL,
screenopts json NOT NULL,
name varchar(50) NOT NULL
);
+16 -11
View File
@@ -7,7 +7,7 @@ CREATE TABLE client (
userpublickeybytes blob NOT NULL,
userprivatekeybytes blob NOT NULL,
winsize json NOT NULL
, clientopts json NOT NULL DEFAULT '', feopts json NOT NULL DEFAULT '{}', cmdstoretype varchar(20) DEFAULT 'session', openaiopts json NOT NULL DEFAULT '{}');
, clientopts json NOT NULL DEFAULT '', feopts json NOT NULL DEFAULT '{}', cmdstoretype varchar(20) DEFAULT 'session', openaiopts json NOT NULL DEFAULT '{}', releaseinfo json NOT NULL DEFAULT '{}');
CREATE TABLE session (
sessionid varchar(36) PRIMARY KEY,
name varchar(50) NOT NULL,
@@ -56,7 +56,7 @@ CREATE TABLE remote (
local boolean NOT NULL,
archived boolean NOT NULL,
remoteidx int NOT NULL
, statevars json NOT NULL DEFAULT '{}', openaiopts json NOT NULL DEFAULT '{}');
, statevars json NOT NULL DEFAULT '{}', openaiopts json NOT NULL DEFAULT '{}', sshconfigsrc varchar(36) NOT NULL DEFAULT 'waveterm-manual');
CREATE TABLE history (
historyid varchar(36) PRIMARY KEY,
ts bigint NOT NULL,
@@ -70,8 +70,7 @@ CREATE TABLE history (
haderror boolean NOT NULL,
cmdstr text NOT NULL,
ismetacmd boolean,
incognito boolean
, linenum int NOT NULL DEFAULT 0);
linenum int NOT NULL DEFAULT 0, exitcode int NULL DEFAULT NULL, durationms int NULL DEFAULT NULL, festate json NOT NULL DEFAULT '{}', tags json NOT NULL DEFAULT '{}', status varchar(10) NOT NULL DEFAULT 'unknown');
CREATE TABLE activity (
day varchar(20) PRIMARY KEY,
uploaded boolean NOT NULL,
@@ -146,7 +145,7 @@ CREATE TABLE IF NOT EXISTS "screen" (
anchor json NOT NULL,
focustype varchar(12) NOT NULL,
archived boolean NOT NULL,
archivedts bigint NOT NULL, webshareopts json NOT NULL DEFAULT 'null',
archivedts bigint NOT NULL, webshareopts json NOT NULL DEFAULT 'null', screenviewopts json DEFAULT '{}',
PRIMARY KEY (screenid)
);
CREATE TABLE IF NOT EXISTS "line" (
@@ -180,12 +179,6 @@ CREATE TABLE webptypos (
PRIMARY KEY (screenid, lineid)
);
CREATE INDEX idx_screenupdate_ids ON screenupdate (screenid, lineid);
CREATE TABLE cmd_migration (
screenid varchar(36) NOT NULL,
lineid varchar(36) NOT NULL,
cmdid varchar(36) NOT NULL,
PRIMARY KEY (screenid, lineid)
);
CREATE TABLE IF NOT EXISTS "cmd" (
screenid varchar(36) NOT NULL,
lineid varchar(36) NOT NULL,
@@ -217,3 +210,15 @@ CREATE TABLE cmd_migrate20 (
cmdid varchar(36) NOT NULL,
PRIMARY KEY (screenid, lineid)
);
CREATE TABLE session_tombstone (
sessionid varchar(36) PRIMARY KEY,
deletedts bigint NOT NULL,
name varchar(50) NOT NULL
);
CREATE TABLE screen_tombstone (
screenid varchar(36) PRIMARY KEY,
sessionid varchar(36) NOT NULL,
deletedts bigint NOT NULL,
screenopts json NOT NULL,
name varchar(50) NOT NULL
);
+46 -49
View File
@@ -132,9 +132,11 @@ type SetVarScope struct {
}
type historyContextType struct {
LineId string
LineNum int64
RemotePtr *sstore.RemotePtrType
LineId string
LineNum int64
RemotePtr *sstore.RemotePtrType
FeState sstore.FeStateType
InitialStatus string
}
type MetaCmdFnType = func(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error)
@@ -161,8 +163,7 @@ func init() {
registerCmdFn("session:open", SessionOpenCommand)
registerCmdAlias("session:new", SessionOpenCommand)
registerCmdFn("session:set", SessionSetCommand)
registerCmdAlias("session:delete", SessionDeleteCommand)
registerCmdFn("session:purge", SessionDeleteCommand)
registerCmdFn("session:delete", SessionDeleteCommand)
registerCmdFn("session:archive", SessionArchiveCommand)
registerCmdFn("session:showall", SessionShowAllCommand)
registerCmdFn("session:show", SessionShowCommand)
@@ -170,7 +171,7 @@ func init() {
registerCmdFn("screen", ScreenCommand)
registerCmdFn("screen:archive", ScreenArchiveCommand)
registerCmdFn("screen:purge", ScreenPurgeCommand)
registerCmdFn("screen:delete", ScreenDeleteCommand)
registerCmdFn("screen:open", ScreenOpenCommand)
registerCmdAlias("screen:new", ScreenOpenCommand)
registerCmdFn("screen:set", ScreenSetCommand)
@@ -199,7 +200,7 @@ func init() {
registerCmdFn("line:bookmark", LineBookmarkCommand)
registerCmdFn("line:pin", LinePinCommand)
registerCmdFn("line:archive", LineArchiveCommand)
registerCmdFn("line:purge", LinePurgeCommand)
registerCmdFn("line:delete", LineDeleteCommand)
registerCmdFn("line:setheight", LineSetHeightCommand)
registerCmdFn("line:view", LineViewCommand)
registerCmdFn("line:set", LineSetCommand)
@@ -624,10 +625,6 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
if err != nil {
return err
}
isIncognito, err := sstore.IsIncognitoScreen(ctx, ids.SessionId, ids.ScreenId)
if err != nil {
return fmt.Errorf("cannot add to history, error looking up incognito status of screen: %v", err)
}
hitem := &sstore.HistoryItemType{
HistoryId: scbase.GenWaveUUID(),
Ts: time.Now().UnixMilli(),
@@ -639,7 +636,15 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
HadError: hadError,
CmdStr: cmdStr,
IsMetaCmd: isMetaCmd,
Incognito: isIncognito,
FeState: historyContext.FeState,
Status: historyContext.InitialStatus,
}
if hitem.Status == "" {
if hadError {
hitem.Status = sstore.CmdStatusError
} else {
hitem.Status = "done"
}
}
if !isMetaCmd && historyContext.RemotePtr != nil {
hitem.Remote = *historyContext.RemotePtr
@@ -754,23 +759,23 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
}
}
func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
func ScreenDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session) // don't force R_Screen
if err != nil {
return nil, fmt.Errorf("/screen:purge cannot purge screen: %w", err)
return nil, fmt.Errorf("/screen:delete cannot delete screen: %w", err)
}
screenId := ids.ScreenId
if len(pk.Args) > 0 {
ri, err := resolveSessionScreen(ctx, ids.SessionId, pk.Args[0], ids.ScreenId)
if err != nil {
return nil, fmt.Errorf("/screen:purge cannot resolve screen arg: %v", err)
return nil, fmt.Errorf("/screen:delete cannot resolve screen arg: %v", err)
}
screenId = ri.Id
}
if screenId == "" {
return nil, fmt.Errorf("/screen:purge no active screen or screen arg passed")
return nil, fmt.Errorf("/screen:delete no active screen or screen arg passed")
}
update, err := sstore.PurgeScreen(ctx, screenId, false)
update, err := sstore.DeleteScreen(ctx, screenId, false)
if err != nil {
return nil, err
}
@@ -1817,7 +1822,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor
} else {
go doOpenAICompletion(cmd, opts, prompt)
}
updateHistoryContext(ctx, line, cmd)
updateHistoryContext(ctx, line, cmd, nil)
updateMap := make(map[string]interface{})
updateMap[sstore.ScreenField_SelectedLine] = line.LineNum
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput
@@ -1963,11 +1968,11 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re
Cmd: cmd,
Screens: []*sstore.ScreenType{screen},
}
updateHistoryContext(ctx, rtnLine, cmd)
updateHistoryContext(ctx, rtnLine, cmd, cmd.FeState)
return update, nil
}
func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstore.CmdType) {
func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstore.CmdType, feState sstore.FeStateType) {
ctxVal := ctx.Value(historyContextKey)
if ctxVal == nil {
return
@@ -1979,7 +1984,11 @@ func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstor
}
if cmd != nil {
hctx.RemotePtr = &cmd.Remote
hctx.InitialStatus = cmd.Status
} else {
hctx.InitialStatus = sstore.CmdStatusDone
}
hctx.FeState = feState
}
func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket {
@@ -2160,7 +2169,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
if err != nil {
return nil, err
}
updateHistoryContext(ctx, rtnLine, nil)
updateHistoryContext(ctx, rtnLine, nil, nil)
updateMap := make(map[string]interface{})
updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput
@@ -2306,19 +2315,19 @@ func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if len(pk.Args) >= 1 {
ritem, err := resolveSession(ctx, pk.Args[0], ids.SessionId)
if err != nil {
return nil, fmt.Errorf("/session:purge error resolving session %q: %w", pk.Args[0], err)
return nil, fmt.Errorf("/session:delete error resolving session %q: %w", pk.Args[0], err)
}
if ritem == nil {
return nil, fmt.Errorf("/session:purge session %q not found", pk.Args[0])
return nil, fmt.Errorf("/session:delete session %q not found", pk.Args[0])
}
sessionId = ritem.Id
} else {
sessionId = ids.SessionId
}
if sessionId == "" {
return nil, fmt.Errorf("/session:purge no sessionid found")
return nil, fmt.Errorf("/session:delete no sessionid found")
}
update, err := sstore.PurgeSession(ctx, sessionId)
update, err := sstore.DeleteSession(ctx, sessionId)
if err != nil {
return nil, fmt.Errorf("cannot delete session: %v", err)
}
@@ -2542,18 +2551,18 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore
if err != nil {
return nil, err
}
if resolveBool(pk.Kwargs["purge"], false) {
update, err := sstore.PurgeScreenLines(ctx, ids.ScreenId)
if resolveBool(pk.Kwargs["archive"], false) {
update, err := sstore.ArchiveScreenLines(ctx, ids.ScreenId)
if err != nil {
return nil, fmt.Errorf("clearing screen: %v", err)
return nil, fmt.Errorf("clearing screen (archiving): %v", err)
}
update.Info = &sstore.InfoMsgType{
InfoMsg: fmt.Sprintf("screen cleared (all lines purged)"),
InfoMsg: fmt.Sprintf("screen cleared (all lines archived)"),
TimeoutMs: 2000,
}
return update, nil
} else {
update, err := sstore.ArchiveScreenLines(ctx, ids.ScreenId)
update, err := sstore.DeleteScreenLines(ctx, ids.ScreenId)
if err != nil {
return nil, fmt.Errorf("clearing screen: %v", err)
}
@@ -2578,23 +2587,11 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
}
historyIds = append(historyIds, historyArg)
}
historyItemsRemoved, err := sstore.PurgeHistoryByIds(ctx, historyIds)
err := sstore.PurgeHistoryByIds(ctx, historyIds)
if err != nil {
return nil, fmt.Errorf("/history:purge error purging items: %v", err)
}
update := &sstore.ModelUpdate{}
for _, historyItem := range historyItemsRemoved {
if historyItem.LineId == "" {
continue
}
lineObj := &sstore.LineType{
ScreenId: historyItem.ScreenId,
LineId: historyItem.LineId,
Remove: true,
}
update.Lines = append(update.Lines, lineObj)
}
return update, nil
return sstore.InfoMsgUpdate("removed history items"), nil
}
const HistoryViewPageSize = 50
@@ -2816,7 +2813,7 @@ func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
}
func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "purge", "setheight", "set"}, "or", false))
return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "delete", "setheight", "set"}, "or", false))
}
func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
@@ -3178,13 +3175,13 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
return &sstore.ModelUpdate{Line: lineObj}, nil
}
func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
func LineDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
if err != nil {
return nil, err
}
if len(pk.Args) == 0 {
return nil, fmt.Errorf("/line:purge requires at least one argument (line number or id)")
return nil, fmt.Errorf("/line:delete requires at least one argument (line number or id)")
}
var lineIds []string
for _, lineArg := range pk.Args {
@@ -3197,9 +3194,9 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
}
lineIds = append(lineIds, lineId)
}
err = sstore.PurgeLinesByIds(ctx, ids.ScreenId, lineIds)
err = sstore.DeleteLinesByIds(ctx, ids.ScreenId, lineIds)
if err != nil {
return nil, fmt.Errorf("/line:purge error purging lines: %v", err)
return nil, fmt.Errorf("/line:delete error purging lines: %v", err)
}
update := &sstore.ModelUpdate{}
for _, lineId := range lineIds {
+20
View File
@@ -45,9 +45,29 @@ func QuickSetInt(ival *int, m map[string]interface{}, name string) {
}
}
func QuickSetNullableInt64(ival **int64, m map[string]any, name string) {
v, ok := m[name]
if !ok {
// set to nil
return
}
sqlInt64, ok := v.(int64)
if ok {
*ival = &sqlInt64
return
}
sqlInt, ok := v.(int)
if ok {
sqlInt64 = int64(sqlInt)
*ival = &sqlInt64
return
}
}
func QuickSetInt64(ival *int64, m map[string]interface{}, name string) {
v, ok := m[name]
if !ok {
// leave as zero
return
}
sqlInt64, ok := v.(int64)
+6 -1
View File
@@ -2043,7 +2043,12 @@ func evalPromptEsc(escCode string, vars map[string]string, state *packet.ShellSt
if err != nil {
return escCode
}
return string([]byte{byte(ival)})
if ival >= 0 && ival <= 255 {
return string([]byte{byte(ival)})
} else {
// if it was out of range just return the string (invalid escape)
return escCode
}
}
if escCode == "e" {
return "\033"
+101 -85
View File
@@ -23,7 +23,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
)
const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito, h.linenum"
const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.linenum, h.exitcode, h.durationms, h.festate, h.tags, h.status"
const DefaultMaxHistoryItems = 1000
var updateWriterCVar = sync.NewCond(&sync.Mutex{})
@@ -220,18 +220,14 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error {
}
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `INSERT INTO history
( historyid, ts, userid, sessionid, screenid, lineid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito, linenum) VALUES
(:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito,:linenum)`
( historyid, ts, userid, sessionid, screenid, lineid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, linenum, exitcode, durationms, festate, tags, status) VALUES
(:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:linenum,:exitcode,:durationms,:festate,:tags,:status)`
tx.NamedExec(query, hitem.ToMap())
return nil
})
return txErr
}
func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) (bool, error) {
return false, nil
}
const HistoryQueryChunkSize = 1000
func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*HistoryItemType) bool) (*HistoryItemType, int) {
@@ -832,16 +828,11 @@ INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr
}
func GetCmdByScreenId(ctx context.Context, screenId string, lineId string) (*CmdType, error) {
var cmd *CmdType
err := WithTx(ctx, func(tx *TxWrap) error {
return WithTxRtn(ctx, func(tx *TxWrap) (*CmdType, error) {
query := `SELECT * FROM cmd WHERE screenid = ? AND lineid = ?`
cmd = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId)
return nil
cmd := dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId)
return cmd, nil
})
if err != nil {
return nil, err
}
return cmd, nil
}
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) {
@@ -854,17 +845,20 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
screenId := ck.GetGroupId()
var rtnCmd *CmdType
txErr := WithTx(ctx, func(tx *TxWrap) error {
lineId := lineIdFromCK(ck)
query := `UPDATE cmd SET status = ?, donets = ?, exitcode = ?, durationms = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, status, donePk.Ts, donePk.ExitCode, donePk.DurationMs, screenId, lineIdFromCK(ck))
tx.Exec(query, status, donePk.Ts, donePk.ExitCode, donePk.DurationMs, screenId, lineId)
query = `UPDATE history SET status = ?, exitcode = ?, durationms = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, status, donePk.ExitCode, donePk.DurationMs, screenId, lineId)
var err error
rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, lineIdFromCK(ck))
rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, lineId)
if err != nil {
return err
}
if isWebShare(tx, screenId) {
insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdExitCode)
insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdDurationMs)
insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdStatus)
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_CmdExitCode)
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_CmdDurationMs)
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_CmdStatus)
}
return nil
})
@@ -928,6 +922,8 @@ func HangupAllRunningCmds(ctx context.Context) error {
if isWebShare(tx, cmdPtr.ScreenId) {
insertScreenLineUpdate(tx, cmdPtr.ScreenId, cmdPtr.LineId, UpdateType_CmdStatus)
}
query = `UPDATE history SET status = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, CmdStatusHangup, cmdPtr.ScreenId, cmdPtr.LineId)
}
return nil
})
@@ -946,10 +942,13 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) ([]*Scree
if isWebShare(tx, cmdPtr.ScreenId) {
insertScreenLineUpdate(tx, cmdPtr.ScreenId, cmdPtr.LineId, UpdateType_CmdStatus)
}
query = `UPDATE history SET status = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, CmdStatusHangup, cmdPtr.ScreenId, cmdPtr.LineId)
screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), cmdPtr.ScreenId, cmdPtr.LineId)
if err != nil {
return nil, err
}
// this doesn't add dups because UpdateScreenFocusForDoneCmd will only return a screen once
if screen != nil {
rtn = append(rtn, screen)
}
@@ -963,6 +962,8 @@ func HangupCmd(ctx context.Context, ck base.CommandKey) (*ScreenType, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) {
query := `UPDATE cmd SET status = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), lineIdFromCK(ck))
query = `UPDATE history SET status = ? WHERE screenid = ? AND lineid = ?`
tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), lineIdFromCK(ck))
if isWebShare(tx, ck.GetGroupId()) {
insertScreenLineUpdate(tx, ck.GetGroupId(), lineIdFromCK(ck), UpdateType_CmdStatus)
}
@@ -1104,25 +1105,30 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err
return txErr
}
func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdatePacket, error) {
// if sessionDel is passed, we do *not* delete the screen directory (session delete will handle that)
func DeleteScreen(ctx context.Context, screenId string, sessionDel bool) (*ModelUpdate, error) {
var sessionId string
var isActive bool
var screenTombstone *ScreenTombstoneType
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT screenid FROM screen WHERE screenid = ?`
if !tx.Exists(query, screenId) {
return fmt.Errorf("cannot purge screen (not found)")
screen, err := GetScreenById(tx.Context(), screenId)
if err != nil {
return fmt.Errorf("cannot get screen to delete: %w", err)
}
if screen == nil {
return fmt.Errorf("cannot delete screen (not found)")
}
webSharing := isWebShare(tx, screenId)
if !sessionDel {
query = `SELECT sessionid FROM screen WHERE screenid = ?`
query := `SELECT sessionid FROM screen WHERE screenid = ?`
sessionId = tx.GetString(query, screenId)
if sessionId == "" {
return fmt.Errorf("cannot purge screen (no sessionid)")
return fmt.Errorf("cannot delete screen (no sessionid)")
}
query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived`
numScreens := tx.GetInt(query, sessionId)
if numScreens <= 1 {
return fmt.Errorf("cannot purge the last screen in a session")
return fmt.Errorf("cannot delete the last screen in a session")
}
isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId)
if isActive {
@@ -1131,14 +1137,24 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP
tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId)
}
}
screenTombstone = &ScreenTombstoneType{
ScreenId: screen.ScreenId,
SessionId: screen.SessionId,
Name: screen.Name,
DeletedTs: time.Now().UnixMilli(),
ScreenOpts: screen.ScreenOpts,
}
query := `INSERT INTO screen_tombstone ( screenid, sessionid, name, deletedts, screenopts)
VALUES (:screenid,:sessionid,:name,:deletedts,:screenopts)`
tx.NamedExec(query, dbutil.ToDBMap(screenTombstone, false))
query = `DELETE FROM screen WHERE screenid = ?`
tx.Exec(query, screenId)
query = `DELETE FROM history WHERE screenid = ?`
tx.Exec(query, screenId)
query = `DELETE FROM line WHERE screenid = ?`
tx.Exec(query, screenId)
query = `DELETE FROM cmd WHERE screenid = ?`
tx.Exec(query, screenId)
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ?`
tx.Exec(query, screenId)
if webSharing {
insertScreenDelUpdate(tx, screenId)
}
@@ -1147,14 +1163,10 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP
if txErr != nil {
return nil, txErr
}
delErr := DeleteScreenDir(ctx, screenId)
if delErr != nil {
log.Printf("error removing screendir")
if !sessionDel {
GoDeleteScreenDirs(screenId)
}
if sessionDel {
return nil, nil
}
update := &ModelUpdate{}
update := &ModelUpdate{ScreenTombstones: []*ScreenTombstoneType{screenTombstone}}
update.Screens = []*ScreenType{{SessionId: sessionId, ScreenId: screenId, Remove: true}}
if isActive {
bareSession, err := GetBareSessionById(ctx, sessionId)
@@ -1412,14 +1424,14 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err
return &ModelUpdate{ScreenLines: screenLines}, nil
}
func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) {
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 = `DELETE FROM history WHERE screenid = ?`
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ?`
tx.Exec(query, screenId)
query = `UPDATE screen SET nextlinenum = 1 WHERE screenid = ?`
tx.Exec(query, screenId)
@@ -1428,7 +1440,11 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error
if txErr != nil {
return nil, txErr
}
go cleanScreenCmds(context.Background(), screenId)
go func() {
cleanCtx, cancelFn := context.WithTimeout(context.Background(), time.Minute)
defer cancelFn()
cleanScreenCmds(cleanCtx, screenId)
}()
screen, err := GetScreenById(ctx, screenId)
if err != nil {
return nil, err
@@ -1492,38 +1508,55 @@ func ScreenReset(ctx context.Context, screenId string) ([]*RemoteInstance, error
})
}
func PurgeSession(ctx context.Context, sessionId string) (UpdatePacket, error) {
func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) {
var newActiveSessionId string
var screenIds []string
var sessionTombstone *SessionTombstoneType
update := &ModelUpdate{}
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT sessionid FROM session WHERE sessionid = ?`
if !tx.Exists(query, sessionId) {
return fmt.Errorf("session does not exist")
bareSession, err := GetBareSessionById(tx.Context(), sessionId)
if err != nil {
return fmt.Errorf("cannot get session to delete: %w", err)
}
query = `SELECT screenid FROM screen WHERE sessionid = ?`
if bareSession == nil {
return fmt.Errorf("cannot delete session (not found)")
}
query := `SELECT screenid FROM screen WHERE sessionid = ?`
screenIds = tx.SelectStrings(query, sessionId)
for _, screenId := range screenIds {
_, err := PurgeScreen(tx.Context(), screenId, true)
screenUpdate, err := DeleteScreen(tx.Context(), screenId, true)
if err != nil {
return fmt.Errorf("error purging screen[%s]: %v", screenId, err)
return fmt.Errorf("error deleting screen[%s]: %v", screenId, err)
}
if len(screenUpdate.Screens) > 0 {
update.Screens = append(update.Screens, screenUpdate.Screens...)
}
if len(screenUpdate.ScreenTombstones) > 0 {
update.ScreenTombstones = append(update.ScreenTombstones, screenUpdate.ScreenTombstones...)
}
}
query = `DELETE FROM session WHERE sessionid = ?`
tx.Exec(query, sessionId)
newActiveSessionId, _ = fixActiveSessionId(tx.Context())
sessionTombstone = &SessionTombstoneType{
SessionId: sessionId,
Name: bareSession.Name,
DeletedTs: time.Now().UnixMilli(),
}
query = `INSERT INTO session_tombstone ( sessionid, name, deletedts)
VALUES (:sessionid,:name,:deletedts)`
tx.NamedExec(query, dbutil.ToDBMap(sessionTombstone, false))
return nil
})
if txErr != nil {
return nil, txErr
}
update := &ModelUpdate{}
GoDeleteScreenDirs(screenIds...)
if newActiveSessionId != "" {
update.ActiveSessionId = newActiveSessionId
}
update.Sessions = append(update.Sessions, &SessionType{SessionId: sessionId, Remove: true})
for _, screenId := range screenIds {
update.Screens = append(update.Screens, &ScreenType{ScreenId: screenId, Remove: true})
}
update.SessionTombstones = []*SessionTombstoneType{sessionTombstone}
return update, nil
}
@@ -1987,31 +2020,24 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar
return txErr
}
func purgeCmdByScreenId(ctx context.Context, screenId string, lineId string) error {
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `DELETE FROM cmd WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
if tx.Err != nil {
// short circuit here because we don't want to delete the ptyfile when the tx will be rolled back
return tx.Err
}
return DeletePtyOutFile(tx.Context(), screenId, lineId)
})
return txErr
}
func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) error {
func DeleteLinesByIds(ctx context.Context, screenId string, lineIds []string) error {
txErr := WithTx(ctx, func(tx *TxWrap) error {
isWS := isWebShare(tx, screenId)
for _, lineId := range lineIds {
query := `DELETE FROM line WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
query = `DELETE FROM history WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
err := purgeCmdByScreenId(tx.Context(), screenId, lineId)
if err != nil {
return err
query := `SELECT status FROM cmd WHERE screenid = ? AND lineid = ?`
cmdStatus := tx.GetString(query, screenId, lineId)
if cmdStatus == CmdStatusRunning {
return fmt.Errorf("cannot delete line[%s:%s], cmd is running", screenId, lineId)
}
query = `DELETE FROM line WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
query = `DELETE FROM cmd WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
// don't delete history anymore, just remove lineid reference
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ? AND lineid = ?`
tx.Exec(query, screenId, lineId)
if isWS {
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel)
}
@@ -2430,21 +2456,11 @@ func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryIte
})
}
func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItemType, error) {
return WithTxRtn(ctx, func(tx *TxWrap) ([]*HistoryItemType, error) {
query := `SELECT * FROM history WHERE historyid IN (SELECT value FROM json_each(?))`
rtn := dbutil.SelectMapsGen[*HistoryItemType](tx, query, quickJsonArr(historyIds))
query = `DELETE FROM history WHERE historyid IN (SELECT value FROM json_each(?))`
func PurgeHistoryByIds(ctx context.Context, historyIds []string) error {
return WithTx(ctx, func(tx *TxWrap) error {
query := `DELETE FROM history WHERE historyid IN (SELECT value FROM json_each(?))`
tx.Exec(query, quickJsonArr(historyIds))
for _, hitem := range rtn {
if hitem.LineId != "" {
err := PurgeLinesByIds(tx.Context(), hitem.ScreenId, []string{hitem.LineId})
if err != nil {
return nil, err
}
}
}
return rtn, nil
return nil
})
}
+20 -2
View File
@@ -12,10 +12,11 @@ import (
"log"
"os"
"path"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
"github.com/google/uuid"
)
func CreateCmdPtyFile(ctx context.Context, screenId string, lineId string, maxSize int64) error {
@@ -177,11 +178,28 @@ func DeletePtyOutFile(ctx context.Context, screenId string, lineId string) error
return err
}
func GoDeleteScreenDirs(screenIds ...string) {
go func() {
for _, screenId := range screenIds {
deleteScreenDirMakeCtx(screenId)
}
}()
}
func deleteScreenDirMakeCtx(screenId string) {
ctx, cancelFn := context.WithTimeout(context.Background(), time.Minute)
defer cancelFn()
err := DeleteScreenDir(ctx, screenId)
if err != nil {
log.Printf("error deleting screendir %s: %v\n", screenId, err)
}
}
func DeleteScreenDir(ctx context.Context, screenId string) error {
screenDir, err := scbase.EnsureScreenDir(screenId)
if err != nil {
return fmt.Errorf("error getting screendir: %w", err)
}
log.Printf("remove-all %s\n", screenDir)
log.Printf("delete screen dir, remove-all %s\n", screenDir)
return os.RemoveAll(screenDir)
}
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"github.com/golang-migrate/migrate/v4"
)
const MaxMigration = 26
const MaxMigration = 27
const MigratePrimaryScreenVersion = 9
const CmdScreenSpecialMigration = 13
const CmdLineSpecialMigration = 20
+48 -17
View File
@@ -73,6 +73,7 @@ const (
CmdStatusError = "error"
CmdStatusDone = "done"
CmdStatusHangup = "hangup"
CmdStatusUnknown = "unknown" // used for history items where we don't have a status
)
const (
@@ -328,6 +329,14 @@ type SessionType struct {
Full bool `json:"full,omitempty"`
}
type SessionTombstoneType struct {
SessionId string `json:"sessionid"`
Name string `json:"name"`
DeletedTs int64 `json:"deletedts"`
}
func (SessionTombstoneType) UseDBMap() {}
type SessionStatsType struct {
SessionId string `json:"sessionid"`
NumScreens int `json:"numscreens"`
@@ -414,7 +423,11 @@ func (h *HistoryItemType) ToMap() map[string]interface{} {
rtn["remoteid"] = h.Remote.RemoteId
rtn["remotename"] = h.Remote.Name
rtn["ismetacmd"] = h.IsMetaCmd
rtn["incognito"] = h.Incognito
rtn["exitcode"] = h.ExitCode
rtn["durationms"] = h.DurationMs
rtn["festate"] = quickJson(h.FeState)
rtn["tags"] = quickJson(h.Tags)
rtn["status"] = h.Status
return rtn
}
@@ -433,7 +446,11 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
quickSetBool(&h.IsMetaCmd, m, "ismetacmd")
quickSetStr(&h.HistoryNum, m, "historynum")
quickSetInt64(&h.LineNum, m, "linenum")
quickSetBool(&h.Incognito, m, "incognito")
dbutil.QuickSetNullableInt64(&h.ExitCode, m, "exitcode")
dbutil.QuickSetNullableInt64(&h.DurationMs, m, "durationms")
quickSetJson(&h.FeState, m, "festate")
quickSetJson(&h.Tags, m, "tags")
quickSetStr(&h.Status, m, "status")
return true
}
@@ -547,6 +564,16 @@ func (s *ScreenType) FromMap(m map[string]interface{}) bool {
return true
}
type ScreenTombstoneType struct {
ScreenId string `json:"screenid"`
SessionId string `json:"sessionid"`
Name string `json:"name"`
DeletedTs int64 `json:"deletedts"`
ScreenOpts ScreenOptsType `json:"screenopts"`
}
func (ScreenTombstoneType) UseDBMap() {}
const (
LayoutFull = "full"
)
@@ -578,24 +605,28 @@ type ScreenAnchorType struct {
}
type HistoryItemType struct {
HistoryId string `json:"historyid"`
Ts int64 `json:"ts"`
UserId string `json:"userid"`
SessionId string `json:"sessionid"`
ScreenId string `json:"screenid"`
LineId string `json:"lineid"`
HadError bool `json:"haderror"`
CmdStr string `json:"cmdstr"`
Remote RemotePtrType `json:"remote"`
IsMetaCmd bool `json:"ismetacmd"`
Incognito bool `json:"incognito,omitempty"`
HistoryId string `json:"historyid"`
Ts int64 `json:"ts"`
UserId string `json:"userid"`
SessionId string `json:"sessionid"`
ScreenId string `json:"screenid"`
LineId string `json:"lineid"`
HadError bool `json:"haderror"`
CmdStr string `json:"cmdstr"`
Remote RemotePtrType `json:"remote"`
IsMetaCmd bool `json:"ismetacmd"`
ExitCode *int64 `json:"exitcode,omitempty"`
DurationMs *int64 `json:"durationms,omitempty"`
FeState FeStateType `json:"festate,omitempty"`
Tags map[string]bool `json:"tags,omitempty"`
LineNum int64 `json:"linenum" dbmap:"-"`
Status string `json:"status"`
// only for updates
Remove bool `json:"remove"`
Remove bool `json:"remove" dbmap:"-"`
// transient (string because of different history orderings)
HistoryNum string `json:"historynum"`
LineNum int64 `json:"linenum"`
HistoryNum string `json:"historynum" dbmap:"-"`
}
type HistoryQueryOpts struct {
@@ -709,7 +740,7 @@ func FeStateFromShellState(state *packet.ShellState) map[string]string {
rtn["VIRTUAL_ENV"] = envMap["VIRTUAL_ENV"]
}
for key, val := range envMap {
if strings.HasPrefix(key, "PROMPTVAR_") {
if strings.HasPrefix(key, "PROMPTVAR_") && rtn[key] != "" {
rtn[key] = val
}
}
+22 -20
View File
@@ -38,26 +38,28 @@ func (*PtyDataUpdate) UpdateType() string {
func (pdu *PtyDataUpdate) Clean() {}
type ModelUpdate struct {
Sessions []*SessionType `json:"sessions,omitempty"`
ActiveSessionId string `json:"activesessionid,omitempty"`
Screens []*ScreenType `json:"screens,omitempty"`
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
Line *LineType `json:"line,omitempty"`
Lines []*LineType `json:"lines,omitempty"`
Cmd *CmdType `json:"cmd,omitempty"`
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
Info *InfoMsgType `json:"info,omitempty"`
ClearInfo bool `json:"clearinfo,omitempty"`
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
History *HistoryInfoType `json:"history,omitempty"`
Interactive bool `json:"interactive"`
Connect bool `json:"connect,omitempty"`
MainView string `json:"mainview,omitempty"`
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
SelectedBookmark string `json:"selectedbookmark,omitempty"`
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
ClientData *ClientData `json:"clientdata,omitempty"`
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
Sessions []*SessionType `json:"sessions,omitempty"`
ActiveSessionId string `json:"activesessionid,omitempty"`
Screens []*ScreenType `json:"screens,omitempty"`
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
Line *LineType `json:"line,omitempty"`
Lines []*LineType `json:"lines,omitempty"`
Cmd *CmdType `json:"cmd,omitempty"`
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
Info *InfoMsgType `json:"info,omitempty"`
ClearInfo bool `json:"clearinfo,omitempty"`
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
History *HistoryInfoType `json:"history,omitempty"`
Interactive bool `json:"interactive"`
Connect bool `json:"connect,omitempty"`
MainView string `json:"mainview,omitempty"`
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
SelectedBookmark string `json:"selectedbookmark,omitempty"`
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
ClientData *ClientData `json:"clientdata,omitempty"`
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"`
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
}
func (*ModelUpdate) UpdateType() string {