diff --git a/cmd/main-server.go b/cmd/main-server.go index 378a34e7..0aeeb2e4 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -565,6 +565,10 @@ func main() { go telemetryLoop() go stdinReadWatch() go runWebSocketServer() + go func() { + time.Sleep(10 * time.Second) + pcloud.StartUpdateWriter() + }() go sstore.RunCmdScreenMigration() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index d457fd51..2830feba 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -12,6 +12,8 @@ import ( "os" "strconv" "strings" + "sync" + "time" "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" "github.com/scripthaus-dev/sh2-server/pkg/scbase" @@ -23,12 +25,18 @@ const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 1 const MaxPtyUpdateSize = (128 * 1024) + 1 const MaxUpdatesPerReq = 10 +const MaxUpdateWriterErrors = 3 +const PCloudDefaultTimeout = 5 * time.Second const TelemetryUrl = "/telemetry" const NoTelemetryUrl = "/no-telemetry" const CreateWebScreenUrl = "/auth/create-web-screen" const WebShareUpdateUrl = "/auth/web-share-update" +var updateWriterCVar = sync.NewCond(&sync.Mutex{}) +var updateWriterMoreData = false +var updateWriterRunning = false + type AuthInfo struct { UserId string `json:"userid"` ClientId string `json:"clientid"` @@ -183,6 +191,7 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* rtn := &WebShareUpdateType{ ScreenId: update.ScreenId, LineId: update.LineId, + UpdateId: update.UpdateId, UpdateType: update.UpdateType, } switch update.UpdateType { @@ -219,7 +228,7 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* return nil, fmt.Errorf("error converting line to web-line: %v", err) } if cmd != nil { - rtn.Cmd, err = webCmdFromCmd(cmd) + rtn.Cmd, err = webCmdFromCmd(update.LineId, cmd) if err != nil { return nil, fmt.Errorf("error converting cmd to web-cmd: %v", err) } @@ -249,6 +258,13 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* } rtn.SVal = cmd.Status + case sstore.UpdateType_CmdTermOpts: + _, 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.TermOpts = &cmd.TermOpts + case sstore.UpdateType_CmdDoneInfo: _, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) if err != nil || cmd == nil { @@ -288,24 +304,24 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* return rtn, nil } -func finalizeWebScreenUpdate(ctx context.Context, screenUpdate *sstore.ScreenUpdateType, webUpdate *WebShareUpdateType) error { - switch screenUpdate.UpdateType { +func finalizeWebScreenUpdate(ctx context.Context, webUpdate *WebShareUpdateType) error { + switch webUpdate.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) + err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) if err != nil { return err } } - err := sstore.SetWebPtyPos(ctx, screenUpdate.ScreenId, screenUpdate.LineId, newPos) + err := sstore.SetWebPtyPos(ctx, webUpdate.ScreenId, webUpdate.LineId, newPos) if err != nil { return err } default: - err := sstore.RemoveScreenUpdate(ctx, screenUpdate.UpdateType) + err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) if err != nil { // this is not great, this *should* never fail and is not easy to recover from return err @@ -314,18 +330,27 @@ func finalizeWebScreenUpdate(ctx context.Context, screenUpdate *sstore.ScreenUpd return nil } -func DoWebScreenUpdates(ctx context.Context, authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) error { +func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) error { var webUpdates []*WebShareUpdateType for _, update := range updateArr { - webUpdate, err := makeWebShareUpdate(ctx, update) + webUpdate, err := makeWebShareUpdate(context.Background(), update) if err != nil { - return fmt.Errorf("error create web-share update updateid:%d: %v", update.UpdateId, err) + // log error, remove update, and continue + log.Printf("[pcloud] error create web-share update updateid:%d: %v", update.UpdateId, err) + err = sstore.RemoveScreenUpdate(context.Background(), update.UpdateId) + if err != nil { + // ignore this error too (although this is really problematic, there is nothing to do) + log.Printf("[pcloud] error removing screen update updateid:%d: %v", update.UpdateId, err) + } + continue } if webUpdate == nil { continue } webUpdates = append(webUpdates, webUpdate) } + ctx, cancelFn := context.WithTimeout(context.Background(), PCloudDefaultTimeout) + defer cancelFn() req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, webUpdates) if err != nil { return fmt.Errorf("cannot create auth-post-req for %s: %v", WebShareUpdateUrl, err) @@ -334,6 +359,13 @@ func DoWebScreenUpdates(ctx context.Context, authInfo AuthInfo, updateArr []*sst if err != nil { return err } + for _, update := range webUpdates { + err = finalizeWebScreenUpdate(context.Background(), update) + if err != nil { + // ignore this error (nothing to do) + log.Printf("[pcloud] error finalizing web-update: %v\n", err) + } + } return nil } @@ -353,5 +385,100 @@ func CreateWebScreen(ctx context.Context, screen *WebShareScreenType) error { return nil } +func updateWriterCheckMoreData() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + for { + if updateWriterMoreData { + updateWriterMoreData = false + break + } + updateWriterCVar.Wait() + } +} + +func setUpdateWriterRunning(running bool) { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + updateWriterRunning = running +} + +func GetUpdateWriterRunning() bool { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + return updateWriterRunning +} + +func StartUpdateWriter() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + if updateWriterRunning { + return + } + updateWriterRunning = true + go runWebShareUpdateWriter() +} + +func computeBackoff(numFailures int) time.Duration { + // TODO remove once API implemented + return time.Hour + + switch numFailures { + case 1: + return 100 * time.Millisecond + case 2: + return 1 * time.Second + case 3: + return 5 * time.Second + case 4: + return time.Minute + case 5: + return 5 * time.Minute + case 6: + return time.Hour + default: + return time.Hour + } +} + +func runWebShareUpdateWriter() { + defer func() { + setUpdateWriterRunning(false) + }() + log.Printf("[pcloud] starting update writer\n") + numErrors := 0 + numSendErrors := 0 + for { + updateArr, err := sstore.GetScreenUpdates(context.Background(), MaxUpdatesPerReq) + if err != nil { + log.Printf("[pcloud] error retrieving updates: %v", err) + time.Sleep(1 * time.Second) + numErrors++ + if numErrors > MaxUpdateWriterErrors { + log.Printf("[pcloud] update-writer, too many read errors, exiting\n") + break + } + } + if len(updateArr) == 0 { + updateWriterCheckMoreData() + } + numErrors = 0 + authInfo, err := getAuthInfo(context.Background()) + err = DoWebScreenUpdates(authInfo, updateArr) + if err != nil { + numSendErrors++ + backoffTime := computeBackoff(numSendErrors) + log.Printf("[pcloud] error processing web-updates (backoff=%v): %v\n", backoffTime, err) + time.Sleep(backoffTime) + continue + } + numSendErrors = 0 + } +} + func NotifyUpdateWriter() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + updateWriterMoreData = true + updateWriterCVar.Signal() } diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 694cd527..8cfc3411 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -1,9 +1,12 @@ package pcloud import ( + "context" "fmt" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -22,6 +25,7 @@ type TelemetryInputType struct { type WebShareUpdateType struct { ScreenId string `json:"screenid"` LineId string `json:"lineid"` + UpdateId int64 `json:"-"` // just for internal use UpdateType string `json:"updatetype"` Screen *WebShareScreenType `json:"screen,omitempty"` @@ -31,6 +35,7 @@ type WebShareUpdateType struct { SVal string `json:"sval,omitempty"` BVal bool `json:"bval,omitempty"` DoneInfo *sstore.CmdDoneInfo `json:"doneinfo,omitempty"` + TermOpts *sstore.TermOpts `json:"termopts,omitempty"` } type WebShareRemotePtr struct { @@ -39,6 +44,18 @@ type WebShareRemotePtr struct { Name string `json:"name,omitempty"` } +func webRemoteFromRemotePtr(rptr sstore.RemotePtrType) *WebShareRemotePtr { + if rptr.RemoteId == "" { + return nil + } + rcopy := remote.GetRemoteById(rptr.RemoteId).GetRemoteCopy() + return &WebShareRemotePtr{ + Alias: rcopy.RemoteAlias, + CanonicalName: rcopy.RemoteCanonicalName, + Name: rptr.Name, + } +} + type WebShareScreenType struct { ScreenId string `json:"screenid"` ShareName string `json:"sharename"` @@ -73,14 +90,24 @@ type WebShareLineType struct { } func webLineFromLine(line *sstore.LineType) (*WebShareLineType, error) { - return nil, nil + rtn := &WebShareLineType{ + LineId: line.LineId, + Ts: line.Ts, + LineNum: line.LineNum, + LineType: line.LineType, + Renderer: line.Renderer, + Text: line.Text, + CmdId: line.CmdId, + Archived: line.Archived, + } + return rtn, nil } type WebShareCmdType struct { LineId string `json:"lineid"` CmdStr string `json:"cmdstr"` RawCmdStr string `json:"rawcmdstr"` - Remote WebShareRemotePtr `json:"remote"` + Remote *WebShareRemotePtr `json:"remote"` FeState sstore.FeStateType `json:"festate"` TermOpts sstore.TermOpts `json:"termopts"` Status string `json:"status"` @@ -90,8 +117,27 @@ type WebShareCmdType struct { RtnStateStr string `json:"rtnstatestr,omitempty"` } -func webCmdFromCmd(cmd *sstore.CmdType) (*WebShareCmdType, error) { - return nil, nil +func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) { + rtn := &WebShareCmdType{ + LineId: lineId, + CmdStr: cmd.CmdStr, + RawCmdStr: cmd.RawCmdStr, + Remote: webRemoteFromRemotePtr(cmd.Remote), + FeState: cmd.FeState, + TermOpts: cmd.TermOpts, + Status: cmd.Status, + StartPk: cmd.StartPk, + DoneInfo: cmd.DoneInfo, + RtnState: cmd.RtnState, + } + if cmd.RtnState { + barr, err := rtnstate.GetRtnStateDiff(context.Background(), cmd.ScreenId, cmd.CmdId) + if err != nil { + return nil, fmt.Errorf("error creating rtnstate diff for cmd:%s: %v", cmd.CmdId, err) + } + rtn.RtnStateStr = string(barr) + } + return rtn, nil } type WebSharePtyData struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index bc7b0daa..bb4b4344 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1399,6 +1399,7 @@ func UpdateCmdTermOpts(ctx context.Context, screenId string, cmdId string, termO txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET termopts = ? WHERE screenid = ? AND cmdid = ?` tx.Exec(query, termOpts, screenId, cmdId) + insertScreenCmdUpdate(tx, screenId, cmdId, UpdateType_CmdTermOpts) return nil }) return txErr @@ -2447,7 +2448,16 @@ func insertScreenCmdUpdate(tx *TxWrap, screenId string, cmdId string, updateType } } -func RemoveScreenUpdate(ctx context.Context, updateId string) error { +func GetScreenUpdates(ctx context.Context, maxNum int) ([]*ScreenUpdateType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenUpdateType, error) { + var updates []*ScreenUpdateType + query := `SELECT * FROM screenupdate ORDER BY updateid LIMIT ?` + tx.Select(&updates, query, maxNum) + return updates, nil + }) +} + +func RemoveScreenUpdate(ctx context.Context, updateId int64) error { return WithTx(ctx, func(tx *TxWrap) error { query := `DELETE FROM screenupdate WHERE updateid = ?` tx.Exec(query, updateId) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 385d0581..46c90ed1 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -88,6 +88,7 @@ const ( UpdateType_LineArchived = "line:archived" UpdateType_LineRenderer = "line:renderer" UpdateType_CmdStatus = "cmd:status" + UpdateType_CmdTermOpts = "cmd:termopts" UpdateType_CmdDoneInfo = "cmd:doneinfo" UpdateType_CmdRtnState = "cmd:rtnstate" UpdateType_PtyPos = "pty:pos"