diff --git a/cmd/main-server.go b/cmd/main-server.go index 5b782edf..378a34e7 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -23,6 +23,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/cmdrunner" "github.com/scripthaus-dev/sh2-server/pkg/pcloud" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/scws" @@ -246,7 +247,7 @@ func HandleRtnState(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) return } - data, err := cmdrunner.GetRtnStateDiff(r.Context(), screenId, cmdId) + data, err := rtnstate.GetRtnStateDiff(r.Context(), screenId, cmdId) if err != nil { w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("cannot get rtnstate diff: %v", err))) diff --git a/db/migrations/000016_webptypos.down.sql b/db/migrations/000016_webptypos.down.sql new file mode 100644 index 00000000..1d5c5243 --- /dev/null +++ b/db/migrations/000016_webptypos.down.sql @@ -0,0 +1,3 @@ +DROP TABLE webptypos; + +DROP INDEX idx_screenupdate_ids; diff --git a/db/migrations/000016_webptypos.up.sql b/db/migrations/000016_webptypos.up.sql new file mode 100644 index 00000000..69ee7486 --- /dev/null +++ b/db/migrations/000016_webptypos.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE webptypos ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + ptypos bigint NOT NULL, + PRIMARY KEY (screenid, cmdid) +); + +CREATE INDEX idx_screenupdate_ids ON screenupdate (screenid, lineid); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a9e93bda..9ee78210 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -16,7 +16,6 @@ import ( "time" "unicode" - "github.com/alessio/shellescape" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -2951,94 +2950,6 @@ func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string return rtn } -const MaxDiffKeyLen = 40 -const MaxDiffValLen = 50 - -func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newState packet.ShellState) { - if newState.Cwd != oldState.Cwd { - buf.WriteString(fmt.Sprintf("cwd %s\n", newState.Cwd)) - } - if !bytes.Equal(newState.ShellVars, oldState.ShellVars) { - newEnvMap := shexec.DeclMapFromState(&newState) - oldEnvMap := shexec.DeclMapFromState(&oldState) - for key, newVal := range newEnvMap { - oldVal, found := oldEnvMap[key] - if !found || !shexec.DeclsEqual(false, oldVal, newVal) { - var exportStr string - if newVal.IsExport() { - exportStr = "export " - } - buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, utilfn.EllipsisStr(key, MaxDiffKeyLen), utilfn.EllipsisStr(newVal.Value, MaxDiffValLen))) - } - } - for key, _ := range oldEnvMap { - _, found := newEnvMap[key] - if !found { - buf.WriteString(fmt.Sprintf("unset %s\n", utilfn.EllipsisStr(key, MaxDiffKeyLen))) - } - } - } - if newState.Aliases != oldState.Aliases { - newAliasMap, _ := ParseAliases(newState.Aliases) - oldAliasMap, _ := ParseAliases(oldState.Aliases) - for aliasName, newAliasVal := range newAliasMap { - oldAliasVal, found := oldAliasMap[aliasName] - if !found || newAliasVal != oldAliasVal { - buf.WriteString(fmt.Sprintf("alias %s\n", utilfn.EllipsisStr(shellescape.Quote(aliasName), MaxDiffKeyLen))) - } - } - for aliasName, _ := range oldAliasMap { - _, found := newAliasMap[aliasName] - if !found { - buf.WriteString(fmt.Sprintf("unalias %s\n", utilfn.EllipsisStr(shellescape.Quote(aliasName), MaxDiffKeyLen))) - } - } - } - if newState.Funcs != oldState.Funcs { - newFuncMap, _ := ParseFuncs(newState.Funcs) - oldFuncMap, _ := ParseFuncs(oldState.Funcs) - for funcName, newFuncVal := range newFuncMap { - oldFuncVal, found := oldFuncMap[funcName] - if !found || newFuncVal != oldFuncVal { - buf.WriteString(fmt.Sprintf("function %s\n", utilfn.EllipsisStr(shellescape.Quote(funcName), MaxDiffKeyLen))) - } - } - for funcName, _ := range oldFuncMap { - _, found := newFuncMap[funcName] - if !found { - buf.WriteString(fmt.Sprintf("unset -f %s\n", utilfn.EllipsisStr(shellescape.Quote(funcName), MaxDiffKeyLen))) - } - } - } -} - -func GetRtnStateDiff(ctx context.Context, screenId string, cmdId string) ([]byte, error) { - cmd, err := sstore.GetCmdByScreenId(ctx, screenId, cmdId) - if err != nil { - return nil, err - } - if cmd == nil { - return nil, nil - } - if !cmd.RtnState { - return nil, nil - } - if cmd.RtnStatePtr.IsEmpty() { - return nil, nil - } - var outputBytes bytes.Buffer - initialState, err := sstore.GetFullState(ctx, cmd.StatePtr) - if err != nil { - return nil, fmt.Errorf("getting initial full state: %v", err) - } - rtnState, err := sstore.GetFullState(ctx, cmd.RtnStatePtr) - if err != nil { - return nil, fmt.Errorf("getting rtn full state: %v", err) - } - displayStateUpdateDiff(&outputBytes, *initialState, *rtnState) - return outputBytes.Bytes(), nil -} - func isValidInScope(scopeName string, varName string) bool { for _, varScope := range SetVarScopes { if varScope.ScopeName == scopeName { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 8d5aea0e..70b62d71 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -269,87 +269,3 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) } return rtnPk, nil } - -func parseAliasStmt(stmt *syntax.Stmt, sourceStr string) (string, string, error) { - cmd := stmt.Cmd - callExpr, ok := cmd.(*syntax.CallExpr) - if !ok { - return "", "", fmt.Errorf("wrong cmd type for alias") - } - if len(callExpr.Args) != 2 { - return "", "", fmt.Errorf("wrong number of words in alias expr wordslen=%d", len(callExpr.Args)) - } - firstWord := callExpr.Args[0] - if firstWord.Lit() != "alias" { - return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')") - } - secondWord := callExpr.Args[1] - var ectx simpleexpand.SimpleExpandContext // no homedir, do not want ~ expansion - val, _ := simpleexpand.SimpleExpandWord(ectx, secondWord, sourceStr) - eqIdx := strings.Index(val, "=") - if eqIdx == -1 { - return "", "", fmt.Errorf("no '=' in alias definition") - } - return val[0:eqIdx], val[eqIdx+1:], nil -} - -func ParseAliases(aliases string) (map[string]string, error) { - r := strings.NewReader(aliases) - parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) - file, err := parser.Parse(r, "aliases") - if err != nil { - return nil, err - } - rtn := make(map[string]string) - for _, stmt := range file.Stmts { - aliasName, aliasVal, err := parseAliasStmt(stmt, aliases) - if err != nil { - // fmt.Printf("stmt-err: %v\n", err) - continue - } - if aliasName != "" { - rtn[aliasName] = aliasVal - } - } - return rtn, nil -} - -func parseFuncStmt(stmt *syntax.Stmt, source string) (string, string, error) { - cmd := stmt.Cmd - funcDecl, ok := cmd.(*syntax.FuncDecl) - if !ok { - return "", "", fmt.Errorf("cmd not FuncDecl") - } - name := funcDecl.Name.Value - // fmt.Printf("func: [%s]\n", name) - funcBody := funcDecl.Body - // fmt.Printf(" %d:%d\n", funcBody.Cmd.Pos().Offset(), funcBody.Cmd.End().Offset()) - bodyStr := source[funcBody.Cmd.Pos().Offset():funcBody.Cmd.End().Offset()] - // fmt.Printf("<<<\n%s\n>>>\n", bodyStr) - // fmt.Printf("\n") - return name, bodyStr, nil -} - -func ParseFuncs(funcs string) (map[string]string, error) { - r := strings.NewReader(funcs) - parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) - file, err := parser.Parse(r, "funcs") - if err != nil { - return nil, err - } - rtn := make(map[string]string) - for _, stmt := range file.Stmts { - funcName, funcVal, err := parseFuncStmt(stmt, funcs) - if err != nil { - // TODO where to put parse errors - continue - } - if strings.HasPrefix(funcName, "_mshell_") { - continue - } - if funcName != "" { - rtn[funcName] = funcVal - } - } - return rtn, nil -} diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 915892c9..a5a83f75 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "log" @@ -12,6 +13,7 @@ import ( "strconv" "strings" + "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -19,37 +21,11 @@ import ( const PCloudEndpoint = "https://api.getprompt.dev/central" const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 1 +const MaxPtyUpdateSize = (128 * 1024) + 1 const TelemetryUrl = "/telemetry" const NoTelemetryUrl = "/no-telemetry" -const CreateCloudSessionUrl = "/auth/create-cloud-session" - -type NoTelemetryInputType struct { - ClientId string `json:"clientid"` - Value bool `json:"value"` -} - -type TelemetryInputType struct { - UserId string `json:"userid"` - ClientId string `json:"clientid"` - CurDay string `json:"curday"` - Activity []*sstore.ActivityType `json:"activity"` -} - -type CloudSession struct { - SessionId string `json:"sessionid"` - ViewKey string `json:"viewkey"` - WriteKey string `json:"writekey"` - EncType string `json:"enctype"` - UpdateVTS int64 `json:"updatevts"` - - EncSessionData []byte `json:"enc_sessiondata" enc:"*"` - Name string `json:"-" enc:"name"` -} - -func (cs *CloudSession) GetOData() string { - return fmt.Sprintf("session:%s", cs.SessionId) -} +const CreateWebScreenUrl = "/auth/create-web-screen" type AuthInfo struct { UserId string `json:"userid"` @@ -194,12 +170,158 @@ func getAuthInfo(ctx context.Context) (AuthInfo, error) { return AuthInfo{UserId: clientData.UserId, ClientId: clientData.ClientId}, nil } -func CreateCloudSession(ctx context.Context) error { +func defaultError(err error, estr string) error { + if err != nil { + return err + } + return errors.New(estr) +} + +func makeWebScreenUpdate(ctx context.Context, update sstore.ScreenUpdateType) (*WebShareUpdateType, error) { + rtn := &WebShareUpdateType{ + ScreenId: update.ScreenId, + LineId: update.LineId, + UpdateType: update.UpdateType, + } + switch update.UpdateType { + case sstore.UpdateType_ScreenNew: + screen, err := sstore.GetScreenById(ctx, update.ScreenId) + if err != nil || screen == nil { + return nil, fmt.Errorf("error getting screen: %v", defaultError(err, "not found")) + } + rtn.Screen, err = webScreenFromScreen(screen) + if err != nil { + return nil, fmt.Errorf("error converting screen to web-screen: %v", err) + } + + case sstore.UpdateType_ScreenDel: + break + + case sstore.UpdateType_ScreenName: + screen, err := sstore.GetScreenById(ctx, update.ScreenId) + if err != nil { + return nil, fmt.Errorf("error getting screen: %v", err) + } + if screen == nil || screen.WebShareOpts == nil || screen.WebShareOpts.ShareName == "" { + return nil, fmt.Errorf("invalid screen sharename (makeWebScreenUpdate)") + } + rtn.SVal = screen.WebShareOpts.ShareName + + case sstore.UpdateType_LineNew: + line, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) + if err != nil || line == nil { + return nil, fmt.Errorf("error getting line/cmd: %v", defaultError(err, "not found")) + } + rtn.Line, err = webLineFromLine(line) + if err != nil { + return nil, fmt.Errorf("error converting line to web-line: %v", err) + } + if cmd != nil { + rtn.Cmd, err = webCmdFromCmd(cmd) + if err != nil { + return nil, fmt.Errorf("error converting cmd to web-cmd: %v", err) + } + } + + case sstore.UpdateType_LineDel: + break + + case sstore.UpdateType_LineArchived: + line, err := sstore.GetLineById(ctx, update.ScreenId, update.LineId) + if err != nil || line == nil { + return nil, fmt.Errorf("error getting line: %v", defaultError(err, "not found")) + } + rtn.BVal = line.Archived + + case sstore.UpdateType_LineRenderer: + line, err := sstore.GetLineById(ctx, update.ScreenId, update.LineId) + if err != nil || line == nil { + return nil, fmt.Errorf("error getting line: %v", defaultError(err, "not found")) + } + rtn.SVal = line.Renderer + + case sstore.UpdateType_CmdStatus: + _, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) + if err != nil || cmd == nil { + return nil, fmt.Errorf("error getting cmd: %v", defaultError(err, "not found")) + } + rtn.SVal = cmd.Status + + case sstore.UpdateType_CmdDoneInfo: + _, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) + if err != nil || cmd == nil { + return nil, fmt.Errorf("error getting cmd: %v", defaultError(err, "not found")) + } + rtn.DoneInfo = cmd.DoneInfo + + case sstore.UpdateType_CmdRtnState: + _, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) + if err != nil || cmd == nil { + return nil, fmt.Errorf("error getting cmd: %v", defaultError(err, "not found")) + } + data, err := rtnstate.GetRtnStateDiff(ctx, update.ScreenId, cmd.CmdId) + if err != nil { + return nil, fmt.Errorf("cannot compute rtnstate: %v", err) + } + rtn.SVal = string(data) + + case sstore.UpdateType_PtyPos: + cmdId, err := sstore.GetCmdIdFromLineId(ctx, update.ScreenId, update.LineId) + if err != nil { + return nil, fmt.Errorf("error getting cmdid: %v", err) + } + ptyPos, err := sstore.GetWebPtyPos(ctx, update.ScreenId, update.LineId) + if err != nil { + return nil, fmt.Errorf("error getting ptypos: %v", err) + } + realOffset, data, err := sstore.ReadPtyOutFile(ctx, update.ScreenId, cmdId, ptyPos, MaxPtyUpdateSize) + if err != nil { + return nil, fmt.Errorf("error getting ptydata: %v", err) + } + rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data} + + default: + return nil, fmt.Errorf("unsupported update type (pcloud/makeWebScreenUpdate): %s\n", update.UpdateType) + } + return rtn, nil +} + +func finalizeWebScreenUpdate(ctx context.Context, screenUpdate sstore.ScreenUpdateType, webUpdate *WebShareUpdateType) error { + switch screenUpdate.UpdateType { + case sstore.UpdateType_PtyPos: + dataEof := len(webUpdate.PtyData.Data) < MaxPtyUpdateSize + newPos := webUpdate.PtyData.PtyPos + int64(len(webUpdate.PtyData.Data)) + if dataEof { + err := sstore.RemoveScreenUpdate(ctx, screenUpdate.UpdateType) + if err != nil { + return err + } + } + err := sstore.SetWebPtyPos(ctx, screenUpdate.ScreenId, screenUpdate.LineId, newPos) + if err != nil { + return err + } + + default: + err := sstore.RemoveScreenUpdate(ctx, screenUpdate.UpdateType) + if err != nil { + // this is not great, this *should* never fail and is not easy to recover from + return err + } + } + return nil +} + +func DoWebScreenUpdate(ctx context.Context, update sstore.ScreenUpdateType) error { + return nil +} + +func CreateWebScreen(ctx context.Context, screen *WebShareScreenType) error { authInfo, err := getAuthInfo(ctx) if err != nil { return err } - req, err := makeAuthPostReq(ctx, CreateCloudSessionUrl, authInfo, nil) + req, err := makeAuthPostReq(ctx, CreateWebScreenUrl, authInfo, screen) if err != nil { return err } diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go new file mode 100644 index 00000000..694cd527 --- /dev/null +++ b/pkg/pcloud/pclouddata.go @@ -0,0 +1,100 @@ +package pcloud + +import ( + "fmt" + + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +type NoTelemetryInputType struct { + ClientId string `json:"clientid"` + Value bool `json:"value"` +} + +type TelemetryInputType struct { + UserId string `json:"userid"` + ClientId string `json:"clientid"` + CurDay string `json:"curday"` + Activity []*sstore.ActivityType `json:"activity"` +} + +type WebShareUpdateType struct { + ScreenId string `json:"screenid"` + LineId string `json:"lineid"` + UpdateType string `json:"updatetype"` + + Screen *WebShareScreenType `json:"screen,omitempty"` + Line *WebShareLineType `json:"line,omitempty"` + Cmd *WebShareCmdType `json:"cmd,omitempty"` + PtyData *WebSharePtyData `json:"ptydata,omitempty"` + SVal string `json:"sval,omitempty"` + BVal bool `json:"bval,omitempty"` + DoneInfo *sstore.CmdDoneInfo `json:"doneinfo,omitempty"` +} + +type WebShareRemotePtr struct { + Alias string `json:"remotealias,omitempty"` + CanonicalName string `json:"remotecanonicalname"` + Name string `json:"name,omitempty"` +} + +type WebShareScreenType struct { + ScreenId string `json:"screenid"` + ShareName string `json:"sharename"` + ViewKey string `json:"viewkey"` +} + +func webScreenFromScreen(s *sstore.ScreenType) (*WebShareScreenType, error) { + if s == nil || s.ScreenId == "" { + return nil, fmt.Errorf("invalid nil screen") + } + if s.WebShareOpts == nil { + return nil, fmt.Errorf("invalid screen, no WebShareOpts") + } + if s.WebShareOpts.ViewKey == "" { + return nil, fmt.Errorf("invalid screen, no ViewKey") + } + if s.WebShareOpts.ShareName == "" { + return nil, fmt.Errorf("invalid screen, no ShareName") + } + return &WebShareScreenType{ScreenId: s.ScreenId, ShareName: s.WebShareOpts.ShareName, ViewKey: s.WebShareOpts.ViewKey}, nil +} + +type WebShareLineType struct { + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineType string `json:"linetype"` + Renderer string `json:"renderer,omitempty"` + Text string `json:"text,omitempty"` + CmdId string `json:"cmdid,omitempty"` + Archived bool `json:"archived,omitempty"` +} + +func webLineFromLine(line *sstore.LineType) (*WebShareLineType, error) { + return nil, nil +} + +type WebShareCmdType struct { + LineId string `json:"lineid"` + CmdStr string `json:"cmdstr"` + RawCmdStr string `json:"rawcmdstr"` + Remote WebShareRemotePtr `json:"remote"` + FeState sstore.FeStateType `json:"festate"` + TermOpts sstore.TermOpts `json:"termopts"` + Status string `json:"status"` + StartPk *packet.CmdStartPacketType `json:"startpk,omitempty"` + DoneInfo *sstore.CmdDoneInfo `json:"doneinfo,omitempty"` + RtnState bool `json:"rtnstate,omitempty"` + RtnStateStr string `json:"rtnstatestr,omitempty"` +} + +func webCmdFromCmd(cmd *sstore.CmdType) (*WebShareCmdType, error) { + return nil, nil +} + +type WebSharePtyData struct { + PtyPos int64 `json:"ptypos,omitempty"` + Data []byte `json:"data,omitempty"` +} diff --git a/pkg/rtnstate/rtnstate.go b/pkg/rtnstate/rtnstate.go new file mode 100644 index 00000000..42e2314c --- /dev/null +++ b/pkg/rtnstate/rtnstate.go @@ -0,0 +1,188 @@ +package rtnstate + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/alessio/shellescape" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/mshell/pkg/simpleexpand" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "mvdan.cc/sh/v3/syntax" +) + +func parseAliasStmt(stmt *syntax.Stmt, sourceStr string) (string, string, error) { + cmd := stmt.Cmd + callExpr, ok := cmd.(*syntax.CallExpr) + if !ok { + return "", "", fmt.Errorf("wrong cmd type for alias") + } + if len(callExpr.Args) != 2 { + return "", "", fmt.Errorf("wrong number of words in alias expr wordslen=%d", len(callExpr.Args)) + } + firstWord := callExpr.Args[0] + if firstWord.Lit() != "alias" { + return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')") + } + secondWord := callExpr.Args[1] + var ectx simpleexpand.SimpleExpandContext // no homedir, do not want ~ expansion + val, _ := simpleexpand.SimpleExpandWord(ectx, secondWord, sourceStr) + eqIdx := strings.Index(val, "=") + if eqIdx == -1 { + return "", "", fmt.Errorf("no '=' in alias definition") + } + return val[0:eqIdx], val[eqIdx+1:], nil +} + +func ParseAliases(aliases string) (map[string]string, error) { + r := strings.NewReader(aliases) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + file, err := parser.Parse(r, "aliases") + if err != nil { + return nil, err + } + rtn := make(map[string]string) + for _, stmt := range file.Stmts { + aliasName, aliasVal, err := parseAliasStmt(stmt, aliases) + if err != nil { + // fmt.Printf("stmt-err: %v\n", err) + continue + } + if aliasName != "" { + rtn[aliasName] = aliasVal + } + } + return rtn, nil +} + +func parseFuncStmt(stmt *syntax.Stmt, source string) (string, string, error) { + cmd := stmt.Cmd + funcDecl, ok := cmd.(*syntax.FuncDecl) + if !ok { + return "", "", fmt.Errorf("cmd not FuncDecl") + } + name := funcDecl.Name.Value + // fmt.Printf("func: [%s]\n", name) + funcBody := funcDecl.Body + // fmt.Printf(" %d:%d\n", funcBody.Cmd.Pos().Offset(), funcBody.Cmd.End().Offset()) + bodyStr := source[funcBody.Cmd.Pos().Offset():funcBody.Cmd.End().Offset()] + // fmt.Printf("<<<\n%s\n>>>\n", bodyStr) + // fmt.Printf("\n") + return name, bodyStr, nil +} + +func ParseFuncs(funcs string) (map[string]string, error) { + r := strings.NewReader(funcs) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + file, err := parser.Parse(r, "funcs") + if err != nil { + return nil, err + } + rtn := make(map[string]string) + for _, stmt := range file.Stmts { + funcName, funcVal, err := parseFuncStmt(stmt, funcs) + if err != nil { + // TODO where to put parse errors + continue + } + if strings.HasPrefix(funcName, "_mshell_") { + continue + } + if funcName != "" { + rtn[funcName] = funcVal + } + } + return rtn, nil +} + +const MaxDiffKeyLen = 40 +const MaxDiffValLen = 50 + +func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newState packet.ShellState) { + if newState.Cwd != oldState.Cwd { + buf.WriteString(fmt.Sprintf("cwd %s\n", newState.Cwd)) + } + if !bytes.Equal(newState.ShellVars, oldState.ShellVars) { + newEnvMap := shexec.DeclMapFromState(&newState) + oldEnvMap := shexec.DeclMapFromState(&oldState) + for key, newVal := range newEnvMap { + oldVal, found := oldEnvMap[key] + if !found || !shexec.DeclsEqual(false, oldVal, newVal) { + var exportStr string + if newVal.IsExport() { + exportStr = "export " + } + buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, utilfn.EllipsisStr(key, MaxDiffKeyLen), utilfn.EllipsisStr(newVal.Value, MaxDiffValLen))) + } + } + for key, _ := range oldEnvMap { + _, found := newEnvMap[key] + if !found { + buf.WriteString(fmt.Sprintf("unset %s\n", utilfn.EllipsisStr(key, MaxDiffKeyLen))) + } + } + } + if newState.Aliases != oldState.Aliases { + newAliasMap, _ := ParseAliases(newState.Aliases) + oldAliasMap, _ := ParseAliases(oldState.Aliases) + for aliasName, newAliasVal := range newAliasMap { + oldAliasVal, found := oldAliasMap[aliasName] + if !found || newAliasVal != oldAliasVal { + buf.WriteString(fmt.Sprintf("alias %s\n", utilfn.EllipsisStr(shellescape.Quote(aliasName), MaxDiffKeyLen))) + } + } + for aliasName, _ := range oldAliasMap { + _, found := newAliasMap[aliasName] + if !found { + buf.WriteString(fmt.Sprintf("unalias %s\n", utilfn.EllipsisStr(shellescape.Quote(aliasName), MaxDiffKeyLen))) + } + } + } + if newState.Funcs != oldState.Funcs { + newFuncMap, _ := ParseFuncs(newState.Funcs) + oldFuncMap, _ := ParseFuncs(oldState.Funcs) + for funcName, newFuncVal := range newFuncMap { + oldFuncVal, found := oldFuncMap[funcName] + if !found || newFuncVal != oldFuncVal { + buf.WriteString(fmt.Sprintf("function %s\n", utilfn.EllipsisStr(shellescape.Quote(funcName), MaxDiffKeyLen))) + } + } + for funcName, _ := range oldFuncMap { + _, found := newFuncMap[funcName] + if !found { + buf.WriteString(fmt.Sprintf("unset -f %s\n", utilfn.EllipsisStr(shellescape.Quote(funcName), MaxDiffKeyLen))) + } + } + } +} + +func GetRtnStateDiff(ctx context.Context, screenId string, cmdId string) ([]byte, error) { + cmd, err := sstore.GetCmdByScreenId(ctx, screenId, cmdId) + if err != nil { + return nil, err + } + if cmd == nil { + return nil, nil + } + if !cmd.RtnState { + return nil, nil + } + if cmd.RtnStatePtr.IsEmpty() { + return nil, nil + } + var outputBytes bytes.Buffer + initialState, err := sstore.GetFullState(ctx, cmd.StatePtr) + if err != nil { + return nil, fmt.Errorf("getting initial full state: %v", err) + } + rtnState, err := sstore.GetFullState(ctx, cmd.RtnStatePtr) + if err != nil { + return nil, fmt.Errorf("getting rtn full state: %v", err) + } + displayStateUpdateDiff(&outputBytes, *initialState, *rtnState) + return outputBytes.Bytes(), nil +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index ffed93c9..bc7b0daa 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -725,6 +725,14 @@ func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (stri return lineId, nil } +func GetCmdIdFromLineId(ctx context.Context, screenId string, lineId string) (string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { + query := `SELECT cmdid FROM line WHERE screenid = ? AND lineid = ?` + cmdId := tx.GetString(query, screenId, lineId) + return cmdId, nil + }) +} + func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType @@ -792,7 +800,7 @@ INSERT INTO cmd ( screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, tx.NamedExec(query, cmdMap) } if isWebShare(tx, line.ScreenId) { - insertScreenUpdate(tx, line.ScreenId, line.LineId, UpdateType_LineNew) + insertScreenLineUpdate(tx, line.ScreenId, line.LineId, UpdateType_LineNew) } return nil }) @@ -829,8 +837,8 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon return err } if isWebShare(tx, screenId) { - insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdDoneInfo) - insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdStatus) + insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdDoneInfo) + insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdStatus) } return nil }) @@ -852,7 +860,7 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE screenid = ? AND cmdid = ?` tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), screenId, ck.GetCmdId()) if isWebShare(tx, screenId) { - insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdRtnState) + insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdRtnState) } return nil }) @@ -870,9 +878,6 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND cmdid = ?` tx.Exec(query, quickJson(errPk), screenId, errPk.CK.GetCmdId()) - if isWebShare(tx, screenId) { - insertScreenUpdateByCmdId(tx, screenId, errPk.CK.GetCmdId(), UpdateType_CmdRunOut) - } return nil }) } @@ -887,16 +892,28 @@ func ReInitFocus(ctx context.Context) error { func HangupAllRunningCmds(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ? WHERE status = ?` + var cmdPtrs []CmdPtr + query := `SELECT screenid, cmdid FROM cmd WHERE status = ?` + tx.Select(&cmdPtrs, query, CmdStatusRunning) + query = `UPDATE cmd SET status = ? WHERE status = ?` tx.Exec(query, CmdStatusHangup, CmdStatusRunning) + for _, cmdPtr := range cmdPtrs { + insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + } return nil }) } func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ? WHERE status = ? AND remoteid = ?` + var cmdPtrs []CmdPtr + query := `SELECT screenid, cmdid FROM cmd WHERE status = ? AND remoteid = ?` + tx.Select(&cmdPtrs, query, CmdStatusRunning, remoteId) + query = `UPDATE cmd SET status = ? WHERE status = ? AND remoteid = ?` tx.Exec(query, CmdStatusHangup, CmdStatusRunning, remoteId) + for _, cmdPtr := range cmdPtrs { + insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + } return nil }) } @@ -905,6 +922,7 @@ func HangupCmd(ctx context.Context, ck base.CommandKey) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE screenid = ? AND cmdid = ?` tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), ck.GetCmdId()) + insertScreenCmdUpdate(tx, ck.GetGroupId(), ck.GetCmdId(), UpdateType_CmdStatus) return nil }) } @@ -1838,7 +1856,7 @@ func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, ren query := `UPDATE line SET renderer = ? WHERE lineid = ?` tx.Exec(query, renderer, lineId) if isWebShare(tx, screenId) { - insertScreenUpdate(tx, screenId, lineId, UpdateType_LineRenderer) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineRenderer) } return nil }) @@ -1867,7 +1885,7 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar query := `UPDATE line SET archived = ? WHERE lineid = ?` tx.Exec(query, archived, lineId) if isWebShare(tx, screenId) { - insertScreenUpdate(tx, screenId, lineId, UpdateType_LineArchived) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineArchived) } return nil }) @@ -1900,7 +1918,7 @@ func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) err } } if isWS { - insertScreenUpdate(tx, screenId, lineId, UpdateType_LineDel) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel) } } return nil @@ -2360,6 +2378,7 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeWeb, quickJson(shareOpts), screenId) + insertScreenUpdate(tx, screenId, UpdateType_ScreenNew) query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? ORDER BY linenum` tx.Exec(query, UpdateType_LineNew, time.Now().UnixMilli(), screenId) @@ -2381,6 +2400,7 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { tx.Exec(query, ShareModeLocal, "null", screenId) query = `DELETE FROM screenupdate WHERE screenid = ?` tx.Exec(query, screenId) + insertScreenUpdate(tx, screenId, UpdateType_ScreenDel) return nil }) } @@ -2389,7 +2409,16 @@ func isWebShare(tx *TxWrap, screenId string) bool { return tx.Exists(`SELECT screenid FROM screen WHERE screenid = ? AND sharemode = ?`, screenId, ShareModeWeb) } -func insertScreenUpdate(tx *TxWrap, screenId string, lineId string, updateType string) { +func insertScreenUpdate(tx *TxWrap, screenId string, updateType string) { + if screenId == "" { + tx.SetErr(errors.New("invalid screen-update, screenid is empty")) + return + } + query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` + tx.Exec(query, screenId, "", updateType, time.Now().UnixMilli()) +} + +func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateType string) { if screenId == "" { tx.SetErr(errors.New("invalid screen-update, screenid is empty")) return @@ -2402,7 +2431,7 @@ func insertScreenUpdate(tx *TxWrap, screenId string, lineId string, updateType s tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) } -func insertScreenUpdateByCmdId(tx *TxWrap, screenId string, cmdId string, updateType string) { +func insertScreenCmdUpdate(tx *TxWrap, screenId string, cmdId string, updateType string) { if screenId == "" { tx.SetErr(errors.New("invalid screen-update, screenid is empty")) return @@ -2414,6 +2443,51 @@ func insertScreenUpdateByCmdId(tx *TxWrap, screenId string, cmdId string, update query := `SELECT lineid FROM line WHERE screenid = ? AND cmdid = ?` lineId := tx.GetString(query, screenId, cmdId) if lineId != "" { - insertScreenUpdate(tx, screenId, lineId, updateType) + insertScreenLineUpdate(tx, screenId, lineId, updateType) } } + +func RemoveScreenUpdate(ctx context.Context, updateId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `DELETE FROM screenupdate WHERE updateid = ?` + tx.Exec(query, updateId) + return nil + }) +} + +func InsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT lineid FROM line WHERE screenid = ? AND cmdid = ?` + lineId := tx.GetString(query, screenId, cmdId) + if lineId == "" { + return fmt.Errorf("invalid ptypos update, no lineid found for %s/%s", screenId, cmdId) + } + query = `SELECT updateid FROM screenupdate WHERE screenid = ? AND lineid = ? AND updatetype = ?` + if !tx.Exists(query, screenId, lineId, UpdateType_PtyPos) { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_PtyPos) + } + return nil + }) +} + +func GetWebPtyPos(ctx context.Context, screenId string, lineId string) (int64, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int64, error) { + query := `SELECT ptypos FROM webptypos WHERE screenid = ? AND lineid = ?` + ptyPos := tx.GetInt(query, screenId, lineId) + return int64(ptyPos), nil + }) +} + +func SetWebPtyPos(ctx context.Context, screenId string, lineId string, ptyPos int64) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM webptypos WHERE screenid = ? AND lineid = ?` + if tx.Exists(query, screenId, lineId) { + query = `UPDATE webptypos SET ptypos = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, ptyPos, screenId, lineId) + } else { + query = `INSERT INTO webptypos (screenid, lineid, ptypos) VALUES (?, ?, ?)` + tx.Exec(query, screenId, lineId, ptyPos) + } + return nil + }) +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 2585c50d..9361f445 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "log" "os" "path" @@ -60,10 +61,15 @@ func AppendToCmdPtyBlob(ctx context.Context, screenId string, cmdId string, data PtyData64: data64, PtyDataLen: int64(len(data)), } + err = InsertPtyPosUpdate(ctx, screenId, cmdId) + if err != nil { + // just log + log.Printf("error inserting ptypos update %s/%s: %v\n", screenId, cmdId, err) + } return update, nil } -// returns (offset, data, err) +// returns (real-offset, data, err) func ReadFullPtyOutFile(ctx context.Context, screenId string, cmdId string) (int64, []byte, error) { ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { @@ -77,6 +83,20 @@ func ReadFullPtyOutFile(ctx context.Context, screenId string, cmdId string) (int return f.ReadAll(ctx) } +// returns (real-offset, data, err) +func ReadPtyOutFile(ctx context.Context, screenId string, cmdId string, offset int64, maxSize int64) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) + if err != nil { + return 0, nil, err + } + f, err := cirfile.OpenCirFile(ptyOutFileName) + if err != nil { + return 0, nil, err + } + defer f.Close() + return f.ReadAtWithMax(ctx, offset, maxSize) +} + type SessionDiskSizeType struct { NumFiles int TotalSize int64 diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 2eaf8ace..b6f46156 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 15 +const MaxMigration = 16 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 28fb3b84..385d0581 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -80,16 +80,17 @@ const ( ) const ( - UpdateType_ScreenName = "screen:sharename" - UpdateType_ScreenCurRemote = "screen:curremote" - UpdateType_LineNew = "line:new" - UpdateType_LineDel = "line:del" - UpdateType_LineArchived = "line:archived" - UpdateType_LineRenderer = "line:renderer" - UpdateType_CmdStatus = "cmd:status" - UpdateType_CmdDoneInfo = "cmd:doneinfo" - UpdateType_CmdRunOut = "cmd:runout" - UpdateType_CmdRtnState = "cmd:rtnstate" + UpdateType_ScreenNew = "screen:new" + UpdateType_ScreenDel = "screen:del" + UpdateType_ScreenName = "screen:sharename" + UpdateType_LineNew = "line:new" + UpdateType_LineDel = "line:del" + UpdateType_LineArchived = "line:archived" + UpdateType_LineRenderer = "line:renderer" + UpdateType_CmdStatus = "cmd:status" + UpdateType_CmdDoneInfo = "cmd:doneinfo" + UpdateType_CmdRtnState = "cmd:rtnstate" + UpdateType_PtyPos = "pty:pos" ) const MaxTzNameLen = 50 @@ -144,6 +145,11 @@ func CloseDB() { globalDB = nil } +type CmdPtr struct { + ScreenId string + CmdId string +} + type ClientWinSizeType struct { Width int `json:"width"` Height int `json:"height"` @@ -228,11 +234,6 @@ type ClientData struct { func (ClientData) UseDBMap() {} -type CloudAclType struct { - UserId string `json:"userid"` - Role string `json:"role"` -} - type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` @@ -249,46 +250,6 @@ type SessionType struct { Full bool `json:"full,omitempty"` } -type CloudSessionType struct { - SessionId string - ViewKey string - WriteKey string - EncKey string - EncType string - Vts int64 - Acl []*CloudAclType -} - -func (cs *CloudSessionType) ToMap() map[string]any { - m := make(map[string]any) - m["sessionid"] = cs.SessionId - m["viewkey"] = cs.ViewKey - m["writekey"] = cs.WriteKey - m["enckey"] = cs.EncKey - m["enctype"] = cs.EncType - m["vts"] = cs.Vts - m["acl"] = quickJsonArr(cs.Acl) - return m -} - -func (cs *CloudSessionType) FromMap(m map[string]interface{}) bool { - quickSetStr(&cs.SessionId, m, "sessionid") - quickSetStr(&cs.ViewKey, m, "viewkey") - quickSetStr(&cs.WriteKey, m, "writekey") - quickSetStr(&cs.EncKey, m, "enckey") - quickSetStr(&cs.EncType, m, "enctype") - quickSetInt64(&cs.Vts, m, "vts") - quickSetJsonArr(&cs.Acl, m, "acl") - return true -} - -type CloudUpdate struct { - UpdateId string - Ts int64 - UpdateType string - UpdateKeys []string -} - type SessionStatsType struct { SessionId string `json:"sessionid"` NumScreens int `json:"numscreens"`