2024-03-26 20:27:34 +01:00
|
|
|
package workspaces
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/workspaces/screen"
|
|
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/workspaces/session"
|
|
|
|
)
|
|
|
|
|
|
|
|
type ConnectUpdate struct {
|
|
|
|
Sessions []*session.SessionType `json:"sessions,omitempty"`
|
|
|
|
Screens []*screen.ScreenType `json:"screens,omitempty"`
|
|
|
|
Remotes []*sstore.RemoteRuntimeState `json:"remotes,omitempty"`
|
|
|
|
ScreenStatusIndicators []*sstore.ScreenStatusIndicatorType `json:"screenstatusindicators,omitempty"`
|
|
|
|
ScreenNumRunningCommands []*sstore.ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
|
|
|
ActiveSessionId string `json:"activesessionid,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ConnectUpdate) GetType() string {
|
|
|
|
return "connect"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all sessions and screens, including remotes
|
|
|
|
func GetConnectUpdate(ctx context.Context) (*ConnectUpdate, error) {
|
|
|
|
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*ConnectUpdate, error) {
|
|
|
|
update := &ConnectUpdate{}
|
|
|
|
sessions := []*session.SessionType{}
|
|
|
|
tx.Select(&sessions, session.GetAllSessionsQuery)
|
|
|
|
sessionMap := make(map[string]*session.SessionType)
|
|
|
|
for _, session := range sessions {
|
|
|
|
sessionMap[session.SessionId] = session
|
|
|
|
update.Sessions = append(update.Sessions, session)
|
|
|
|
}
|
|
|
|
query := `SELECT * FROM screen ORDER BY archived, screenidx, archivedts`
|
|
|
|
screens := dbutil.SelectMapsGen[*screen.ScreenType](tx, query)
|
|
|
|
for _, screen := range screens {
|
|
|
|
update.Screens = append(update.Screens, screen)
|
|
|
|
}
|
|
|
|
query = `SELECT * FROM remote_instance`
|
|
|
|
riArr := dbutil.SelectMapsGen[*sstore.RemoteInstance](tx, query)
|
|
|
|
for _, ri := range riArr {
|
|
|
|
s := sessionMap[ri.SessionId]
|
|
|
|
if s != nil {
|
|
|
|
s.Remotes = append(s.Remotes, ri)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
query = `SELECT activesessionid FROM client`
|
|
|
|
update.ActiveSessionId = tx.GetString(query)
|
|
|
|
return update, nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func InsertScreen(ctx context.Context, sessionId string, origScreenName string, opts screen.ScreenCreateOpts, activate bool) (*scbus.ModelUpdatePacketType, error) {
|
|
|
|
var newScreenId string
|
|
|
|
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
|
|
|
query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived`
|
|
|
|
if !tx.Exists(query, sessionId) {
|
|
|
|
return fmt.Errorf("cannot create screen, no session found (or session archived)")
|
|
|
|
}
|
|
|
|
localRemoteId := tx.GetString(`SELECT remoteid FROM remote WHERE remotealias = ?`, sstore.LocalRemoteAlias)
|
|
|
|
if localRemoteId == "" {
|
|
|
|
return fmt.Errorf("cannot create screen, no local remote found")
|
|
|
|
}
|
|
|
|
maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId)
|
|
|
|
var screenName string
|
|
|
|
if origScreenName == "" {
|
|
|
|
screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId)
|
|
|
|
screenName = sstore.FmtUniqueName("", "s%d", maxScreenIdx+1, screenNames)
|
|
|
|
} else {
|
|
|
|
screenName = origScreenName
|
|
|
|
}
|
|
|
|
var baseScreen *screen.ScreenType
|
|
|
|
if opts.HasCopy() {
|
|
|
|
if opts.BaseScreenId == "" {
|
|
|
|
return fmt.Errorf("invalid screen create opts, copy option with no base screen specified")
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
baseScreen, err = screen.GetScreenById(tx.Context(), opts.BaseScreenId)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if baseScreen == nil {
|
|
|
|
return fmt.Errorf("cannot create screen, base screen not found")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newScreenId = scbase.GenWaveUUID()
|
|
|
|
screen := &screen.ScreenType{
|
|
|
|
SessionId: sessionId,
|
|
|
|
ScreenId: newScreenId,
|
|
|
|
Name: screenName,
|
|
|
|
ScreenIdx: int64(maxScreenIdx) + 1,
|
|
|
|
ScreenOpts: screen.ScreenOptsType{},
|
|
|
|
OwnerId: "",
|
|
|
|
ShareMode: sstore.ShareModeLocal,
|
|
|
|
CurRemote: sstore.RemotePtrType{RemoteId: localRemoteId},
|
|
|
|
NextLineNum: 1,
|
|
|
|
SelectedLine: 0,
|
|
|
|
Anchor: screen.ScreenAnchorType{},
|
|
|
|
FocusType: screen.ScreenFocusInput,
|
|
|
|
Archived: false,
|
|
|
|
ArchivedTs: 0,
|
|
|
|
}
|
|
|
|
query = `INSERT INTO screen ( sessionid, screenid, name, screenidx, screenopts, screenviewopts, ownerid, sharemode, webshareopts, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts)
|
|
|
|
VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,:screenviewopts,:ownerid,:sharemode,:webshareopts,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)`
|
|
|
|
tx.NamedExec(query, screen.ToMap())
|
|
|
|
if activate {
|
|
|
|
query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?`
|
|
|
|
tx.Exec(query, newScreenId, sessionId)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
newScreen, err := screen.GetScreenById(ctx, newScreenId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
update := scbus.MakeUpdatePacket()
|
|
|
|
update.AddUpdate(*newScreen)
|
|
|
|
if activate {
|
|
|
|
bareSession, err := session.GetBareSessionById(ctx, sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
update.AddUpdate(*bareSession)
|
|
|
|
sstore.UpdateWithCurrentOpenAICmdInfoChat(newScreenId, update)
|
|
|
|
}
|
|
|
|
return update, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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, update *scbus.ModelUpdatePacketType) (*scbus.ModelUpdatePacketType, error) {
|
|
|
|
var sessionId string
|
|
|
|
var isActive bool
|
|
|
|
var screenTombstone *screen.ScreenTombstoneType
|
|
|
|
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
2024-03-26 20:44:29 +01:00
|
|
|
scr, err := screen.GetScreenById(tx.Context(), screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot get screen to delete: %w", err)
|
|
|
|
}
|
2024-03-26 20:44:29 +01:00
|
|
|
if scr == nil {
|
2024-03-26 20:27:34 +01:00
|
|
|
return fmt.Errorf("cannot delete screen (not found)")
|
|
|
|
}
|
|
|
|
webSharing := sstore.IsWebShare(tx, screenId)
|
|
|
|
if !sessionDel {
|
|
|
|
query := `SELECT sessionid FROM screen WHERE screenid = ?`
|
|
|
|
sessionId = tx.GetString(query, screenId)
|
|
|
|
if 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 delete the last screen in a session")
|
|
|
|
}
|
|
|
|
isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId)
|
|
|
|
if isActive {
|
|
|
|
screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId)
|
2024-03-26 20:44:29 +01:00
|
|
|
nextId := sstore.GetNextId(screenIds, screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId)
|
|
|
|
}
|
|
|
|
}
|
2024-03-26 20:44:29 +01:00
|
|
|
screenTombstone = &screen.ScreenTombstoneType{
|
|
|
|
ScreenId: scr.ScreenId,
|
|
|
|
SessionId: scr.SessionId,
|
|
|
|
Name: scr.Name,
|
2024-03-26 20:27:34 +01:00
|
|
|
DeletedTs: time.Now().UnixMilli(),
|
2024-03-26 20:44:29 +01:00
|
|
|
ScreenOpts: scr.ScreenOpts,
|
2024-03-26 20:27:34 +01:00
|
|
|
}
|
|
|
|
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 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 {
|
2024-03-26 20:44:29 +01:00
|
|
|
screen.InsertScreenDelUpdate(tx, screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
if !sessionDel {
|
2024-03-26 20:44:29 +01:00
|
|
|
sstore.GoDeleteScreenDirs(screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
}
|
|
|
|
if update == nil {
|
|
|
|
update = scbus.MakeUpdatePacket()
|
|
|
|
}
|
|
|
|
update.AddUpdate(*screenTombstone)
|
2024-03-26 20:44:29 +01:00
|
|
|
update.AddUpdate(screen.ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true})
|
2024-03-26 20:27:34 +01:00
|
|
|
if isActive {
|
2024-03-26 20:44:29 +01:00
|
|
|
bareSession, err := session.GetBareSessionById(ctx, sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
update.AddUpdate(*bareSession)
|
|
|
|
}
|
|
|
|
return update, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (scbus.UpdatePacket, error) {
|
|
|
|
var isActive bool
|
|
|
|
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
|
|
|
query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?`
|
|
|
|
if !tx.Exists(query, sessionId, screenId) {
|
|
|
|
return fmt.Errorf("cannot close screen (not found)")
|
|
|
|
}
|
|
|
|
if sstore.IsWebShare(tx, screenId) {
|
|
|
|
return fmt.Errorf("cannot archive screen while web-sharing. stop web-sharing before trying to archive.")
|
|
|
|
}
|
|
|
|
query = `SELECT archived FROM screen WHERE sessionid = ? AND screenid = ?`
|
|
|
|
closeVal := tx.GetBool(query, sessionId, screenId)
|
|
|
|
if closeVal {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived`
|
|
|
|
numScreens := tx.GetInt(query, sessionId)
|
|
|
|
if numScreens <= 1 {
|
|
|
|
return fmt.Errorf("cannot archive the last screen in a session")
|
|
|
|
}
|
|
|
|
query = `UPDATE screen SET archived = 1, archivedts = ?, screenidx = 0 WHERE sessionid = ? AND screenid = ?`
|
|
|
|
tx.Exec(query, time.Now().UnixMilli(), sessionId, screenId)
|
|
|
|
isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId)
|
|
|
|
if isActive {
|
|
|
|
screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId)
|
|
|
|
nextId := sstore.GetNextId(screenIds, screenId)
|
|
|
|
tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
newScreen, err := screen.GetScreenById(ctx, screenId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot retrive archived screen: %w", err)
|
|
|
|
}
|
|
|
|
update := scbus.MakeUpdatePacket()
|
|
|
|
update.AddUpdate(*newScreen)
|
|
|
|
if isActive {
|
|
|
|
bareSession, err := session.GetBareSessionById(ctx, sessionId)
|
2024-03-26 20:27:34 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
update.AddUpdate(*bareSession)
|
|
|
|
}
|
|
|
|
return update, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns sessionId
|
|
|
|
// if sessionName == "", it will be generated
|
|
|
|
func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*scbus.ModelUpdatePacketType, error) {
|
|
|
|
var newScreen *screen.ScreenType
|
|
|
|
newSessionId := scbase.GenWaveUUID()
|
|
|
|
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
|
|
|
names := tx.SelectStrings(`SELECT name FROM session`)
|
|
|
|
sessionName = sstore.FmtUniqueName(sessionName, "workspace-%d", len(names)+1, names)
|
|
|
|
maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`)
|
|
|
|
query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, archivedts, sharemode)
|
|
|
|
VALUES (?, ?, '', ?, 0, 0, 0, ?)`
|
|
|
|
tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, sstore.ShareModeLocal)
|
|
|
|
screenUpdate, err := InsertScreen(tx.Context(), newSessionId, "", screen.ScreenCreateOpts{}, true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
screenUpdateItems := scbus.GetUpdateItems[screen.ScreenType](screenUpdate)
|
|
|
|
if len(screenUpdateItems) < 1 {
|
|
|
|
return fmt.Errorf("no screen update items")
|
|
|
|
}
|
|
|
|
newScreen = screenUpdateItems[0]
|
|
|
|
if activate {
|
|
|
|
query = `UPDATE client SET activesessionid = ?`
|
|
|
|
tx.Exec(query, newSessionId)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
sess, err := session.GetSessionById(ctx, newSessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
update := scbus.MakeUpdatePacket()
|
|
|
|
update.AddUpdate(*sess)
|
|
|
|
update.AddUpdate(*newScreen)
|
|
|
|
if activate {
|
|
|
|
update.AddUpdate(session.ActiveSessionIdUpdate(newSessionId))
|
|
|
|
}
|
|
|
|
return update, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*scbus.ModelUpdatePacketType, error) {
|
|
|
|
session.SetActiveSessionId(ctx, sessionId)
|
|
|
|
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
|
|
|
query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?`
|
|
|
|
if !tx.Exists(query, sessionId, screenId) {
|
|
|
|
return fmt.Errorf("cannot switch to screen, screen=%s does not exist in session=%s", screenId, sessionId)
|
|
|
|
}
|
|
|
|
query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?`
|
|
|
|
tx.Exec(query, screenId, sessionId)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
bareSession, err := session.GetBareSessionById(ctx, sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
update := scbus.MakeUpdatePacket()
|
2024-03-26 20:44:29 +01:00
|
|
|
update.AddUpdate(session.ActiveSessionIdUpdate(sessionId))
|
2024-03-26 20:27:34 +01:00
|
|
|
update.AddUpdate(*bareSession)
|
2024-03-26 20:44:29 +01:00
|
|
|
memState := sstore.GetScreenMemState(screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
if memState != nil {
|
2024-03-26 20:44:29 +01:00
|
|
|
update.AddUpdate(sstore.CmdLineUpdate(memState.CmdInputText))
|
|
|
|
sstore.UpdateWithCurrentOpenAICmdInfoChat(screenId, update)
|
2024-03-26 20:27:34 +01:00
|
|
|
|
|
|
|
// Clear any previous status indicator for this screen
|
2024-03-26 20:44:29 +01:00
|
|
|
err := sstore.ResetStatusIndicator_Update(update, screenId)
|
2024-03-26 20:27:34 +01:00
|
|
|
if err != nil {
|
|
|
|
// This is not a fatal error, so just log it
|
|
|
|
log.Printf("error resetting status indicator when switching screens: %v\n", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return update, nil
|
|
|
|
}
|