From 22aa999a7b56019b0bab80bfa7dd12dcd341b7df Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 12 Jun 2022 13:39:48 -0700 Subject: [PATCH 001/397] initial checkin of main-server --- cmd/main-server.go | 330 +++++++++++++++++++++++++++++++++++++++++ go.mod | 15 ++ go.sum | 12 ++ pkg/wsshell/wsshell.go | 131 ++++++++++++++++ 4 files changed, 488 insertions(+) create mode 100644 cmd/main-server.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/wsshell/wsshell.go diff --git a/cmd/main-server.go b/cmd/main-server.go new file mode 100644 index 000000000..cdc507d4a --- /dev/null +++ b/cmd/main-server.go @@ -0,0 +1,330 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/gorilla/mux" + + "github.com/scripthaus-dev/sh2-runner/pkg/base" + "github.com/scripthaus-dev/sh2-runner/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/wsshell" +) + +const HttpReadTimeout = 5 * time.Second +const HttpWriteTimeout = 21 * time.Second +const HttpMaxHeaderBytes = 60000 +const HttpTimeoutDuration = 21 * time.Second + +var GlobalRunnerProc *RunnerProc + +type PtyTailWs struct { + Shell *wsshell.WSShell + SessionId string + CmdId string + Position string + Watcher *fsnotify.Watcher +} + +type RunnerProc struct { + Cmd *exec.Cmd + Input *packet.PacketSender + Output chan packet.PacketType +} + +func TailFile(tailWs *PtyTailWs) error { +outer: + for { + select { + case event, ok := <-tailWs.Watcher.Events: + if !ok { + break outer + } + if event.Op&fsnotify.Write == fsnotify.Write { + tailWs.Shell.WriteChan <- []byte("*") + } + + case _, ok := <-tailWs.Watcher.Errors: + if !ok { + break outer + } + + case <-tailWs.Shell.CloseChan: + break outer + } + } + return nil +} + +func HandleWs(w http.ResponseWriter, r *http.Request) { + shell, err := wsshell.StartWS(w, r) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("cannot ugprade websocket: %v", err))) + return + } + defer shell.Conn.Close() + tailWs := &PtyTailWs{ + Shell: shell, + } + tailWs.Watcher, err = fsnotify.NewWatcher() + if err != nil { + fmt.Printf("Error creating watcher: %v\n", err) + return + } + defer tailWs.Watcher.Close() + go func() { + defer shell.Conn.Close() + TailFile(tailWs) + }() + for msg := range shell.ReadChan { + jmsg := map[string]interface{}{} + err = json.Unmarshal(msg, &jmsg) + if err != nil { + fmt.Printf("error unmarshalling ws message: %v\n", err) + break + } + sessionId, ok := jmsg["sessionid"].(string) + if !ok || sessionId == "" { + fmt.Printf("bad ws message, no sessionid\n") + break + } + cmdId, ok := jmsg["cmdid"].(string) + if !ok || cmdId == "" { + fmt.Printf("bad ws message, no cmdId\n") + break + } + if tailWs.SessionId != "" { + fmt.Printf("bad ws message, sessionid already set\n") + break + } + tailWs.SessionId = sessionId + tailWs.CmdId = cmdId + pathStr := GetPtyOutFile(sessionId, cmdId) + err = tailWs.Watcher.Add(pathStr) + if err != nil { + fmt.Printf("error adding watcher: %v\n", err) + break + } + } +} + +func GetPtyOutFile(sessionId string, cmdId string) string { + pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId) + return pathStr +} + +func GetPtyOut(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + cmdId := qvals.Get("cmdid") + if sessionId == "" || cmdId == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid"))) + return + } + pathStr := GetPtyOutFile(sessionId, cmdId) + fd, err := os.Open(pathStr) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err))) + return + } + w.WriteHeader(http.StatusOK) + io.Copy(w, fd) +} + +type runCommandParams struct { + SessionId string `json:"sessionid"` + Command string `json:"command"` +} + +func WriteJsonError(w http.ResponseWriter, errVal error) { + w.WriteHeader(500) + errMap := make(map[string]interface{}) + errMap["error"] = errVal.Error() + barr, _ := json.Marshal(errMap) + w.Write(barr) + return +} + +func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { + rtnMap := make(map[string]interface{}) + rtnMap["success"] = true + if data != nil { + rtnMap["data"] = data + } + barr, err := json.Marshal(rtnMap) + if err != nil { + WriteJsonError(w, err) + return + } + w.WriteHeader(200) + w.Write(barr) + return +} + +func HandleRunCommand(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + if r.Method == "GET" || r.Method == "OPTIONS" { + w.WriteHeader(200) + return + } + decoder := json.NewDecoder(r.Body) + var params runCommandParams + err := decoder.Decode(¶ms) + if err != nil { + WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) + return + } + fmt.Printf("RUN COMMAND sessionid[%s] cmd[%s]\n", params.SessionId, params.Command) + WriteJsonSuccess(w, nil) + return +} + +// /api/start-session +// returns: +// * userid +// * sessionid +// +// /api/ptyout (pos=[position]) - returns contents of ptyout file +// params: +// * sessionid +// * cmdid +// * pos +// returns: +// * stream of ptyout file (text, utf-8) +// +// POST /api/run-command +// params +// * userid +// * sessionid +// returns +// * cmdid +// +// /api/refresh-session +// params +// * sessionid +// * start -- can be negative +// * numlines +// returns +// * permissions (readonly, comment, command) +// * lines +// * lineid +// * ts +// * userid +// * linetype +// * text +// * cmdid + +// /ws +// ->watch-session: +// * sessionid +// ->watch: +// * sessionid +// * cmdid +// ->focus: +// * sessionid +// * cmdid +// ->input: +// * sessionid +// * cmdid +// * data +// ->signal: +// * sessionid +// * cmdid +// * data +// <-data: +// * sessionid +// * cmdid +// * pos +// * data +// <-session-data: +// * sessionid +// * line + +// session-doc +// timestamp | user | cmd-type | data +// cmd-type = comment +// cmd-type = command, commandid=ABC + +// how to know if command is still executing? is command done? + +// local -- .ptyout, .stdin +// remote -- transfer controller program +// controller-startcmd -- start command (with options) => returns cmdid +// controller-watchsession [sessionid] +// transfer [cmdid:pos] pairs. streams back anything new written to ptyout on stdout +// stdin-packet [cmdid:user:data] +// startcmd will figure out the correct +// + +func LaunchRunnerProc() (*RunnerProc, error) { + runnerPath, err := base.GetScRunnerPath() + if err != nil { + return nil, err + } + ecmd := exec.Command(runnerPath) + inputWriter, err := ecmd.StdinPipe() + if err != nil { + return nil, err + } + outputReader, err := ecmd.StdoutPipe() + if err != nil { + return nil, err + } + ecmd.Stderr = nil // /dev/null + ecmd.Start() + rtn := &RunnerProc{Cmd: ecmd} + rtn.Output = packet.PacketParser(outputReader) + rtn.Input = packet.MakePacketSender(inputWriter) + return rtn, nil +} + +func ProcessPackets(runner *RunnerProc) { + for pk := range runner.Output { + fmt.Printf("runner-packet: %v\n", pk) + } +} + +func main() { + runnerProc, err := LaunchRunnerProc() + if err != nil { + fmt.Printf("error launching runner-proc: %v\n", err) + return + } + GlobalRunnerProc = runnerProc + go ProcessPackets(runnerProc) + fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid) + gr := mux.NewRouter() + gr.HandleFunc("/api/ptyout", GetPtyOut) + gr.HandleFunc("/ws", HandleWs) + gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") + server := &http.Server{ + Addr: "localhost:8080", + ReadTimeout: HttpReadTimeout, + WriteTimeout: HttpWriteTimeout, + MaxHeaderBytes: HttpMaxHeaderBytes, + Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"), + } + server.SetKeepAlivesEnabled(false) + fmt.Printf("Running on http://localhost:8080\n") + err = server.ListenAndServe() + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d806aa779 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/scripthaus-dev/sh2-server + +go 1.17 + +require ( + github.com/creack/pty v1.1.18 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + github.com/scripthaus-dev/sh2-runner v0.0.0 +) + +replace "github.com/scripthaus-dev/sh2-runner" v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/sh2-runner/ diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..a1b065d43 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go new file mode 100644 index 000000000..c969b6878 --- /dev/null +++ b/pkg/wsshell/wsshell.go @@ -0,0 +1,131 @@ +package wsshell + +import ( + "encoding/json" + "log" + "net/http" + "net/url" + "sync" + "time" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 4 * 1024, + WriteBufferSize: 4 * 1024, + HandshakeTimeout: 1 * time.Second, + CheckOrigin: func(r *http.Request) bool { return true }, +} + +type WSShell struct { + Conn *websocket.Conn + RemoteAddr string + ConnId string + Query url.Values + OpenTime time.Time + NumPings int + LastPing time.Time + LastRecv time.Time + Header http.Header + + CloseChan chan bool + WriteChan chan []byte + ReadChan chan []byte +} + +func (ws *WSShell) WritePump() { + writeWait := 2 * time.Second + pingPeriod := 2 * time.Second + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + ws.Conn.Close() + }() + for { + select { + case <-ticker.C: + now := time.Now() + pingMessage := map[string]interface{}{"type": "ping", "stime": now.Unix()} + jsonVal, _ := json.Marshal(pingMessage) + _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWait)) // no error + err := ws.Conn.WriteMessage(websocket.TextMessage, jsonVal) + ws.NumPings++ + ws.LastPing = now + if err != nil { + log.Printf("WritePump %s err: %v\n", ws.RemoteAddr, err) + return + } + + case msgBytes := <-ws.WriteChan: + _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWait)) // no error + err := ws.Conn.WriteMessage(websocket.TextMessage, msgBytes) + if err != nil { + log.Printf("WritePump %s err: %v\n", ws.RemoteAddr, err) + return + } + } + } +} + +func (ws *WSShell) ReadPump() { + readWait := 5 * time.Second + defer func() { + ws.Conn.Close() + }() + ws.Conn.SetReadLimit(4096) + ws.Conn.SetReadDeadline(time.Now().Add(readWait)) + for { + _, message, err := ws.Conn.ReadMessage() + if err != nil { + log.Printf("ReadPump %s Err: %v\n", ws.RemoteAddr, err) + break + } + jmsg := map[string]interface{}{} + err = json.Unmarshal(message, &jmsg) + if err != nil { + log.Printf("Error unmarshalling json: %v\n", err) + break + } + ws.Conn.SetReadDeadline(time.Now().Add(readWait)) + ws.LastRecv = time.Now() + if str, ok := jmsg["type"].(string); ok && str == "pong" { + // nothing + continue + } + ws.ReadChan <- message + } + +} + +func StartWS(w http.ResponseWriter, r *http.Request) (*WSShell, error) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return nil, err + } + ws := WSShell{Conn: conn, ConnId: uuid.New().String(), OpenTime: time.Now()} + ws.CloseChan = make(chan bool) + ws.WriteChan = make(chan []byte, 10) + ws.ReadChan = make(chan []byte, 10) + ws.RemoteAddr = r.RemoteAddr + ws.Query = r.URL.Query() + ws.Header = r.Header + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + ws.WritePump() + }() + wg.Add(1) + go func() { + defer wg.Done() + ws.ReadPump() + }() + go func() { + wg.Wait() + close(ws.CloseChan) + close(ws.ReadChan) + }() + return &ws, nil +} From d80517caa06d54575024cc98d6e08a7e472221dd Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jun 2022 11:11:56 -0700 Subject: [PATCH 002/397] run command submitted via run-command --- cmd/main-server.go | 42 +++++++++++++++++++++++++++++++++------- pkg/sstore/sstore.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 pkg/sstore/sstore.go diff --git a/cmd/main-server.go b/cmd/main-server.go index cdc507d4a..4da0259ea 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -7,13 +7,16 @@ import ( "net/http" "os" "os/exec" + "strings" "time" "github.com/fsnotify/fsnotify" + "github.com/google/uuid" "github.com/gorilla/mux" "github.com/scripthaus-dev/sh2-runner/pkg/base" "github.com/scripthaus-dev/sh2-runner/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -144,12 +147,8 @@ func GetPtyOut(w http.ResponseWriter, r *http.Request) { io.Copy(w, fd) } -type runCommandParams struct { - SessionId string `json:"sessionid"` - Command string `json:"command"` -} - func WriteJsonError(w http.ResponseWriter, errVal error) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) errMap := make(map[string]interface{}) errMap["error"] = errVal.Error() @@ -159,6 +158,7 @@ func WriteJsonError(w http.ResponseWriter, errVal error) { } func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") rtnMap := make(map[string]interface{}) rtnMap["success"] = true if data != nil { @@ -174,6 +174,15 @@ func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { return } +type runCommandParams struct { + SessionId string `json:"sessionid"` + Command string `json:"command"` +} + +type runCommandResponse struct { + Line *sstore.LineType `json:"line"` +} + func HandleRunCommand(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") @@ -191,8 +200,27 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) return } - fmt.Printf("RUN COMMAND sessionid[%s] cmd[%s]\n", params.SessionId, params.Command) - WriteJsonSuccess(w, nil) + if _, err = uuid.Parse(params.SessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", params.SessionId, err)) + return + } + commandStr := strings.TrimSpace(params.Command) + if commandStr == "" { + WriteJsonError(w, fmt.Errorf("invalid emtpty command")) + return + } + rtnLine := sstore.MakeNewLineCmd(commandStr) + runPacket := packet.MakeRunPacket() + runPacket.SessionId = params.SessionId + runPacket.CmdId = rtnLine.CmdId + runPacket.ChDir = "" + runPacket.Env = nil + runPacket.Command = commandStr + fmt.Printf("run-packet %v\n", runPacket) + WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) + go func() { + GlobalRunnerProc.Input.SendPacket(runPacket) + }() return } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go new file mode 100644 index 000000000..1234a8626 --- /dev/null +++ b/pkg/sstore/sstore.go @@ -0,0 +1,46 @@ +package sstore + +import ( + "time" + + "github.com/google/uuid" +) + +var NextLineId = 10 + +const LineTypeCmd = "cmd" +const LineTypeText = "text" + +type LineType struct { + LineId int `json:"lineid"` + Ts int64 `json:"ts"` + UserId string `json:"userid"` + LineType string `json:"linetype"` + Text string `json:"text,omitempty"` + CmdId string `json:"cmdid,omitempty"` + CmdText string `json:"cmdtext,omitempty"` + CmdRemote string `json:"cmdremote,omitempty"` +} + +func MakeNewLineCmd(cmdText string) *LineType { + rtn := &LineType{} + rtn.LineId = NextLineId + NextLineId++ + rtn.Ts = time.Now().UnixMilli() + rtn.UserId = "mike" + rtn.LineType = LineTypeCmd + rtn.CmdId = uuid.New().String() + rtn.CmdText = cmdText + return rtn +} + +func MakeNewLineText(text string) *LineType { + rtn := &LineType{} + rtn.LineId = NextLineId + NextLineId++ + rtn.Ts = time.Now().UnixMilli() + rtn.UserId = "mike" + rtn.LineType = LineTypeText + rtn.Text = text + return rtn +} From f8f3ce65fbcd595ac67288cdb604dbdfc1b54849 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Jun 2022 22:39:09 -0700 Subject: [PATCH 003/397] get cmddata sent via websocket connection --- cmd/main-server.go | 134 ++++++++++++++++++++--------------------- pkg/wsshell/wsshell.go | 10 +++ 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 4da0259ea..a145a3465 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -8,9 +8,9 @@ import ( "os" "os/exec" "strings" + "sync" "time" - "github.com/fsnotify/fsnotify" "github.com/google/uuid" "github.com/gorilla/mux" @@ -27,42 +27,29 @@ const HttpTimeoutDuration = 21 * time.Second var GlobalRunnerProc *RunnerProc -type PtyTailWs struct { - Shell *wsshell.WSShell - SessionId string - CmdId string - Position string - Watcher *fsnotify.Watcher +type WsConnType struct { + Id string + Shell *wsshell.WSShell } type RunnerProc struct { - Cmd *exec.Cmd - Input *packet.PacketSender - Output chan packet.PacketType + Lock *sync.Mutex + Cmd *exec.Cmd + Input *packet.PacketSender + Output chan packet.PacketType + WsConnMap map[string]*WsConnType } -func TailFile(tailWs *PtyTailWs) error { -outer: - for { - select { - case event, ok := <-tailWs.Watcher.Events: - if !ok { - break outer - } - if event.Op&fsnotify.Write == fsnotify.Write { - tailWs.Shell.WriteChan <- []byte("*") - } +func (rp *RunnerProc) AddWsConn(ws *WsConnType) { + rp.Lock.Lock() + defer rp.Lock.Unlock() + rp.WsConnMap[ws.Id] = ws +} - case _, ok := <-tailWs.Watcher.Errors: - if !ok { - break outer - } - - case <-tailWs.Shell.CloseChan: - break outer - } - } - return nil +func (rp *RunnerProc) RemoveWsConn(ws *WsConnType) { + rp.Lock.Lock() + defer rp.Lock.Unlock() + delete(rp.WsConnMap, ws.Id) } func HandleWs(w http.ResponseWriter, r *http.Request) { @@ -72,19 +59,11 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("cannot ugprade websocket: %v", err))) return } - defer shell.Conn.Close() - tailWs := &PtyTailWs{ - Shell: shell, - } - tailWs.Watcher, err = fsnotify.NewWatcher() - if err != nil { - fmt.Printf("Error creating watcher: %v\n", err) - return - } - defer tailWs.Watcher.Close() - go func() { - defer shell.Conn.Close() - TailFile(tailWs) + wsConn := &WsConnType{Id: uuid.New().String(), Shell: shell} + GlobalRunnerProc.AddWsConn(wsConn) + defer func() { + GlobalRunnerProc.RemoveWsConn(wsConn) + wsConn.Shell.Conn.Close() }() for msg := range shell.ReadChan { jmsg := map[string]interface{}{} @@ -93,28 +72,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { fmt.Printf("error unmarshalling ws message: %v\n", err) break } - sessionId, ok := jmsg["sessionid"].(string) - if !ok || sessionId == "" { - fmt.Printf("bad ws message, no sessionid\n") - break - } - cmdId, ok := jmsg["cmdid"].(string) - if !ok || cmdId == "" { - fmt.Printf("bad ws message, no cmdId\n") - break - } - if tailWs.SessionId != "" { - fmt.Printf("bad ws message, sessionid already set\n") - break - } - tailWs.SessionId = sessionId - tailWs.CmdId = cmdId - pathStr := GetPtyOutFile(sessionId, cmdId) - err = tailWs.Watcher.Add(pathStr) - if err != nil { - fmt.Printf("error adding watcher: %v\n", err) - break - } + fmt.Printf("got ws message: %v\n", jmsg) } } @@ -220,6 +178,11 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) go func() { GlobalRunnerProc.Input.SendPacket(runPacket) + getPacket := packet.MakeGetCmdPacket() + getPacket.SessionId = runPacket.SessionId + getPacket.CmdId = runPacket.CmdId + getPacket.Tail = true + GlobalRunnerProc.Input.SendPacket(getPacket) }() return } @@ -317,15 +280,48 @@ func LaunchRunnerProc() (*RunnerProc, error) { } ecmd.Stderr = nil // /dev/null ecmd.Start() - rtn := &RunnerProc{Cmd: ecmd} + rtn := &RunnerProc{Lock: &sync.Mutex{}, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} rtn.Output = packet.PacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) return rtn, nil } -func ProcessPackets(runner *RunnerProc) { +func (runner *RunnerProc) ForwardDataPacket(pk *packet.CmdDataPacketType) int { + barr, err := json.Marshal(pk) + if err != nil { + fmt.Printf("cannot marshal cmddata packet %s/%s: %v)\n", pk.SessionId, pk.CmdId, err) + return 0 + } + runner.Lock.Lock() + defer runner.Lock.Unlock() + numSent := 0 + for _, ws := range runner.WsConnMap { + ok := ws.Shell.NonBlockingWrite(barr) + if !ok { + fmt.Printf("write was dropped, no queue space in '%s'\n", ws.Id) + continue + } + numSent++ + } + return numSent +} + +func (runner *RunnerProc) ProcessPackets() { for pk := range runner.Output { + if pk.GetType() == packet.CmdDataPacketStr { + dataPacket := pk.(*packet.CmdDataPacketType) + runner.ForwardDataPacket(dataPacket) + fmt.Printf("cmd-data %s/%s pty=%d run=%d\n", dataPacket.SessionId, dataPacket.CmdId, len(dataPacket.PtyData), len(dataPacket.RunData)) + + continue + } + if pk.GetType() == packet.RunnerInitPacketStr { + initPacket := pk.(*packet.RunnerInitPacketType) + fmt.Printf("runner-init %s\n", initPacket.ScHomeDir) + continue + } fmt.Printf("runner-packet: %v\n", pk) + } } @@ -336,7 +332,7 @@ func main() { return } GlobalRunnerProc = runnerProc - go ProcessPackets(runnerProc) + go runnerProc.ProcessPackets() fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid) gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", GetPtyOut) diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index c969b6878..cd620e3b6 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -35,6 +35,16 @@ type WSShell struct { ReadChan chan []byte } +func (ws *WSShell) NonBlockingWrite(data []byte) bool { + select { + case ws.WriteChan <- data: + return true + + default: + return false + } +} + func (ws *WSShell) WritePump() { writeWait := 2 * time.Second pingPeriod := 2 * time.Second From 0c1cacc20da784f706221fbe7eddb40a2820bcb5 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 15 Jun 2022 16:27:22 -0700 Subject: [PATCH 004/397] capture stderr from runner process. new packet format to distinguish it from real packets. warn when the runner exits --- cmd/main-server.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index a145a3465..6238a24f1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -16,6 +16,7 @@ import ( "github.com/scripthaus-dev/sh2-runner/pkg/base" "github.com/scripthaus-dev/sh2-runner/pkg/packet" + "github.com/scripthaus-dev/sh2-runner/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -38,6 +39,8 @@ type RunnerProc struct { Input *packet.PacketSender Output chan packet.PacketType WsConnMap map[string]*WsConnType + IsLocal bool + DoneCh chan bool } func (rp *RunnerProc) AddWsConn(ws *WsConnType) { @@ -278,11 +281,18 @@ func LaunchRunnerProc() (*RunnerProc, error) { if err != nil { return nil, err } - ecmd.Stderr = nil // /dev/null + ecmd.Stderr = ecmd.Stdout // /dev/null ecmd.Start() - rtn := &RunnerProc{Lock: &sync.Mutex{}, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} + rtn := &RunnerProc{Lock: &sync.Mutex{}, IsLocal: true, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} rtn.Output = packet.PacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) + rtn.DoneCh = make(chan bool) + go func() { + exitErr := ecmd.Wait() + exitCode := shexec.GetExitCode(exitErr) + fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) + close(rtn.DoneCh) + }() return rtn, nil } @@ -312,7 +322,6 @@ func (runner *RunnerProc) ProcessPackets() { dataPacket := pk.(*packet.CmdDataPacketType) runner.ForwardDataPacket(dataPacket) fmt.Printf("cmd-data %s/%s pty=%d run=%d\n", dataPacket.SessionId, dataPacket.CmdId, len(dataPacket.PtyData), len(dataPacket.RunData)) - continue } if pk.GetType() == packet.RunnerInitPacketStr { @@ -320,8 +329,17 @@ func (runner *RunnerProc) ProcessPackets() { fmt.Printf("runner-init %s\n", initPacket.ScHomeDir) continue } + if pk.GetType() == packet.MessagePacketStr { + msgPacket := pk.(*packet.MessagePacketType) + fmt.Printf("# %s\n", msgPacket.Message) + continue + } + if pk.GetType() == packet.RawPacketStr { + rawPacket := pk.(*packet.RawPacketType) + fmt.Printf("stderr> %s\n", rawPacket.Data) + continue + } fmt.Printf("runner-packet: %v\n", pk) - } } From 6f7a883cfd88fd5914db56c44c565d76d698d187 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 15 Jun 2022 18:13:16 -0700 Subject: [PATCH 005/397] updates for websocket ping/pong logic, longer timeouts --- cmd/main-server.go | 47 +++++++++++++++++++++++++++++++----------- pkg/wsshell/wsshell.go | 42 ++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 6238a24f1..d379e3819 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -26,6 +26,9 @@ const HttpWriteTimeout = 21 * time.Second const HttpMaxHeaderBytes = 60000 const HttpTimeoutDuration = 21 * time.Second +const WebSocketServerAddr = "localhost:8081" +const MainServerAddr = "localhost:8080" + var GlobalRunnerProc *RunnerProc type WsConnType struct { @@ -39,7 +42,7 @@ type RunnerProc struct { Input *packet.PacketSender Output chan packet.PacketType WsConnMap map[string]*WsConnType - IsLocal bool + Local bool DoneCh chan bool } @@ -58,8 +61,7 @@ func (rp *RunnerProc) RemoveWsConn(ws *WsConnType) { func HandleWs(w http.ResponseWriter, r *http.Request) { shell, err := wsshell.StartWS(w, r) if err != nil { - w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("cannot ugprade websocket: %v", err))) + fmt.Printf("WebSocket Upgrade Failed %T: %v\n", w, err) return } wsConn := &WsConnType{Id: uuid.New().String(), Shell: shell} @@ -68,6 +70,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { GlobalRunnerProc.RemoveWsConn(wsConn) wsConn.Shell.Conn.Close() }() + fmt.Printf("WebSocket opened %s\n", shell.RemoteAddr) for msg := range shell.ReadChan { jmsg := map[string]interface{}{} err = json.Unmarshal(msg, &jmsg) @@ -181,11 +184,13 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) go func() { GlobalRunnerProc.Input.SendPacket(runPacket) - getPacket := packet.MakeGetCmdPacket() - getPacket.SessionId = runPacket.SessionId - getPacket.CmdId = runPacket.CmdId - getPacket.Tail = true - GlobalRunnerProc.Input.SendPacket(getPacket) + if !GlobalRunnerProc.Local { + getPacket := packet.MakeGetCmdPacket() + getPacket.SessionId = runPacket.SessionId + getPacket.CmdId = runPacket.CmdId + getPacket.Tail = true + GlobalRunnerProc.Input.SendPacket(getPacket) + } }() return } @@ -283,7 +288,7 @@ func LaunchRunnerProc() (*RunnerProc, error) { } ecmd.Stderr = ecmd.Stdout // /dev/null ecmd.Start() - rtn := &RunnerProc{Lock: &sync.Mutex{}, IsLocal: true, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} + rtn := &RunnerProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} rtn.Output = packet.PacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) rtn.DoneCh = make(chan bool) @@ -343,6 +348,24 @@ func (runner *RunnerProc) ProcessPackets() { } } +func runWebSocketServer() { + gr := mux.NewRouter() + gr.HandleFunc("/ws", HandleWs) + server := &http.Server{ + Addr: WebSocketServerAddr, + ReadTimeout: HttpReadTimeout, + WriteTimeout: HttpWriteTimeout, + MaxHeaderBytes: HttpMaxHeaderBytes, + Handler: gr, + } + server.SetKeepAlivesEnabled(false) + fmt.Printf("Running websocket server on %s\n", WebSocketServerAddr) + err := server.ListenAndServe() + if err != nil { + fmt.Printf("[error] trying to run websocket server: %v\n", err) + } +} + func main() { runnerProc, err := LaunchRunnerProc() if err != nil { @@ -352,19 +375,19 @@ func main() { GlobalRunnerProc = runnerProc go runnerProc.ProcessPackets() fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid) + go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", GetPtyOut) - gr.HandleFunc("/ws", HandleWs) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ - Addr: "localhost:8080", + Addr: MainServerAddr, ReadTimeout: HttpReadTimeout, WriteTimeout: HttpWriteTimeout, MaxHeaderBytes: HttpMaxHeaderBytes, Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"), } server.SetKeepAlivesEnabled(false) - fmt.Printf("Running on http://localhost:8080\n") + fmt.Printf("Running main server on %s\n", MainServerAddr) err = server.ListenAndServe() if err != nil { fmt.Printf("ERROR: %v\n", err) diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index cd620e3b6..a3d586987 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -12,6 +12,10 @@ import ( "github.com/gorilla/websocket" ) +const readWaitTimeout = 15 * time.Second +const writeWaitTimeout = 10 * time.Second +const pingPeriodTickTime = 10 * time.Second + var upgrader = websocket.Upgrader{ ReadBufferSize: 4 * 1024, WriteBufferSize: 4 * 1024, @@ -45,31 +49,38 @@ func (ws *WSShell) NonBlockingWrite(data []byte) bool { } } +func (ws *WSShell) WritePing() error { + now := time.Now() + pingMessage := map[string]interface{}{"type": "ping", "stime": now.Unix()} + jsonVal, _ := json.Marshal(pingMessage) + _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWaitTimeout)) // no error + err := ws.Conn.WriteMessage(websocket.TextMessage, jsonVal) + ws.NumPings++ + ws.LastPing = now + if err != nil { + return err + } + return nil +} + func (ws *WSShell) WritePump() { - writeWait := 2 * time.Second - pingPeriod := 2 * time.Second - ticker := time.NewTicker(pingPeriod) + ticker := time.NewTicker(pingPeriodTickTime) defer func() { ticker.Stop() ws.Conn.Close() }() + ws.WritePing() for { select { case <-ticker.C: - now := time.Now() - pingMessage := map[string]interface{}{"type": "ping", "stime": now.Unix()} - jsonVal, _ := json.Marshal(pingMessage) - _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWait)) // no error - err := ws.Conn.WriteMessage(websocket.TextMessage, jsonVal) - ws.NumPings++ - ws.LastPing = now + err := ws.WritePing() if err != nil { log.Printf("WritePump %s err: %v\n", ws.RemoteAddr, err) return } case msgBytes := <-ws.WriteChan: - _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWait)) // no error + _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWaitTimeout)) // no error err := ws.Conn.WriteMessage(websocket.TextMessage, msgBytes) if err != nil { log.Printf("WritePump %s err: %v\n", ws.RemoteAddr, err) @@ -80,7 +91,7 @@ func (ws *WSShell) WritePump() { } func (ws *WSShell) ReadPump() { - readWait := 5 * time.Second + readWait := readWaitTimeout defer func() { ws.Conn.Close() }() @@ -104,6 +115,13 @@ func (ws *WSShell) ReadPump() { // nothing continue } + if str, ok := jmsg["type"].(string); ok && str == "ping" { + now := time.Now() + pongMessage := map[string]interface{}{"type": "pong", "stime": now.Unix()} + jsonVal, _ := json.Marshal(pongMessage) + ws.WriteChan <- jsonVal + continue + } ws.ReadChan <- message } From a5fedb3668f4679c18a8c61acef506e5d36f2069 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Jun 2022 01:09:56 -0700 Subject: [PATCH 006/397] minor changes to ws --- cmd/main-server.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index d379e3819..3bb882758 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -15,6 +15,7 @@ import ( "github.com/gorilla/mux" "github.com/scripthaus-dev/sh2-runner/pkg/base" + "github.com/scripthaus-dev/sh2-runner/pkg/cmdtail" "github.com/scripthaus-dev/sh2-runner/pkg/packet" "github.com/scripthaus-dev/sh2-runner/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -32,8 +33,9 @@ const MainServerAddr = "localhost:8080" var GlobalRunnerProc *RunnerProc type WsConnType struct { - Id string - Shell *wsshell.WSShell + Id string + Shell *wsshell.WSShell + Tailer *cmdtail.Tailer } type RunnerProc struct { @@ -71,14 +73,13 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { wsConn.Shell.Conn.Close() }() fmt.Printf("WebSocket opened %s\n", shell.RemoteAddr) - for msg := range shell.ReadChan { - jmsg := map[string]interface{}{} - err = json.Unmarshal(msg, &jmsg) + for msgBytes := range shell.ReadChan { + pk, err := packet.ParseJsonPacket(msgBytes) if err != nil { fmt.Printf("error unmarshalling ws message: %v\n", err) - break + continue } - fmt.Printf("got ws message: %v\n", jmsg) + fmt.Printf("got ws message: %v\n", pk) } } From 862014bd824a61d9b2318ca6979228eab246f576 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Jun 2022 15:51:41 -0700 Subject: [PATCH 007/397] add windowid --- cmd/main-server.go | 3 ++- pkg/sstore/sstore.go | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 3bb882758..736d35f0c 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -141,6 +141,7 @@ func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { type runCommandParams struct { SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` Command string `json:"command"` } @@ -174,7 +175,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid emtpty command")) return } - rtnLine := sstore.MakeNewLineCmd(commandStr) + rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId, commandStr) runPacket := packet.MakeRunPacket() runPacket.SessionId = params.SessionId runPacket.CmdId = rtnLine.CmdId diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1234a8626..b9a4c417e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -12,6 +12,8 @@ const LineTypeCmd = "cmd" const LineTypeText = "text" type LineType struct { + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` LineId int `json:"lineid"` Ts int64 `json:"ts"` UserId string `json:"userid"` @@ -22,8 +24,10 @@ type LineType struct { CmdRemote string `json:"cmdremote,omitempty"` } -func MakeNewLineCmd(cmdText string) *LineType { +func MakeNewLineCmd(sessionId string, windowId string, cmdText string) *LineType { rtn := &LineType{} + rtn.SessionId = sessionId + rtn.WindowId = windowId rtn.LineId = NextLineId NextLineId++ rtn.Ts = time.Now().UnixMilli() @@ -34,8 +38,10 @@ func MakeNewLineCmd(cmdText string) *LineType { return rtn } -func MakeNewLineText(text string) *LineType { +func MakeNewLineText(sessionId string, windowId string, text string) *LineType { rtn := &LineType{} + rtn.SessionId = sessionId + rtn.WindowId = windowId rtn.LineId = NextLineId NextLineId++ rtn.Ts = time.Now().UnixMilli() From 2a5cde908a4da725544dfa2b51e306f54c3a8bcc Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Jun 2022 22:22:47 -0700 Subject: [PATCH 008/397] got streaming ptyout via websockets working --- cmd/main-server.go | 215 +++++++++++++++++++++++++++++++---------- pkg/wsshell/wsshell.go | 29 +++++- 2 files changed, 191 insertions(+), 53 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 736d35f0c..3dca07eb2 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -2,8 +2,10 @@ package main import ( "encoding/json" + "errors" "fmt" "io" + "io/fs" "net/http" "os" "os/exec" @@ -29,35 +31,137 @@ const HttpTimeoutDuration = 21 * time.Second const WebSocketServerAddr = "localhost:8081" const MainServerAddr = "localhost:8080" +const WSStateReconnectTime = 30 * time.Second +const WSStatePacketChSize = 20 var GlobalRunnerProc *RunnerProc +var GlobalLock = &sync.Mutex{} +var WSStateMap = make(map[string]*WSState) // clientid -> WsState -type WsConnType struct { - Id string - Shell *wsshell.WSShell - Tailer *cmdtail.Tailer +func setWSState(state *WSState) { + GlobalLock.Lock() + defer GlobalLock.Unlock() + WSStateMap[state.ClientId] = state +} + +func getWSState(clientId string) *WSState { + GlobalLock.Lock() + defer GlobalLock.Unlock() + return WSStateMap[clientId] +} + +func removeWSStateAfterTimeout(clientId string, connectTime time.Time, waitDuration time.Duration) { + go func() { + time.Sleep(waitDuration) + GlobalLock.Lock() + defer GlobalLock.Unlock() + state := WSStateMap[clientId] + if state == nil || state.ConnectTime != connectTime { + return + } + delete(WSStateMap, clientId) + err := state.CloseTailer() + if err != nil { + fmt.Printf("[error] closing tailer on ws %v\n", err) + } + }() +} + +type WSState struct { + Lock *sync.Mutex + ClientId string + ConnectTime time.Time + Shell *wsshell.WSShell + Tailer *cmdtail.Tailer + PacketCh chan packet.PacketType +} + +func MakeWSState(clientId string) (*WSState, error) { + var err error + rtn := &WSState{} + rtn.Lock = &sync.Mutex{} + rtn.ClientId = clientId + rtn.ConnectTime = time.Now() + rtn.PacketCh = make(chan packet.PacketType, WSStatePacketChSize) + rtn.Tailer, err = cmdtail.MakeTailer(rtn.PacketCh) + if err != nil { + return nil, err + } + go func() { + defer close(rtn.PacketCh) + rtn.Tailer.Run() + }() + go rtn.runTailerToWS() + return rtn, nil +} + +func (ws *WSState) CloseTailer() error { + return ws.Tailer.Close() +} + +func (ws *WSState) getShell() *wsshell.WSShell { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.Shell +} + +func (ws *WSState) runTailerToWS() { + for pk := range ws.PacketCh { + if pk.GetType() == "cmddata" { + dataPacket := pk.(*packet.CmdDataPacketType) + err := ws.writePacket(dataPacket) + if err != nil { + fmt.Printf("[error] writing packet to ws: %v\n", err) + } + continue + } + fmt.Printf("tailer-to-ws, bad packet %v\n", pk.GetType()) + } +} + +func (ws *WSState) writePacket(pk packet.PacketType) error { + shell := ws.getShell() + if shell == nil || shell.IsClosed() { + return fmt.Errorf("cannot write packet, empty or closed wsshell") + } + err := shell.WriteJson(pk) + if err != nil { + return err + } + return nil +} + +func (ws *WSState) getConnectTime() time.Time { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.ConnectTime +} + +func (ws *WSState) updateConnectTime() { + ws.Lock.Lock() + defer ws.Lock.Unlock() + ws.ConnectTime = time.Now() +} + +func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) { + ws.Lock.Lock() + defer ws.Lock.Unlock() + if ws.Shell == nil { + ws.Shell = shell + return + } + ws.Shell.Conn.Close() + ws.Shell = shell + return } type RunnerProc struct { - Lock *sync.Mutex - Cmd *exec.Cmd - Input *packet.PacketSender - Output chan packet.PacketType - WsConnMap map[string]*WsConnType - Local bool - DoneCh chan bool -} - -func (rp *RunnerProc) AddWsConn(ws *WsConnType) { - rp.Lock.Lock() - defer rp.Lock.Unlock() - rp.WsConnMap[ws.Id] = ws -} - -func (rp *RunnerProc) RemoveWsConn(ws *WsConnType) { - rp.Lock.Lock() - defer rp.Lock.Unlock() - delete(rp.WsConnMap, ws.Id) + Lock *sync.Mutex + Cmd *exec.Cmd + Input *packet.PacketSender + Output chan packet.PacketType + Local bool + DoneCh chan bool } func HandleWs(w http.ResponseWriter, r *http.Request) { @@ -66,20 +170,46 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { fmt.Printf("WebSocket Upgrade Failed %T: %v\n", w, err) return } - wsConn := &WsConnType{Id: uuid.New().String(), Shell: shell} - GlobalRunnerProc.AddWsConn(wsConn) + defer shell.Conn.Close() + clientId := r.URL.Query().Get("clientid") + if clientId == "" { + close(shell.WriteChan) + return + } + state := getWSState(clientId) + if state == nil { + state, err = MakeWSState(clientId) + if err != nil { + fmt.Printf("cannot make wsstate: %v\n", err) + close(shell.WriteChan) + return + } + state.replaceExistingShell(shell) + setWSState(state) + } else { + state.updateConnectTime() + state.replaceExistingShell(shell) + } + stateConnectTime := state.getConnectTime() defer func() { - GlobalRunnerProc.RemoveWsConn(wsConn) - wsConn.Shell.Conn.Close() + removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime) }() - fmt.Printf("WebSocket opened %s\n", shell.RemoteAddr) + shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error + fmt.Printf("WebSocket opened %s %s\n", shell.RemoteAddr, state.ClientId) for msgBytes := range shell.ReadChan { pk, err := packet.ParseJsonPacket(msgBytes) if err != nil { fmt.Printf("error unmarshalling ws message: %v\n", err) continue } - fmt.Printf("got ws message: %v\n", pk) + if pk.GetType() == "getcmd" { + err = state.Tailer.AddWatch(pk.(*packet.GetCmdPacketType)) + if err != nil { + fmt.Printf("error adding watch to tailer: %v\n", err) + } + continue + } + fmt.Printf("got ws bad message: %v\n", pk.GetType()) } } @@ -104,6 +234,10 @@ func GetPtyOut(w http.ResponseWriter, r *http.Request) { pathStr := GetPtyOutFile(sessionId, cmdId) fd, err := os.Open(pathStr) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + w.WriteHeader(http.StatusOK) + return + } w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err))) return @@ -290,7 +424,7 @@ func LaunchRunnerProc() (*RunnerProc, error) { } ecmd.Stderr = ecmd.Stdout // /dev/null ecmd.Start() - rtn := &RunnerProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd, WsConnMap: make(map[string]*WsConnType)} + rtn := &RunnerProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} rtn.Output = packet.PacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) rtn.DoneCh = make(chan bool) @@ -303,31 +437,10 @@ func LaunchRunnerProc() (*RunnerProc, error) { return rtn, nil } -func (runner *RunnerProc) ForwardDataPacket(pk *packet.CmdDataPacketType) int { - barr, err := json.Marshal(pk) - if err != nil { - fmt.Printf("cannot marshal cmddata packet %s/%s: %v)\n", pk.SessionId, pk.CmdId, err) - return 0 - } - runner.Lock.Lock() - defer runner.Lock.Unlock() - numSent := 0 - for _, ws := range runner.WsConnMap { - ok := ws.Shell.NonBlockingWrite(barr) - if !ok { - fmt.Printf("write was dropped, no queue space in '%s'\n", ws.Id) - continue - } - numSent++ - } - return numSent -} - func (runner *RunnerProc) ProcessPackets() { for pk := range runner.Output { if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - runner.ForwardDataPacket(dataPacket) fmt.Printf("cmd-data %s/%s pty=%d run=%d\n", dataPacket.SessionId, dataPacket.CmdId, len(dataPacket.PtyData), len(dataPacket.RunData)) continue } diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index a3d586987..2d9abb935 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -15,6 +15,7 @@ import ( const readWaitTimeout = 15 * time.Second const writeWaitTimeout = 10 * time.Second const pingPeriodTickTime = 10 * time.Second +const initialPingTime = 1 * time.Second var upgrader = websocket.Upgrader{ ReadBufferSize: 4 * 1024, @@ -63,13 +64,25 @@ func (ws *WSShell) WritePing() error { return nil } +func (ws *WSShell) WriteJson(val interface{}) error { + barr, err := json.Marshal(val) + if err != nil { + return err + } + ws.WriteChan <- barr + return nil +} + func (ws *WSShell) WritePump() { ticker := time.NewTicker(pingPeriodTickTime) defer func() { ticker.Stop() ws.Conn.Close() }() - ws.WritePing() + go func() { + time.Sleep(initialPingTime) + ws.WritePing() + }() for { select { case <-ticker.C: @@ -79,7 +92,10 @@ func (ws *WSShell) WritePump() { return } - case msgBytes := <-ws.WriteChan: + case msgBytes, ok := <-ws.WriteChan: + if !ok { + return + } _ = ws.Conn.SetWriteDeadline(time.Now().Add(writeWaitTimeout)) // no error err := ws.Conn.WriteMessage(websocket.TextMessage, msgBytes) if err != nil { @@ -124,7 +140,16 @@ func (ws *WSShell) ReadPump() { } ws.ReadChan <- message } +} +func (ws *WSShell) IsClosed() bool { + select { + case <-ws.CloseChan: + return true + + default: + return false + } } func StartWS(w http.ResponseWriter, r *http.Request) (*WSShell, error) { From 848100247c85933c20b7c806a32dcf6230059249 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Jun 2022 15:30:42 -0700 Subject: [PATCH 009/397] got stdin fifo input working --- cmd/main-server.go | 57 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 3dca07eb2..9a9a6bd27 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -34,6 +34,8 @@ const MainServerAddr = "localhost:8080" const WSStateReconnectTime = 30 * time.Second const WSStatePacketChSize = 20 +const MaxInputDataSize = 1000 + var GlobalRunnerProc *RunnerProc var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*WSState) // clientid -> WsState @@ -205,14 +207,67 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { if pk.GetType() == "getcmd" { err = state.Tailer.AddWatch(pk.(*packet.GetCmdPacketType)) if err != nil { - fmt.Printf("error adding watch to tailer: %v\n", err) + fmt.Printf("[error] adding watch to tailer: %v\n", err) } continue } + if pk.GetType() == "input" { + go func() { + err = sendCmdInput(pk.(*packet.InputPacketType)) + if err != nil { + fmt.Printf("[error] sending command input: %v\n", err) + } + }() + continue + } fmt.Printf("got ws bad message: %v\n", pk.GetType()) } } +// todo: sync multiple writes to the same fifoName into a single go-routine and do liveness checking on fifo +// if this returns an error, likely the fifo is dead and the cmd should be marked as 'done' +func writeToFifo(fifoName string, data []byte) error { + rwfd, err := os.OpenFile(fifoName, os.O_RDWR, 0600) + if err != nil { + return err + } + defer rwfd.Close() + fifoWriter, err := os.OpenFile(fifoName, os.O_WRONLY, 0600) // blocking open (open won't block because of rwfd) + if err != nil { + return err + } + defer fifoWriter.Close() + // this *could* block if the fifo buffer is full + // unlikely because if the reader is dead, and len(data) < pipe size, then the buffer will be empty and will clear after rwfd is closed + _, err = fifoWriter.Write(data) + if err != nil { + return err + } + return nil +} + +func sendCmdInput(pk *packet.InputPacketType) error { + var err error + if _, err = uuid.Parse(pk.SessionId); err != nil { + return fmt.Errorf("invalid sessionid '%s': %w", pk.SessionId, err) + } + if _, err = uuid.Parse(pk.CmdId); err != nil { + return fmt.Errorf("invalid cmdid '%s': %w", pk.CmdId, err) + } + if len(pk.InputData) > MaxInputDataSize { + return fmt.Errorf("input data size too large, len=%d (max=%d)", len(pk.InputData), MaxInputDataSize) + } + fileNames, err := base.GetCommandFileNames(pk.SessionId, pk.CmdId) + if err != nil { + return err + } + err = writeToFifo(fileNames.StdinFifo, []byte(pk.InputData)) + if err != nil { + return err + } + return nil +} + func GetPtyOutFile(sessionId string, cmdId string) string { pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId) return pathStr From 859a1611b89c6fc0ff937219c576edf14ba8bd0e Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Jun 2022 18:11:16 -0700 Subject: [PATCH 010/397] get runnerproc args from init packet --- cmd/main-server.go | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9a9a6bd27..8bc9072a4 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -158,12 +158,30 @@ func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) { } type RunnerProc struct { - Lock *sync.Mutex - Cmd *exec.Cmd - Input *packet.PacketSender - Output chan packet.PacketType - Local bool - DoneCh chan bool + Lock *sync.Mutex + Cmd *exec.Cmd + Input *packet.PacketSender + Output chan packet.PacketType + Local bool + DoneCh chan bool + CurDir string + HomeDir string + User string + Host string + Env []string + Initialized bool +} + +func (r *RunnerProc) GetPrompt() string { + r.Lock.Lock() + defer r.Lock.Unlock() + var curDir = r.CurDir + if r.CurDir == r.HomeDir { + curDir = "~" + } else if strings.HasPrefix(r.CurDir, r.HomeDir+"/") { + curDir = "~/" + r.CurDir[0:len(r.HomeDir)+1] + } + return fmt.Sprintf("[%s@%s %s]", r.User, r.Host, curDir) } func HandleWs(w http.ResponseWriter, r *http.Request) { @@ -501,7 +519,17 @@ func (runner *RunnerProc) ProcessPackets() { } if pk.GetType() == packet.RunnerInitPacketStr { initPacket := pk.(*packet.RunnerInitPacketType) - fmt.Printf("runner-init %s\n", initPacket.ScHomeDir) + fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.ScHomeDir, initPacket.User, initPacket.HomeDir) + runner.Lock.Lock() + runner.Initialized = true + runner.User = initPacket.User + runner.CurDir = initPacket.HomeDir + runner.HomeDir = initPacket.HomeDir + runner.Env = initPacket.Env + if runner.Local { + runner.Host = "local" + } + runner.Lock.Unlock() continue } if pk.GetType() == packet.MessagePacketStr { From 4e18bbe44ef1aa5f0a364cc5bae323aaf005905d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Jun 2022 21:57:23 -0700 Subject: [PATCH 011/397] process comment commands --- cmd/main-server.go | 9 ++++++++- pkg/sstore/sstore.go | 31 +++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 8bc9072a4..473403726 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -382,7 +382,14 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid emtpty command")) return } - rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId, commandStr) + if strings.HasPrefix(commandStr, "/comment ") { + text := strings.TrimSpace(commandStr[9:]) + rtnLine := sstore.MakeNewLineText(params.SessionId, params.WindowId, text) + WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) + return + } + rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) + rtnLine.CmdText = commandStr runPacket := packet.MakeRunPacket() runPacket.SessionId = params.SessionId runPacket.CmdId = rtnLine.CmdId diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b9a4c417e..ddbbc10c8 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -1,16 +1,29 @@ package sstore import ( + "sync" "time" "github.com/google/uuid" ) var NextLineId = 10 +var NextLineLock = &sync.Mutex{} const LineTypeCmd = "cmd" const LineTypeText = "text" +type SessionType struct { + SessionId string `json:"sessionid"` + Remote string `json:"remote"` + Cwd string `json:"cwd"` +} + +type WindowType struct { + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` +} + type LineType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` @@ -22,19 +35,18 @@ type LineType struct { CmdId string `json:"cmdid,omitempty"` CmdText string `json:"cmdtext,omitempty"` CmdRemote string `json:"cmdremote,omitempty"` + CmdCwd string `json:"cmdcwd,omitempty"` } -func MakeNewLineCmd(sessionId string, windowId string, cmdText string) *LineType { +func MakeNewLineCmd(sessionId string, windowId string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId - rtn.LineId = NextLineId - NextLineId++ + rtn.LineId = GetNextLine() rtn.Ts = time.Now().UnixMilli() rtn.UserId = "mike" rtn.LineType = LineTypeCmd rtn.CmdId = uuid.New().String() - rtn.CmdText = cmdText return rtn } @@ -42,11 +54,18 @@ func MakeNewLineText(sessionId string, windowId string, text string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId - rtn.LineId = NextLineId - NextLineId++ + rtn.LineId = GetNextLine() rtn.Ts = time.Now().UnixMilli() rtn.UserId = "mike" rtn.LineType = LineTypeText rtn.Text = text return rtn } + +func GetNextLine() int { + NextLineLock.Lock() + defer NextLineLock.Unlock() + rtn := NextLineId + NextLineId++ + return rtn +} From eb7ac5136eda3cb92b5c163cbfc89ae1d3154ffa Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 23 Jun 2022 10:15:56 -0700 Subject: [PATCH 012/397] updates, switch to mshell package --- cmd/main-server.go | 99 +++++++++++++++++++++++++++++++++++----------- go.mod | 4 +- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 473403726..20d44fbd1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -16,10 +16,10 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/scripthaus-dev/sh2-runner/pkg/base" - "github.com/scripthaus-dev/sh2-runner/pkg/cmdtail" - "github.com/scripthaus-dev/sh2-runner/pkg/packet" - "github.com/scripthaus-dev/sh2-runner/pkg/shexec" + "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/scripthaus-dev/mshell/pkg/cmdtail" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -36,7 +36,7 @@ const WSStatePacketChSize = 20 const MaxInputDataSize = 1000 -var GlobalRunnerProc *RunnerProc +var GlobalMShellProc *MShellProc var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*WSState) // clientid -> WsState @@ -157,7 +157,12 @@ func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) { return } -type RunnerProc struct { +type RpcEntry struct { + PacketId string + RespCh chan packet.RpcPacketType +} + +type MShellProc struct { Lock *sync.Mutex Cmd *exec.Cmd Input *packet.PacketSender @@ -170,9 +175,10 @@ type RunnerProc struct { Host string Env []string Initialized bool + RpcMap map[string]*RpcEntry } -func (r *RunnerProc) GetPrompt() string { +func (r *MShellProc) GetPrompt() string { r.Lock.Lock() defer r.Lock.Unlock() var curDir = r.CurDir @@ -388,24 +394,32 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) return } + if strings.HasPrefix(commandStr, "cd ") { + newDir := strings.TrimSpace(commandStr[3:]) + cdPacket := packet.MakeCdPacket() + cdPacket.PacketId = uuid.New().String() + cdPacket.Dir = newDir + GlobalMShellProc.Input.SendPacket(cdPacket) + return + } rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) rtnLine.CmdText = commandStr runPacket := packet.MakeRunPacket() runPacket.SessionId = params.SessionId runPacket.CmdId = rtnLine.CmdId - runPacket.ChDir = "" + runPacket.Cwd = "" runPacket.Env = nil runPacket.Command = commandStr fmt.Printf("run-packet %v\n", runPacket) WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) go func() { - GlobalRunnerProc.Input.SendPacket(runPacket) - if !GlobalRunnerProc.Local { + GlobalMShellProc.Input.SendPacket(runPacket) + if !GlobalMShellProc.Local { getPacket := packet.MakeGetCmdPacket() getPacket.SessionId = runPacket.SessionId getPacket.CmdId = runPacket.CmdId getPacket.Tail = true - GlobalRunnerProc.Input.SendPacket(getPacket) + GlobalMShellProc.Input.SendPacket(getPacket) } }() return @@ -488,12 +502,9 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // startcmd will figure out the correct // -func LaunchRunnerProc() (*RunnerProc, error) { - runnerPath, err := base.GetScRunnerPath() - if err != nil { - return nil, err - } - ecmd := exec.Command(runnerPath) +func LaunchMShell() (*MShellProc, error) { + msPath := base.GetMShellPath() + ecmd := exec.Command(msPath) inputWriter, err := ecmd.StdinPipe() if err != nil { return nil, err @@ -503,10 +514,14 @@ func LaunchRunnerProc() (*RunnerProc, error) { return nil, err } ecmd.Stderr = ecmd.Stdout // /dev/null - ecmd.Start() - rtn := &RunnerProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} + err = ecmd.Start() + if err != nil { + return nil, err + } + rtn := &MShellProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} rtn.Output = packet.PacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) + rtn.RpcMap = make(map[string]*RpcEntry) rtn.DoneCh = make(chan bool) go func() { exitErr := ecmd.Wait() @@ -517,8 +532,48 @@ func LaunchRunnerProc() (*RunnerProc, error) { return rtn, nil } -func (runner *RunnerProc) ProcessPackets() { +func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcPacketType, error) { + if pk == nil { + return nil, fmt.Errorf("PacketRpc passed nil packet") + } + id := pk.GetPacketId() + respCh := make(chan packet.RpcPacketType) + runner.Lock.Lock() + runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} + runner.Lock.Unlock() + defer func() { + runner.Lock.Lock() + delete(runner.RpcMap, id) + runner.Lock.Unlock() + }() + runner.Input.SendPacket(pk) + timer := time.NewTimer(timeout) + defer timer.Stop() + select { + case rtnPk := <-respCh: + return rtnPk, nil + + case <-timer.C: + return nil, fmt.Errorf("PacketRpc timeout") + } +} + +func (runner *MShellProc) ProcessPackets() { for pk := range runner.Output { + if rpcPk, ok := pk.(packet.RpcPacketType); ok { + rpcId := rpcPk.GetPacketId() + runner.Lock.Lock() + entry := runner.RpcMap[rpcId] + if entry != nil { + delete(runner.RpcMap, rpcId) + go func() { + entry.RespCh <- rpcPk + close(entry.RespCh) + }() + } + runner.Lock.Unlock() + + } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) fmt.Printf("cmd-data %s/%s pty=%d run=%d\n", dataPacket.SessionId, dataPacket.CmdId, len(dataPacket.PtyData), len(dataPacket.RunData)) @@ -572,12 +627,12 @@ func runWebSocketServer() { } func main() { - runnerProc, err := LaunchRunnerProc() + runnerProc, err := LaunchMShell() if err != nil { fmt.Printf("error launching runner-proc: %v\n", err) return } - GlobalRunnerProc = runnerProc + GlobalMShellProc = runnerProc go runnerProc.ProcessPackets() fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid) go runWebSocketServer() diff --git a/go.mod b/go.mod index d806aa779..234a4331f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - github.com/scripthaus-dev/sh2-runner v0.0.0 + github.com/scripthaus-dev/mshell v0.0.0 ) -replace "github.com/scripthaus-dev/sh2-runner" v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/sh2-runner/ +replace "github.com/scripthaus-dev/mshell" v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ From 7340d890897952479fe0b133851ed181c608cb52 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Jun 2022 17:02:19 -0700 Subject: [PATCH 013/397] get sh2-server compiling again --- cmd/main-server.go | 41 ++++++++++++++++++++--------------------- go.mod | 18 +++++++++++------- go.sum | 2 ++ pkg/scbase/scbase.go | 21 +++++++++++++++++++++ pkg/sstore/sstore.go | 8 ++++++++ 5 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 pkg/scbase/scbase.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 20d44fbd1..a177f7e9d 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -85,7 +85,8 @@ func MakeWSState(clientId string) (*WSState, error) { rtn.ClientId = clientId rtn.ConnectTime = time.Now() rtn.PacketCh = make(chan packet.PacketType, WSStatePacketChSize) - rtn.Tailer, err = cmdtail.MakeTailer(rtn.PacketCh) + chSender := packet.MakeChannelPacketSender(rtn.PacketCh) + rtn.Tailer, err = cmdtail.MakeTailer(chSender) if err != nil { return nil, err } @@ -166,7 +167,7 @@ type MShellProc struct { Lock *sync.Mutex Cmd *exec.Cmd Input *packet.PacketSender - Output chan packet.PacketType + Output *packet.PacketParser Local bool DoneCh chan bool CurDir string @@ -271,17 +272,14 @@ func writeToFifo(fifoName string, data []byte) error { } func sendCmdInput(pk *packet.InputPacketType) error { - var err error - if _, err = uuid.Parse(pk.SessionId); err != nil { - return fmt.Errorf("invalid sessionid '%s': %w", pk.SessionId, err) - } - if _, err = uuid.Parse(pk.CmdId); err != nil { - return fmt.Errorf("invalid cmdid '%s': %w", pk.CmdId, err) + err := pk.CK.Validate("input packet") + if err != nil { + return err } if len(pk.InputData) > MaxInputDataSize { return fmt.Errorf("input data size too large, len=%d (max=%d)", len(pk.InputData), MaxInputDataSize) } - fileNames, err := base.GetCommandFileNames(pk.SessionId, pk.CmdId) + fileNames, err := base.GetCommandFileNames(pk.CK) if err != nil { return err } @@ -405,8 +403,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) rtnLine.CmdText = commandStr runPacket := packet.MakeRunPacket() - runPacket.SessionId = params.SessionId - runPacket.CmdId = rtnLine.CmdId + runPacket.CK = base.MakeCommandKey(params.SessionId, rtnLine.CmdId) runPacket.Cwd = "" runPacket.Env = nil runPacket.Command = commandStr @@ -416,8 +413,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { GlobalMShellProc.Input.SendPacket(runPacket) if !GlobalMShellProc.Local { getPacket := packet.MakeGetCmdPacket() - getPacket.SessionId = runPacket.SessionId - getPacket.CmdId = runPacket.CmdId + getPacket.CK = runPacket.CK getPacket.Tail = true GlobalMShellProc.Input.SendPacket(getPacket) } @@ -503,7 +499,10 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // func LaunchMShell() (*MShellProc, error) { - msPath := base.GetMShellPath() + msPath, err := base.GetMShellPath() + if err != nil { + return nil, err + } ecmd := exec.Command(msPath) inputWriter, err := ecmd.StdinPipe() if err != nil { @@ -513,13 +512,13 @@ func LaunchMShell() (*MShellProc, error) { if err != nil { return nil, err } - ecmd.Stderr = ecmd.Stdout // /dev/null + ecmd.Stderr = ecmd.Stdout err = ecmd.Start() if err != nil { return nil, err } rtn := &MShellProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} - rtn.Output = packet.PacketParser(outputReader) + rtn.Output = packet.MakePacketParser(outputReader) rtn.Input = packet.MakePacketSender(inputWriter) rtn.RpcMap = make(map[string]*RpcEntry) rtn.DoneCh = make(chan bool) @@ -559,7 +558,7 @@ func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Durati } func (runner *MShellProc) ProcessPackets() { - for pk := range runner.Output { + for pk := range runner.Output.MainCh { if rpcPk, ok := pk.(packet.RpcPacketType); ok { rpcId := rpcPk.GetPacketId() runner.Lock.Lock() @@ -576,12 +575,12 @@ func (runner *MShellProc) ProcessPackets() { } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - fmt.Printf("cmd-data %s/%s pty=%d run=%d\n", dataPacket.SessionId, dataPacket.CmdId, len(dataPacket.PtyData), len(dataPacket.RunData)) + fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, len(dataPacket.PtyData), len(dataPacket.RunData)) continue } - if pk.GetType() == packet.RunnerInitPacketStr { - initPacket := pk.(*packet.RunnerInitPacketType) - fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.ScHomeDir, initPacket.User, initPacket.HomeDir) + if pk.GetType() == packet.InitPacketStr { + initPacket := pk.(*packet.InitPacketType) + fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.MShellHomeDir, initPacket.User, initPacket.HomeDir) runner.Lock.Lock() runner.Initialized = true runner.User = initPacket.User diff --git a/go.mod b/go.mod index 234a4331f..fb0ee179d 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,17 @@ module github.com/scripthaus-dev/sh2-server go 1.17 require ( - github.com/creack/pty v1.1.18 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 github.com/scripthaus-dev/mshell v0.0.0 ) -replace "github.com/scripthaus-dev/mshell" v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ +require ( + github.com/alessio/shellescape v1.4.1 // indirect + github.com/creack/pty v1.1.18 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect +) + +replace github.com/scripthaus-dev/mshell v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ diff --git a/go.sum b/go.sum index a1b065d43..e0a22d41c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go new file mode 100644 index 000000000..830903b20 --- /dev/null +++ b/pkg/scbase/scbase.go @@ -0,0 +1,21 @@ +package scbase + +import ( + "os" + "path" +) + +const HomeVarName = "HOME" +const ScHomeVarName = "SCRIPTHAUS_HOME" + +func GetScHomeDir() string { + scHome := os.Getenv(ScHomeVarName) + if scHome == "" { + homeVar := os.Getenv(HomeVarName) + if homeVar == "" { + homeVar = "/" + } + scHome = path.Join(homeVar, "scripthaus") + } + return scHome +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index ddbbc10c8..8ed346223 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -1,10 +1,12 @@ package sstore import ( + "path" "sync" "time" "github.com/google/uuid" + "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) var NextLineId = 10 @@ -12,6 +14,12 @@ var NextLineLock = &sync.Mutex{} const LineTypeCmd = "cmd" const LineTypeText = "text" +const DBFileName = "scripthaus.db" + +func GetSessionDBName(sessionId string) string { + scHome := scbase.GetScHomeDir() + return path.Join(scHome, DBFileName) +} type SessionType struct { SessionId string `json:"sessionid"` From 3f01ff44c311055e105a7473e6a51d64b4fcbe5b Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 10:48:14 -0700 Subject: [PATCH 014/397] add migration commands into sh2-server (because migrate cli doesn't ship with sqlite3) --- cmd/main-server.go | 163 +-- db/migrations/000001_init.down.sql | 7 + db/migrations/000001_init.up.sql | 67 ++ go.mod | 6 + go.sum | 1798 ++++++++++++++++++++++++++++ pkg/sstore/migrate.go | 118 ++ pkg/sstore/sstore.go | 67 +- 7 files changed, 2069 insertions(+), 157 deletions(-) create mode 100644 db/migrations/000001_init.down.sql create mode 100644 db/migrations/000001_init.up.sql create mode 100644 pkg/sstore/migrate.go diff --git a/cmd/main-server.go b/cmd/main-server.go index a177f7e9d..9c9164ecd 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -8,7 +8,6 @@ import ( "io/fs" "net/http" "os" - "os/exec" "strings" "sync" "time" @@ -19,7 +18,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/cmdtail" "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -36,7 +35,7 @@ const WSStatePacketChSize = 20 const MaxInputDataSize = 1000 -var GlobalMShellProc *MShellProc +var GlobalMShellProc *remote.MShellProc var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*WSState) // clientid -> WsState @@ -158,39 +157,6 @@ func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) { return } -type RpcEntry struct { - PacketId string - RespCh chan packet.RpcPacketType -} - -type MShellProc struct { - Lock *sync.Mutex - Cmd *exec.Cmd - Input *packet.PacketSender - Output *packet.PacketParser - Local bool - DoneCh chan bool - CurDir string - HomeDir string - User string - Host string - Env []string - Initialized bool - RpcMap map[string]*RpcEntry -} - -func (r *MShellProc) GetPrompt() string { - r.Lock.Lock() - defer r.Lock.Unlock() - var curDir = r.CurDir - if r.CurDir == r.HomeDir { - curDir = "~" - } else if strings.HasPrefix(r.CurDir, r.HomeDir+"/") { - curDir = "~/" + r.CurDir[0:len(r.HomeDir)+1] - } - return fmt.Sprintf("[%s@%s %s]", r.User, r.Host, curDir) -} - func HandleWs(w http.ResponseWriter, r *http.Request) { shell, err := wsshell.StartWS(w, r) if err != nil { @@ -401,7 +367,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { return } rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) - rtnLine.CmdText = commandStr + // rtnLine.CmdText = commandStr runPacket := packet.MakeRunPacket() runPacket.CK = base.MakeCommandKey(params.SessionId, rtnLine.CmdId) runPacket.Cwd = "" @@ -498,115 +464,6 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // startcmd will figure out the correct // -func LaunchMShell() (*MShellProc, error) { - msPath, err := base.GetMShellPath() - if err != nil { - return nil, err - } - ecmd := exec.Command(msPath) - inputWriter, err := ecmd.StdinPipe() - if err != nil { - return nil, err - } - outputReader, err := ecmd.StdoutPipe() - if err != nil { - return nil, err - } - ecmd.Stderr = ecmd.Stdout - err = ecmd.Start() - if err != nil { - return nil, err - } - rtn := &MShellProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} - rtn.Output = packet.MakePacketParser(outputReader) - rtn.Input = packet.MakePacketSender(inputWriter) - rtn.RpcMap = make(map[string]*RpcEntry) - rtn.DoneCh = make(chan bool) - go func() { - exitErr := ecmd.Wait() - exitCode := shexec.GetExitCode(exitErr) - fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) - close(rtn.DoneCh) - }() - return rtn, nil -} - -func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcPacketType, error) { - if pk == nil { - return nil, fmt.Errorf("PacketRpc passed nil packet") - } - id := pk.GetPacketId() - respCh := make(chan packet.RpcPacketType) - runner.Lock.Lock() - runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} - runner.Lock.Unlock() - defer func() { - runner.Lock.Lock() - delete(runner.RpcMap, id) - runner.Lock.Unlock() - }() - runner.Input.SendPacket(pk) - timer := time.NewTimer(timeout) - defer timer.Stop() - select { - case rtnPk := <-respCh: - return rtnPk, nil - - case <-timer.C: - return nil, fmt.Errorf("PacketRpc timeout") - } -} - -func (runner *MShellProc) ProcessPackets() { - for pk := range runner.Output.MainCh { - if rpcPk, ok := pk.(packet.RpcPacketType); ok { - rpcId := rpcPk.GetPacketId() - runner.Lock.Lock() - entry := runner.RpcMap[rpcId] - if entry != nil { - delete(runner.RpcMap, rpcId) - go func() { - entry.RespCh <- rpcPk - close(entry.RespCh) - }() - } - runner.Lock.Unlock() - - } - if pk.GetType() == packet.CmdDataPacketStr { - dataPacket := pk.(*packet.CmdDataPacketType) - fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, len(dataPacket.PtyData), len(dataPacket.RunData)) - continue - } - if pk.GetType() == packet.InitPacketStr { - initPacket := pk.(*packet.InitPacketType) - fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.MShellHomeDir, initPacket.User, initPacket.HomeDir) - runner.Lock.Lock() - runner.Initialized = true - runner.User = initPacket.User - runner.CurDir = initPacket.HomeDir - runner.HomeDir = initPacket.HomeDir - runner.Env = initPacket.Env - if runner.Local { - runner.Host = "local" - } - runner.Lock.Unlock() - continue - } - if pk.GetType() == packet.MessagePacketStr { - msgPacket := pk.(*packet.MessagePacketType) - fmt.Printf("# %s\n", msgPacket.Message) - continue - } - if pk.GetType() == packet.RawPacketStr { - rawPacket := pk.(*packet.RawPacketType) - fmt.Printf("stderr> %s\n", rawPacket.Data) - continue - } - fmt.Printf("runner-packet: %v\n", pk) - } -} - func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) @@ -626,7 +483,19 @@ func runWebSocketServer() { } func main() { - runnerProc, err := LaunchMShell() + if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") { + err := sstore.MigrateCommandOpts(os.Args[1:]) + if err != nil { + fmt.Printf("[error] %v\n", err) + } + return + } + err := sstore.TryMigrateUp() + if err != nil { + fmt.Printf("[error] %v\n", err) + return + } + runnerProc, err := remote.LaunchMShell() if err != nil { fmt.Printf("error launching runner-proc: %v\n", err) return diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql new file mode 100644 index 000000000..b91103179 --- /dev/null +++ b/db/migrations/000001_init.down.sql @@ -0,0 +1,7 @@ +DROP TABLE session; +DROP TABLE window; +DROP TABLE session_remote; +DROP TABLE line; +DROP TABLE remote; +DROP TABLE session_cmd; +DROP TABLE history; diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql new file mode 100644 index 000000000..b015d1b14 --- /dev/null +++ b/db/migrations/000001_init.up.sql @@ -0,0 +1,67 @@ +CREATE TABLE session ( + sessionid varchar(36) PRIMARY KEY, + name varchar(50) NOT NULL +); +CREATE UNIQUE INDEX session_name_unique ON session(name); + +CREATE TABLE window ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + curremote varchar(50) NOT NULL, + version int NOT NULL, + PRIMARY KEY (sessionid, windowid) +); +CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); + +CREATE TABLE session_remote ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + remoteid varchar(36) NOT NULL, + cwd varchar(300) NOT NULL, + PRIMARY KEY (sessionid, windowid, remotename) +); + +CREATE TABLE line ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + lineid int NOT NULL, + userid varchar(36) NOT NULL, + ts bigint NOT NULL, + linetype varchar(10) NOT NULL, + text text NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (sessionid, windowid, lineid) +); + +CREATE TABLE remote ( + remoteid varchar(36) PRIMARY KEY, + remotetype varchar(10) NOT NULL, + remotename varchar(50) NOT NULL, + connectopts varchar(300) NOT NULL +); + +CREATE TABLE session_cmd ( + sessionid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + status varchar(10) NOT NULL, + startts bigint NOT NULL, + pid int NOT NULL, + runnerpid int NOT NULL, + donets bigint NOT NULL, + exitcode int NOT NULL, + ptyout BLOB NOT NULL, + runout BLOB NOT NULL, + PRIMARY KEY (sessionid, cmdid) +); + +CREATE TABLE history ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + userid varchar(36) NOT NULL, + ts int64 NOT NULL, + lineid varchar(36) NOT NULL, + PRIMARY KEY (sessionid, windowid, lineid) +); diff --git a/go.mod b/go.mod index fb0ee179d..511d18ff9 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/scripthaus-dev/sh2-server go 1.17 require ( + github.com/golang-migrate/migrate/v4 v4.15.2 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 + github.com/jmoiron/sqlx v1.3.5 + github.com/mattn/go-sqlite3 v1.14.14 github.com/scripthaus-dev/mshell v0.0.0 ) @@ -13,6 +16,9 @@ require ( github.com/alessio/shellescape v1.4.1 // indirect github.com/creack/pty v1.1.18 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + go.uber.org/atomic v1.7.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect ) diff --git a/go.sum b/go.sum index e0a22d41c..f5a32cd6f 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,1812 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/spanner v1.28.0/go.mod h1:7m6mtQZn/hMbMfx62ct5EWrGND4DNqkXyrmBPRS+OJo= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= +github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= +github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= +github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= +github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= +modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= +modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go new file mode 100644 index 000000000..043222cb0 --- /dev/null +++ b/pkg/sstore/migrate.go @@ -0,0 +1,118 @@ +package sstore + +import ( + "fmt" + "os" + "path" + "strconv" + + _ "github.com/golang-migrate/migrate/v4/database/sqlite3" + _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/mattn/go-sqlite3" + + "github.com/golang-migrate/migrate/v4" +) + +func MakeMigrate() (*migrate.Migrate, error) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + migrationPathUrl := fmt.Sprintf("file://%s", path.Join(wd, "db", "migrations")) + dbUrl := fmt.Sprintf("sqlite3://%s", GetSessionDBName()) + m, err := migrate.New(migrationPathUrl, dbUrl) + if err != nil { + return nil, err + } + return m, nil +} + +func MigrateUp() error { + m, err := MakeMigrate() + if err != nil { + return err + } + err = m.Up() + if err != nil { + return err + } + return nil +} + +func MigrateVersion() (uint, bool, error) { + m, err := MakeMigrate() + if err != nil { + return 0, false, err + } + return m.Version() +} + +func MigrateDown() error { + m, err := MakeMigrate() + if err != nil { + return err + } + err = m.Down() + if err != nil { + return err + } + return nil +} + +func MigrateGoto(n uint) error { + m, err := MakeMigrate() + if err != nil { + return err + } + err = m.Migrate(n) + if err != nil { + return err + } + return nil +} + +func TryMigrateUp() error { + err := MigrateUp() + if err != nil && err.Error() == migrate.ErrNoChange.Error() { + err = nil + } + if err != nil { + return err + } + return MigratePrintVersion() +} + +func MigratePrintVersion() error { + version, dirty, err := MigrateVersion() + if err != nil { + return fmt.Errorf("error getting db version: %v", err) + } + if dirty { + return fmt.Errorf("error db is dirty, version=%d", version) + } + fmt.Printf("[db] version=%d\n", version) + return nil +} + +func MigrateCommandOpts(opts []string) error { + var err error + if opts[0] == "--migrate-up" { + err = MigrateUp() + } else if opts[0] == "--migrate-down" { + err = MigrateDown() + } else if opts[0] == "--migrate-goto" { + n, err := strconv.Atoi(opts[1]) + if err == nil { + err = MigrateGoto(uint(n)) + } + } else { + err = fmt.Errorf("invalid migration command") + } + if err != nil && err.Error() == migrate.ErrNoChange.Error() { + err = nil + } + if err != nil { + return err + } + return MigratePrintVersion() +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 8ed346223..79ea10b4a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -6,7 +6,11 @@ import ( "time" "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/scbase" + + _ "github.com/mattn/go-sqlite3" ) var NextLineId = 10 @@ -14,22 +18,45 @@ var NextLineLock = &sync.Mutex{} const LineTypeCmd = "cmd" const LineTypeText = "text" -const DBFileName = "scripthaus.db" +const DBFileName = "sh2.db" -func GetSessionDBName(sessionId string) string { +func GetSessionDBName() string { scHome := scbase.GetScHomeDir() return path.Join(scHome, DBFileName) } +func OpenConnPool() (*sqlx.DB, error) { + connPool, err := sqlx.Open("sqlite3", GetSessionDBName()) + if err != nil { + return nil, err + } + return connPool, nil +} + type SessionType struct { - SessionId string `json:"sessionid"` - Remote string `json:"remote"` - Cwd string `json:"cwd"` + SessionId string `json:"sessionid"` + Remote string `json:"remote"` + Name string `json:"name"` + Windows []*WindowType `json:"windows"` + Cmds []*CmdType `json:"cmds"` } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + CurRemote string `json:"curremote"` + Remotes []*SessionRemote `json:"remotes"` + Lines []*LineType `json:"lines"` + Version int `json:"version"` +} + +type SessionRemote struct { + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteId string `json"remoteid"` + RemoteName string `json:"name"` + Cwd string `json:"cwd"` } type LineType struct { @@ -41,9 +68,29 @@ type LineType struct { LineType string `json:"linetype"` Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` - CmdText string `json:"cmdtext,omitempty"` - CmdRemote string `json:"cmdremote,omitempty"` - CmdCwd string `json:"cmdcwd,omitempty"` +} + +type RemoteType struct { + RemoteId string `json:"remoteid"` + RemoteType string `json:"remotetype"` + RemoteName string `json:"remotename"` + ConnectOpts string `json:"connectopts"` + Connected bool `json:"connected"` +} + +type CmdType struct { + RowId int64 `json:"rowid"` + SessionId string `json:"sessionid"` + CmdId string `json:"cmdid"` + RemoteId string `json:"remoteid"` + Status string `json:"status"` + StartTs int64 `json:"startts"` + DoneTs int64 `json:"donets"` + Pid int `json:"pid"` + RunnerPid int `json:"runnerpid"` + ExitCode int `json:"exitcode"` + + RunOut packet.PacketType `json:"runout"` } func MakeNewLineCmd(sessionId string, windowId string) *LineType { From 02029b394897a107f26cd6ea434e8f6e6b66f94e Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 10:50:09 -0700 Subject: [PATCH 015/397] add schema --- db/schema.sql | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 db/schema.sql diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 000000000..9f8a7db88 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,63 @@ +CREATE TABLE schema_migrations (version uint64,dirty bool); +CREATE UNIQUE INDEX version_unique ON schema_migrations (version); +CREATE TABLE session ( + sessionid varchar(36) PRIMARY KEY, + name varchar(50) NOT NULL +); +CREATE UNIQUE INDEX session_name_unique ON session(name); +CREATE TABLE window ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + curremote varchar(50) NOT NULL, + version int NOT NULL, + PRIMARY KEY (sessionid, windowid) +); +CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); +CREATE TABLE session_remote ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + remoteid varchar(36) NOT NULL, + cwd varchar(300) NOT NULL, + PRIMARY KEY (sessionid, windowid, remotename) +); +CREATE TABLE line ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + lineid int NOT NULL, + userid varchar(36) NOT NULL, + ts bigint NOT NULL, + linetype varchar(10) NOT NULL, + text text NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (sessionid, windowid, lineid) +); +CREATE TABLE remote ( + remoteid varchar(36) PRIMARY KEY, + remotetype varchar(10) NOT NULL, + remotename varchar(50) NOT NULL, + connectopts varchar(300) NOT NULL +); +CREATE TABLE session_cmd ( + sessionid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + status varchar(10) NOT NULL, + startts bigint NOT NULL, + pid int NOT NULL, + runnerpid int NOT NULL, + donets bigint NOT NULL, + exitcode int NOT NULL, + ptyout BLOB NOT NULL, + runout BLOB NOT NULL, + PRIMARY KEY (sessionid, cmdid) +); +CREATE TABLE history ( + sessionid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + userid varchar(36) NOT NULL, + ts int64 NOT NULL, + lineid varchar(36) NOT NULL, + PRIMARY KEY (sessionid, windowid, lineid) +); From 643f08e58455cfbfc0843be16029deacfa51e975 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 12:17:19 -0700 Subject: [PATCH 016/397] checkpoint, working on setting up db --- cmd/main-server.go | 17 ++++ db/migrations/000001_init.up.sql | 3 +- db/schema.sql | 3 +- pkg/remote/remote.go | 160 +++++++++++++++++++++++++++++++ pkg/scpacket/scpacket.go | 35 +++++++ pkg/sstore/sstore.go | 138 +++++++++++++++++++++++++- 6 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 pkg/remote/remote.go create mode 100644 pkg/scpacket/scpacket.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 9c9164ecd..4e3b7a8e2 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "errors" "fmt" @@ -495,6 +496,22 @@ func main() { fmt.Printf("[error] %v\n", err) return } + numSessions, err := sstore.NumSessions(context.Background()) + if err != nil { + fmt.Printf("[error] getting num sessions: %v\n", err) + return + } + err = sstore.EnsureLocalRemote(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring local remote: %v\n", err) + return + } + fmt.Printf("[db] sessions count=%d\n", numSessions) + if numSessions == 0 { + sstore.CreateInitialSession(context.Background()) + } + return + runnerProc, err := remote.LaunchMShell() if err != nil { fmt.Printf("error launching runner-proc: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index b015d1b14..bfb5d1235 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -39,7 +39,8 @@ CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, - connectopts varchar(300) NOT NULL + connectopts varchar(300) NOT NULL, + ptyout BLOB NOT NULL ); CREATE TABLE session_cmd ( diff --git a/db/schema.sql b/db/schema.sql index 9f8a7db88..d41f17587 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -37,7 +37,8 @@ CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, - connectopts varchar(300) NOT NULL + connectopts varchar(300) NOT NULL, + ptyout BLOB NOT NULL ); CREATE TABLE session_cmd ( sessionid varchar(36) NOT NULL, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go new file mode 100644 index 000000000..23a623d63 --- /dev/null +++ b/pkg/remote/remote.go @@ -0,0 +1,160 @@ +package remote + +import ( + "fmt" + "os/exec" + "strings" + "sync" + "time" + + "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" +) + +const RemoteTypeMShell = "mshell" + +type MShellProc struct { + Lock *sync.Mutex + RemoteId string + WindowId string + RemoteName string + Cmd *exec.Cmd + Input *packet.PacketSender + Output *packet.PacketParser + Local bool + DoneCh chan bool + CurDir string + HomeDir string + User string + Host string + Env []string + Connected bool + RpcMap map[string]*RpcEntry +} + +type RpcEntry struct { + PacketId string + RespCh chan packet.RpcPacketType +} + +func LaunchMShell() (*MShellProc, error) { + msPath, err := base.GetMShellPath() + if err != nil { + return nil, err + } + ecmd := exec.Command(msPath) + inputWriter, err := ecmd.StdinPipe() + if err != nil { + return nil, err + } + outputReader, err := ecmd.StdoutPipe() + if err != nil { + return nil, err + } + ecmd.Stderr = ecmd.Stdout + err = ecmd.Start() + if err != nil { + return nil, err + } + rtn := &MShellProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} + rtn.Output = packet.MakePacketParser(outputReader) + rtn.Input = packet.MakePacketSender(inputWriter) + rtn.RpcMap = make(map[string]*RpcEntry) + rtn.DoneCh = make(chan bool) + go func() { + exitErr := ecmd.Wait() + exitCode := shexec.GetExitCode(exitErr) + fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) + close(rtn.DoneCh) + }() + return rtn, nil +} + +func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcPacketType, error) { + if pk == nil { + return nil, fmt.Errorf("PacketRpc passed nil packet") + } + id := pk.GetPacketId() + respCh := make(chan packet.RpcPacketType) + runner.Lock.Lock() + runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} + runner.Lock.Unlock() + defer func() { + runner.Lock.Lock() + delete(runner.RpcMap, id) + runner.Lock.Unlock() + }() + runner.Input.SendPacket(pk) + timer := time.NewTimer(timeout) + defer timer.Stop() + select { + case rtnPk := <-respCh: + return rtnPk, nil + + case <-timer.C: + return nil, fmt.Errorf("PacketRpc timeout") + } +} + +func (runner *MShellProc) ProcessPackets() { + for pk := range runner.Output.MainCh { + if rpcPk, ok := pk.(packet.RpcPacketType); ok { + rpcId := rpcPk.GetPacketId() + runner.Lock.Lock() + entry := runner.RpcMap[rpcId] + if entry != nil { + delete(runner.RpcMap, rpcId) + go func() { + entry.RespCh <- rpcPk + close(entry.RespCh) + }() + } + runner.Lock.Unlock() + + } + if pk.GetType() == packet.CmdDataPacketStr { + dataPacket := pk.(*packet.CmdDataPacketType) + fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, len(dataPacket.PtyData), len(dataPacket.RunData)) + continue + } + if pk.GetType() == packet.InitPacketStr { + initPacket := pk.(*packet.InitPacketType) + fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.MShellHomeDir, initPacket.User, initPacket.HomeDir) + runner.Lock.Lock() + runner.Connected = true + runner.User = initPacket.User + runner.CurDir = initPacket.HomeDir + runner.HomeDir = initPacket.HomeDir + runner.Env = initPacket.Env + if runner.Local { + runner.Host = "local" + } + runner.Lock.Unlock() + continue + } + if pk.GetType() == packet.MessagePacketStr { + msgPacket := pk.(*packet.MessagePacketType) + fmt.Printf("# %s\n", msgPacket.Message) + continue + } + if pk.GetType() == packet.RawPacketStr { + rawPacket := pk.(*packet.RawPacketType) + fmt.Printf("stderr> %s\n", rawPacket.Data) + continue + } + fmt.Printf("runner-packet: %v\n", pk) + } +} + +func (r *MShellProc) GetPrompt() string { + r.Lock.Lock() + defer r.Lock.Unlock() + var curDir = r.CurDir + if r.CurDir == r.HomeDir { + curDir = "~" + } else if strings.HasPrefix(r.CurDir, r.HomeDir+"/") { + curDir = "~/" + r.CurDir[0:len(r.HomeDir)+1] + } + return fmt.Sprintf("[%s@%s %s]", r.User, r.Host, curDir) +} diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go new file mode 100644 index 000000000..e4d40b636 --- /dev/null +++ b/pkg/scpacket/scpacket.go @@ -0,0 +1,35 @@ +package scpacket + +import ( + "reflect" + + "github.com/scripthaus-dev/mshell/pkg/packet" +) + +const FeCommandPacketStr = "fecmd" + +type RemoteState struct { + RemoteId string `json:"remoteid"` + RemoteName string `json:"remotename"` + Cwd string `json:"cwd"` +} + +type FeCommandPacketType struct { + Type string `json:"type"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + CmdStr string `json:"cmdstr"` + RemoteState RemoteState `json:"remotestate"` +} + +func init() { + packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(&FeCommandPacketType{})) +} + +func (*FeCommandPacketType) GetType() string { + return FeCommandPacketStr +} + +func MakeFeCommandPacket() *FeCommandPacketType { + return &FeCommandPacketType{Type: FeCommandPacketStr} +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 79ea10b4a..6f91fa22e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -1,12 +1,16 @@ package sstore import ( + "context" + "database/sql" + "fmt" "path" "sync" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/scbase" @@ -20,17 +24,26 @@ const LineTypeCmd = "cmd" const LineTypeText = "text" const DBFileName = "sh2.db" +const DefaultSessionName = "default" +const DefaultWindowName = "default" +const LocalRemoteName = "local" + +var globalDBLock = &sync.Mutex{} +var globalDB *sqlx.DB +var globalDBErr error + func GetSessionDBName() string { scHome := scbase.GetScHomeDir() return path.Join(scHome, DBFileName) } -func OpenConnPool() (*sqlx.DB, error) { - connPool, err := sqlx.Open("sqlite3", GetSessionDBName()) - if err != nil { - return nil, err +func GetDB() (*sqlx.DB, error) { + globalDBLock.Lock() + defer globalDBLock.Unlock() + if globalDB == nil && globalDBErr == nil { + globalDB, globalDBErr = sqlx.Open("sqlite3", GetSessionDBName()) } - return connPool, nil + return globalDB, globalDBErr } type SessionType struct { @@ -71,6 +84,7 @@ type LineType struct { } type RemoteType struct { + RowId int64 `json:"rowid"` RemoteId string `json:"remoteid"` RemoteType string `json:"remotetype"` RemoteName string `json:"remotename"` @@ -124,3 +138,117 @@ func GetNextLine() int { NextLineId++ return rtn } + +func NumSessions(ctx context.Context) (int, error) { + db, err := GetDB() + if err != nil { + return 0, err + } + query := "SELECT count(*) FROM session" + var count int + err = db.GetContext(ctx, &count, query) + if err != nil { + return 0, err + } + return count, nil +} + +func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + query := `SELECT rowid, remoteid, remotetype, remotename, connectopts FROM remote WHERE remoteid = ?` + var remote RemoteType + err = db.GetContext(ctx, &remote, query, remoteId) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &remote, nil +} + +func InsertRemote(ctx context.Context, remote *RemoteType) error { + if remote == nil { + return fmt.Errorf("cannot insert nil remote") + } + if remote.RowId != 0 { + return fmt.Errorf("cannot insert a remote that already has rowid set, rowid=%d", remote.RowId) + } + db, err := GetDB() + if err != nil { + return err + } + query := `INSERT INTO remote (remoteid, remotetype, remotename, connectopts, ptyout) VALUES (:remoteid, :remotetype, :remotename, :connectopts, '')` + result, err := db.NamedExec(query, remote) + if err != nil { + return err + } + remote.RowId, err = result.LastInsertId() + if err != nil { + return fmt.Errorf("cannot get lastinsertid from insert remote: %w", err) + } + return nil +} + +func EnsureLocalRemote(ctx context.Context) error { + remoteId, err := base.GetRemoteId() + if err != nil { + return err + } + remote, err := GetRemoteById(ctx, remoteId) + if err != nil { + return err + } + if remote != nil { + return nil + } + // create the local remote + localRemote := &RemoteType{ + RemoteId: remoteId, + RemoteType: "ssh", + RemoteName: LocalRemoteName, + } + err = InsertRemote(ctx, localRemote) + if err != nil { + return err + } + return nil +} + +func CreateInitialSession(ctx context.Context) error { + db, err := GetDB() + if err != nil { + return err + } + session := &SessionType{ + SessionId: uuid.New().String(), + Name: DefaultSessionName, + } + window := &WindowType{ + SessionId: session.SessionId, + WindowId: uuid.New().String(), + Name: DefaultWindowName, + CurRemote: LocalRemoteName, + } + remoteId, err := base.GetRemoteId() + if err != nil { + return err + } + localRemote := &RemoteType{ + RemoteId: remoteId, + RemoteType: "ssh", + RemoteName: LocalRemoteName, + } + sessRemote := &SessionRemote{ + SessionId: session.SessionId, + WindowId: window.WindowId, + RemoteId: remoteId, + RemoteName: localRemote.RemoteName, + Cwd: base.GetHomeDir(), + } + fmt.Printf("db=%v s=%v w=%v r=%v sr=%v\n", db, session, window, localRemote, sessRemote) + return nil +} From b85be3457cc0ef03a25ce14395f86f9b53dfefd4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 14:07:13 -0700 Subject: [PATCH 017/397] checkpoint, working on db calls --- db/migrations/000001_init.up.sql | 4 +- pkg/sstore/dbops.go | 147 +++++++++++++++++++++++++++++++ pkg/sstore/sstore.go | 97 ++++++++------------ pkg/sstore/txwrap.go | 88 ++++++++++++++++++ 4 files changed, 274 insertions(+), 62 deletions(-) create mode 100644 pkg/sstore/dbops.go create mode 100644 pkg/sstore/txwrap.go diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index bfb5d1235..942d3bbfc 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -39,6 +39,8 @@ CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, + hostname varchar(200) NOT NULL, + lastconnectts bigint NOT NULL, connectopts varchar(300) NOT NULL, ptyout BLOB NOT NULL ); @@ -62,7 +64,7 @@ CREATE TABLE history ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, userid varchar(36) NOT NULL, - ts int64 NOT NULL, + ts bigint NOT NULL, lineid varchar(36) NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go new file mode 100644 index 000000000..a09f19db1 --- /dev/null +++ b/pkg/sstore/dbops.go @@ -0,0 +1,147 @@ +package sstore + +import ( + "context" + "database/sql" + "fmt" + + "github.com/google/uuid" +) + +func NumSessions(ctx context.Context) (int, error) { + db, err := GetDB() + if err != nil { + return 0, err + } + query := "SELECT count(*) FROM session" + var count int + err = db.GetContext(ctx, &count, query) + if err != nil { + return 0, err + } + return count, nil +} + +func GetRemoteByName(ctx context.Context, remoteName string) (*RemoteType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + query := `SELECT rowid, remoteid, remotetype, remotename, hostname, connectopts, lastconnectts FROM remote WHERE remotename = ?` + var remote RemoteType + err = db.GetContext(ctx, &remote, query, remoteName) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &remote, nil +} + +func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + query := `SELECT rowid, remoteid, remotetype, remotename, hostname, connectopts, lastconnectts FROM remote WHERE remoteid = ?` + var remote RemoteType + err = db.GetContext(ctx, &remote, query, remoteId) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &remote, nil +} + +func InsertRemote(ctx context.Context, remote *RemoteType) error { + if remote == nil { + return fmt.Errorf("cannot insert nil remote") + } + if remote.RowId != 0 { + return fmt.Errorf("cannot insert a remote that already has rowid set, rowid=%d", remote.RowId) + } + db, err := GetDB() + if err != nil { + return err + } + query := `INSERT INTO remote (remoteid, remotetype, remotename, hostname, connectopts, lastconnectts, ptyout) VALUES (:remoteid, :remotetype, :remotename, :hostname, :connectopts, 0, '')` + result, err := db.NamedExec(query, remote) + if err != nil { + return err + } + remote.RowId, err = result.LastInsertId() + if err != nil { + return fmt.Errorf("cannot get lastinsertid from insert remote: %w", err) + } + return nil +} + +func GetSessionById(ctx context.Context, id string) (*SessionType, error) { + db, err := GetDB() + query := `SELECT * FROM session WHERE sessionid = ?` + var session SessionType + err = db.GetContext(ctx, &session, query, id) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &session, nil +} + +func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { + db, err := GetDB() + query := `SELECT * FROM session WHERE name = ?` + var session SessionType + err = db.GetContext(ctx, &session, query, name) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return &session, nil +} + +// also creates window, and sessionremote +func InsertSessionWithName(ctx context.Context, sessionName string) error { + if sessionName == "" { + return fmt.Errorf("invalid session name '%s'", sessionName) + } + session := &SessionType{ + SessionId: uuid.New().String(), + Name: sessionName, + } + localRemote, err := GetRemoteByName(ctx, LocalRemoteName) + if err != nil { + return err + } + return WithTx(ctx, func(tx *TxWrap) error { + query := `INSERT INTO session (sessionid, name) VALUES (:sessionid, :name)` + tx.NamedExecWrap(query, session) + + window := &WindowType{ + SessionId: session.SessionId, + WindowId: uuid.New().String(), + Name: DefaultWindowName, + CurRemote: LocalRemoteName, + } + query = `INSERT INTO window (sessionid, windowid, name, curremote, version) VALUES (:sessionid, :windowid, :name, :curremote, :version)` + tx.NamedExecWrap(query, window) + + sr := &SessionRemote{ + SessionId: session.SessionId, + WindowId: window.WindowId, + RemoteName: localRemote.RemoteName, + RemoteId: localRemote.RemoteId, + Cwd: DefaultCwd, + } + query = `INSERT INTO session_remote (sessionid, windowid, remotename, remoteid, cwd) VALUES (:sessionid, :windowid, :remotename, :remoteid, :cwd)` + tx.NamedExecWrap(query, sr) + return nil + }) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 6f91fa22e..59313057e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -2,8 +2,9 @@ package sstore import ( "context" - "database/sql" "fmt" + "log" + "os" "path" "sync" "time" @@ -28,6 +29,8 @@ const DefaultSessionName = "default" const DefaultWindowName = "default" const LocalRemoteName = "local" +const DefaultCwd = "~" + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error @@ -84,12 +87,17 @@ type LineType struct { } type RemoteType struct { - RowId int64 `json:"rowid"` - RemoteId string `json:"remoteid"` - RemoteType string `json:"remotetype"` - RemoteName string `json:"remotename"` - ConnectOpts string `json:"connectopts"` - Connected bool `json:"connected"` + RowId int64 `json:"rowid"` + RemoteId string `json:"remoteid"` + RemoteType string `json:"remotetype"` + RemoteName string `json:"remotename"` + HostName string `json:"hostname"` + LastConnectTs int64 `json:"lastconnectts"` + ConnectOpts string `json:"connectopts"` + + // runtime + Connected bool `json:"connected"` + InitPk *packet.InitPacketType `json:"-"` } type CmdType struct { @@ -139,60 +147,6 @@ func GetNextLine() int { return rtn } -func NumSessions(ctx context.Context) (int, error) { - db, err := GetDB() - if err != nil { - return 0, err - } - query := "SELECT count(*) FROM session" - var count int - err = db.GetContext(ctx, &count, query) - if err != nil { - return 0, err - } - return count, nil -} - -func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { - db, err := GetDB() - if err != nil { - return nil, err - } - query := `SELECT rowid, remoteid, remotetype, remotename, connectopts FROM remote WHERE remoteid = ?` - var remote RemoteType - err = db.GetContext(ctx, &remote, query, remoteId) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - return &remote, nil -} - -func InsertRemote(ctx context.Context, remote *RemoteType) error { - if remote == nil { - return fmt.Errorf("cannot insert nil remote") - } - if remote.RowId != 0 { - return fmt.Errorf("cannot insert a remote that already has rowid set, rowid=%d", remote.RowId) - } - db, err := GetDB() - if err != nil { - return err - } - query := `INSERT INTO remote (remoteid, remotetype, remotename, connectopts, ptyout) VALUES (:remoteid, :remotetype, :remotename, :connectopts, '')` - result, err := db.NamedExec(query, remote) - if err != nil { - return err - } - remote.RowId, err = result.LastInsertId() - if err != nil { - return fmt.Errorf("cannot get lastinsertid from insert remote: %w", err) - } - return nil -} - func EnsureLocalRemote(ctx context.Context) error { remoteId, err := base.GetRemoteId() if err != nil { @@ -205,16 +159,37 @@ func EnsureLocalRemote(ctx context.Context) error { if remote != nil { return nil } + hostName, err := os.Hostname() + if err != nil { + return fmt.Errorf("cannot get hostname: %w", err) + } // create the local remote localRemote := &RemoteType{ RemoteId: remoteId, RemoteType: "ssh", RemoteName: LocalRemoteName, + HostName: hostName, } err = InsertRemote(ctx, localRemote) if err != nil { return err } + log.Printf("[db] added remote '%s', id=%s\n", localRemote.RemoteName, localRemote.RemoteId) + return nil +} + +func EnsureDefaultSession(ctx context.Context) error { + session, err := GetSessionByName(ctx, DefaultSessionName) + if err != nil { + return err + } + if session != nil { + return nil + } + err = InsertSessionWithName(ctx, DefaultSessionName) + if err != nil { + return err + } return nil } diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go new file mode 100644 index 000000000..68f05edac --- /dev/null +++ b/pkg/sstore/txwrap.go @@ -0,0 +1,88 @@ +package sstore + +import ( + "context" + "database/sql" + + "github.com/jmoiron/sqlx" +) + +type TxWrap struct { + Txx *sqlx.Tx + Err error +} + +func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { + db, err := GetDB() + if err != nil { + return err + } + tx, beginErr := db.BeginTxx(ctx, nil) + if beginErr != nil { + return beginErr + } + txWrap := &TxWrap{Txx: tx} + defer func() { + if p := recover(); p != nil { + tx.Rollback() + panic(p) + } + if rtnErr != nil { + tx.Rollback() + } else { + rtnErr = tx.Commit() + } + }() + fnErr := fn(txWrap) + if fnErr != nil { + return fnErr + } + if txWrap.Err != nil { + return txWrap.Err + } + return nil +} + +func (tx *TxWrap) NamedExecWrap(query string, arg interface{}) sql.Result { + if tx.Err != nil { + return nil + } + result, err := tx.Txx.NamedExec(query, arg) + if err != nil { + tx.Err = err + } + return result +} + +func (tx *TxWrap) ExecWrap(query string, args ...interface{}) sql.Result { + if tx.Err != nil { + return nil + } + result, err := tx.Txx.Exec(query, args...) + if err != nil { + tx.Err = err + } + return result +} + +func (tx *TxWrap) GetWrap(dest interface{}, query string, args ...interface{}) error { + if tx.Err != nil { + return nil + } + err := tx.Txx.Get(dest, query, args...) + if err != nil && err != sql.ErrNoRows { + tx.Err = err + } + return err +} + +func (tx *TxWrap) SelectWrap(dest interface{}, query string, args ...interface{}) error { + if tx.Err != nil { + return nil + } + err := tx.Txx.Select(dest, query, args...) + if err != nil { + tx.Err = err + } + return err +} From 60199713e84fa08bed1c91709854c5fb174fc72f Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 14:45:33 -0700 Subject: [PATCH 018/397] checkpoint, ensuredefaultsession --- cmd/main-server.go | 13 +++++-------- pkg/sstore/dbops.go | 22 ++++++++++++++-------- pkg/sstore/sstore.go | 25 ++++++++++++------------- pkg/sstore/txwrap.go | 20 ++++++++++++-------- scripthaus.md | 12 ++++++++++++ 5 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 scripthaus.md diff --git a/cmd/main-server.go b/cmd/main-server.go index 4e3b7a8e2..0e484c545 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -496,20 +496,17 @@ func main() { fmt.Printf("[error] %v\n", err) return } - numSessions, err := sstore.NumSessions(context.Background()) - if err != nil { - fmt.Printf("[error] getting num sessions: %v\n", err) - return - } err = sstore.EnsureLocalRemote(context.Background()) if err != nil { fmt.Printf("[error] ensuring local remote: %v\n", err) return } - fmt.Printf("[db] sessions count=%d\n", numSessions) - if numSessions == 0 { - sstore.CreateInitialSession(context.Background()) + defaultSession, err := sstore.EnsureDefaultSession(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring default session: %v\n", err) + return } + fmt.Printf("session: %#v\n", defaultSession) return runnerProc, err := remote.LaunchMShell() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a09f19db1..cae208c62 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -94,17 +94,23 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { } func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { - db, err := GetDB() - query := `SELECT * FROM session WHERE name = ?` - var session SessionType - err = db.GetContext(ctx, &session, query, name) - if err == sql.ErrNoRows { - return nil, nil - } + var rtnSession *SessionType + err := WithTx(ctx, func(tx *TxWrap) error { + var session SessionType + query := `SELECT * FROM session WHERE name = ?` + found := tx.GetWrap(&session, query, name) + if !found { + return nil + } + rtnSession = &session + query = `SELECT sessionid, windowid, name, curremote, version FROM window WHERE sessionid = ?` + tx.SelectWrap(&session.Windows, query, session.SessionId) + return nil + }) if err != nil { return nil, err } - return &session, nil + return rtnSession, nil } // also creates window, and sessionremote diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 59313057e..b6f31dbf6 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -51,20 +51,19 @@ func GetDB() (*sqlx.DB, error) { type SessionType struct { SessionId string `json:"sessionid"` - Remote string `json:"remote"` Name string `json:"name"` Windows []*WindowType `json:"windows"` Cmds []*CmdType `json:"cmds"` } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - CurRemote string `json:"curremote"` - Remotes []*SessionRemote `json:"remotes"` - Lines []*LineType `json:"lines"` - Version int `json:"version"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + CurRemote string `json:"curremote"` + Remotes []*RemoteType `json:"remotes"` + Lines []*LineType `json:"lines"` + Version int `json:"version"` } type SessionRemote struct { @@ -178,19 +177,19 @@ func EnsureLocalRemote(ctx context.Context) error { return nil } -func EnsureDefaultSession(ctx context.Context) error { +func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { session, err := GetSessionByName(ctx, DefaultSessionName) if err != nil { - return err + return nil, err } if session != nil { - return nil + return session, nil } err = InsertSessionWithName(ctx, DefaultSessionName) if err != nil { - return err + return nil, err } - return nil + return GetSessionByName(ctx, DefaultSessionName) } func CreateInitialSession(ctx context.Context) error { diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 68f05edac..b58b8442d 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -65,24 +65,28 @@ func (tx *TxWrap) ExecWrap(query string, args ...interface{}) sql.Result { return result } -func (tx *TxWrap) GetWrap(dest interface{}, query string, args ...interface{}) error { +func (tx *TxWrap) GetWrap(dest interface{}, query string, args ...interface{}) bool { if tx.Err != nil { - return nil + return false } err := tx.Txx.Get(dest, query, args...) - if err != nil && err != sql.ErrNoRows { - tx.Err = err + if err != nil && err == sql.ErrNoRows { + return false } - return err + if err != nil { + tx.Err = err + return false + } + return true } -func (tx *TxWrap) SelectWrap(dest interface{}, query string, args ...interface{}) error { +func (tx *TxWrap) SelectWrap(dest interface{}, query string, args ...interface{}) { if tx.Err != nil { - return nil + return } err := tx.Txx.Select(dest, query, args...) if err != nil { tx.Err = err } - return err + return } diff --git a/scripthaus.md b/scripthaus.md new file mode 100644 index 000000000..08a6c52db --- /dev/null +++ b/scripthaus.md @@ -0,0 +1,12 @@ +# SH2 Server Commands + +```bash +# @scripthaus command dump-schema +sqlite3 /Users/mike/scripthaus/sh2.db .schema > db/schema.sql +``` + +```bash +# @scripthaus command opendb +sqlite3 /Users/mike/scripthaus/sh2.db +``` + From f08bd2427e9eed3d2f32631ebbb43827b7ab76ac Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 14:57:42 -0700 Subject: [PATCH 019/397] working on remote runtime --- pkg/remote/remote.go | 37 +++++++++++++++++++++---------------- pkg/sstore/sstore.go | 4 ---- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 23a623d63..f023190a6 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -14,23 +14,24 @@ import ( const RemoteTypeMShell = "mshell" +type Store struct { + Lock *sync.Mutex + Map map[string]*MShellProc +} + type MShellProc struct { - Lock *sync.Mutex - RemoteId string - WindowId string - RemoteName string - Cmd *exec.Cmd - Input *packet.PacketSender - Output *packet.PacketParser - Local bool - DoneCh chan bool - CurDir string - HomeDir string - User string - Host string - Env []string - Connected bool - RpcMap map[string]*RpcEntry + Lock *sync.Mutex + Remote *RemoteType + + // runtime + Connected bool + InitPk *packet.InitPacketType + Cmd *exec.Cmd + Input *packet.PacketSender + Output *packet.PacketParser + Local bool + DoneCh chan bool + RpcMap map[string]*RpcEntry } type RpcEntry struct { @@ -38,6 +39,10 @@ type RpcEntry struct { RespCh chan packet.RpcPacketType } +func LoadRemotes() { + +} + func LaunchMShell() (*MShellProc, error) { msPath, err := base.GetMShellPath() if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b6f31dbf6..fa6c30643 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -93,10 +93,6 @@ type RemoteType struct { HostName string `json:"hostname"` LastConnectTs int64 `json:"lastconnectts"` ConnectOpts string `json:"connectopts"` - - // runtime - Connected bool `json:"connected"` - InitPk *packet.InitPacketType `json:"-"` } type CmdType struct { From e9a09d071ee371f6b7ffbfcb677b9a16f90948f7 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Jul 2022 17:38:36 -0700 Subject: [PATCH 020/397] checkpoint, compiling with mshell --server --- cmd/main-server.go | 27 ++--- db/migrations/000001_init.up.sql | 11 +- pkg/remote/remote.go | 191 +++++++++++++++++++++---------- pkg/sstore/dbops.go | 23 +++- pkg/sstore/sstore.go | 70 +++-------- scripthaus.md | 4 + 6 files changed, 191 insertions(+), 135 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 0e484c545..b2b2cdef1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -36,7 +36,6 @@ const WSStatePacketChSize = 20 const MaxInputDataSize = 1000 -var GlobalMShellProc *remote.MShellProc var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*WSState) // clientid -> WsState @@ -364,7 +363,11 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { cdPacket := packet.MakeCdPacket() cdPacket.PacketId = uuid.New().String() cdPacket.Dir = newDir - GlobalMShellProc.Input.SendPacket(cdPacket) + localRemote := remote.GetRemote("local") + if localRemote != nil { + localRemote.Input.SendPacket(cdPacket) + return + } return } rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) @@ -377,12 +380,9 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { fmt.Printf("run-packet %v\n", runPacket) WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) go func() { - GlobalMShellProc.Input.SendPacket(runPacket) - if !GlobalMShellProc.Local { - getPacket := packet.MakeGetCmdPacket() - getPacket.CK = runPacket.CK - getPacket.Tail = true - GlobalMShellProc.Input.SendPacket(getPacket) + localRemote := remote.GetRemote("local") + if localRemote != nil { + localRemote.Input.SendPacket(runPacket) } }() return @@ -506,17 +506,12 @@ func main() { fmt.Printf("[error] ensuring default session: %v\n", err) return } - fmt.Printf("session: %#v\n", defaultSession) - return - - runnerProc, err := remote.LaunchMShell() + fmt.Printf("session: %v\n", defaultSession) + err = remote.LoadRemotes(context.Background()) if err != nil { - fmt.Printf("error launching runner-proc: %v\n", err) + fmt.Printf("[error] loading remotes: %v\n", err) return } - GlobalMShellProc = runnerProc - go runnerProc.ProcessPackets() - fmt.Printf("Started local runner pid[%d]\n", runnerProc.Cmd.Process.Pid) go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", GetPtyOut) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 942d3bbfc..622a3b6f6 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -39,9 +39,16 @@ CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, - hostname varchar(200) NOT NULL, + autoconnect boolean NOT NULL, + + -- ssh specific opts + sshhost varchar(300) NOT NULL, + sshopts varchar(300) NOT NULL, + sshidentity varchar(300) NOT NULL, + sshuser varchar(100) NOT NULL, + + -- runtime data lastconnectts bigint NOT NULL, - connectopts varchar(300) NOT NULL, ptyout BLOB NOT NULL ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index f023190a6..5123e5ff0 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1,19 +1,32 @@ package remote import ( + "context" "fmt" + "io" + "os" "os/exec" - "strings" "sync" "time" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) const RemoteTypeMShell = "mshell" +const ( + StatusInit = "init" + StatusConnecting = "connecting" + StatusConnected = "connected" + StatusDisconnected = "disconnected" + StatusError = "error" +) + +var GlobalStore *Store + type Store struct { Lock *sync.Mutex Map map[string]*MShellProc @@ -21,17 +34,18 @@ type Store struct { type MShellProc struct { Lock *sync.Mutex - Remote *RemoteType + Remote *sstore.RemoteType // runtime - Connected bool - InitPk *packet.InitPacketType - Cmd *exec.Cmd - Input *packet.PacketSender - Output *packet.PacketParser - Local bool - DoneCh chan bool - RpcMap map[string]*RpcEntry + Status string + InitPk *packet.InitPacketType + Cmd *exec.Cmd + Input *packet.PacketSender + Output *packet.PacketParser + DoneCh chan bool + RpcMap map[string]*RpcEntry + + Err error } type RpcEntry struct { @@ -39,57 +53,117 @@ type RpcEntry struct { RespCh chan packet.RpcPacketType } -func LoadRemotes() { - +func LoadRemotes(ctx context.Context) error { + GlobalStore = &Store{ + Lock: &sync.Mutex{}, + Map: make(map[string]*MShellProc), + } + allRemotes, err := sstore.GetAllRemotes(ctx) + if err != nil { + return err + } + for _, remote := range allRemotes { + msh := MakeMShell(remote) + GlobalStore.Map[remote.RemoteName] = msh + if remote.AutoConnect { + go msh.Launch() + } + } + return nil } -func LaunchMShell() (*MShellProc, error) { +func GetRemote(name string) *MShellProc { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + return GlobalStore.Map[name] +} + +func MakeMShell(r *sstore.RemoteType) *MShellProc { + rtn := &MShellProc{Lock: &sync.Mutex{}, Remote: r, Status: StatusInit} + return rtn +} + +func (msh *MShellProc) Launch() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msPath, err := base.GetMShellPath() if err != nil { - return nil, err + msh.Status = StatusError + msh.Err = err + return } - ecmd := exec.Command(msPath) + ecmd := exec.Command(msPath, "--server") + msh.Cmd = ecmd inputWriter, err := ecmd.StdinPipe() if err != nil { - return nil, err + msh.Status = StatusError + msh.Err = fmt.Errorf("create stdin pipe: %w", err) + return } - outputReader, err := ecmd.StdoutPipe() + stdoutReader, err := ecmd.StdoutPipe() if err != nil { - return nil, err + msh.Status = StatusError + msh.Err = fmt.Errorf("create stdout pipe: %w", err) + return } - ecmd.Stderr = ecmd.Stdout + stderrReader, err := ecmd.StderrPipe() + if err != nil { + msh.Status = StatusError + msh.Err = fmt.Errorf("create stderr pipe: %w", err) + return + } + go func() { + io.Copy(os.Stderr, stderrReader) + }() err = ecmd.Start() if err != nil { - return nil, err + msh.Status = StatusError + msh.Err = fmt.Errorf("starting mshell server: %w", err) + return } - rtn := &MShellProc{Lock: &sync.Mutex{}, Local: true, Cmd: ecmd} - rtn.Output = packet.MakePacketParser(outputReader) - rtn.Input = packet.MakePacketSender(inputWriter) - rtn.RpcMap = make(map[string]*RpcEntry) - rtn.DoneCh = make(chan bool) + fmt.Printf("Started remote '%s' pid=%d\n", msh.Remote.RemoteName, msh.Cmd.Process.Pid) + msh.Status = StatusConnecting + msh.Output = packet.MakePacketParser(stdoutReader) + msh.Input = packet.MakePacketSender(inputWriter) + msh.RpcMap = make(map[string]*RpcEntry) + msh.DoneCh = make(chan bool) go func() { exitErr := ecmd.Wait() exitCode := shexec.GetExitCode(exitErr) + msh.WithLock(func() { + if msh.Status == StatusConnected || msh.Status == StatusConnecting { + msh.Status = StatusDisconnected + } + }) fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) - close(rtn.DoneCh) + close(msh.DoneCh) }() - return rtn, nil + go msh.ProcessPackets() + return +} + +func (msh *MShellProc) IsConnected() bool { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Status == StatusConnected } func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcPacketType, error) { + if !runner.IsConnected() { + return nil, fmt.Errorf("runner is not connected") + } if pk == nil { return nil, fmt.Errorf("PacketRpc passed nil packet") } id := pk.GetPacketId() respCh := make(chan packet.RpcPacketType) - runner.Lock.Lock() - runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} - runner.Lock.Unlock() - defer func() { - runner.Lock.Lock() + runner.WithLock(func() { + runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} + }) + defer runner.WithLock(func() { delete(runner.RpcMap, id) - runner.Lock.Unlock() - }() + }) runner.Input.SendPacket(pk) timer := time.NewTimer(timeout) defer timer.Stop() @@ -102,21 +176,32 @@ func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Durati } } +func (runner *MShellProc) WithLock(fn func()) { + runner.Lock.Lock() + defer runner.Lock.Unlock() + fn() +} + func (runner *MShellProc) ProcessPackets() { + defer runner.WithLock(func() { + if runner.Status == StatusConnected || runner.Status == StatusConnecting { + runner.Status = StatusDisconnected + } + }) for pk := range runner.Output.MainCh { if rpcPk, ok := pk.(packet.RpcPacketType); ok { rpcId := rpcPk.GetPacketId() - runner.Lock.Lock() - entry := runner.RpcMap[rpcId] - if entry != nil { + runner.WithLock(func() { + entry := runner.RpcMap[rpcId] + if entry == nil { + return + } delete(runner.RpcMap, rpcId) go func() { entry.RespCh <- rpcPk close(entry.RespCh) }() - } - runner.Lock.Unlock() - + }) } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) @@ -126,16 +211,10 @@ func (runner *MShellProc) ProcessPackets() { if pk.GetType() == packet.InitPacketStr { initPacket := pk.(*packet.InitPacketType) fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.MShellHomeDir, initPacket.User, initPacket.HomeDir) - runner.Lock.Lock() - runner.Connected = true - runner.User = initPacket.User - runner.CurDir = initPacket.HomeDir - runner.HomeDir = initPacket.HomeDir - runner.Env = initPacket.Env - if runner.Local { - runner.Host = "local" - } - runner.Lock.Unlock() + runner.WithLock(func() { + runner.InitPk = initPacket + runner.Status = StatusConnected + }) continue } if pk.GetType() == packet.MessagePacketStr { @@ -151,15 +230,3 @@ func (runner *MShellProc) ProcessPackets() { fmt.Printf("runner-packet: %v\n", pk) } } - -func (r *MShellProc) GetPrompt() string { - r.Lock.Lock() - defer r.Lock.Unlock() - var curDir = r.CurDir - if r.CurDir == r.HomeDir { - curDir = "~" - } else if strings.HasPrefix(r.CurDir, r.HomeDir+"/") { - curDir = "~/" + r.CurDir[0:len(r.HomeDir)+1] - } - return fmt.Sprintf("[%s@%s %s]", r.User, r.Host, curDir) -} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index cae208c62..1bb1767e2 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -22,12 +22,28 @@ func NumSessions(ctx context.Context) (int, error) { return count, nil } +const remoteSelectCols = "rowid, remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts" + +func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + query := fmt.Sprintf(`SELECT %s FROM remote`, remoteSelectCols) + var remoteArr []*RemoteType + err = db.SelectContext(ctx, &remoteArr, query) + if err != nil { + return nil, err + } + return remoteArr, nil +} + func GetRemoteByName(ctx context.Context, remoteName string) (*RemoteType, error) { db, err := GetDB() if err != nil { return nil, err } - query := `SELECT rowid, remoteid, remotetype, remotename, hostname, connectopts, lastconnectts FROM remote WHERE remotename = ?` + query := fmt.Sprintf(`SELECT %s FROM remote WHERE remotename = ?`, remoteSelectCols) var remote RemoteType err = db.GetContext(ctx, &remote, query, remoteName) if err == sql.ErrNoRows { @@ -44,7 +60,7 @@ func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { if err != nil { return nil, err } - query := `SELECT rowid, remoteid, remotetype, remotename, hostname, connectopts, lastconnectts FROM remote WHERE remoteid = ?` + query := fmt.Sprintf(`SELECT %s FROM remote WHERE remoteid = ?`, remoteSelectCols) var remote RemoteType err = db.GetContext(ctx, &remote, query, remoteId) if err == sql.ErrNoRows { @@ -67,7 +83,8 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote (remoteid, remotetype, remotename, hostname, connectopts, lastconnectts, ptyout) VALUES (:remoteid, :remotetype, :remotename, :hostname, :connectopts, 0, '')` + query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts, ptyout) VALUES + (:remoteid,:remotetype,:remotename,:autoconnect,:sshhost,:sshopts,:sshidentity,:sshuser, 0 , '')` result, err := db.NamedExec(query, remote) if err != nil { return err diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index fa6c30643..19aa0c326 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -2,9 +2,7 @@ package sstore import ( "context" - "fmt" "log" - "os" "path" "sync" "time" @@ -86,13 +84,20 @@ type LineType struct { } type RemoteType struct { - RowId int64 `json:"rowid"` - RemoteId string `json:"remoteid"` - RemoteType string `json:"remotetype"` - RemoteName string `json:"remotename"` - HostName string `json:"hostname"` - LastConnectTs int64 `json:"lastconnectts"` - ConnectOpts string `json:"connectopts"` + RowId int64 `json:"rowid"` + RemoteId string `json:"remoteid"` + RemoteType string `json:"remotetype"` + RemoteName string `json:"remotename"` + AutoConnect bool `json:"autoconnect"` + + // type=ssh options + SSHHost string `json:"sshhost"` + SSHOpts string `json:"sshopts"` + SSHIdentity string `json:"sshidentity"` + SSHUser string `json:"sshuser"` + + // runtime data + LastConnectTs int64 `json:"lastconnectts"` } type CmdType struct { @@ -154,16 +159,12 @@ func EnsureLocalRemote(ctx context.Context) error { if remote != nil { return nil } - hostName, err := os.Hostname() - if err != nil { - return fmt.Errorf("cannot get hostname: %w", err) - } // create the local remote localRemote := &RemoteType{ - RemoteId: remoteId, - RemoteType: "ssh", - RemoteName: LocalRemoteName, - HostName: hostName, + RemoteId: remoteId, + RemoteType: "ssh", + RemoteName: LocalRemoteName, + AutoConnect: true, } err = InsertRemote(ctx, localRemote) if err != nil { @@ -187,38 +188,3 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { } return GetSessionByName(ctx, DefaultSessionName) } - -func CreateInitialSession(ctx context.Context) error { - db, err := GetDB() - if err != nil { - return err - } - session := &SessionType{ - SessionId: uuid.New().String(), - Name: DefaultSessionName, - } - window := &WindowType{ - SessionId: session.SessionId, - WindowId: uuid.New().String(), - Name: DefaultWindowName, - CurRemote: LocalRemoteName, - } - remoteId, err := base.GetRemoteId() - if err != nil { - return err - } - localRemote := &RemoteType{ - RemoteId: remoteId, - RemoteType: "ssh", - RemoteName: LocalRemoteName, - } - sessRemote := &SessionRemote{ - SessionId: session.SessionId, - WindowId: window.WindowId, - RemoteId: remoteId, - RemoteName: localRemote.RemoteName, - Cwd: base.GetHomeDir(), - } - fmt.Printf("db=%v s=%v w=%v r=%v sr=%v\n", db, session, window, localRemote, sessRemote) - return nil -} diff --git a/scripthaus.md b/scripthaus.md index 08a6c52db..6ae418626 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -10,3 +10,7 @@ sqlite3 /Users/mike/scripthaus/sh2.db .schema > db/schema.sql sqlite3 /Users/mike/scripthaus/sh2.db ``` +```bash +# @scripthaus command build +go build -o server cmd/main-server.go +``` From 17172b158c8e09ddb0448f532c751be64ccc3b71 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 2 Jul 2022 13:31:56 -0700 Subject: [PATCH 021/397] checkpoint --- cmd/main-server.go | 72 +++++++++++++++++++++++++++++++++++++++----- pkg/sstore/dbops.go | 39 ++++++++++++++++++++++++ pkg/sstore/sstore.go | 14 ++++----- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index b2b2cdef1..371f0bde8 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -20,6 +20,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/cmdtail" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -256,6 +257,53 @@ func sendCmdInput(pk *packet.InputPacketType) error { return nil } +// params: name +func GetSession(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + name := qvals.Get("name") + if name == "" { + WriteJsonError(w, fmt.Errorf("must specify a name")) + return + } + session, err := sstore.GetSessionByName(r.Context(), name) + if err != nil { + WriteJsonError(w, err) + return + } + WriteJsonSuccess(w, session) + return +} + +// params: sessionid, windowid +func GetWindowLines(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + windowId := qvals.Get("windowid") + if _, err := uuid.Parse(sessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) + return + } + if _, err := uuid.Parse(windowId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid windowid: %w", err)) + return + } + lines, err := sstore.GetWindowLines(r.Context(), sessionId, windowId) + if err != nil { + WriteJsonError(w, err) + return + } + WriteJsonSuccess(w, lines) + return +} + func GetPtyOutFile(sessionId string, cmdId string) string { pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId) return pathStr @@ -337,24 +385,24 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { return } decoder := json.NewDecoder(r.Body) - var params runCommandParams - err := decoder.Decode(¶ms) + var commandPk scpacket.FeCommandPacketType + err := decoder.Decode(&commandPk) if err != nil { WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) return } - if _, err = uuid.Parse(params.SessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", params.SessionId, err)) + if _, err = uuid.Parse(commandPk.SessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", commandPk.SessionId, err)) return } - commandStr := strings.TrimSpace(params.Command) + commandStr := strings.TrimSpace(commandPk.CmdStr) if commandStr == "" { WriteJsonError(w, fmt.Errorf("invalid emtpty command")) return } if strings.HasPrefix(commandStr, "/comment ") { text := strings.TrimSpace(commandStr[9:]) - rtnLine := sstore.MakeNewLineText(params.SessionId, params.WindowId, text) + rtnLine := sstore.MakeNewLineText(commandPk.SessionId, commandPk.WindowId, text) WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) return } @@ -370,10 +418,10 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { } return } - rtnLine := sstore.MakeNewLineCmd(params.SessionId, params.WindowId) + rtnLine := sstore.MakeNewLineCmd(commandPk.SessionId, commandPk.WindowId) // rtnLine.CmdText = commandStr runPacket := packet.MakeRunPacket() - runPacket.CK = base.MakeCommandKey(params.SessionId, rtnLine.CmdId) + runPacket.CK = base.MakeCommandKey(commandPk.SessionId, rtnLine.CmdId) runPacket.Cwd = "" runPacket.Env = nil runPacket.Command = commandStr @@ -393,6 +441,12 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // * userid // * sessionid // +// /api/get-session +// params: +// * name +// returns: +// * session +// // /api/ptyout (pos=[position]) - returns contents of ptyout file // params: // * sessionid @@ -515,6 +569,8 @@ func main() { go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", GetPtyOut) + gr.HandleFunc("/api/get-session", GetSession) + gr.HandleFunc("/api/get-window-lines", GetWindowLines) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1bb1767e2..decff430b 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -130,6 +130,20 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return rtnSession, nil } +func GetWindowLines(ctx context.Context, sessionId string, windowId string) ([]*LineType, error) { + var lines []*LineType + db, err := GetDB() + if err != nil { + return nil, err + } + query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` + err = db.SelectContext(ctx, &lines, query, sessionId, windowId) + if err != nil { + return nil, err + } + return lines, nil +} + // also creates window, and sessionremote func InsertSessionWithName(ctx context.Context, sessionName string) error { if sessionName == "" { @@ -168,3 +182,28 @@ func InsertSessionWithName(ctx context.Context, sessionName string) error { return nil }) } + +func InsertLine(ctx context.Context, line *LineType) error { + if line == nil { + return fmt.Errorf("line cannot be nil") + } + if line.LineId != 0 { + return fmt.Errorf("new line cannot have LineId set") + } + return WithTx(ctx, func(tx *TxWrap) error { + var windowId string + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + hasWindow := tx.GetWrap(&windowId, query, line.SessionId, line.WindowId) + if !hasWindow { + return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) + } + var maxLineId int + query = `SELECT max(lineid) FROM line WHERE sessionid = ? AND windowid = ?` + tx.GetWrap(&maxLineId, query, line.SessionId, line.WindowId) + line.LineId = maxLineId + 1 + query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid) + VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid)` + tx.NamedExecWrap(query, line) + return nil + }) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 19aa0c326..1a4b1f5db 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -55,13 +55,13 @@ type SessionType struct { } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - CurRemote string `json:"curremote"` - Remotes []*RemoteType `json:"remotes"` - Lines []*LineType `json:"lines"` - Version int `json:"version"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + CurRemote string `json:"curremote"` + Remotes []*SessionRemote `json:"remotes"` + Lines []*LineType `json:"lines"` + Version int `json:"version"` } type SessionRemote struct { From fc18df06013294189ed1d7079342ff22590b5997 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Jul 2022 12:01:56 -0700 Subject: [PATCH 022/397] checkpoint --- cmd/main-server.go | 49 ++++++++++++++++++++-------------------- pkg/scpacket/scpacket.go | 1 + 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 371f0bde8..982a83e97 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -258,7 +258,7 @@ func sendCmdInput(pk *packet.InputPacketType) error { } // params: name -func GetSession(w http.ResponseWriter, r *http.Request) { +func HandleGetSession(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") @@ -279,7 +279,7 @@ func GetSession(w http.ResponseWriter, r *http.Request) { } // params: sessionid, windowid -func GetWindowLines(w http.ResponseWriter, r *http.Request) { +func HandleGetWindowLines(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") @@ -309,7 +309,7 @@ func GetPtyOutFile(sessionId string, cmdId string) string { return pathStr } -func GetPtyOut(w http.ResponseWriter, r *http.Request) { +func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") @@ -364,12 +364,6 @@ func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { return } -type runCommandParams struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - Command string `json:"command"` -} - type runCommandResponse struct { Line *sstore.LineType `json:"line"` } @@ -395,16 +389,24 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", commandPk.SessionId, err)) return } - commandStr := strings.TrimSpace(commandPk.CmdStr) - if commandStr == "" { - WriteJsonError(w, fmt.Errorf("invalid emtpty command")) + line, err := ProcessFeCommandPacket(&commandPk) + if err != nil { + WriteJsonError(w, err) return } + WriteJsonSuccess(w, &runCommandResponse{Line: line}) + return +} + +func ProcessFeCommandPacket(pk *scpacket.FeCommandPacketType) (*sstore.LineType, error) { + commandStr := strings.TrimSpace(pk.CmdStr) + if commandStr == "" { + return nil, fmt.Errorf("invalid emtpty command") + } if strings.HasPrefix(commandStr, "/comment ") { text := strings.TrimSpace(commandStr[9:]) - rtnLine := sstore.MakeNewLineText(commandPk.SessionId, commandPk.WindowId, text) - WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) - return + rtnLine := sstore.MakeNewLineText(pk.SessionId, pk.WindowId, text) + return rtnLine, nil } if strings.HasPrefix(commandStr, "cd ") { newDir := strings.TrimSpace(commandStr[3:]) @@ -414,26 +416,23 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { localRemote := remote.GetRemote("local") if localRemote != nil { localRemote.Input.SendPacket(cdPacket) - return } - return + return nil, nil } - rtnLine := sstore.MakeNewLineCmd(commandPk.SessionId, commandPk.WindowId) - // rtnLine.CmdText = commandStr + rtnLine := sstore.MakeNewLineCmd(pk.SessionId, pk.WindowId) runPacket := packet.MakeRunPacket() - runPacket.CK = base.MakeCommandKey(commandPk.SessionId, rtnLine.CmdId) + runPacket.CK = base.MakeCommandKey(pk.SessionId, rtnLine.CmdId) runPacket.Cwd = "" runPacket.Env = nil runPacket.Command = commandStr fmt.Printf("run-packet %v\n", runPacket) - WriteJsonSuccess(w, &runCommandResponse{Line: rtnLine}) go func() { localRemote := remote.GetRemote("local") if localRemote != nil { localRemote.Input.SendPacket(runPacket) } }() - return + return rtnLine, nil } // /api/start-session @@ -568,9 +567,9 @@ func main() { } go runWebSocketServer() gr := mux.NewRouter() - gr.HandleFunc("/api/ptyout", GetPtyOut) - gr.HandleFunc("/api/get-session", GetSession) - gr.HandleFunc("/api/get-window-lines", GetWindowLines) + gr.HandleFunc("/api/ptyout", HandleGetPtyOut) + gr.HandleFunc("/api/get-session", HandleGetSession) + gr.HandleFunc("/api/get-window-lines", HandleGetWindowLines) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index e4d40b636..d3c264845 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -18,6 +18,7 @@ type FeCommandPacketType struct { Type string `json:"type"` SessionId string `json:"sessionid"` WindowId string `json:"windowid"` + UserId string `json:"userid"` CmdStr string `json:"cmdstr"` RemoteState RemoteState `json:"remotestate"` } From 5b2e88ec324e0ee1b30dcead4d54f12bad36a28e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 4 Jul 2022 22:18:01 -0700 Subject: [PATCH 023/397] checkpoint --- cmd/main-server.go | 12 +++++++++++ pkg/remote/remote.go | 28 ++++++++++++++++++++++++ pkg/sstore/dbops.go | 51 ++++++++++++++++++-------------------------- pkg/sstore/sstore.go | 22 +++++++++---------- 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 982a83e97..ce720d412 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -278,6 +278,17 @@ func HandleGetSession(w http.ResponseWriter, r *http.Request) { return } +// params: [none] +func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + remotes := remote.GetAllRemoteState() + WriteJsonSuccess(w, remotes) + return +} + // params: sessionid, windowid func HandleGetWindowLines(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -570,6 +581,7 @@ func main() { gr.HandleFunc("/api/ptyout", HandleGetPtyOut) gr.HandleFunc("/api/get-session", HandleGetSession) gr.HandleFunc("/api/get-window-lines", HandleGetWindowLines) + gr.HandleFunc("/api/get-remotes", HandleGetRemotes) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 5123e5ff0..98634dcdc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -32,6 +32,14 @@ type Store struct { Map map[string]*MShellProc } +type RemoteState struct { + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + RemoteName string `json:"remotename"` + Status string `json:"status"` + Cwd string `json:"cwd"` +} + type MShellProc struct { Lock *sync.Mutex Remote *sstore.RemoteType @@ -78,6 +86,26 @@ func GetRemote(name string) *MShellProc { return GlobalStore.Map[name] } +func GetAllRemoteState() []RemoteState { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + + var rtn []RemoteState + for _, proc := range GlobalStore.Map { + state := RemoteState{ + RemoteType: proc.Remote.RemoteType, + RemoteId: proc.Remote.RemoteId, + RemoteName: proc.Remote.RemoteName, + Status: proc.Status, + } + if proc.InitPk != nil { + state.Cwd = proc.InitPk.HomeDir + } + rtn = append(rtn, state) + } + return rtn +} + func MakeMShell(r *sstore.RemoteType) *MShellProc { rtn := &MShellProc{Lock: &sync.Mutex{}, Remote: r, Status: StatusInit} return rtn diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index decff430b..5f9dbd864 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -97,31 +97,19 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { } func GetSessionById(ctx context.Context, id string) (*SessionType, error) { - db, err := GetDB() - query := `SELECT * FROM session WHERE sessionid = ?` - var session SessionType - err = db.GetContext(ctx, &session, query, id) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - return &session, nil -} - -func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { var rtnSession *SessionType err := WithTx(ctx, func(tx *TxWrap) error { var session SessionType - query := `SELECT * FROM session WHERE name = ?` - found := tx.GetWrap(&session, query, name) + query := `SELECT * FROM session WHERE sessionid = ?` + found := tx.GetWrap(&session, query, id) if !found { return nil } rtnSession = &session query = `SELECT sessionid, windowid, name, curremote, version FROM window WHERE sessionid = ?` tx.SelectWrap(&session.Windows, query, session.SessionId) + query = `SELECT * FROM session_remote WHERE sessionid = ?` + tx.SelectWrap(&session.Remotes, query, session.SessionId) return nil }) if err != nil { @@ -130,6 +118,23 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return rtnSession, nil } +func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + var sessionId string + query := `SELECT sessionid FROM session WHERE name = ?` + err = db.GetContext(ctx, &sessionId, query, name) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return GetSessionById(ctx, sessionId) +} + func GetWindowLines(ctx context.Context, sessionId string, windowId string) ([]*LineType, error) { var lines []*LineType db, err := GetDB() @@ -153,10 +158,6 @@ func InsertSessionWithName(ctx context.Context, sessionName string) error { SessionId: uuid.New().String(), Name: sessionName, } - localRemote, err := GetRemoteByName(ctx, LocalRemoteName) - if err != nil { - return err - } return WithTx(ctx, func(tx *TxWrap) error { query := `INSERT INTO session (sessionid, name) VALUES (:sessionid, :name)` tx.NamedExecWrap(query, session) @@ -169,16 +170,6 @@ func InsertSessionWithName(ctx context.Context, sessionName string) error { } query = `INSERT INTO window (sessionid, windowid, name, curremote, version) VALUES (:sessionid, :windowid, :name, :curremote, :version)` tx.NamedExecWrap(query, window) - - sr := &SessionRemote{ - SessionId: session.SessionId, - WindowId: window.WindowId, - RemoteName: localRemote.RemoteName, - RemoteId: localRemote.RemoteId, - Cwd: DefaultCwd, - } - query = `INSERT INTO session_remote (sessionid, windowid, remotename, remoteid, cwd) VALUES (:sessionid, :windowid, :remotename, :remoteid, :cwd)` - tx.NamedExecWrap(query, sr) return nil }) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1a4b1f5db..d8696c8fb 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -48,20 +48,20 @@ func GetDB() (*sqlx.DB, error) { } type SessionType struct { - SessionId string `json:"sessionid"` - Name string `json:"name"` - Windows []*WindowType `json:"windows"` - Cmds []*CmdType `json:"cmds"` + SessionId string `json:"sessionid"` + Name string `json:"name"` + Windows []*WindowType `json:"windows"` + Cmds []*CmdType `json:"cmds"` + Remotes []*SessionRemote `json:"remotes"` } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - CurRemote string `json:"curremote"` - Remotes []*SessionRemote `json:"remotes"` - Lines []*LineType `json:"lines"` - Version int `json:"version"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + CurRemote string `json:"curremote"` + Lines []*LineType `json:"lines"` + Version int `json:"version"` } type SessionRemote struct { From 123fdfe3bb49cdb2e9a1a5cf98511ca44cbd87f3 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 5 Jul 2022 10:51:47 -0700 Subject: [PATCH 024/397] comment flow working --- cmd/main-server.go | 16 +++++++++++----- pkg/sstore/dbops.go | 2 +- pkg/sstore/sstore.go | 28 ++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index ce720d412..a1884287a 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -400,7 +400,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", commandPk.SessionId, err)) return } - line, err := ProcessFeCommandPacket(&commandPk) + line, err := ProcessFeCommandPacket(r.Context(), &commandPk) if err != nil { WriteJsonError(w, err) return @@ -409,14 +409,17 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { return } -func ProcessFeCommandPacket(pk *scpacket.FeCommandPacketType) (*sstore.LineType, error) { +func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketType) (*sstore.LineType, error) { commandStr := strings.TrimSpace(pk.CmdStr) if commandStr == "" { return nil, fmt.Errorf("invalid emtpty command") } if strings.HasPrefix(commandStr, "/comment ") { text := strings.TrimSpace(commandStr[9:]) - rtnLine := sstore.MakeNewLineText(pk.SessionId, pk.WindowId, text) + rtnLine, err := sstore.AddCommentLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, text) + if err != nil { + return nil, err + } return rtnLine, nil } if strings.HasPrefix(commandStr, "cd ") { @@ -430,10 +433,13 @@ func ProcessFeCommandPacket(pk *scpacket.FeCommandPacketType) (*sstore.LineType, } return nil, nil } - rtnLine := sstore.MakeNewLineCmd(pk.SessionId, pk.WindowId) + rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId) + if err != nil { + return nil, err + } runPacket := packet.MakeRunPacket() runPacket.CK = base.MakeCommandKey(pk.SessionId, rtnLine.CmdId) - runPacket.Cwd = "" + runPacket.Cwd = pk.RemoteState.Cwd runPacket.Env = nil runPacket.Command = commandStr fmt.Printf("run-packet %v\n", runPacket) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5f9dbd864..1becc4a78 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -189,7 +189,7 @@ func InsertLine(ctx context.Context, line *LineType) error { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } var maxLineId int - query = `SELECT max(lineid) FROM line WHERE sessionid = ? AND windowid = ?` + query = `SELECT COALESCE(max(lineid), 0) FROM line WHERE sessionid = ? AND windowid = ?` tx.GetWrap(&maxLineId, query, line.SessionId, line.WindowId) line.LineId = maxLineId + 1 query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d8696c8fb..e0cfb2d33 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -115,30 +115,46 @@ type CmdType struct { RunOut packet.PacketType `json:"runout"` } -func MakeNewLineCmd(sessionId string, windowId string) *LineType { +func makeNewLineCmd(sessionId string, windowId string, userId string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId - rtn.LineId = GetNextLine() rtn.Ts = time.Now().UnixMilli() - rtn.UserId = "mike" + rtn.UserId = userId rtn.LineType = LineTypeCmd rtn.CmdId = uuid.New().String() return rtn } -func MakeNewLineText(sessionId string, windowId string, text string) *LineType { +func makeNewLineText(sessionId string, windowId string, userId string, text string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId - rtn.LineId = GetNextLine() rtn.Ts = time.Now().UnixMilli() - rtn.UserId = "mike" + rtn.UserId = userId rtn.LineType = LineTypeText rtn.Text = text return rtn } +func AddCommentLine(ctx context.Context, sessionId string, windowId string, userId string, commentText string) (*LineType, error) { + rtnLine := makeNewLineText(sessionId, windowId, userId, commentText) + err := InsertLine(ctx, rtnLine) + if err != nil { + return nil, err + } + return rtnLine, nil +} + +func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string) (*LineType, error) { + rtnLine := makeNewLineCmd(sessionId, windowId, userId) + err := InsertLine(ctx, rtnLine) + if err != nil { + return nil, err + } + return rtnLine, nil +} + func GetNextLine() int { NextLineLock.Lock() defer NextLineLock.Unlock() From 2755be315d06f06f425867c70b39e185d2a934c9 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 5 Jul 2022 16:54:49 -0700 Subject: [PATCH 025/397] checkpoint for migrating to remoteinstance --- cmd/main-server.go | 28 ++++-------- db/migrations/000001_init.down.sql | 2 +- db/migrations/000001_init.up.sql | 11 +++-- pkg/remote/remote.go | 70 +++++++++++++++++++++-------- pkg/sstore/dbops.go | 4 +- pkg/sstore/sstore.go | 71 +++++++++++++++++++++--------- 6 files changed, 121 insertions(+), 65 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index a1884287a..2e2f2cca7 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -377,6 +377,7 @@ func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { type runCommandResponse struct { Line *sstore.LineType `json:"line"` + Cmd *sstore.CmdType `json:"cmd"` } func HandleRunCommand(w http.ResponseWriter, r *http.Request) { @@ -400,16 +401,16 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", commandPk.SessionId, err)) return } - line, err := ProcessFeCommandPacket(r.Context(), &commandPk) + resp, err := ProcessFeCommandPacket(r.Context(), &commandPk) if err != nil { WriteJsonError(w, err) return } - WriteJsonSuccess(w, &runCommandResponse{Line: line}) + WriteJsonSuccess(w, resp) return } -func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketType) (*sstore.LineType, error) { +func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketType) (*runCommandResponse, error) { commandStr := strings.TrimSpace(pk.CmdStr) if commandStr == "" { return nil, fmt.Errorf("invalid emtpty command") @@ -420,14 +421,14 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp if err != nil { return nil, err } - return rtnLine, nil + return &runCommandResponse{Line: rtnLine}, nil } if strings.HasPrefix(commandStr, "cd ") { newDir := strings.TrimSpace(commandStr[3:]) cdPacket := packet.MakeCdPacket() - cdPacket.PacketId = uuid.New().String() + cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir - localRemote := remote.GetRemote("local") + localRemote := remote.GetRemoteById(pk.RemoteState.RemoteId) if localRemote != nil { localRemote.Input.SendPacket(cdPacket) } @@ -437,19 +438,8 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp if err != nil { return nil, err } - runPacket := packet.MakeRunPacket() - runPacket.CK = base.MakeCommandKey(pk.SessionId, rtnLine.CmdId) - runPacket.Cwd = pk.RemoteState.Cwd - runPacket.Env = nil - runPacket.Command = commandStr - fmt.Printf("run-packet %v\n", runPacket) - go func() { - localRemote := remote.GetRemote("local") - if localRemote != nil { - localRemote.Input.SendPacket(runPacket) - } - }() - return rtnLine, nil + err = remote.RunCommand(pk, rtnLine.CmdId) + return &runCommandResponse{Line: rtnLine}, nil } // /api/start-session diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql index b91103179..6887fccdc 100644 --- a/db/migrations/000001_init.down.sql +++ b/db/migrations/000001_init.down.sql @@ -1,6 +1,6 @@ DROP TABLE session; DROP TABLE window; -DROP TABLE session_remote; +DROP TABLE remote_instance; DROP TABLE line; DROP TABLE remote; DROP TABLE session_cmd; diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 622a3b6f6..2ad0b2ec7 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -14,13 +14,14 @@ CREATE TABLE window ( ); CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); -CREATE TABLE session_remote ( +CREATE TABLE remote_instance ( + riid varchar(36) PRIMARY KEY, + name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - remotename varchar(50) NOT NULL, remoteid varchar(36) NOT NULL, - cwd varchar(300) NOT NULL, - PRIMARY KEY (sessionid, windowid, remotename) + sessionscope boolean NOT NULL, + state json NOT NULL ); CREATE TABLE line ( @@ -55,7 +56,9 @@ CREATE TABLE remote ( CREATE TABLE session_cmd ( sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, + rsid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, + remotestate json NOT NULL, status varchar(10) NOT NULL, startts bigint NOT NULL, pid int NOT NULL, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 98634dcdc..e9896958a 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -6,12 +6,14 @@ import ( "io" "os" "os/exec" + "strings" "sync" "time" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -29,15 +31,15 @@ var GlobalStore *Store type Store struct { Lock *sync.Mutex - Map map[string]*MShellProc + Map map[string]*MShellProc // key=remoteid } type RemoteState struct { - RemoteType string `json:"remotetype"` - RemoteId string `json:"remoteid"` - RemoteName string `json:"remotename"` - Status string `json:"status"` - Cwd string `json:"cwd"` + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + RemoteName string `json:"remotename"` + Status string `json:"status"` + DefaultState *sstore.RemoteState `json:"defaultstate"` } type MShellProc struct { @@ -57,8 +59,8 @@ type MShellProc struct { } type RpcEntry struct { - PacketId string - RespCh chan packet.RpcPacketType + ReqId string + RespCh chan packet.RpcResponsePacketType } func LoadRemotes(ctx context.Context) error { @@ -72,7 +74,7 @@ func LoadRemotes(ctx context.Context) error { } for _, remote := range allRemotes { msh := MakeMShell(remote) - GlobalStore.Map[remote.RemoteName] = msh + GlobalStore.Map[remote.RemoteId] = msh if remote.AutoConnect { go msh.Launch() } @@ -80,10 +82,21 @@ func LoadRemotes(ctx context.Context) error { return nil } -func GetRemote(name string) *MShellProc { +func GetRemoteByName(name string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() - return GlobalStore.Map[name] + for _, msh := range GlobalStore.Map { + if msh.Remote.RemoteName == name { + return msh + } + } + return nil +} + +func GetRemoteById(remoteId string) *MShellProc { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + return GlobalStore.Map[remoteId] } func GetAllRemoteState() []RemoteState { @@ -99,7 +112,7 @@ func GetAllRemoteState() []RemoteState { Status: proc.Status, } if proc.InitPk != nil { - state.Cwd = proc.InitPk.HomeDir + state.DefaultState = &sstore.RemoteState{Cwd: proc.InitPk.HomeDir} } rtn = append(rtn, state) } @@ -177,17 +190,37 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } -func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcPacketType, error) { +func RunCommand(pk *scpacket.FeCommandPacketType, cmdId string) error { + msh := GetRemoteById(pk.RemoteState.RemoteId) + if msh == nil { + return fmt.Errorf("no remote id=%s found", pk.RemoteState.RemoteId) + } + if !msh.IsConnected() { + return fmt.Errorf("remote '%s' is not connected", msh.Remote.RemoteName) + } + runPacket := packet.MakeRunPacket() + runPacket.CK = base.MakeCommandKey(pk.SessionId, cmdId) + runPacket.Cwd = pk.RemoteState.Cwd + runPacket.Env = nil + runPacket.Command = strings.TrimSpace(pk.CmdStr) + fmt.Printf("run-packet %v\n", runPacket) + go func() { + msh.Input.SendPacket(runPacket) + }() + return nil +} + +func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcResponsePacketType, error) { if !runner.IsConnected() { return nil, fmt.Errorf("runner is not connected") } if pk == nil { return nil, fmt.Errorf("PacketRpc passed nil packet") } - id := pk.GetPacketId() - respCh := make(chan packet.RpcPacketType) + id := pk.GetReqId() + respCh := make(chan packet.RpcResponsePacketType) runner.WithLock(func() { - runner.RpcMap[id] = &RpcEntry{PacketId: id, RespCh: respCh} + runner.RpcMap[id] = &RpcEntry{ReqId: id, RespCh: respCh} }) defer runner.WithLock(func() { delete(runner.RpcMap, id) @@ -217,8 +250,9 @@ func (runner *MShellProc) ProcessPackets() { } }) for pk := range runner.Output.MainCh { - if rpcPk, ok := pk.(packet.RpcPacketType); ok { - rpcId := rpcPk.GetPacketId() + fmt.Printf("MSH> %s\n", packet.AsString(pk)) + if rpcPk, ok := pk.(packet.RpcResponsePacketType); ok { + rpcId := rpcPk.GetResponseId() runner.WithLock(func() { entry := runner.RpcMap[rpcId] if entry == nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1becc4a78..71b13d0ca 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -108,7 +108,7 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { rtnSession = &session query = `SELECT sessionid, windowid, name, curremote, version FROM window WHERE sessionid = ?` tx.SelectWrap(&session.Windows, query, session.SessionId) - query = `SELECT * FROM session_remote WHERE sessionid = ?` + query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) return nil }) @@ -149,7 +149,7 @@ func GetWindowLines(ctx context.Context, sessionId string, windowId string) ([]* return lines, nil } -// also creates window, and sessionremote +// also creates window func InsertSessionWithName(ctx context.Context, sessionName string) error { if sessionName == "" { return fmt.Errorf("invalid session name '%s'", sessionName) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index e0cfb2d33..967f8dc46 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -2,6 +2,9 @@ package sstore import ( "context" + "database/sql/driver" + "encoding/json" + "fmt" "log" "path" "sync" @@ -48,11 +51,11 @@ func GetDB() (*sqlx.DB, error) { } type SessionType struct { - SessionId string `json:"sessionid"` - Name string `json:"name"` - Windows []*WindowType `json:"windows"` - Cmds []*CmdType `json:"cmds"` - Remotes []*SessionRemote `json:"remotes"` + SessionId string `json:"sessionid"` + Name string `json:"name"` + Windows []*WindowType `json:"windows"` + Cmds []*CmdType `json:"cmds"` + Remotes []*RemoteInstance `json:"remotes"` } type WindowType struct { @@ -64,12 +67,36 @@ type WindowType struct { Version int `json:"version"` } -type SessionRemote struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - RemoteId string `json"remoteid"` - RemoteName string `json:"name"` - Cwd string `json:"cwd"` +type RemoteState struct { + Cwd string `json:"cwd"` +} + +func (s *RemoteState) Scan(val interface{}) error { + if strVal, ok := val.(string); ok { + if strVal == "" { + return nil + } + err := json.Unmarshal([]byte(strVal), s) + if err != nil { + return err + } + return nil + } + return fmt.Errorf("cannot scan '%T' into RemoteState", val) +} + +func (s *RemoteState) Value() (driver.Value, error) { + return json.Marshal(s) +} + +type RemoteInstance struct { + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteId string `json"remoteid"` + SessionScope bool `json:"sessionscope"` + State RemoteState `json:"state"` } type LineType struct { @@ -101,16 +128,18 @@ type RemoteType struct { } type CmdType struct { - RowId int64 `json:"rowid"` - SessionId string `json:"sessionid"` - CmdId string `json:"cmdid"` - RemoteId string `json:"remoteid"` - Status string `json:"status"` - StartTs int64 `json:"startts"` - DoneTs int64 `json:"donets"` - Pid int `json:"pid"` - RunnerPid int `json:"runnerpid"` - ExitCode int `json:"exitcode"` + RowId int64 `json:"rowid"` + SessionId string `json:"sessionid"` + CmdId string `json:"cmdid"` + RSId string `json:"rsid"` + RemoteId string `json:"remoteid"` + RemoteState string `json:"remotestate"` + Status string `json:"status"` + StartTs int64 `json:"startts"` + DoneTs int64 `json:"donets"` + Pid int `json:"pid"` + RunnerPid int `json:"runnerpid"` + ExitCode int `json:"exitcode"` RunOut packet.PacketType `json:"runout"` } From 98e46399bebf7db323a8dff87c721852cfc09290 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 6 Jul 2022 19:01:00 -0700 Subject: [PATCH 026/397] checkpoint, getting closer to running a command via mshell server --- cmd/main-server.go | 18 +++- db/migrations/000001_init.up.sql | 5 +- pkg/remote/remote.go | 170 ++++++++++++------------------- pkg/scbase/scbase.go | 72 +++++++++++++ pkg/sstore/dbops.go | 15 +-- pkg/sstore/fileops.go | 24 +++++ pkg/sstore/sstore.go | 2 - 7 files changed, 182 insertions(+), 124 deletions(-) create mode 100644 pkg/sstore/fileops.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 2e2f2cca7..b8541e117 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -429,16 +429,25 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir localRemote := remote.GetRemoteById(pk.RemoteState.RemoteId) - if localRemote != nil { - localRemote.Input.SendPacket(cdPacket) + if localRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot execute command") } + resp, err := localRemote.PacketRpc(ctx, cdPacket) + if err != nil { + return nil, err + } + fmt.Printf("GOT cd RESP: %v\n", resp) return nil, nil } rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId) if err != nil { return nil, err } - err = remote.RunCommand(pk, rtnLine.CmdId) + startPk, err := remote.RunCommand(ctx, pk, rtnLine.CmdId) + if err != nil { + return nil, err + } + fmt.Printf("START CMD: %s\n", packet.AsString(startPk)) return &runCommandResponse{Line: rtnLine}, nil } @@ -572,6 +581,9 @@ func main() { fmt.Printf("[error] loading remotes: %v\n", err) return } + + sstore.AppendToCmdPtyBlob(context.Background(), "", "", nil) + go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 2ad0b2ec7..ea0438427 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -49,8 +49,7 @@ CREATE TABLE remote ( sshuser varchar(100) NOT NULL, -- runtime data - lastconnectts bigint NOT NULL, - ptyout BLOB NOT NULL + lastconnectts bigint NOT NULL ); CREATE TABLE session_cmd ( @@ -65,8 +64,6 @@ CREATE TABLE session_cmd ( runnerpid int NOT NULL, donets bigint NOT NULL, exitcode int NOT NULL, - ptyout BLOB NOT NULL, - runout BLOB NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e9896958a..af773dbcb 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -2,14 +2,13 @@ package remote import ( "context" + "errors" "fmt" - "io" - "os" "os/exec" "strings" "sync" - "time" + "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" @@ -18,10 +17,12 @@ import ( ) const RemoteTypeMShell = "mshell" +const DefaultTermRows = 25 +const DefaultTermCols = 80 +const DefaultTerm = "xterm-256color" const ( StatusInit = "init" - StatusConnecting = "connecting" StatusConnected = "connected" StatusDisconnected = "disconnected" StatusError = "error" @@ -47,20 +48,9 @@ type MShellProc struct { Remote *sstore.RemoteType // runtime - Status string - InitPk *packet.InitPacketType - Cmd *exec.Cmd - Input *packet.PacketSender - Output *packet.PacketParser - DoneCh chan bool - RpcMap map[string]*RpcEntry - - Err error -} - -type RpcEntry struct { - ReqId string - RespCh chan packet.RpcResponsePacketType + Status string + ServerProc *shexec.ClientProc + Err error } func LoadRemotes(ctx context.Context) error { @@ -111,8 +101,8 @@ func GetAllRemoteState() []RemoteState { RemoteName: proc.Remote.RemoteName, Status: proc.Status, } - if proc.InitPk != nil { - state.DefaultState = &sstore.RemoteState{Cwd: proc.InitPk.HomeDir} + if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { + state.DefaultState = &sstore.RemoteState{Cwd: proc.ServerProc.InitPk.HomeDir} } rtn = append(rtn, state) } @@ -135,50 +125,24 @@ func (msh *MShellProc) Launch() { return } ecmd := exec.Command(msPath, "--server") - msh.Cmd = ecmd - inputWriter, err := ecmd.StdinPipe() + cproc, err := shexec.MakeClientProc(ecmd) if err != nil { msh.Status = StatusError - msh.Err = fmt.Errorf("create stdin pipe: %w", err) - return - } - stdoutReader, err := ecmd.StdoutPipe() - if err != nil { - msh.Status = StatusError - msh.Err = fmt.Errorf("create stdout pipe: %w", err) - return - } - stderrReader, err := ecmd.StderrPipe() - if err != nil { - msh.Status = StatusError - msh.Err = fmt.Errorf("create stderr pipe: %w", err) + msh.Err = err return } + msh.ServerProc = cproc + fmt.Printf("START MAKECLIENTPROC: %#v\n", msh.ServerProc.InitPk) + msh.Status = StatusConnected go func() { - io.Copy(os.Stderr, stderrReader) - }() - err = ecmd.Start() - if err != nil { - msh.Status = StatusError - msh.Err = fmt.Errorf("starting mshell server: %w", err) - return - } - fmt.Printf("Started remote '%s' pid=%d\n", msh.Remote.RemoteName, msh.Cmd.Process.Pid) - msh.Status = StatusConnecting - msh.Output = packet.MakePacketParser(stdoutReader) - msh.Input = packet.MakePacketSender(inputWriter) - msh.RpcMap = make(map[string]*RpcEntry) - msh.DoneCh = make(chan bool) - go func() { - exitErr := ecmd.Wait() + exitErr := cproc.Cmd.Wait() exitCode := shexec.GetExitCode(exitErr) msh.WithLock(func() { - if msh.Status == StatusConnected || msh.Status == StatusConnecting { + if msh.Status == StatusConnected { msh.Status = StatusDisconnected } }) fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) - close(msh.DoneCh) }() go msh.ProcessPackets() return @@ -190,51 +154,62 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } -func RunCommand(pk *scpacket.FeCommandPacketType, cmdId string) error { +func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId string) (*packet.CmdStartPacketType, error) { msh := GetRemoteById(pk.RemoteState.RemoteId) if msh == nil { - return fmt.Errorf("no remote id=%s found", pk.RemoteState.RemoteId) + return nil, fmt.Errorf("no remote id=%s found", pk.RemoteState.RemoteId) } if !msh.IsConnected() { - return fmt.Errorf("remote '%s' is not connected", msh.Remote.RemoteName) + return nil, fmt.Errorf("remote '%s' is not connected", msh.Remote.RemoteName) } runPacket := packet.MakeRunPacket() + runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(pk.SessionId, cmdId) runPacket.Cwd = pk.RemoteState.Cwd runPacket.Env = nil + runPacket.UsePty = true + runPacket.TermOpts = &packet.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, Term: DefaultTerm} runPacket.Command = strings.TrimSpace(pk.CmdStr) - fmt.Printf("run-packet %v\n", runPacket) - go func() { - msh.Input.SendPacket(runPacket) - }() - return nil + fmt.Printf("RUN-CMD> %s\n", runPacket.CK) + msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) + err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) + if err != nil { + return nil, fmt.Errorf("sending run packet to remote: %w", err) + } + rtnPk := msh.ServerProc.Output.WaitForResponse(ctx, runPacket.ReqId) + if startPk, ok := rtnPk.(*packet.CmdStartPacketType); ok { + return startPk, nil + } + if respPk, ok := rtnPk.(*packet.ResponsePacketType); ok { + if respPk.Error != "" { + return nil, errors.New(respPk.Error) + } + } + return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } -func (runner *MShellProc) PacketRpc(pk packet.RpcPacketType, timeout time.Duration) (packet.RpcResponsePacketType, error) { - if !runner.IsConnected() { +func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { + if !msh.IsConnected() { return nil, fmt.Errorf("runner is not connected") } if pk == nil { return nil, fmt.Errorf("PacketRpc passed nil packet") } - id := pk.GetReqId() - respCh := make(chan packet.RpcResponsePacketType) - runner.WithLock(func() { - runner.RpcMap[id] = &RpcEntry{ReqId: id, RespCh: respCh} - }) - defer runner.WithLock(func() { - delete(runner.RpcMap, id) - }) - runner.Input.SendPacket(pk) - timer := time.NewTimer(timeout) - defer timer.Stop() - select { - case rtnPk := <-respCh: - return rtnPk, nil - - case <-timer.C: - return nil, fmt.Errorf("PacketRpc timeout") + reqId := pk.GetReqId() + msh.ServerProc.Output.RegisterRpc(reqId) + defer msh.ServerProc.Output.UnRegisterRpc(reqId) + err := msh.ServerProc.Input.SendPacketCtx(ctx, pk) + if err != nil { + return nil, err } + rtnPk := msh.ServerProc.Output.WaitForResponse(ctx, reqId) + if rtnPk == nil { + return nil, ctx.Err() + } + if respPk, ok := rtnPk.(*packet.ResponsePacketType); ok { + return respPk, nil + } + return nil, fmt.Errorf("invalid response packet received: %s", packet.AsString(rtnPk)) } func (runner *MShellProc) WithLock(fn func()) { @@ -245,38 +220,25 @@ func (runner *MShellProc) WithLock(fn func()) { func (runner *MShellProc) ProcessPackets() { defer runner.WithLock(func() { - if runner.Status == StatusConnected || runner.Status == StatusConnecting { + if runner.Status == StatusConnected { runner.Status = StatusDisconnected } }) - for pk := range runner.Output.MainCh { - fmt.Printf("MSH> %s\n", packet.AsString(pk)) - if rpcPk, ok := pk.(packet.RpcResponsePacketType); ok { - rpcId := rpcPk.GetResponseId() - runner.WithLock(func() { - entry := runner.RpcMap[rpcId] - if entry == nil { - return - } - delete(runner.RpcMap, rpcId) - go func() { - entry.RespCh <- rpcPk - close(entry.RespCh) - }() - }) + for pk := range runner.ServerProc.Output.MainCh { + fmt.Printf("MSH> %s | %#v\n", packet.AsString(pk), pk) + if pk.GetType() == packet.DataPacketStr { + dataPacket := pk.(*packet.DataPacketType) + fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPacket.CK, dataPacket.FdNum, packet.B64DecodedLen(dataPacket.Data64), dataPacket.Eof, dataPacket.Error) + continue } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, len(dataPacket.PtyData), len(dataPacket.RunData)) + fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) continue } - if pk.GetType() == packet.InitPacketStr { - initPacket := pk.(*packet.InitPacketType) - fmt.Printf("runner-init %s user=%s dir=%s\n", initPacket.MShellHomeDir, initPacket.User, initPacket.HomeDir) - runner.WithLock(func() { - runner.InitPk = initPacket - runner.Status = StatusConnected - }) + if pk.GetType() == packet.CmdDonePacketStr { + donePacket := pk.(*packet.CmdDonePacketType) + fmt.Printf("cmd-done %s\n", donePacket.CK) continue } if pk.GetType() == packet.MessagePacketStr { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 830903b20..27e147062 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -1,12 +1,21 @@ package scbase import ( + "errors" + "fmt" + "io/fs" "os" "path" + "sync" ) const HomeVarName = "HOME" const ScHomeVarName = "SCRIPTHAUS_HOME" +const SessionsDirBaseName = "sessions" +const RemotesDirBaseName = "remotes" + +var SessionDirCache = make(map[string]string) +var BaseLock = &sync.Mutex{} func GetScHomeDir() string { scHome := os.Getenv(ScHomeVarName) @@ -19,3 +28,66 @@ func GetScHomeDir() string { } return scHome } + +func EnsureSessionDir(sessionId string) (string, error) { + BaseLock.Lock() + sdir, ok := SessionDirCache[sessionId] + BaseLock.Unlock() + if ok { + return sdir, nil + } + scHome := GetScHomeDir() + sdir = path.Join(scHome, SessionsDirBaseName, sessionId) + err := ensureDir(sdir) + if err != nil { + return "", err + } + BaseLock.Lock() + SessionDirCache[sessionId] = sdir + BaseLock.Unlock() + return sdir, nil +} + +func ensureDir(dirName string) error { + info, err := os.Stat(dirName) + if errors.Is(err, fs.ErrNotExist) { + err = os.MkdirAll(dirName, 0700) + if err != nil { + return err + } + info, err = os.Stat(dirName) + } + if err != nil { + return err + } + if !info.IsDir() { + return fmt.Errorf("'%s' must be a directory", dirName) + } + return nil +} + +func PtyOutFile(sessionId string, cmdId string) (string, error) { + sdir, err := EnsureSessionDir(sessionId) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%s.ptyout", sdir, cmdId), nil +} + +func RunOutFile(sessionId string, cmdId string) (string, error) { + sdir, err := EnsureSessionDir(sessionId) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%s.runout", sdir, cmdId), nil +} + +func RemotePtyOut(remoteId string) (string, error) { + scHome := GetScHomeDir() + rdir := path.Join(scHome, RemotesDirBaseName) + err := ensureDir(rdir) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%s.ptyout", rdir, remoteId), nil +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 71b13d0ca..54904d403 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -22,7 +22,7 @@ func NumSessions(ctx context.Context) (int, error) { return count, nil } -const remoteSelectCols = "rowid, remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts" +const remoteSelectCols = "remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts" func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { db, err := GetDB() @@ -76,23 +76,16 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if remote == nil { return fmt.Errorf("cannot insert nil remote") } - if remote.RowId != 0 { - return fmt.Errorf("cannot insert a remote that already has rowid set, rowid=%d", remote.RowId) - } db, err := GetDB() if err != nil { return err } - query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts, ptyout) VALUES - (:remoteid,:remotetype,:remotename,:autoconnect,:sshhost,:sshopts,:sshidentity,:sshuser, 0 , '')` - result, err := db.NamedExec(query, remote) + query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts) VALUES + (:remoteid,:remotetype,:remotename,:autoconnect,:sshhost,:sshopts,:sshidentity,:sshuser, 0 )` + _, err = db.NamedExec(query, remote) if err != nil { return err } - remote.RowId, err = result.LastInsertId() - if err != nil { - return fmt.Errorf("cannot get lastinsertid from insert remote: %w", err) - } return nil } diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go new file mode 100644 index 000000000..5971a8996 --- /dev/null +++ b/pkg/sstore/fileops.go @@ -0,0 +1,24 @@ +package sstore + +import ( + "context" + "os" + + "github.com/scripthaus-dev/sh2-server/pkg/scbase" +) + +func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte) error { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return err + } + fd, err := os.OpenFile(ptyOutFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + _, err = fd.Write(data) + if err != nil { + return err + } + return nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 967f8dc46..4346244e6 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -111,7 +111,6 @@ type LineType struct { } type RemoteType struct { - RowId int64 `json:"rowid"` RemoteId string `json:"remoteid"` RemoteType string `json:"remotetype"` RemoteName string `json:"remotename"` @@ -128,7 +127,6 @@ type RemoteType struct { } type CmdType struct { - RowId int64 `json:"rowid"` SessionId string `json:"sessionid"` CmdId string `json:"cmdid"` RSId string `json:"rsid"` From d4acb7958525c5af681b4e1b32ecab7be55b61d4 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 6 Jul 2022 22:46:22 -0700 Subject: [PATCH 027/397] data getting written to ptyout files in scripthaus dir --- cmd/main-server.go | 18 +++++++++++++++++- pkg/remote/remote.go | 45 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index b8541e117..14f77cfdc 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -20,6 +20,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/cmdtail" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" @@ -333,7 +334,22 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid"))) return } - pathStr := GetPtyOutFile(sessionId, cmdId) + if _, err := uuid.Parse(sessionId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid sessionid: %v", err))) + return + } + if _, err := uuid.Parse(cmdId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) + return + } + pathStr, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("cannot get ptyout file name: %v", err))) + return + } fd, err := os.Open(pathStr) if err != nil { if errors.Is(err, fs.ErrNotExist) { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index af773dbcb..884669ee8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -2,6 +2,7 @@ package remote import ( "context" + "encoding/base64" "errors" "fmt" "os/exec" @@ -132,7 +133,6 @@ func (msh *MShellProc) Launch() { return } msh.ServerProc = cproc - fmt.Printf("START MAKECLIENTPROC: %#v\n", msh.ServerProc.InitPk) msh.Status = StatusConnected go func() { exitErr := cproc.Cmd.Wait() @@ -170,7 +170,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, Term: DefaultTerm} runPacket.Command = strings.TrimSpace(pk.CmdStr) - fmt.Printf("RUN-CMD> %s\n", runPacket.CK) + fmt.Printf("RUN-CMD> %s reqid=%s (msh=%v)\n", runPacket.CK, runPacket.ReqId, msh.Remote) msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { @@ -218,6 +218,17 @@ func (runner *MShellProc) WithLock(fn func()) { fn() } +func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *packet.DataAckPacketType { + ack := packet.MakeDataAckPacket() + ack.CK = ck + ack.FdNum = fdNum + ack.AckLen = ackLen + if err != nil { + ack.Error = err.Error() + } + return ack +} + func (runner *MShellProc) ProcessPackets() { defer runner.WithLock(func() { if runner.Status == StatusConnected { @@ -225,10 +236,27 @@ func (runner *MShellProc) ProcessPackets() { } }) for pk := range runner.ServerProc.Output.MainCh { - fmt.Printf("MSH> %s | %#v\n", packet.AsString(pk), pk) if pk.GetType() == packet.DataPacketStr { - dataPacket := pk.(*packet.DataPacketType) - fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPacket.CK, dataPacket.FdNum, packet.B64DecodedLen(dataPacket.Data64), dataPacket.Eof, dataPacket.Error) + dataPk := pk.(*packet.DataPacketType) + realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) + if err != nil { + ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) + runner.ServerProc.Input.SendPacket(ack) + continue + } + var ack *packet.DataAckPacketType + if len(realData) > 0 { + err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData) + if err != nil { + ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) + } else { + ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) + } + } + if ack != nil { + runner.ServerProc.Input.SendPacket(ack) + } + fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) continue } if pk.GetType() == packet.CmdDataPacketStr { @@ -251,6 +279,11 @@ func (runner *MShellProc) ProcessPackets() { fmt.Printf("stderr> %s\n", rawPacket.Data) continue } - fmt.Printf("runner-packet: %v\n", pk) + if pk.GetType() == packet.CmdStartPacketStr { + startPk := pk.(*packet.CmdStartPacketType) + fmt.Printf("start> reqid=%s (%p)\n", startPk.RespId, runner.ServerProc.Output) + continue + } + fmt.Printf("MSH> %s\n", packet.AsString(pk)) } } From f7666fe480fd2b8855952d357c3662761f47473b Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 00:10:37 -0700 Subject: [PATCH 028/397] checkpoint on storing cmd in db --- cmd/main-server.go | 9 ++- db/migrations/000001_init.down.sql | 2 +- db/migrations/000001_init.up.sql | 13 ++-- pkg/remote/remote.go | 34 +++++++-- pkg/sstore/dbops.go | 43 +++++++++++ pkg/sstore/sstore.go | 110 +++++++++++++++++++++++++---- pkg/sstore/txwrap.go | 22 ++++++ 7 files changed, 204 insertions(+), 29 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 14f77cfdc..5d2316223 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -459,12 +459,15 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp if err != nil { return nil, err } - startPk, err := remote.RunCommand(ctx, pk, rtnLine.CmdId) + cmd, err := remote.RunCommand(ctx, pk, rtnLine.CmdId) if err != nil { return nil, err } - fmt.Printf("START CMD: %s\n", packet.AsString(startPk)) - return &runCommandResponse{Line: rtnLine}, nil + err = sstore.InsertCmd(ctx, cmd) + if err != nil { + return nil, err + } + return &runCommandResponse{Line: rtnLine, Cmd: cmd}, nil } // /api/start-session diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql index 6887fccdc..f523b435e 100644 --- a/db/migrations/000001_init.down.sql +++ b/db/migrations/000001_init.down.sql @@ -3,5 +3,5 @@ DROP TABLE window; DROP TABLE remote_instance; DROP TABLE line; DROP TABLE remote; -DROP TABLE session_cmd; +DROP TABLE cmd; DROP TABLE history; diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index ea0438427..6d54ab367 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -52,18 +52,17 @@ CREATE TABLE remote ( lastconnectts bigint NOT NULL ); -CREATE TABLE session_cmd ( +CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, - rsid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, + cmdstr text NOT NULL, remotestate json NOT NULL, + termopts json NOT NULL, status varchar(10) NOT NULL, - startts bigint NOT NULL, - pid int NOT NULL, - runnerpid int NOT NULL, - donets bigint NOT NULL, - exitcode int NOT NULL, + startpk json NOT NULL, + donepk json NOT NULL, + runout json NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 884669ee8..d47fc0e2e 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -154,7 +154,15 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } -func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId string) (*packet.CmdStartPacketType, error) { +func convertRemoteState(rs scpacket.RemoteState) sstore.RemoteState { + return sstore.RemoteState{Cwd: rs.Cwd} +} + +func makeTermOpts() sstore.TermOpts { + return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true} +} + +func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId string) (*sstore.CmdType, error) { msh := GetRemoteById(pk.RemoteState.RemoteId) if msh == nil { return nil, fmt.Errorf("no remote id=%s found", pk.RemoteState.RemoteId) @@ -177,15 +185,29 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str return nil, fmt.Errorf("sending run packet to remote: %w", err) } rtnPk := msh.ServerProc.Output.WaitForResponse(ctx, runPacket.ReqId) - if startPk, ok := rtnPk.(*packet.CmdStartPacketType); ok { - return startPk, nil - } - if respPk, ok := rtnPk.(*packet.ResponsePacketType); ok { + startPk, ok := rtnPk.(*packet.CmdStartPacketType) + if !ok { + respPk, ok := rtnPk.(*packet.ResponsePacketType) + if !ok { + return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) + } if respPk.Error != "" { return nil, errors.New(respPk.Error) } + return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } - return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) + cmd := &sstore.CmdType{ + SessionId: pk.SessionId, + CmdId: startPk.CK.GetCmdId(), + RemoteId: msh.Remote.RemoteId, + RemoteState: convertRemoteState(pk.RemoteState), + TermOpts: makeTermOpts(), + Status: "running", + StartPk: startPk, + DonePk: nil, + RunOut: nil, + } + return cmd, nil } func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 54904d403..d6b07262b 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -103,6 +103,11 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { tx.SelectWrap(&session.Windows, query, session.SessionId) query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) + query = `SELECT * FROM cmd WHERE sessionid = ?` + marr := tx.SelectMaps(query, session.SessionId) + for _, m := range marr { + session.Cmds = append(session.Cmds, CmdFromMap(m)) + } return nil }) if err != nil { @@ -191,3 +196,41 @@ func InsertLine(ctx context.Context, line *LineType) error { return nil }) } + +func InsertCmd(ctx context.Context, cmd *CmdType) error { + if cmd == nil { + return fmt.Errorf("cmd cannot be nil") + } + return WithTx(ctx, func(tx *TxWrap) error { + var sessionId string + query := `SELECT sessionid FROM session WHERE sessionid = ?` + hasSession := tx.GetWrap(&sessionId, query, cmd.SessionId) + if !hasSession { + return fmt.Errorf("session not found, cannot insert cmd") + } + cmdMap := cmd.ToMap() + query = ` +INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout) + VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout) +` + tx.NamedExecWrap(query, cmdMap) + return nil + }) +} + +func GetCmd(ctx context.Context, sessionId string, cmdId string) (*CmdType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + var m map[string]interface{} + query := `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` + err = db.GetContext(ctx, &m, query, sessionId, cmdId) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return CmdFromMap(m), nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 4346244e6..63f632524 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -89,6 +89,30 @@ func (s *RemoteState) Value() (driver.Value, error) { return json.Marshal(s) } +type TermOpts struct { + Rows int `json:"rows"` + Cols int `json:"cols"` + FlexRows bool `json:"flexrows,omitempty"` +} + +func (opts *TermOpts) Scan(val interface{}) error { + if strVal, ok := val.(string); ok { + if strVal == "" { + return nil + } + err := json.Unmarshal([]byte(strVal), opts) + if err != nil { + return err + } + return nil + } + return fmt.Errorf("cannot scan '%T' into TermOpts", val) +} + +func (opts *TermOpts) Value() (driver.Value, error) { + return json.Marshal(opts) +} + type RemoteInstance struct { RIId string `json:"riid"` Name string `json:"name"` @@ -127,19 +151,81 @@ type RemoteType struct { } type CmdType struct { - SessionId string `json:"sessionid"` - CmdId string `json:"cmdid"` - RSId string `json:"rsid"` - RemoteId string `json:"remoteid"` - RemoteState string `json:"remotestate"` - Status string `json:"status"` - StartTs int64 `json:"startts"` - DoneTs int64 `json:"donets"` - Pid int `json:"pid"` - RunnerPid int `json:"runnerpid"` - ExitCode int `json:"exitcode"` + SessionId string `json:"sessionid"` + CmdId string `json:"cmdid"` + RemoteId string `json:"remoteid"` + CmdStr string `json:"cmdstr"` + RemoteState RemoteState `json:"remotestate"` + TermOpts TermOpts `json:"termopts"` + Status string `json:"status"` + StartPk *packet.CmdStartPacketType `json:"startpk"` + DonePk *packet.CmdDonePacketType `json:"donepk"` + RunOut []packet.PacketType `json:"runout"` +} - RunOut packet.PacketType `json:"runout"` +func quickJson(v interface{}) string { + if v == nil { + return "" + } + barr, _ := json.Marshal(v) + return string(barr) +} + +func (cmd *CmdType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["sessionid"] = cmd.SessionId + rtn["cmdid"] = cmd.CmdId + rtn["remoteid"] = cmd.RemoteId + rtn["cmdstr"] = cmd.CmdStr + rtn["remotestate"] = quickJson(cmd.RemoteState) + rtn["termopts"] = quickJson(cmd.TermOpts) + rtn["status"] = cmd.Status + rtn["startpk"] = quickJson(cmd.StartPk) + rtn["donepk"] = quickJson(cmd.DonePk) + rtn["runout"] = quickJson(cmd.RunOut) + return rtn +} + +func quickSetStr(strVal *string, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + *strVal = str +} + +func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + if str == "" { + return + } + json.Unmarshal([]byte(str), ptr) +} + +func CmdFromMap(m map[string]interface{}) *CmdType { + var cmd CmdType + quickSetStr(&cmd.SessionId, m, "sessionid") + quickSetStr(&cmd.CmdId, m, "cmdid") + quickSetStr(&cmd.RemoteId, m, "remoteid") + quickSetStr(&cmd.CmdStr, m, "cmdstr") + quickSetJson(&cmd.RemoteState, m, "remotestate") + quickSetJson(&cmd.TermOpts, m, "termopts") + quickSetStr(&cmd.Status, m, "status") + quickSetJson(&cmd.StartPk, m, "startpk") + quickSetJson(&cmd.DonePk, m, "donepk") + quickSetJson(&cmd.RunOut, m, "runout") + return &cmd } func makeNewLineCmd(sessionId string, windowId string, userId string) *LineType { diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index b58b8442d..2bac4030f 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -90,3 +90,25 @@ func (tx *TxWrap) SelectWrap(dest interface{}, query string, args ...interface{} } return } + +func (tx *TxWrap) SelectMaps(query string, args ...interface{}) []map[string]interface{} { + if tx.Err != nil { + return nil + } + rows, err := tx.Txx.Queryx(query, args...) + if err != nil { + tx.Err = err + return nil + } + var rtn []map[string]interface{} + for rows.Next() { + m := make(map[string]interface{}) + err = rows.MapScan(m) + if err != nil { + tx.Err = err + return nil + } + rtn = append(rtn, m) + } + return rtn +} From e4bf4b4ef8fb435c70719209797e543010f9d66a Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 13:26:46 -0700 Subject: [PATCH 029/397] checkpoint, pass remote vars to client for prompt str, get commands showing --- db/migrations/000001_init.up.sql | 10 +--- pkg/remote/remote.go | 11 ++++ pkg/sstore/dbops.go | 85 +++++++++++++--------------- pkg/sstore/quick.go | 68 +++++++++++++++++++++++ pkg/sstore/sstore.go | 95 ++++++++++++++++++-------------- pkg/sstore/txwrap.go | 17 ++++++ 6 files changed, 189 insertions(+), 97 deletions(-) create mode 100644 pkg/sstore/quick.go diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 6d54ab367..8796f3028 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -41,14 +41,8 @@ CREATE TABLE remote ( remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, autoconnect boolean NOT NULL, - - -- ssh specific opts - sshhost varchar(300) NOT NULL, - sshopts varchar(300) NOT NULL, - sshidentity varchar(300) NOT NULL, - sshuser varchar(100) NOT NULL, - - -- runtime data + initpk json NOT NULL, + sshopts json NOT NULL, lastconnectts bigint NOT NULL ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d47fc0e2e..b954b01f9 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -40,6 +40,7 @@ type RemoteState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` RemoteName string `json:"remotename"` + RemoteVars map[string]string `json:"remotevars"` Status string `json:"status"` DefaultState *sstore.RemoteState `json:"defaultstate"` } @@ -102,9 +103,18 @@ func GetAllRemoteState() []RemoteState { RemoteName: proc.Remote.RemoteName, Status: proc.Status, } + vars := make(map[string]string) + vars["user"], vars["host"] = proc.Remote.GetUserHost() if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{Cwd: proc.ServerProc.InitPk.HomeDir} + vars["home"] = proc.ServerProc.InitPk.HomeDir + vars["remoteuser"] = proc.ServerProc.InitPk.User + vars["remotehost"] = proc.ServerProc.InitPk.HostName + if proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.SSHHost == "" { + vars["local"] = "1" + } } + state.RemoteVars = vars rtn = append(rtn, state) } return rtn @@ -199,6 +209,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str cmd := &sstore.CmdType{ SessionId: pk.SessionId, CmdId: startPk.CK.GetCmdId(), + CmdStr: runPacket.Command, RemoteId: msh.Remote.RemoteId, RemoteState: convertRemoteState(pk.RemoteState), TermOpts: makeTermOpts(), diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d6b07262b..108cf39d1 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -22,54 +22,48 @@ func NumSessions(ctx context.Context) (int, error) { return count, nil } -const remoteSelectCols = "remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts" - func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { - db, err := GetDB() + var rtn []*RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote` + marr := tx.SelectMaps(query) + for _, m := range marr { + rtn = append(rtn, RemoteFromMap(m)) + } + return nil + }) if err != nil { return nil, err } - query := fmt.Sprintf(`SELECT %s FROM remote`, remoteSelectCols) - var remoteArr []*RemoteType - err = db.SelectContext(ctx, &remoteArr, query) - if err != nil { - return nil, err - } - return remoteArr, nil + return rtn, nil } func GetRemoteByName(ctx context.Context, remoteName string) (*RemoteType, error) { - db, err := GetDB() + var remote *RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote WHERE remotename = ?` + m := tx.GetMap(query, remoteName) + remote = RemoteFromMap(m) + return nil + }) if err != nil { return nil, err } - query := fmt.Sprintf(`SELECT %s FROM remote WHERE remotename = ?`, remoteSelectCols) - var remote RemoteType - err = db.GetContext(ctx, &remote, query, remoteName) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - return &remote, nil + return remote, nil } func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { - db, err := GetDB() + var remote *RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote WHERE remoteid = ?` + m := tx.GetMap(query, remoteId) + remote = RemoteFromMap(m) + return nil + }) if err != nil { return nil, err } - query := fmt.Sprintf(`SELECT %s FROM remote WHERE remoteid = ?`, remoteSelectCols) - var remote RemoteType - err = db.GetContext(ctx, &remote, query, remoteId) - if err == sql.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - return &remote, nil + return remote, nil } func InsertRemote(ctx context.Context, remote *RemoteType) error { @@ -80,9 +74,9 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, sshhost, sshopts, sshidentity, sshuser, lastconnectts) VALUES - (:remoteid,:remotetype,:remotename,:autoconnect,:sshhost,:sshopts,:sshidentity,:sshuser, 0 )` - _, err = db.NamedExec(query, remote) + query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, initpk, sshopts, lastconnectts) VALUES + (:remoteid,:remotetype,:remotename,:autoconnect,:initpk,:sshopts,:lastconnectts)` + _, err = db.NamedExec(query, remote.ToMap()) if err != nil { return err } @@ -218,19 +212,16 @@ INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, st }) } -func GetCmd(ctx context.Context, sessionId string, cmdId string) (*CmdType, error) { - db, err := GetDB() +func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, error) { + var cmd *CmdType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` + m := tx.GetMap(query, sessionId, cmdId) + cmd = CmdFromMap(m) + return nil + }) if err != nil { return nil, err } - var m map[string]interface{} - query := `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - err = db.GetContext(ctx, &m, query, sessionId, cmdId) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - return CmdFromMap(m), nil + return cmd, nil } diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go new file mode 100644 index 000000000..a4b8aaef1 --- /dev/null +++ b/pkg/sstore/quick.go @@ -0,0 +1,68 @@ +package sstore + +import "encoding/json" + +func quickSetStr(strVal *string, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + *strVal = str +} + +func quickSetInt64(ival *int64, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlInt, ok := v.(int64) + if !ok { + return + } + *ival = sqlInt +} + +func quickSetBool(bval *bool, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlInt, ok := v.(int64) + if ok { + if sqlInt > 0 { + *bval = true + } + return + } + sqlBool, ok := v.(bool) + if ok { + *bval = sqlBool + } +} + +func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + if str == "" { + return + } + json.Unmarshal([]byte(str), ptr) +} + +func quickJson(v interface{}) string { + if v == nil { + return "" + } + barr, _ := json.Marshal(v) + return string(barr) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 63f632524..bca56f4f5 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "path" + "strings" "sync" "time" @@ -134,20 +135,35 @@ type LineType struct { CmdId string `json:"cmdid,omitempty"` } -type RemoteType struct { - RemoteId string `json:"remoteid"` - RemoteType string `json:"remotetype"` - RemoteName string `json:"remotename"` - AutoConnect bool `json:"autoconnect"` - - // type=ssh options +type SSHOpts struct { SSHHost string `json:"sshhost"` - SSHOpts string `json:"sshopts"` + SSHOptsStr string `json:"sshopts"` SSHIdentity string `json:"sshidentity"` SSHUser string `json:"sshuser"` +} - // runtime data - LastConnectTs int64 `json:"lastconnectts"` +type RemoteType struct { + RemoteId string `json:"remoteid"` + RemoteType string `json:"remotetype"` + RemoteName string `json:"remotename"` + AutoConnect bool `json:"autoconnect"` + InitPk *packet.InitPacketType `json:"inipk"` + SSHOpts *SSHOpts `json:"sshopts"` + LastConnectTs int64 `json:"lastconnectts"` +} + +func (r *RemoteType) GetUserHost() (string, string) { + if r.SSHOpts == nil { + return "", "" + } + if r.SSHOpts.SSHUser != "" { + return r.SSHOpts.SSHUser, r.SSHOpts.SSHHost + } + atIdx := strings.Index(r.SSHOpts.SSHHost, "@") + if atIdx == -1 { + return "", r.SSHOpts.SSHHost + } + return r.SSHOpts.SSHHost[0:atIdx], r.SSHOpts.SSHHost[atIdx+1:] } type CmdType struct { @@ -163,12 +179,31 @@ type CmdType struct { RunOut []packet.PacketType `json:"runout"` } -func quickJson(v interface{}) string { - if v == nil { - return "" +func (r *RemoteType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["remoteid"] = r.RemoteId + rtn["remotetype"] = r.RemoteType + rtn["remotename"] = r.RemoteName + rtn["autoconnect"] = r.AutoConnect + rtn["initpk"] = quickJson(r.InitPk) + rtn["sshopts"] = quickJson(r.SSHOpts) + rtn["lastconnectts"] = r.LastConnectTs + return rtn +} + +func RemoteFromMap(m map[string]interface{}) *RemoteType { + if len(m) == 0 { + return nil } - barr, _ := json.Marshal(v) - return string(barr) + var r RemoteType + quickSetStr(&r.RemoteId, m, "remoteid") + quickSetStr(&r.RemoteType, m, "remotetype") + quickSetStr(&r.RemoteName, m, "remotename") + quickSetBool(&r.AutoConnect, m, "autoconnect") + quickSetJson(&r.InitPk, m, "initpk") + quickSetJson(&r.SSHOpts, m, "sshopts") + quickSetInt64(&r.LastConnectTs, m, "lastconnectts") + return &r } func (cmd *CmdType) ToMap() map[string]interface{} { @@ -186,34 +221,10 @@ func (cmd *CmdType) ToMap() map[string]interface{} { return rtn } -func quickSetStr(strVal *string, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - str, ok := v.(string) - if !ok { - return - } - *strVal = str -} - -func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - str, ok := v.(string) - if !ok { - return - } - if str == "" { - return - } - json.Unmarshal([]byte(str), ptr) -} - func CmdFromMap(m map[string]interface{}) *CmdType { + if len(m) == 0 { + return nil + } var cmd CmdType quickSetStr(&cmd.SessionId, m, "sessionid") quickSetStr(&cmd.CmdId, m, "cmdid") diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 2bac4030f..43734e639 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -112,3 +112,20 @@ func (tx *TxWrap) SelectMaps(query string, args ...interface{}) []map[string]int } return rtn } + +func (tx *TxWrap) GetMap(query string, args ...interface{}) map[string]interface{} { + if tx.Err != nil { + return nil + } + row := tx.Txx.QueryRowx(query, args...) + m := make(map[string]interface{}) + err := row.MapScan(m) + if err != nil { + if err == sql.ErrNoRows { + return nil + } + tx.Err = err + return nil + } + return m +} From 4cc55c46caf14a2d61faa83047bca4366f7788c1 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 16:29:14 -0700 Subject: [PATCH 030/397] only allow one instance of sh2 to run at a time (flock). HUP all running processes when sh2 starts or remote connection ends --- cmd/main-server.go | 13 +++++++++++-- pkg/remote/remote.go | 36 +++++++++++++++++++++++++++++++++--- pkg/scbase/scbase.go | 18 ++++++++++++++++++ pkg/sstore/dbops.go | 39 +++++++++++++++++++++++++++++++++++++++ pkg/sstore/sstore.go | 6 ++++++ 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 5d2316223..1656ce01f 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -572,6 +572,12 @@ func runWebSocketServer() { } func main() { + scLock, err := scbase.AcquireSCLock() + if err != nil || scLock == nil { + fmt.Printf("[error] cannot acquire sh2 lock: %v\n", err) + return + } + if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") { err := sstore.MigrateCommandOpts(os.Args[1:]) if err != nil { @@ -579,7 +585,7 @@ func main() { } return } - err := sstore.TryMigrateUp() + err = sstore.TryMigrateUp() if err != nil { fmt.Printf("[error] %v\n", err) return @@ -601,7 +607,10 @@ func main() { return } - sstore.AppendToCmdPtyBlob(context.Background(), "", "", nil) + err = sstore.HangupAllRunningCmds(context.Background()) + if err != nil { + fmt.Printf("[error] calling HUP on all running commands\n") + } go runWebSocketServer() gr := mux.NewRouter() diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index b954b01f9..bb7726cdc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -152,6 +152,7 @@ func (msh *MShellProc) Launch() { msh.Status = StatusDisconnected } }) + fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) }() go msh.ProcessPackets() @@ -206,6 +207,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str } return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } + status := sstore.CmdStatusRunning + if runPacket.Detached { + status = sstore.CmdStatusDetached + } cmd := &sstore.CmdType{ SessionId: pk.SessionId, CmdId: startPk.CK.GetCmdId(), @@ -213,7 +218,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str RemoteId: msh.Remote.RemoteId, RemoteState: convertRemoteState(pk.RemoteState), TermOpts: makeTermOpts(), - Status: "running", + Status: status, StartPk: startPk, DonePk: nil, RunOut: nil, @@ -262,11 +267,33 @@ func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *pa return ack } +func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { + err := sstore.UpdateCmdDonePk(context.Background(), donePk) + if err != nil { + fmt.Printf("[error] updating cmddone: %v\n", err) + return + } + return +} + +func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { + err := sstore.AppendCmdErrorPk(context.Background(), errPk) + if err != nil { + fmt.Printf("[error] adding cmderr: %v\n", err) + return + } + return +} + func (runner *MShellProc) ProcessPackets() { defer runner.WithLock(func() { if runner.Status == StatusConnected { runner.Status = StatusDisconnected } + err := sstore.HangupRunningCmdsByRemoteId(context.Background(), runner.Remote.RemoteId) + if err != nil { + fmt.Printf("[error] calling HUP on remoteid=%d cmds\n", runner.Remote.RemoteId) + } }) for pk := range runner.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { @@ -298,8 +325,11 @@ func (runner *MShellProc) ProcessPackets() { continue } if pk.GetType() == packet.CmdDonePacketStr { - donePacket := pk.(*packet.CmdDonePacketType) - fmt.Printf("cmd-done %s\n", donePacket.CK) + runner.handleCmdDonePacket(pk.(*packet.CmdDonePacketType)) + continue + } + if pk.GetType() == packet.CmdErrorPacketStr { + runner.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType)) continue } if pk.GetType() == packet.MessagePacketStr { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 27e147062..8578ed328 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -7,12 +7,15 @@ import ( "os" "path" "sync" + + "golang.org/x/sys/unix" ) const HomeVarName = "HOME" const ScHomeVarName = "SCRIPTHAUS_HOME" const SessionsDirBaseName = "sessions" const RemotesDirBaseName = "remotes" +const SCLockFile = "sh2.lock" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} @@ -29,6 +32,21 @@ func GetScHomeDir() string { return scHome } +func AcquireSCLock() (*os.File, error) { + homeDir := GetScHomeDir() + lockFileName := path.Join(homeDir, SCLockFile) + fd, err := os.Create(lockFileName) + if err != nil { + return nil, err + } + err = unix.Flock(int(fd.Fd()), unix.LOCK_EX|unix.LOCK_NB) + if err != nil { + fd.Close() + return nil, err + } + return fd, nil +} + func EnsureSessionDir(sessionId string) (string, error) { BaseLock.Lock() sdir, ok := SessionDirCache[sessionId] diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 108cf39d1..f957a3cd4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/packet" ) func NumSessions(ctx context.Context) (int, error) { @@ -225,3 +226,41 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, } return cmd, nil } + +func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) error { + if donePk == nil || donePk.CK.IsEmpty() { + return fmt.Errorf("invalid cmddone packet (no ck)") + } + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET status = ?, donepk = ? WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, CmdStatusDone, quickJson(donePk), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + return nil + }) +} + +func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { + if errPk == nil || errPk.CK.IsEmpty() { + return fmt.Errorf("invalid cmderror packet (no ck)") + } + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, quickJson(errPk), errPk.CK.GetSessionId(), errPk.CK.GetCmdId()) + return nil + }) +} + +func HangupAllRunningCmds(ctx context.Context) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET status = ? WHERE status = ?` + tx.ExecWrap(query, CmdStatusHangup, CmdStatusRunning) + 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 = ?` + tx.ExecWrap(query, CmdStatusHangup, CmdStatusRunning, remoteId) + return nil + }) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index bca56f4f5..c867c8e4d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -33,6 +33,12 @@ const LocalRemoteName = "local" const DefaultCwd = "~" +const CmdStatusRunning = "running" +const CmdStatusDetached = "detached" +const CmdStatusError = "error" +const CmdStatusDone = "done" +const CmdStatusHangup = "hangup" + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error From 45dfeb69f6c8bfc1750878642148263493ef6700 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 21:39:25 -0700 Subject: [PATCH 031/397] updates to allow cmd tailing to work with mshell --- cmd/main-server.go | 26 ++++++++++++++++---------- pkg/remote/remote.go | 6 +++++- pkg/scbase/scbase.go | 13 +++++++++++++ pkg/sstore/dbops.go | 25 ++++++------------------- pkg/sstore/fileops.go | 3 +++ pkg/sstore/sstore.go | 13 ++++++------- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 1656ce01f..21cdc332a 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -87,7 +87,8 @@ func MakeWSState(clientId string) (*WSState, error) { rtn.ConnectTime = time.Now() rtn.PacketCh = make(chan packet.PacketType, WSStatePacketChSize) chSender := packet.MakeChannelPacketSender(rtn.PacketCh) - rtn.Tailer, err = cmdtail.MakeTailer(chSender) + gen := scbase.ScFileNameGenerator{ScHome: scbase.GetScHomeDir()} + rtn.Tailer, err = cmdtail.MakeTailer(chSender, gen) if err != nil { return nil, err } @@ -198,9 +199,18 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { continue } if pk.GetType() == "getcmd" { - err = state.Tailer.AddWatch(pk.(*packet.GetCmdPacketType)) + getPk := pk.(*packet.GetCmdPacketType) + done, err := state.Tailer.AddWatch(getPk) if err != nil { + // TODO: send responseerror + respPk := packet.MakeErrorResponsePacket(getPk.ReqId, err) fmt.Printf("[error] adding watch to tailer: %v\n", err) + fmt.Printf("%v\n", respPk) + } + if done { + respPk := packet.MakeResponsePacket(getPk.ReqId, true) + fmt.Printf("%v\n", respPk) + // TODO: send response } continue } @@ -455,15 +465,12 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp fmt.Printf("GOT cd RESP: %v\n", resp) return nil, nil } - rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId) + cmdId := uuid.New().String() + cmd, err := remote.RunCommand(ctx, pk, cmdId) if err != nil { return nil, err } - cmd, err := remote.RunCommand(ctx, pk, rtnLine.CmdId) - if err != nil { - return nil, err - } - err = sstore.InsertCmd(ctx, cmd) + rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, cmd) if err != nil { return nil, err } @@ -595,12 +602,11 @@ func main() { fmt.Printf("[error] ensuring local remote: %v\n", err) return } - defaultSession, err := sstore.EnsureDefaultSession(context.Background()) + _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) return } - fmt.Printf("session: %v\n", defaultSession) err = remote.LoadRemotes(context.Background()) if err != nil { fmt.Printf("[error] loading remotes: %v\n", err) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index bb7726cdc..5941233ab 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -223,6 +223,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str DonePk: nil, RunOut: nil, } + err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil) + if err != nil { + return nil, err + } return cmd, nil } @@ -316,7 +320,7 @@ func (runner *MShellProc) ProcessPackets() { if ack != nil { runner.ServerProc.Input.SendPacket(ack) } - fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) + // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) continue } if pk.GetType() == packet.CmdDataPacketStr { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 8578ed328..f30b53092 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -8,6 +8,7 @@ import ( "path" "sync" + "github.com/scripthaus-dev/mshell/pkg/base" "golang.org/x/sys/unix" ) @@ -109,3 +110,15 @@ func RemotePtyOut(remoteId string) (string, error) { } return fmt.Sprintf("%s/%s.ptyout", rdir, remoteId), nil } + +type ScFileNameGenerator struct { + ScHome string +} + +func (g ScFileNameGenerator) PtyOutFile(ck base.CommandKey) string { + return path.Join(g.ScHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".ptyout") +} + +func (g ScFileNameGenerator) RunOutFile(ck base.CommandKey) string { + return path.Join(g.ScHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".runout") +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f957a3cd4..23d2b5af6 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -167,7 +167,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string) error { }) } -func InsertLine(ctx context.Context, line *LineType) error { +func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line == nil { return fmt.Errorf("line cannot be nil") } @@ -188,27 +188,14 @@ func InsertLine(ctx context.Context, line *LineType) error { query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid) VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid)` tx.NamedExecWrap(query, line) - return nil - }) -} - -func InsertCmd(ctx context.Context, cmd *CmdType) error { - if cmd == nil { - return fmt.Errorf("cmd cannot be nil") - } - return WithTx(ctx, func(tx *TxWrap) error { - var sessionId string - query := `SELECT sessionid FROM session WHERE sessionid = ?` - hasSession := tx.GetWrap(&sessionId, query, cmd.SessionId) - if !hasSession { - return fmt.Errorf("session not found, cannot insert cmd") - } - cmdMap := cmd.ToMap() - query = ` + if cmd != nil { + cmdMap := cmd.ToMap() + query = ` INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout) VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout) ` - tx.NamedExecWrap(query, cmdMap) + tx.NamedExecWrap(query, cmdMap) + } return nil }) } diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 5971a8996..67026ba49 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -16,6 +16,9 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat if err != nil { return err } + if len(data) == 0 { + return nil + } _, err = fd.Write(data) if err != nil { return err diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c867c8e4d..3f50ee08f 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -11,7 +11,6 @@ import ( "sync" "time" - "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -245,14 +244,14 @@ func CmdFromMap(m map[string]interface{}) *CmdType { return &cmd } -func makeNewLineCmd(sessionId string, windowId string, userId string) *LineType { +func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId rtn.Ts = time.Now().UnixMilli() rtn.UserId = userId rtn.LineType = LineTypeCmd - rtn.CmdId = uuid.New().String() + rtn.CmdId = cmdId return rtn } @@ -269,16 +268,16 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri func AddCommentLine(ctx context.Context, sessionId string, windowId string, userId string, commentText string) (*LineType, error) { rtnLine := makeNewLineText(sessionId, windowId, userId, commentText) - err := InsertLine(ctx, rtnLine) + err := InsertLine(ctx, rtnLine, nil) if err != nil { return nil, err } return rtnLine, nil } -func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string) (*LineType, error) { - rtnLine := makeNewLineCmd(sessionId, windowId, userId) - err := InsertLine(ctx, rtnLine) +func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string, cmd *CmdType) (*LineType, error) { + rtnLine := makeNewLineCmd(sessionId, windowId, userId, cmd.CmdId) + err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err } From 5fcbe209bb193b09f70e616b181da6520cc9f5a6 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 22:13:45 -0700 Subject: [PATCH 032/397] implement globalstore callback for cmd status changes --- pkg/remote/remote.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 5941233ab..d6cfda2bd 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -32,8 +32,9 @@ const ( var GlobalStore *Store type Store struct { - Lock *sync.Mutex - Map map[string]*MShellProc // key=remoteid + Lock *sync.Mutex + Map map[string]*MShellProc // key=remoteid + CmdStatusCallback func(ck base.CommandKey, status string) } type RemoteState struct { @@ -53,6 +54,8 @@ type MShellProc struct { Status string ServerProc *shexec.ClientProc Err error + + RunningCmds []base.CommandKey } func LoadRemotes(ctx context.Context) error { @@ -227,9 +230,16 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str if err != nil { return nil, err } + msh.AddRunningCmd(startPk.CK) return cmd, nil } +func (msh *MShellProc) AddRunningCmd(ck base.CommandKey) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.RunningCmds = append(msh.RunningCmds, ck) +} + func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { if !msh.IsConnected() { return nil, fmt.Errorf("runner is not connected") @@ -277,6 +287,9 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { fmt.Printf("[error] updating cmddone: %v\n", err) return } + if GlobalStore.CmdStatusCallback != nil { + GlobalStore.CmdStatusCallback(donePk.CK, sstore.CmdStatusDone) + } return } @@ -289,6 +302,15 @@ func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { return } +func (msh *MShellProc) notifyHangups_nolock() { + if GlobalStore.CmdStatusCallback != nil { + for _, ck := range msh.RunningCmds { + GlobalStore.CmdStatusCallback(ck, sstore.CmdStatusHangup) + } + } + msh.RunningCmds = nil +} + func (runner *MShellProc) ProcessPackets() { defer runner.WithLock(func() { if runner.Status == StatusConnected { @@ -298,6 +320,7 @@ func (runner *MShellProc) ProcessPackets() { if err != nil { fmt.Printf("[error] calling HUP on remoteid=%d cmds\n", runner.Remote.RemoteId) } + runner.notifyHangups_nolock() }) for pk := range runner.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { From 368c16eb60c9b5e019d9ff5e70f80b64d4774254 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 7 Jul 2022 22:46:28 -0700 Subject: [PATCH 033/397] get input working --- cmd/main-server.go | 22 ++++++++++++---------- pkg/remote/remote.go | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 21cdc332a..924243fed 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -16,7 +16,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/cmdtail" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" @@ -254,18 +253,21 @@ func sendCmdInput(pk *packet.InputPacketType) error { if err != nil { return err } - if len(pk.InputData) > MaxInputDataSize { - return fmt.Errorf("input data size too large, len=%d (max=%d)", len(pk.InputData), MaxInputDataSize) + if pk.RemoteId == "" { + return fmt.Errorf("input must set remoteid") } - fileNames, err := base.GetCommandFileNames(pk.CK) - if err != nil { - return err + if len(pk.InputData64) == 0 && pk.SigNum == 0 { + return fmt.Errorf("empty input packet") } - err = writeToFifo(fileNames.StdinFifo, []byte(pk.InputData)) - if err != nil { - return err + inputLen := packet.B64DecodedLen(pk.InputData64) + if inputLen > MaxInputDataSize { + return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) } - return nil + msh := remote.GetRemoteById(pk.RemoteId) + if msh == nil { + return fmt.Errorf("cannot connect to remote") + } + return msh.SendInput(pk) } // params: name diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d6cfda2bd..a25d15edf 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -168,6 +168,31 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } +func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { + msh.Lock.Lock() + defer msh.Lock.Unlock() + for _, runningCk := range msh.RunningCmds { + if runningCk == ck { + return true + } + } + return false +} + +func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { + if !msh.IsConnected() { + return fmt.Errorf("remote is not connected, cannot send input") + } + if !msh.IsCmdRunning(pk.CK) { + return fmt.Errorf("cannot send input, cmd is not running") + } + dataPk := packet.MakeDataPacket() + dataPk.CK = pk.CK + dataPk.FdNum = 0 // stdin + dataPk.Data64 = pk.InputData64 + return msh.ServerProc.Input.SendPacket(dataPk) +} + func convertRemoteState(rs scpacket.RemoteState) sstore.RemoteState { return sstore.RemoteState{Cwd: rs.Cwd} } From ec6c1f5a9a7354664194dd4e5789ffa59a2d9659 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 8 Jul 2022 13:23:45 -0700 Subject: [PATCH 034/397] working on multiple sessions/windows --- cmd/main-server.go | 84 ++++++++++++++++++++++--- pkg/sstore/dbops.go | 140 ++++++++++++++++++++++++++++++++++-------- pkg/sstore/fileops.go | 1 + pkg/sstore/sstore.go | 2 +- 4 files changed, 192 insertions(+), 35 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 924243fed..9f42e1cc0 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -270,19 +270,19 @@ func sendCmdInput(pk *packet.InputPacketType) error { return msh.SendInput(pk) } -// params: name +// params: sessionid func HandleGetSession(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") w.Header().Set("Cache-Control", "no-cache") qvals := r.URL.Query() - name := qvals.Get("name") - if name == "" { - WriteJsonError(w, fmt.Errorf("must specify a name")) + sessionId := qvals.Get("sessionid") + if sessionId == "" { + WriteJsonError(w, fmt.Errorf("must specify a sessionid")) return } - session, err := sstore.GetSessionByName(r.Context(), name) + session, err := sstore.GetSessionById(r.Context(), sessionId) if err != nil { WriteJsonError(w, err) return @@ -291,6 +291,69 @@ func HandleGetSession(w http.ResponseWriter, r *http.Request) { return } +func HandleGetAllSessions(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + list, err := sstore.GetAllSessions(r.Context()) + if err != nil { + WriteJsonError(w, fmt.Errorf("cannot get all sessions: %w", err)) + return + } + WriteJsonSuccess(w, list) + return +} + +// params: name +func HandleCreateSession(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + name := qvals.Get("name") + sessionId, err := sstore.InsertSessionWithName(r.Context(), name) + if err != nil { + WriteJsonError(w, fmt.Errorf("inserting session: %w", err)) + return + } + session, err := sstore.GetSessionById(r.Context(), sessionId) + if err != nil { + WriteJsonError(w, fmt.Errorf("getting new session: %w", err)) + return + } + WriteJsonSuccess(w, session) + return +} + +// params: sessionid, name +func HandleCreateWindow(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + if _, err := uuid.Parse(sessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) + return + } + name := qvals.Get("name") + windowId, err := sstore.InsertWindow(r.Context(), sessionId, name) + if err != nil { + WriteJsonError(w, fmt.Errorf("inserting new window: %w", err)) + return + } + window, err := sstore.GetWindowById(r.Context(), sessionId, windowId) + if err != nil { + WriteJsonError(w, fmt.Errorf("getting new window: %w", err)) + return + } + WriteJsonSuccess(w, window) + return +} + // params: [none] func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -303,7 +366,7 @@ func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { } // params: sessionid, windowid -func HandleGetWindowLines(w http.ResponseWriter, r *http.Request) { +func HandleGetWindow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") @@ -319,12 +382,12 @@ func HandleGetWindowLines(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid windowid: %w", err)) return } - lines, err := sstore.GetWindowLines(r.Context(), sessionId, windowId) + window, err := sstore.GetWindowById(r.Context(), sessionId, windowId) if err != nil { WriteJsonError(w, err) return } - WriteJsonSuccess(w, lines) + WriteJsonSuccess(w, window) return } @@ -623,9 +686,12 @@ func main() { go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) + gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) + gr.HandleFunc("/api/create-session", HandleCreateSession) gr.HandleFunc("/api/get-session", HandleGetSession) - gr.HandleFunc("/api/get-window-lines", HandleGetWindowLines) + gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/get-remotes", HandleGetRemotes) + gr.HandleFunc("/api/create-window", HandleCreateWindow) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 23d2b5af6..17af26f56 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -84,6 +84,20 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { return nil } +func GetAllSessions(ctx context.Context) ([]*SessionType, error) { + db, err := GetDB() + if err != nil { + return nil, err + } + var rtn []*SessionType + query := `SELECT * FROM session` + err = db.SelectContext(ctx, &rtn, query) + if err != nil { + return nil, err + } + return rtn, nil +} + func GetSessionById(ctx context.Context, id string) (*SessionType, error) { var rtnSession *SessionType err := WithTx(ctx, func(tx *TxWrap) error { @@ -128,35 +142,56 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return GetSessionById(ctx, sessionId) } -func GetWindowLines(ctx context.Context, sessionId string, windowId string) ([]*LineType, error) { - var lines []*LineType - db, err := GetDB() - if err != nil { - return nil, err - } - query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` - err = db.SelectContext(ctx, &lines, query, sessionId, windowId) - if err != nil { - return nil, err - } - return lines, nil +func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { + var rtnWindow *WindowType + txErr := WithTx(ctx, func(tx *TxWrap) error { + var window WindowType + query := `SELECT * FROM window WHERE sessionid = ? AND windowid = ?` + found := tx.GetWrap(&window, query, sessionId, windowId) + if !found { + return nil + } + rtnWindow = &window + query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` + tx.SelectWrap(&window.Lines, query, sessionId, windowId) + return nil + }) + return rtnWindow, txErr } -// also creates window -func InsertSessionWithName(ctx context.Context, sessionName string) error { - if sessionName == "" { - return fmt.Errorf("invalid session name '%s'", sessionName) - } - session := &SessionType{ - SessionId: uuid.New().String(), - Name: sessionName, - } - return WithTx(ctx, func(tx *TxWrap) error { +// also creates default window, returns sessionId +// if sessionName == "", it will be generated +func InsertSessionWithName(ctx context.Context, sessionName string) (string, error) { + newSessionId := uuid.New().String() + txErr := WithTx(ctx, func(tx *TxWrap) error { + if sessionName == "" { + var names []string + query := `SELECT name FROM session` + tx.GetWrap(&names, query) + snum := len(names) + 1 + for { + sessionName = fmt.Sprintf("session-%d", snum) + if !containsStr(names, sessionName) { + break + } + snum++ + } + } else { + var dupSessionId string + query := `SELECT sessionid FROM session WHERE name = ?` + tx.GetWrap(&dupSessionId, query, sessionName) + if dupSessionId != "" { + return fmt.Errorf("cannot create session with duplicate name") + } + } + newSession := &SessionType{ + SessionId: newSessionId, + Name: sessionName, + } query := `INSERT INTO session (sessionid, name) VALUES (:sessionid, :name)` - tx.NamedExecWrap(query, session) - + tx.NamedExecWrap(query, newSession) window := &WindowType{ - SessionId: session.SessionId, + SessionId: newSessionId, WindowId: uuid.New().String(), Name: DefaultWindowName, CurRemote: LocalRemoteName, @@ -165,6 +200,61 @@ func InsertSessionWithName(ctx context.Context, sessionName string) error { tx.NamedExecWrap(query, window) return nil }) + return newSessionId, txErr +} + +func containsStr(strs []string, testStr string) bool { + for _, s := range strs { + if s == testStr { + return true + } + } + return false +} + +// if windowName == "", it will be generated +// returns (windowid, err) +func InsertWindow(ctx context.Context, sessionId string, windowName string) (string, error) { + var newWindowId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + var testSessionId string + query := `SELECT sesssionid FROM session WHERE sessionid = ?` + sessionExists := tx.GetWrap(&testSessionId, query, sessionId) + if !sessionExists { + return fmt.Errorf("cannot insert window, session does not exist") + } + if windowName == "" { + var names []string + query = `SELECT name FROM window WHERE sessionid = ?` + tx.GetWrap(&names, query, sessionId) + wnum := len(names) + 1 + for { + windowName = fmt.Sprintf("w%d", wnum) + if !containsStr(names, windowName) { + break + } + wnum++ + } + } else { + var testWindowId string + query = `SELECT windowid FROM window WHERE sessionid = ? AND name = ?` + windowExists := tx.GetWrap(&testWindowId, query, sessionId, windowName) + if windowExists { + return fmt.Errorf("cannot insert window, name already exists in session") + } + } + newWindowId = uuid.New().String() + window := &WindowType{ + SessionId: sessionId, + WindowId: newWindowId, + Name: windowName, + CurRemote: LocalRemoteName, + } + query = `INSERT INTO window (sessionid, windowid, name, curremote, version) VALUES (:sessionid, :windowid, :name, :curremote, :version)` + tx.NamedExecWrap(query, window) + return nil + }) + return newWindowId, txErr } func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 67026ba49..71a0daea2 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -16,6 +16,7 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat if err != nil { return err } + defer fd.Close() if len(data) == 0 { return nil } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 3f50ee08f..9e665f5b4 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -327,7 +327,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { if session != nil { return session, nil } - err = InsertSessionWithName(ctx, DefaultSessionName) + _, err = InsertSessionWithName(ctx, DefaultSessionName) if err != nil { return nil, err } From f8753830ffd32868907b9a2323fb52c188c39c48 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 8 Jul 2022 13:28:22 -0700 Subject: [PATCH 035/397] close ptyout file --- cmd/main-server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9f42e1cc0..a87f86bb4 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -435,6 +435,7 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err))) return } + defer fd.Close() w.WriteHeader(http.StatusOK) io.Copy(w, fd) } From 2957a03864a112126c4d7774380742aaf7d1bce4 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 12 Jul 2022 13:50:44 -0700 Subject: [PATCH 036/397] checkpoint --- pkg/sstore/dbops.go | 88 ++++++++++++++++++++++++++------------------ pkg/sstore/sstore.go | 43 +++++++++++----------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 17af26f56..fc22df08b 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -85,17 +85,47 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { } func GetAllSessions(ctx context.Context) ([]*SessionType, error) { - db, err := GetDB() - if err != nil { - return nil, err - } var rtn []*SessionType - query := `SELECT * FROM session` - err = db.SelectContext(ctx, &rtn, query) - if err != nil { - return nil, err - } - return rtn, nil + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM session` + tx.SelectWrap(&rtn, query) + var windows []*WindowType + query = `SELECT * FROM window` + tx.SelectWrap(&windows, query) + winMap := make(map[string][]*WindowType) + for _, win := range windows { + winArr := winMap[win.SessionId] + winArr = append(winArr, win) + winMap[win.SessionId] = winArr + } + for _, session := range rtn { + session.Windows = winMap[session.SessionId] + } + return nil + }) + return rtn, err +} + +func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { + var rtnWindow *WindowType + err := WithTx(ctx, func(tx *TxWrap) error { + var window WindowType + query := `SELECT * FROM window WHERE sessionid = ? AND windowid = ?` + found := tx.GetWrap(&window, query, sessionId, windowId) + if !found { + return nil + } + rtnWindow = &window + query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` + tx.SelectWrap(&window.Lines, query, sessionId, windowId) + query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` + cmdMaps := tx.SelectMaps(query, sessionId, windowId) + for _, m := range cmdMaps { + window.Cmds = append(window.Cmds, CmdFromMap(m)) + } + return nil + }) + return rtnWindow, err } func GetSessionById(ctx context.Context, id string) (*SessionType, error) { @@ -108,7 +138,7 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { return nil } rtnSession = &session - query = `SELECT sessionid, windowid, name, curremote, version FROM window WHERE sessionid = ?` + query = `SELECT sessionid, windowid, name, curremote FROM window WHERE sessionid = ?` tx.SelectWrap(&session.Windows, query, session.SessionId) query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) @@ -142,23 +172,6 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return GetSessionById(ctx, sessionId) } -func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { - var rtnWindow *WindowType - txErr := WithTx(ctx, func(tx *TxWrap) error { - var window WindowType - query := `SELECT * FROM window WHERE sessionid = ? AND windowid = ?` - found := tx.GetWrap(&window, query, sessionId, windowId) - if !found { - return nil - } - rtnWindow = &window - query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` - tx.SelectWrap(&window.Lines, query, sessionId, windowId) - return nil - }) - return rtnWindow, txErr -} - // also creates default window, returns sessionId // if sessionName == "", it will be generated func InsertSessionWithName(ctx context.Context, sessionName string) (string, error) { @@ -188,7 +201,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string) (string, err SessionId: newSessionId, Name: sessionName, } - query := `INSERT INTO session (sessionid, name) VALUES (:sessionid, :name)` + query := `INSERT INTO session (sessionid, name, notifynum) VALUES (:sessionid, :name, :notifynum)` tx.NamedExecWrap(query, newSession) window := &WindowType{ SessionId: newSessionId, @@ -196,8 +209,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string) (string, err Name: DefaultWindowName, CurRemote: LocalRemoteName, } - query = `INSERT INTO window (sessionid, windowid, name, curremote, version) VALUES (:sessionid, :windowid, :name, :curremote, :version)` - tx.NamedExecWrap(query, window) + txInsertWindow(tx, window) return nil }) return newSessionId, txErr @@ -250,13 +262,17 @@ func InsertWindow(ctx context.Context, sessionId string, windowName string) (str Name: windowName, CurRemote: LocalRemoteName, } - query = `INSERT INTO window (sessionid, windowid, name, curremote, version) VALUES (:sessionid, :windowid, :name, :curremote, :version)` - tx.NamedExecWrap(query, window) + txInsertWindow(tx, window) return nil }) return newWindowId, txErr } +func txInsertWindow(tx *TxWrap, window *WindowType) { + query := `INSERT INTO window (sessionid, windowid, name, curremote) VALUES (:sessionid, :windowid, :name, :curremote)` + tx.NamedExecWrap(query, window) +} + func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line == nil { return fmt.Errorf("line cannot be nil") @@ -271,7 +287,7 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if !hasWindow { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } - var maxLineId int + var maxLineId int64 query = `SELECT COALESCE(max(lineid), 0) FROM line WHERE sessionid = ? AND windowid = ?` tx.GetWrap(&maxLineId, query, line.SessionId, line.WindowId) line.LineId = maxLineId + 1 @@ -281,8 +297,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil { cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout) - VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout) +INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) + VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) ` tx.NamedExecWrap(query, cmdMap) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 9e665f5b4..b4009e206 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -19,9 +19,6 @@ import ( _ "github.com/mattn/go-sqlite3" ) -var NextLineId = 10 -var NextLineLock = &sync.Mutex{} - const LineTypeCmd = "cmd" const LineTypeText = "text" const DBFileName = "sh2.db" @@ -51,7 +48,7 @@ func GetDB() (*sqlx.DB, error) { globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil && globalDBErr == nil { - globalDB, globalDBErr = sqlx.Open("sqlite3", GetSessionDBName()) + globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", GetSessionDBName())) } return globalDB, globalDBErr } @@ -59,18 +56,25 @@ func GetDB() (*sqlx.DB, error) { type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` + NotifyNum int64 `json:"notifynum"` Windows []*WindowType `json:"windows"` Cmds []*CmdType `json:"cmds"` Remotes []*RemoteInstance `json:"remotes"` } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - CurRemote string `json:"curremote"` - Lines []*LineType `json:"lines"` - Version int `json:"version"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + CurRemote string `json:"curremote"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` + History []*HistoryItemType `json:"history"` + Remotes []*RemoteInstance `json:"remotes"` +} + +type HistoryItemType struct { + CmdStr string `json:"cmdstr"` } type RemoteState struct { @@ -96,9 +100,9 @@ func (s *RemoteState) Value() (driver.Value, error) { } type TermOpts struct { - Rows int `json:"rows"` - Cols int `json:"cols"` - FlexRows bool `json:"flexrows,omitempty"` + Rows int64 `json:"rows"` + Cols int64 `json:"cols"` + FlexRows bool `json:"flexrows,omitempty"` } func (opts *TermOpts) Scan(val interface{}) error { @@ -132,7 +136,7 @@ type RemoteInstance struct { type LineType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - LineId int `json:"lineid"` + LineId int64 `json:"lineid"` Ts int64 `json:"ts"` UserId string `json:"userid"` LineType string `json:"linetype"` @@ -181,6 +185,7 @@ type CmdType struct { Status string `json:"status"` StartPk *packet.CmdStartPacketType `json:"startpk"` DonePk *packet.CmdDonePacketType `json:"donepk"` + UsedRows int64 `json:"usedrows"` RunOut []packet.PacketType `json:"runout"` } @@ -223,6 +228,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["startpk"] = quickJson(cmd.StartPk) rtn["donepk"] = quickJson(cmd.DonePk) rtn["runout"] = quickJson(cmd.RunOut) + rtn["usedrows"] = cmd.UsedRows return rtn } @@ -241,6 +247,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetJson(&cmd.StartPk, m, "startpk") quickSetJson(&cmd.DonePk, m, "donepk") quickSetJson(&cmd.RunOut, m, "runout") + quickSetInt64(&cmd.UsedRows, m, "usedrows") return &cmd } @@ -284,14 +291,6 @@ func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId s return rtnLine, nil } -func GetNextLine() int { - NextLineLock.Lock() - defer NextLineLock.Unlock() - rtn := NextLineId - NextLineId++ - return rtn -} - func EnsureLocalRemote(ctx context.Context) error { remoteId, err := base.GetRemoteId() if err != nil { From 6351082900fbc8e8e446a45cab5e6b23b8adc515 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 12 Jul 2022 14:27:16 -0700 Subject: [PATCH 037/397] checkpoint --- db/migrations/000001_init.down.sql | 2 + db/migrations/000001_init.up.sql | 23 +++++++- pkg/sstore/dbops.go | 14 +++-- pkg/sstore/quick.go | 24 ++++++++- pkg/sstore/sstore.go | 87 +++++++++++++++++++----------- 5 files changed, 111 insertions(+), 39 deletions(-) diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql index f523b435e..fcb937708 100644 --- a/db/migrations/000001_init.down.sql +++ b/db/migrations/000001_init.down.sql @@ -1,5 +1,7 @@ DROP TABLE session; DROP TABLE window; +DROP TABLE screen; +DROP TABLE screen_window; DROP TABLE remote_instance; DROP TABLE line; DROP TABLE remote; diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 8796f3028..5c3e48e51 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -1,6 +1,8 @@ CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, - name varchar(50) NOT NULL + name varchar(50) NOT NULL, + sessionidx int NOT NULL, + notifynum int NOT NULL ); CREATE UNIQUE INDEX session_name_unique ON session(name); @@ -9,11 +11,27 @@ CREATE TABLE window ( windowid varchar(36) NOT NULL, name varchar(50) NOT NULL, curremote varchar(50) NOT NULL, - version int NOT NULL, + winopts json NOT NULL, PRIMARY KEY (sessionid, windowid) ); CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); +CREATE TABLE screen ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + screenidx int NOT NULL, + PRIMARY KEY (sessionid, screenid) +); + +CREATE TABLE screen_window ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + layout json NOT NULL, + PRIMARY KEY (sessionid, screenid, windowid) +); + CREATE TABLE remote_instance ( riid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, @@ -57,6 +75,7 @@ CREATE TABLE cmd ( startpk json NOT NULL, donepk json NOT NULL, runout json NOT NULL, + usedrows int NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index fc22df08b..3355d6984 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -138,7 +138,7 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { return nil } rtnSession = &session - query = `SELECT sessionid, windowid, name, curremote FROM window WHERE sessionid = ?` + query = `SELECT * FROM window WHERE sessionid = ?` tx.SelectWrap(&session.Windows, query, session.SessionId) query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) @@ -197,11 +197,15 @@ func InsertSessionWithName(ctx context.Context, sessionName string) (string, err return fmt.Errorf("cannot create session with duplicate name") } } + var maxSessionIdx int64 + query := `SELECT COALESCE(max(sessionidx), 0) FROM session` + tx.GetWrap(&maxSessionIdx, query) newSession := &SessionType{ - SessionId: newSessionId, - Name: sessionName, + SessionId: newSessionId, + Name: sessionName, + SessionIdx: maxSessionIdx + 1, } - query := `INSERT INTO session (sessionid, name, notifynum) VALUES (:sessionid, :name, :notifynum)` + query = `INSERT INTO session (sessionid, name, sessionidx, notifynum) VALUES (:sessionid, :name, :sessionidx, :notifynum)` tx.NamedExecWrap(query, newSession) window := &WindowType{ SessionId: newSessionId, @@ -269,7 +273,7 @@ func InsertWindow(ctx context.Context, sessionId string, windowName string) (str } func txInsertWindow(tx *TxWrap, window *WindowType) { - query := `INSERT INTO window (sessionid, windowid, name, curremote) VALUES (:sessionid, :windowid, :name, :curremote)` + query := `INSERT INTO window (sessionid, windowid, name, curremote, winopts) VALUES (:sessionid, :windowid, :name, :curremote, :winopts)` tx.NamedExecWrap(query, window) } diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index a4b8aaef1..c111b71ab 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -1,6 +1,10 @@ package sstore -import "encoding/json" +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) func quickSetStr(strVal *string, m map[string]interface{}, name string) { v, ok := m[name] @@ -66,3 +70,21 @@ func quickJson(v interface{}) string { barr, _ := json.Marshal(v) return string(barr) } + +func quickScanJson(ptr interface{}, val interface{}) error { + strVal, ok := val.(string) + if !ok { + return fmt.Errorf("cannot scan '%T' into '%T'", val, ptr) + } + if strVal == "" { + return nil + } + return json.Unmarshal([]byte(strVal), ptr) +} + +func quickValueJson(v interface{}) (driver.Value, error) { + if v == nil { + return "", nil + } + return json.Marshal(v) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b4009e206..aa0b97eef 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -3,7 +3,6 @@ package sstore import ( "context" "database/sql/driver" - "encoding/json" "fmt" "log" "path" @@ -54,12 +53,24 @@ func GetDB() (*sqlx.DB, error) { } type SessionType struct { - SessionId string `json:"sessionid"` - Name string `json:"name"` - NotifyNum int64 `json:"notifynum"` - Windows []*WindowType `json:"windows"` - Cmds []*CmdType `json:"cmds"` - Remotes []*RemoteInstance `json:"remotes"` + SessionId string `json:"sessionid"` + Name string `json:"name"` + SessionIdx int64 `json:"sessionidx"` + NotifyNum int64 `json:"notifynum"` + Windows []*WindowType `json:"windows"` + Cmds []*CmdType `json:"cmds"` + Remotes []*RemoteInstance `json:"remotes"` +} + +type WindowOptsType struct { +} + +func (opts *WindowOptsType) Scan(val interface{}) error { + return quickScanJson(opts, val) +} + +func (opts *WindowOptsType) Value() (driver.Value, error) { + return quickValueJson(opts) } type WindowType struct { @@ -71,6 +82,40 @@ type WindowType struct { Cmds []*CmdType `json:"cmds"` History []*HistoryItemType `json:"history"` Remotes []*RemoteInstance `json:"remotes"` + WinOpts WindowOptsType `json:"winopts"` +} + +type ScreenType struct { + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + ScreenIdx int64 `json:"screenidx"` + Name string `json:"name"` +} + +type LayoutType struct { + ZIndex int64 `json:"zindex"` + Float bool `json:"float"` + Top string `json:"top"` + Bottom string `json:"bottom"` + Left string `json:"left"` + Right string `json:"right"` + Width string `json:"width"` + Height string `json:"height"` +} + +func (l *LayoutType) Scan(val interface{}) error { + return quickScanJson(l, val) +} + +func (l *LayoutType) Value() (driver.Value, error) { + return quickValueJson(l) +} + +type ScreenWindowType struct { + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Layout LayoutType `json:"layout"` } type HistoryItemType struct { @@ -82,21 +127,11 @@ type RemoteState struct { } func (s *RemoteState) Scan(val interface{}) error { - if strVal, ok := val.(string); ok { - if strVal == "" { - return nil - } - err := json.Unmarshal([]byte(strVal), s) - if err != nil { - return err - } - return nil - } - return fmt.Errorf("cannot scan '%T' into RemoteState", val) + return quickScanJson(s, val) } func (s *RemoteState) Value() (driver.Value, error) { - return json.Marshal(s) + return quickValueJson(s) } type TermOpts struct { @@ -106,21 +141,11 @@ type TermOpts struct { } func (opts *TermOpts) Scan(val interface{}) error { - if strVal, ok := val.(string); ok { - if strVal == "" { - return nil - } - err := json.Unmarshal([]byte(strVal), opts) - if err != nil { - return err - } - return nil - } - return fmt.Errorf("cannot scan '%T' into TermOpts", val) + return quickScanJson(opts, val) } func (opts *TermOpts) Value() (driver.Value, error) { - return json.Marshal(opts) + return quickValueJson(opts) } type RemoteInstance struct { From 11087c10be8700c022c7d1221455608d9ab62d91 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 12 Jul 2022 16:10:46 -0700 Subject: [PATCH 038/397] txwrap can now be properly nested. add json conversion functions to quick --- pkg/sstore/dbops.go | 14 ++++++++--- pkg/sstore/quick.go | 18 ++++++++++---- pkg/sstore/sstore.go | 13 ++++++---- pkg/sstore/txwrap.go | 59 +++++++++++++++++++++++++++++--------------- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3355d6984..f9dbc38a2 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -10,7 +10,7 @@ import ( ) func NumSessions(ctx context.Context) (int, error) { - db, err := GetDB() + db, err := GetDB(ctx) if err != nil { return 0, err } @@ -71,7 +71,7 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if remote == nil { return fmt.Errorf("cannot insert nil remote") } - db, err := GetDB() + db, err := GetDB(ctx) if err != nil { return err } @@ -156,7 +156,7 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { } func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { - db, err := GetDB() + db, err := GetDB(ctx) if err != nil { return nil, err } @@ -228,6 +228,14 @@ func containsStr(strs []string, testStr string) bool { return false } +func InsertScreen(ctx context.Context, sessionId string, screenName string) (string, error) { + var newScreenId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + return nil + }) + return newScreenId, txErr +} + // if windowName == "", it will be generated // returns (windowid, err) func InsertWindow(ctx context.Context, sessionId string, windowName string) (string, error) { diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index c111b71ab..e84ebd872 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -72,19 +72,27 @@ func quickJson(v interface{}) string { } func quickScanJson(ptr interface{}, val interface{}) error { - strVal, ok := val.(string) + barrVal, ok := val.([]byte) if !ok { - return fmt.Errorf("cannot scan '%T' into '%T'", val, ptr) + strVal, ok := val.(string) + if !ok { + return fmt.Errorf("cannot scan '%T' into '%T'", val, ptr) + } + barrVal = []byte(strVal) } - if strVal == "" { + if len(barrVal) == 0 { return nil } - return json.Unmarshal([]byte(strVal), ptr) + return json.Unmarshal(barrVal, ptr) } func quickValueJson(v interface{}) (driver.Value, error) { if v == nil { return "", nil } - return json.Marshal(v) + barr, err := json.Marshal(v) + if err != nil { + return nil, err + } + return string(barr), nil } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index aa0b97eef..817d56640 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -43,7 +43,10 @@ func GetSessionDBName() string { return path.Join(scHome, DBFileName) } -func GetDB() (*sqlx.DB, error) { +func GetDB(ctx context.Context) (*sqlx.DB, error) { + if IsTxWrapContext(ctx) { + return nil, fmt.Errorf("cannot call GetDB from within a running transaction") + } globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil && globalDBErr == nil { @@ -69,7 +72,7 @@ func (opts *WindowOptsType) Scan(val interface{}) error { return quickScanJson(opts, val) } -func (opts *WindowOptsType) Value() (driver.Value, error) { +func (opts WindowOptsType) Value() (driver.Value, error) { return quickValueJson(opts) } @@ -107,7 +110,7 @@ func (l *LayoutType) Scan(val interface{}) error { return quickScanJson(l, val) } -func (l *LayoutType) Value() (driver.Value, error) { +func (l LayoutType) Value() (driver.Value, error) { return quickValueJson(l) } @@ -130,7 +133,7 @@ func (s *RemoteState) Scan(val interface{}) error { return quickScanJson(s, val) } -func (s *RemoteState) Value() (driver.Value, error) { +func (s RemoteState) Value() (driver.Value, error) { return quickValueJson(s) } @@ -144,7 +147,7 @@ func (opts *TermOpts) Scan(val interface{}) error { return quickScanJson(opts, val) } -func (opts *TermOpts) Value() (driver.Value, error) { +func (opts TermOpts) Value() (driver.Value, error) { return quickValueJson(opts) } diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 43734e639..49862a005 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -10,32 +10,47 @@ import ( type TxWrap struct { Txx *sqlx.Tx Err error + Ctx context.Context +} + +type txWrapKey struct{} + +func IsTxWrapContext(ctx context.Context) bool { + ctxVal := ctx.Value(txWrapKey{}) + return ctxVal != nil } func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { - db, err := GetDB() - if err != nil { - return err + var txWrap *TxWrap + ctxVal := ctx.Value(txWrapKey{}) + if ctxVal != nil { + txWrap = ctxVal.(*TxWrap) } - tx, beginErr := db.BeginTxx(ctx, nil) - if beginErr != nil { - return beginErr + if txWrap == nil { + db, err := GetDB(ctx) + if err != nil { + return err + } + tx, beginErr := db.BeginTxx(ctx, nil) + if beginErr != nil { + return beginErr + } + txWrap = &TxWrap{Txx: tx, Ctx: ctx} + defer func() { + if p := recover(); p != nil { + txWrap.Txx.Rollback() + panic(p) + } + if rtnErr != nil { + txWrap.Txx.Rollback() + } else { + rtnErr = txWrap.Txx.Commit() + } + }() } - txWrap := &TxWrap{Txx: tx} - defer func() { - if p := recover(); p != nil { - tx.Rollback() - panic(p) - } - if rtnErr != nil { - tx.Rollback() - } else { - rtnErr = tx.Commit() - } - }() fnErr := fn(txWrap) - if fnErr != nil { - return fnErr + if txWrap.Err == nil && fnErr != nil { + txWrap.Err = fnErr } if txWrap.Err != nil { return txWrap.Err @@ -43,6 +58,10 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { return nil } +func (tx *TxWrap) Context() context.Context { + return context.WithValue(tx.Ctx, txWrapKey{}, tx) +} + func (tx *TxWrap) NamedExecWrap(query string, arg interface{}) sql.Result { if tx.Err != nil { return nil From db841f295108c050ff04a64cef8dd0cc423a26aa Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 12 Jul 2022 21:51:17 -0700 Subject: [PATCH 039/397] checkpoint, adding 'screen' concept to contain windows --- cmd/main-server.go | 12 +-- db/migrations/000001_init.up.sql | 7 +- pkg/sstore/dbops.go | 163 +++++++++++++++---------------- pkg/sstore/sstore.go | 66 +++++++++---- pkg/sstore/txwrap.go | 26 +++++ 5 files changed, 156 insertions(+), 118 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index a87f86bb4..56e1eba5b 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -340,17 +340,7 @@ func HandleCreateWindow(w http.ResponseWriter, r *http.Request) { return } name := qvals.Get("name") - windowId, err := sstore.InsertWindow(r.Context(), sessionId, name) - if err != nil { - WriteJsonError(w, fmt.Errorf("inserting new window: %w", err)) - return - } - window, err := sstore.GetWindowById(r.Context(), sessionId, windowId) - if err != nil { - WriteJsonError(w, fmt.Errorf("getting new window: %w", err)) - return - } - WriteJsonSuccess(w, window) + fmt.Printf("insert-window %s\n", name) return } diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 5c3e48e51..594b12f49 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -2,25 +2,25 @@ CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, sessionidx int NOT NULL, + activescreenid varchar(36) NOT NULL, notifynum int NOT NULL ); -CREATE UNIQUE INDEX session_name_unique ON session(name); CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - name varchar(50) NOT NULL, curremote varchar(50) NOT NULL, winopts json NOT NULL, PRIMARY KEY (sessionid, windowid) ); -CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); CREATE TABLE screen ( sessionid varchar(36) NOT NULL, screenid varchar(36) NOT NULL, name varchar(50) NOT NULL, + activewindowid varchar(36) NOT NULL, screenidx int NOT NULL, + screenopts json NOT NULL, PRIMARY KEY (sessionid, screenid) ); @@ -28,6 +28,7 @@ CREATE TABLE screen_window ( sessionid varchar(36) NOT NULL, screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, + name varchar(50) NOT NULL, layout json NOT NULL, PRIMARY KEY (sessionid, screenid, windowid) ); diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f9dbc38a2..5240c3a1c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strings" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -101,6 +102,32 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { for _, session := range rtn { session.Windows = winMap[session.SessionId] } + var screens []*ScreenType + query = `SELECT * FROM screen ORDER BY screenidx` + tx.SelectWrap(&screens, query) + screenMap := make(map[string][]*ScreenType) + for _, screen := range screens { + screenArr := screenMap[screen.SessionId] + screenArr = append(screenArr, screen) + screenMap[screen.SessionId] = screenArr + } + for _, session := range rtn { + session.Screens = screenMap[session.SessionId] + } + var sws []*ScreenWindowType + query = `SELECT * FROM screen_window` + tx.SelectWrap(&sws, query) + screenIdMap := make(map[string]*ScreenType) + for _, screen := range screens { + screenIdMap[screen.SessionId+screen.ScreenId] = screen + } + for _, sw := range sws { + screen := screenIdMap[sw.SessionId+sw.ScreenId] + if screen == nil { + continue + } + screen.Windows = append(screen.Windows, sw) + } return nil }) return rtn, err @@ -177,43 +204,17 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { func InsertSessionWithName(ctx context.Context, sessionName string) (string, error) { newSessionId := uuid.New().String() txErr := WithTx(ctx, func(tx *TxWrap) error { - if sessionName == "" { - var names []string - query := `SELECT name FROM session` - tx.GetWrap(&names, query) - snum := len(names) + 1 - for { - sessionName = fmt.Sprintf("session-%d", snum) - if !containsStr(names, sessionName) { - break - } - snum++ - } - } else { - var dupSessionId string - query := `SELECT sessionid FROM session WHERE name = ?` - tx.GetWrap(&dupSessionId, query, sessionName) - if dupSessionId != "" { - return fmt.Errorf("cannot create session with duplicate name") - } + names := tx.SelectStrings(`SELECT name FROM session`) + sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) + maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum) VALUES (?, ?, '', ?, ?)` + tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) + screenId, err := InsertScreen(tx.Context(), newSessionId, "") + if err != nil { + return err } - var maxSessionIdx int64 - query := `SELECT COALESCE(max(sessionidx), 0) FROM session` - tx.GetWrap(&maxSessionIdx, query) - newSession := &SessionType{ - SessionId: newSessionId, - Name: sessionName, - SessionIdx: maxSessionIdx + 1, - } - query = `INSERT INTO session (sessionid, name, sessionidx, notifynum) VALUES (:sessionid, :name, :sessionidx, :notifynum)` - tx.NamedExecWrap(query, newSession) - window := &WindowType{ - SessionId: newSessionId, - WindowId: uuid.New().String(), - Name: DefaultWindowName, - CurRemote: LocalRemoteName, - } - txInsertWindow(tx, window) + query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` + tx.ExecWrap(query, screenId, newSessionId) return nil }) return newSessionId, txErr @@ -228,61 +229,57 @@ func containsStr(strs []string, testStr string) bool { return false } +func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []string) string { + var fmtStr string + if name != "" { + if !containsStr(strs, name) { + return name + } + fmtStr = name + "-%d" + startIdx = 2 + } else { + fmtStr = defaultFmtStr + } + if strings.Index(fmtStr, "%d") == -1 { + panic("invalid fmtStr: " + fmtStr) + } + for { + testName := fmt.Sprintf(fmtStr, startIdx) + if containsStr(strs, testName) { + startIdx++ + continue + } + return testName + } +} + func InsertScreen(ctx context.Context, sessionId string, screenName string) (string, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM session WHERE sessionid = ?` + if !tx.Exists(query, sessionId) { + return fmt.Errorf("cannot create screen, no session found") + } + newWindowId := txCreateWindow(tx, sessionId) + maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) + screenName = fmtUniqueName(screenName, "s%d", maxScreenIdx+1, screenNames) + newScreenId = uuid.New().String() + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts) VALUES (?, ?, ?, ?, ?, ?)` + tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) + layout := LayoutType{Type: LayoutFull} + query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout) VALUES (?, ?, ?, ?, ?)` + tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout) return nil }) return newScreenId, txErr } -// if windowName == "", it will be generated -// returns (windowid, err) -func InsertWindow(ctx context.Context, sessionId string, windowName string) (string, error) { - var newWindowId string - txErr := WithTx(ctx, func(tx *TxWrap) error { - var testSessionId string - query := `SELECT sesssionid FROM session WHERE sessionid = ?` - sessionExists := tx.GetWrap(&testSessionId, query, sessionId) - if !sessionExists { - return fmt.Errorf("cannot insert window, session does not exist") - } - if windowName == "" { - var names []string - query = `SELECT name FROM window WHERE sessionid = ?` - tx.GetWrap(&names, query, sessionId) - wnum := len(names) + 1 - for { - windowName = fmt.Sprintf("w%d", wnum) - if !containsStr(names, windowName) { - break - } - wnum++ - } - } else { - var testWindowId string - query = `SELECT windowid FROM window WHERE sessionid = ? AND name = ?` - windowExists := tx.GetWrap(&testWindowId, query, sessionId, windowName) - if windowExists { - return fmt.Errorf("cannot insert window, name already exists in session") - } - } - newWindowId = uuid.New().String() - window := &WindowType{ - SessionId: sessionId, - WindowId: newWindowId, - Name: windowName, - CurRemote: LocalRemoteName, - } - txInsertWindow(tx, window) - return nil - }) - return newWindowId, txErr -} - -func txInsertWindow(tx *TxWrap, window *WindowType) { - query := `INSERT INTO window (sessionid, windowid, name, curremote, winopts) VALUES (:sessionid, :windowid, :name, :curremote, :winopts)` - tx.NamedExecWrap(query, window) +func txCreateWindow(tx *TxWrap, sessionId string) string { + windowId := uuid.New().String() + query := `INSERT INTO window (sessionid, windowid, curremote, winopts) VALUES (?, ?, ?, ?)` + tx.ExecWrap(query, sessionId, windowId, LocalRemoteName, WindowOptsType{}) + return windowId } func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 817d56640..e72b78955 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -25,6 +25,7 @@ const DBFileName = "sh2.db" const DefaultSessionName = "default" const DefaultWindowName = "default" const LocalRemoteName = "local" +const DefaultScreenWindowName = "w1" const DefaultCwd = "~" @@ -56,13 +57,15 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { } type SessionType struct { - SessionId string `json:"sessionid"` - Name string `json:"name"` - SessionIdx int64 `json:"sessionidx"` - NotifyNum int64 `json:"notifynum"` - Windows []*WindowType `json:"windows"` - Cmds []*CmdType `json:"cmds"` - Remotes []*RemoteInstance `json:"remotes"` + SessionId string `json:"sessionid"` + Name string `json:"name"` + SessionIdx int64 `json:"sessionidx"` + ActiveScreenId string `json:"activescreenid"` + NotifyNum int64 `json:"notifynum"` + Screens []*ScreenType `json:"screens"` + Windows []*WindowType `json:"windows"` + Cmds []*CmdType `json:"cmds"` + Remotes []*RemoteInstance `json:"remotes"` } type WindowOptsType struct { @@ -79,31 +82,51 @@ func (opts WindowOptsType) Value() (driver.Value, error) { type WindowType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - Name string `json:"name"` CurRemote string `json:"curremote"` + WinOpts WindowOptsType `json:"winopts"` Lines []*LineType `json:"lines"` Cmds []*CmdType `json:"cmds"` History []*HistoryItemType `json:"history"` Remotes []*RemoteInstance `json:"remotes"` - WinOpts WindowOptsType `json:"winopts"` +} + +type ScreenOptsType struct { + TabColor string `json:"tabcolor"` +} + +func (opts *ScreenOptsType) Scan(val interface{}) error { + return quickScanJson(opts, val) +} + +func (opts ScreenOptsType) Value() (driver.Value, error) { + return quickValueJson(opts) } type ScreenType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - ScreenIdx int64 `json:"screenidx"` - Name string `json:"name"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + ScreenIdx int64 `json:"screenidx"` + Name string `json:"name"` + ActiveWindowId string `json:"activewindowid"` + ScreenOpts ScreenOptsType `json:"screenopts"` + Windows []*ScreenWindowType `json:"windows"` } +const ( + LayoutFull = "full" +) + type LayoutType struct { - ZIndex int64 `json:"zindex"` - Float bool `json:"float"` - Top string `json:"top"` - Bottom string `json:"bottom"` - Left string `json:"left"` - Right string `json:"right"` - Width string `json:"width"` - Height string `json:"height"` + Type string `json:"type"` + Parent string `json:"parent,omitempty"` + ZIndex int64 `json:"zindex,omitempty"` + Float bool `json:"float,omitempty"` + Top string `json:"top,omitempty"` + Bottom string `json:"bottom,omitempty"` + Left string `json:"left,omitempty"` + Right string `json:"right,omitempty"` + Width string `json:"width,omitempty"` + Height string `json:"height,omitempty"` } func (l *LayoutType) Scan(val interface{}) error { @@ -118,6 +141,7 @@ type ScreenWindowType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` WindowId string `json:"windowid"` + Name string `json:"name"` Layout LayoutType `json:"layout"` } diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 49862a005..8033c5f9e 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -25,6 +25,9 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { ctxVal := ctx.Value(txWrapKey{}) if ctxVal != nil { txWrap = ctxVal.(*TxWrap) + if txWrap.Err != nil { + return txWrap.Err + } } if txWrap == nil { db, err := GetDB(ctx) @@ -84,6 +87,29 @@ func (tx *TxWrap) ExecWrap(query string, args ...interface{}) sql.Result { return result } +func (tx *TxWrap) Exists(query string, args ...interface{}) bool { + var dest interface{} + return tx.GetWrap(&dest, query, args...) +} + +func (tx *TxWrap) GetString(query string, args ...interface{}) string { + var rtnStr string + tx.GetWrap(&rtnStr, query, args...) + return rtnStr +} + +func (tx *TxWrap) SelectStrings(query string, args ...interface{}) []string { + var rtnArr []string + tx.SelectWrap(&rtnArr, query, args...) + return rtnArr +} + +func (tx *TxWrap) GetInt(query string, args ...interface{}) int { + var rtnInt int + tx.GetWrap(&rtnInt, query, args...) + return rtnInt +} + func (tx *TxWrap) GetWrap(dest interface{}, query string, args ...interface{}) bool { if tx.Err != nil { return false From c1ace6f5d66ca47276dd64faabe37fb8fef5fb71 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 13 Jul 2022 14:16:08 -0700 Subject: [PATCH 040/397] updates for getting pty updates sent again --- cmd/main-server.go | 178 +++------------------------------------ pkg/remote/remote.go | 4 +- pkg/scpacket/scpacket.go | 18 +++- pkg/scws/scws.go | 178 +++++++++++++++++++++++++++++++++++++++ pkg/sstore/fileops.go | 46 +++++++++- pkg/sstore/quick.go | 8 +- pkg/sstore/sstore.go | 3 + pkg/sstore/updatebus.go | 102 ++++++++++++++++++++++ pkg/wsshell/wsshell.go | 4 + 9 files changed, 364 insertions(+), 177 deletions(-) create mode 100644 pkg/scws/scws.go create mode 100644 pkg/sstore/updatebus.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 56e1eba5b..0c7e61e94 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -16,11 +16,11 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/scripthaus-dev/mshell/pkg/cmdtail" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "github.com/scripthaus-dev/sh2-server/pkg/scws" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) @@ -35,18 +35,16 @@ const MainServerAddr = "localhost:8080" const WSStateReconnectTime = 30 * time.Second const WSStatePacketChSize = 20 -const MaxInputDataSize = 1000 - var GlobalLock = &sync.Mutex{} -var WSStateMap = make(map[string]*WSState) // clientid -> WsState +var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState -func setWSState(state *WSState) { +func setWSState(state *scws.WSState) { GlobalLock.Lock() defer GlobalLock.Unlock() WSStateMap[state.ClientId] = state } -func getWSState(clientId string) *WSState { +func getWSState(clientId string) *scws.WSState { GlobalLock.Lock() defer GlobalLock.Unlock() return WSStateMap[clientId] @@ -62,103 +60,10 @@ func removeWSStateAfterTimeout(clientId string, connectTime time.Time, waitDurat return } delete(WSStateMap, clientId) - err := state.CloseTailer() - if err != nil { - fmt.Printf("[error] closing tailer on ws %v\n", err) - } + state.UnWatchScreen() }() } -type WSState struct { - Lock *sync.Mutex - ClientId string - ConnectTime time.Time - Shell *wsshell.WSShell - Tailer *cmdtail.Tailer - PacketCh chan packet.PacketType -} - -func MakeWSState(clientId string) (*WSState, error) { - var err error - rtn := &WSState{} - rtn.Lock = &sync.Mutex{} - rtn.ClientId = clientId - rtn.ConnectTime = time.Now() - rtn.PacketCh = make(chan packet.PacketType, WSStatePacketChSize) - chSender := packet.MakeChannelPacketSender(rtn.PacketCh) - gen := scbase.ScFileNameGenerator{ScHome: scbase.GetScHomeDir()} - rtn.Tailer, err = cmdtail.MakeTailer(chSender, gen) - if err != nil { - return nil, err - } - go func() { - defer close(rtn.PacketCh) - rtn.Tailer.Run() - }() - go rtn.runTailerToWS() - return rtn, nil -} - -func (ws *WSState) CloseTailer() error { - return ws.Tailer.Close() -} - -func (ws *WSState) getShell() *wsshell.WSShell { - ws.Lock.Lock() - defer ws.Lock.Unlock() - return ws.Shell -} - -func (ws *WSState) runTailerToWS() { - for pk := range ws.PacketCh { - if pk.GetType() == "cmddata" { - dataPacket := pk.(*packet.CmdDataPacketType) - err := ws.writePacket(dataPacket) - if err != nil { - fmt.Printf("[error] writing packet to ws: %v\n", err) - } - continue - } - fmt.Printf("tailer-to-ws, bad packet %v\n", pk.GetType()) - } -} - -func (ws *WSState) writePacket(pk packet.PacketType) error { - shell := ws.getShell() - if shell == nil || shell.IsClosed() { - return fmt.Errorf("cannot write packet, empty or closed wsshell") - } - err := shell.WriteJson(pk) - if err != nil { - return err - } - return nil -} - -func (ws *WSState) getConnectTime() time.Time { - ws.Lock.Lock() - defer ws.Lock.Unlock() - return ws.ConnectTime -} - -func (ws *WSState) updateConnectTime() { - ws.Lock.Lock() - defer ws.Lock.Unlock() - ws.ConnectTime = time.Now() -} - -func (ws *WSState) replaceExistingShell(shell *wsshell.WSShell) { - ws.Lock.Lock() - defer ws.Lock.Unlock() - if ws.Shell == nil { - ws.Shell = shell - return - } - ws.Shell.Conn.Close() - ws.Shell = shell - return -} - func HandleWs(w http.ResponseWriter, r *http.Request) { shell, err := wsshell.StartWS(w, r) if err != nil { @@ -173,57 +78,20 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { } state := getWSState(clientId) if state == nil { - state, err = MakeWSState(clientId) - if err != nil { - fmt.Printf("cannot make wsstate: %v\n", err) - close(shell.WriteChan) - return - } - state.replaceExistingShell(shell) + state = scws.MakeWSState(clientId) + state.ReplaceShell(shell) setWSState(state) } else { - state.updateConnectTime() - state.replaceExistingShell(shell) + state.UpdateConnectTime() + state.ReplaceShell(shell) } - stateConnectTime := state.getConnectTime() + stateConnectTime := state.GetConnectTime() defer func() { removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime) }() shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error - fmt.Printf("WebSocket opened %s %s\n", shell.RemoteAddr, state.ClientId) - for msgBytes := range shell.ReadChan { - pk, err := packet.ParseJsonPacket(msgBytes) - if err != nil { - fmt.Printf("error unmarshalling ws message: %v\n", err) - continue - } - if pk.GetType() == "getcmd" { - getPk := pk.(*packet.GetCmdPacketType) - done, err := state.Tailer.AddWatch(getPk) - if err != nil { - // TODO: send responseerror - respPk := packet.MakeErrorResponsePacket(getPk.ReqId, err) - fmt.Printf("[error] adding watch to tailer: %v\n", err) - fmt.Printf("%v\n", respPk) - } - if done { - respPk := packet.MakeResponsePacket(getPk.ReqId, true) - fmt.Printf("%v\n", respPk) - // TODO: send response - } - continue - } - if pk.GetType() == "input" { - go func() { - err = sendCmdInput(pk.(*packet.InputPacketType)) - if err != nil { - fmt.Printf("[error] sending command input: %v\n", err) - } - }() - continue - } - fmt.Printf("got ws bad message: %v\n", pk.GetType()) - } + fmt.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr) + state.RunWSRead() } // todo: sync multiple writes to the same fifoName into a single go-routine and do liveness checking on fifo @@ -248,28 +116,6 @@ func writeToFifo(fifoName string, data []byte) error { return nil } -func sendCmdInput(pk *packet.InputPacketType) error { - err := pk.CK.Validate("input packet") - if err != nil { - return err - } - if pk.RemoteId == "" { - return fmt.Errorf("input must set remoteid") - } - if len(pk.InputData64) == 0 && pk.SigNum == 0 { - return fmt.Errorf("empty input packet") - } - inputLen := packet.B64DecodedLen(pk.InputData64) - if inputLen > MaxInputDataSize { - return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) - } - msh := remote.GetRemoteById(pk.RemoteId) - if msh == nil { - return fmt.Errorf("cannot connect to remote") - } - return msh.SendInput(pk) -} - // params: sessionid func HandleGetSession(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a25d15edf..e53ae02b3 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -251,7 +251,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str DonePk: nil, RunOut: nil, } - err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil) + err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil, sstore.PosAppend) if err != nil { return nil, err } @@ -358,7 +358,7 @@ func (runner *MShellProc) ProcessPackets() { } var ack *packet.DataAckPacketType if len(realData) > 0 { - err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData) + err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, sstore.PosAppend) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index d3c264845..8718e5bc9 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -7,6 +7,7 @@ import ( ) const FeCommandPacketStr = "fecmd" +const WatchScreenPacketStr = "watchscreen" type RemoteState struct { RemoteId string `json:"remoteid"` @@ -24,7 +25,8 @@ type FeCommandPacketType struct { } func init() { - packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(&FeCommandPacketType{})) + packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{})) + packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{})) } func (*FeCommandPacketType) GetType() string { @@ -34,3 +36,17 @@ func (*FeCommandPacketType) GetType() string { func MakeFeCommandPacket() *FeCommandPacketType { return &FeCommandPacketType{Type: FeCommandPacketStr} } + +type WatchScreenPacketType struct { + Type string `json:"type"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` +} + +func (*WatchScreenPacketType) GetType() string { + return WatchScreenPacketStr +} + +func MakeWatchScreenPacket() *WatchScreenPacketType { + return &WatchScreenPacketType{Type: WatchScreenPacketStr} +} diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go new file mode 100644 index 000000000..6ad4cd980 --- /dev/null +++ b/pkg/scws/scws.go @@ -0,0 +1,178 @@ +package scws + +import ( + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/scripthaus-dev/sh2-server/pkg/wsshell" +) + +const WSStatePacketChSize = 20 +const MaxInputDataSize = 1000 + +type WSState struct { + Lock *sync.Mutex + ClientId string + ConnectTime time.Time + Shell *wsshell.WSShell + UpdateCh chan interface{} + UpdateQueue []interface{} + + SessionId string + ScreenId string +} + +func MakeWSState(clientId string) *WSState { + rtn := &WSState{} + rtn.Lock = &sync.Mutex{} + rtn.ClientId = clientId + rtn.ConnectTime = time.Now() + return rtn +} + +func (ws *WSState) GetShell() *wsshell.WSShell { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.Shell +} + +func (ws *WSState) WriteUpdate(update interface{}) error { + shell := ws.GetShell() + if shell == nil { + return fmt.Errorf("cannot write update, empty shell") + } + err := shell.WriteJson(update) + if err != nil { + return err + } + return nil +} + +func (ws *WSState) UpdateConnectTime() { + ws.Lock.Lock() + defer ws.Lock.Unlock() + ws.ConnectTime = time.Now() +} + +func (ws *WSState) GetConnectTime() time.Time { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.ConnectTime +} + +func (ws *WSState) WatchScreen(sessionId string, screenId string) { + ws.Lock.Lock() + defer ws.Lock.Unlock() + if ws.SessionId == sessionId && ws.ScreenId == screenId { + return + } + ws.SessionId = sessionId + ws.ScreenId = screenId + ws.UpdateCh = sstore.MainBus.RegisterChannel(ws.ClientId, ws.SessionId) + go ws.RunUpdates() +} + +func (ws *WSState) UnWatchScreen() { + ws.Lock.Lock() + defer ws.Lock.Unlock() + sstore.MainBus.UnregisterChannel(ws.ClientId) + ws.SessionId = "" + ws.ScreenId = "" +} + +func (ws *WSState) getUpdateCh() chan interface{} { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.UpdateCh +} + +func (ws *WSState) RunUpdates() { + updateCh := ws.getUpdateCh() + if updateCh == nil { + return + } + for update := range updateCh { + shell := ws.GetShell() + if shell != nil { + shell.WriteJson(update) + } + } +} + +func (ws *WSState) ReplaceShell(shell *wsshell.WSShell) { + ws.Lock.Lock() + defer ws.Lock.Unlock() + if ws.Shell == nil { + ws.Shell = shell + return + } + ws.Shell.Conn.Close() + ws.Shell = shell + return +} + +func (ws *WSState) RunWSRead() { + shell := ws.GetShell() + if shell == nil { + return + } + for msgBytes := range shell.ReadChan { + pk, err := packet.ParseJsonPacket(msgBytes) + if err != nil { + fmt.Printf("error unmarshalling ws message: %v\n", err) + continue + } + if pk.GetType() == "input" { + go func() { + err = sendCmdInput(pk.(*packet.InputPacketType)) + if err != nil { + fmt.Printf("[error] sending command input: %v\n", err) + } + }() + continue + } + if pk.GetType() == "watchscreen" { + wsPk := pk.(*scpacket.WatchScreenPacketType) + if _, err := uuid.Parse(wsPk.SessionId); err != nil { + fmt.Printf("[error] invalid watchscreen sessionid: %v\n", err) + continue + } + if _, err := uuid.Parse(wsPk.ScreenId); err != nil { + fmt.Printf("[error] invalid watchscreen screenid: %v\n", err) + continue + } + ws.WatchScreen(wsPk.SessionId, wsPk.ScreenId) + fmt.Printf("[ws] watch screen clientid=%s %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) + continue + } + fmt.Printf("got ws bad message: %v\n", pk.GetType()) + } +} + +func sendCmdInput(pk *packet.InputPacketType) error { + err := pk.CK.Validate("input packet") + if err != nil { + return err + } + if pk.RemoteId == "" { + return fmt.Errorf("input must set remoteid") + } + if len(pk.InputData64) == 0 && pk.SigNum == 0 { + return fmt.Errorf("empty input packet") + } + inputLen := packet.B64DecodedLen(pk.InputData64) + if inputLen > MaxInputDataSize { + return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) + } + msh := remote.GetRemoteById(pk.RemoteId) + if msh == nil { + return fmt.Errorf("cannot connect to remote") + } + return msh.SendInput(pk) +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 71a0daea2..e296129c2 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -2,19 +2,48 @@ package sstore import ( "context" + "encoding/base64" + "fmt" "os" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte) error { +const PosAppend = -1 + +// when calling with PosAppend, this is not multithread safe (since file could be modified). +// we need to know the real position of the write to send a proper pty update to the frontends +// in practice this is fine since we only use PosAppend in non-detached mode where +// we are reading/writing a stream in order with a single goroutine +func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) error { ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) if err != nil { return err } - fd, err := os.OpenFile(ptyOutFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err + var fd *os.File + var realPos int64 + if pos == PosAppend { + fd, err = os.OpenFile(ptyOutFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + finfo, err := fd.Stat() + if err != nil { + return err + } + realPos = finfo.Size() + } else { + fd, err = os.OpenFile(ptyOutFileName, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + realPos, err = fd.Seek(pos, 0) + if err != nil { + return err + } + if realPos != pos { + return fmt.Errorf("could not seek to pos:%d (realpos=%d)", pos, realPos) + } } defer fd.Close() if len(data) == 0 { @@ -24,5 +53,14 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat if err != nil { return err } + data64 := base64.StdEncoding.EncodeToString(data) + update := &PtyDataUpdate{ + SessionId: sessionId, + CmdId: cmdId, + PtyPos: realPos, + PtyData64: data64, + PtyDataLen: int64(len(data)), + } + MainBus.SendUpdate(sessionId, update) return nil } diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index e84ebd872..dcc3e789c 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -58,14 +58,14 @@ func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { return } if str == "" { - return + str = "{}" } json.Unmarshal([]byte(str), ptr) } func quickJson(v interface{}) string { if v == nil { - return "" + return "{}" } barr, _ := json.Marshal(v) return string(barr) @@ -81,14 +81,14 @@ func quickScanJson(ptr interface{}, val interface{}) error { barrVal = []byte(strVal) } if len(barrVal) == 0 { - return nil + barrVal = []byte("{}") } return json.Unmarshal(barrVal, ptr) } func quickValueJson(v interface{}) (driver.Value, error) { if v == nil { - return "", nil + return "{}", nil } barr, err := json.Marshal(v) if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index e72b78955..a604d0555 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -147,6 +147,7 @@ type ScreenWindowType struct { type HistoryItemType struct { CmdStr string `json:"cmdstr"` + Remove bool `json:"remove"` } type RemoteState struct { @@ -194,6 +195,7 @@ type LineType struct { LineType string `json:"linetype"` Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` + Remove bool `json:"remove,omitempty"` } type SSHOpts struct { @@ -239,6 +241,7 @@ type CmdType struct { DonePk *packet.CmdDonePacketType `json:"donepk"` UsedRows int64 `json:"usedrows"` RunOut []packet.PacketType `json:"runout"` + Remove bool `json:"remove"` } func (r *RemoteType) ToMap() map[string]interface{} { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go new file mode 100644 index 000000000..22a754817 --- /dev/null +++ b/pkg/sstore/updatebus.go @@ -0,0 +1,102 @@ +package sstore + +import "sync" + +var MainBus *UpdateBus = MakeUpdateBus() + +type UpdateCmd struct { + CmdId string + Status string +} + +type PtyDataUpdate struct { + SessionId string `json:"sessionid"` + CmdId string `json:"cmdid"` + PtyPos int64 `json:"ptypos"` + PtyData64 string `json:"ptydata64"` + PtyDataLen int64 `json:"ptydatalen"` +} + +type WindowUpdate struct { + Window WindowType `json:"window"` + Remove bool `json:"remove,omitempty"` +} + +type SessionUpdate struct { + Session SessionType `json:"session"` + Remove bool `json:"remove,omitempty"` +} + +type CmdUpdate struct { + Cmd CmdType `json:"cmd"` + Remove bool `json:"remove,omitempty"` +} + +type ScreenUpdate struct { + Screen CmdType `json:"screen"` + Remove bool `json:"remove,omitempty"` +} + +type UpdateChannel struct { + SessionId string + ClientId string + Ch chan interface{} +} + +func (uch UpdateChannel) Match(sessionId string) bool { + if sessionId == "" { + return true + } + return sessionId == uch.SessionId +} + +type UpdateBus struct { + Lock *sync.Mutex + Channels map[string]UpdateChannel +} + +func MakeUpdateBus() *UpdateBus { + return &UpdateBus{ + Lock: &sync.Mutex{}, + Channels: make(map[string]UpdateChannel), + } +} + +func (bus *UpdateBus) RegisterChannel(clientId string, sessionId string) chan interface{} { + bus.Lock.Lock() + defer bus.Lock.Unlock() + uch, found := bus.Channels[clientId] + if found { + close(uch.Ch) + uch.SessionId = sessionId + uch.Ch = make(chan interface{}) + } else { + uch = UpdateChannel{ + ClientId: clientId, + SessionId: sessionId, + Ch: make(chan interface{}), + } + } + bus.Channels[clientId] = uch + return uch.Ch +} + +func (bus *UpdateBus) UnregisterChannel(clientId string) { + bus.Lock.Lock() + defer bus.Lock.Unlock() + uch, found := bus.Channels[clientId] + if found { + close(uch.Ch) + delete(bus.Channels, clientId) + } +} + +func (bus *UpdateBus) SendUpdate(sessionId string, update interface{}) { + bus.Lock.Lock() + defer bus.Lock.Unlock() + for _, uch := range bus.Channels { + if uch.Match(sessionId) { + uch.Ch <- update + } + } +} diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index 2d9abb935..8417e81a6 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -2,6 +2,7 @@ package wsshell import ( "encoding/json" + "fmt" "log" "net/http" "net/url" @@ -65,6 +66,9 @@ func (ws *WSShell) WritePing() error { } func (ws *WSShell) WriteJson(val interface{}) error { + if ws.IsClosed() { + return fmt.Errorf("cannot write packet, empty or closed wsshell") + } barr, err := json.Marshal(val) if err != nil { return err From 275e177218014ed01a03f5dc9e22cd24b71b9398 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 14 Jul 2022 18:39:40 -0700 Subject: [PATCH 041/397] checkpoint --- cmd/main-server.go | 118 ++++++++++++++++++++++++++++++++------- pkg/scpacket/scpacket.go | 1 + pkg/sstore/dbops.go | 88 +++++++++++++++++++++-------- pkg/sstore/sstore.go | 16 +++++- pkg/sstore/updatebus.go | 8 +-- 5 files changed, 179 insertions(+), 52 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 0c7e61e94..3a9a8dfca 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -9,6 +9,7 @@ import ( "io/fs" "net/http" "os" + "strconv" "strings" "sync" "time" @@ -190,6 +191,30 @@ func HandleCreateWindow(w http.ResponseWriter, r *http.Request) { return } +// params: sessionid, name, activate +func HandleCreateScreen(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + if _, err := uuid.Parse(sessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) + return + } + name := qvals.Get("name") + activateStr := qvals.Get("activate") + activate := activateStr != "" + screenId, err := sstore.InsertScreen(r.Context(), sessionId, name, activate) + if err != nil { + WriteJsonError(w, fmt.Errorf("inserting screen: %w", err)) + return + } + WriteJsonSuccess(w, screenId) + return +} + // params: [none] func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -339,20 +364,45 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { } func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketType) (*runCommandResponse, error) { + // parse metacmd commandStr := strings.TrimSpace(pk.CmdStr) if commandStr == "" { return nil, fmt.Errorf("invalid emtpty command") } - if strings.HasPrefix(commandStr, "/comment ") { - text := strings.TrimSpace(commandStr[9:]) + metaCmd := "" + metaSubCmd := "" + if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { + metaCmd = "cd" + commandStr = strings.TrimSpace(commandStr[2:]) + } else if commandStr[0] == '/' { + spaceIdx := strings.Index(commandStr, " ") + if spaceIdx == -1 { + metaCmd = commandStr[1:] + commandStr = "" + } else { + metaCmd = commandStr[1:spaceIdx] + commandStr = strings.TrimSpace(commandStr[spaceIdx+1:]) + } + colonIdx := strings.Index(metaCmd, ":") + if colonIdx != -1 { + metaCmd = metaCmd[0:colonIdx] + metaSubCmd = metaCmd[colonIdx+1:] + } + if metaCmd == "" { + return nil, fmt.Errorf("invalid command, got bare '/', with no command") + } + } + + // execute metacmd + if metaCmd == "comment" { + text := commandStr rtnLine, err := sstore.AddCommentLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, text) if err != nil { return nil, err } return &runCommandResponse{Line: rtnLine}, nil - } - if strings.HasPrefix(commandStr, "cd ") { - newDir := strings.TrimSpace(commandStr[3:]) + } else if metaCmd == "cd" { + newDir := commandStr cdPacket := packet.MakeCdPacket() cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir @@ -366,17 +416,53 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp } fmt.Printf("GOT cd RESP: %v\n", resp) return nil, nil + } else if metaCmd == "s" || metaCmd == "screen" { + err := RunScreenCmd(ctx, pk.SessionId, pk.ScreenId, metaSubCmd, commandStr) + if err != nil { + return nil, err + } + return nil, nil + } else if metaCmd == "" { + cmdId := uuid.New().String() + cmd, err := remote.RunCommand(ctx, pk, cmdId) + if err != nil { + return nil, err + } + rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, cmd) + if err != nil { + return nil, err + } + return &runCommandResponse{Line: rtnLine, Cmd: cmd}, nil + } else { + fmt.Printf("INVALID metacmd '%s'\n", metaCmd) + return nil, fmt.Errorf("Invalid command type /%s", metaCmd) } - cmdId := uuid.New().String() - cmd, err := remote.RunCommand(ctx, pk, cmdId) +} + +func RunScreenCmd(ctx context.Context, sessionId string, screenId string, subCmd string, commandStr string) error { + if subCmd != "" { + return fmt.Errorf("invalid /screen subcommand '%s'", subCmd) + } + if commandStr == "" { + return fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") + } + screens, err := sstore.GetSessionScreens(ctx, sessionId) if err != nil { - return nil, err + return fmt.Errorf("could not retreive screens for session=%s", sessionId) } - rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, cmd) - if err != nil { - return nil, err + screenNum, err := strconv.Atoi(commandStr) + if err == nil { + if screenNum < 1 || screenNum > len(screens) { + return fmt.Errorf("could not switch to screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) + } + return sstore.SwitchScreenById(ctx, sessionId, screens[screenNum-1].ScreenId) } - return &runCommandResponse{Line: rtnLine, Cmd: cmd}, nil + for _, screen := range screens { + if screen.Name == commandStr { + return sstore.SwitchScreenById(ctx, sessionId, screen.ScreenId) + } + } + return fmt.Errorf("could not switch to screen '%s' (name not found)", commandStr) } // /api/start-session @@ -384,12 +470,6 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp // * userid // * sessionid // -// /api/get-session -// params: -// * name -// returns: -// * session -// // /api/ptyout (pos=[position]) - returns contents of ptyout file // params: // * sessionid @@ -525,10 +605,10 @@ func main() { gr.HandleFunc("/api/ptyout", HandleGetPtyOut) gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) gr.HandleFunc("/api/create-session", HandleCreateSession) - gr.HandleFunc("/api/get-session", HandleGetSession) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/get-remotes", HandleGetRemotes) gr.HandleFunc("/api/create-window", HandleCreateWindow) + gr.HandleFunc("/api/create-screen", HandleCreateScreen) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 8718e5bc9..0c0b34599 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -18,6 +18,7 @@ type RemoteState struct { type FeCommandPacketType struct { Type string `json:"type"` SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` WindowId string `json:"windowid"` UserId string `json:"userid"` CmdStr string `json:"cmdstr"` diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5240c3a1c..a9e173f91 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -90,18 +90,6 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session` tx.SelectWrap(&rtn, query) - var windows []*WindowType - query = `SELECT * FROM window` - tx.SelectWrap(&windows, query) - winMap := make(map[string][]*WindowType) - for _, win := range windows { - winArr := winMap[win.SessionId] - winArr = append(winArr, win) - winMap[win.SessionId] = winArr - } - for _, session := range rtn { - session.Windows = winMap[session.SessionId] - } var screens []*ScreenType query = `SELECT * FROM screen ORDER BY screenidx` tx.SelectWrap(&screens, query) @@ -155,6 +143,16 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win return rtnWindow, err } +func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { + var rtn []*ScreenType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY screenidx` + tx.SelectWrap(&rtn, query, sessionId) + return nil + }) + return rtn, txErr +} + func GetSessionById(ctx context.Context, id string) (*SessionType, error) { var rtnSession *SessionType err := WithTx(ctx, func(tx *TxWrap) error { @@ -165,15 +163,10 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { return nil } rtnSession = &session - query = `SELECT * FROM window WHERE sessionid = ?` - tx.SelectWrap(&session.Windows, query, session.SessionId) + query = `SELECT * FROM screen WHERE sessionid = ? ORDER BY screenidx` + tx.SelectWrap(&session.Screens, query, session.SessionId) query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) - query = `SELECT * FROM cmd WHERE sessionid = ?` - marr := tx.SelectMaps(query, session.SessionId) - for _, m := range marr { - session.Cmds = append(session.Cmds, CmdFromMap(m)) - } return nil }) if err != nil { @@ -209,15 +202,16 @@ func InsertSessionWithName(ctx context.Context, sessionName string) (string, err maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum) VALUES (?, ?, '', ?, ?)` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) - screenId, err := InsertScreen(tx.Context(), newSessionId, "") + _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { return err } - query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` - tx.ExecWrap(query, screenId, newSessionId) return nil }) - return newSessionId, txErr + if txErr != nil { + return "", txErr + } + return newSessionId, nil } func containsStr(strs []string, testStr string) bool { @@ -253,7 +247,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin } } -func InsertScreen(ctx context.Context, sessionId string, screenName string) (string, error) { +func InsertScreen(ctx context.Context, sessionId string, screenName string, activate bool) (string, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid FROM session WHERE sessionid = ?` @@ -270,11 +264,35 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string) (str layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout) VALUES (?, ?, ?, ?, ?)` tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout) + if activate { + query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` + tx.ExecWrap(query, newScreenId, sessionId) + } return nil }) return newScreenId, txErr } +func GetScreenById(ctx context.Context, sessionId string, screenId string) (*ScreenType, error) { + var rtnScreen *ScreenType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM screen WHERE sessionid = ? AND screenid = ?` + var screen ScreenType + found := tx.GetWrap(&screen, query, sessionId, screenId) + if !found { + return nil + } + rtnScreen = &screen + query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ?` + tx.SelectWrap(&screen.Windows, query, sessionId, screenId) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtnScreen, nil +} + func txCreateWindow(tx *TxWrap, sessionId string) string { windowId := uuid.New().String() query := `INSERT INTO window (sessionid, windowid, curremote, winopts) VALUES (?, ?, ?, ?)` @@ -366,3 +384,25 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { return nil }) } + +func SwitchScreenById(ctx context.Context, sessionId string, screenId string) error { + txErr := WithTx(ctx, func(tx *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.ExecWrap(query, screenId, sessionId) + return nil + }) + sessionUpdate := SessionType{ + SessionId: sessionId, + ActiveScreenId: screenId, + NotifyNum: -1, + } + update := &SessionUpdate{ + Sessions: []SessionType{sessionUpdate}, + } + MainBus.SendUpdate("", update) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index a604d0555..d35148643 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -63,9 +63,11 @@ type SessionType struct { ActiveScreenId string `json:"activescreenid"` NotifyNum int64 `json:"notifynum"` Screens []*ScreenType `json:"screens"` - Windows []*WindowType `json:"windows"` - Cmds []*CmdType `json:"cmds"` Remotes []*RemoteInstance `json:"remotes"` + + // only for updates + Remove bool `json:"remove,omitempty"` + Full bool `json:"full,omitempty"` } type WindowOptsType struct { @@ -88,6 +90,9 @@ type WindowType struct { Cmds []*CmdType `json:"cmds"` History []*HistoryItemType `json:"history"` Remotes []*RemoteInstance `json:"remotes"` + + // only for updates + Remove bool `json:"remove,omitempty"` } type ScreenOptsType struct { @@ -110,6 +115,10 @@ type ScreenType struct { ActiveWindowId string `json:"activewindowid"` ScreenOpts ScreenOptsType `json:"screenopts"` Windows []*ScreenWindowType `json:"windows"` + + // only for updates + Remove bool `json:"remove,omitempty"` + Full bool `json:"full,omitempty"` } const ( @@ -143,6 +152,9 @@ type ScreenWindowType struct { WindowId string `json:"windowid"` Name string `json:"name"` Layout LayoutType `json:"layout"` + + // only for updates + Remove bool `json:"remove,omitempty"` } type HistoryItemType struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 22a754817..1540149d3 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -23,8 +23,7 @@ type WindowUpdate struct { } type SessionUpdate struct { - Session SessionType `json:"session"` - Remove bool `json:"remove,omitempty"` + Sessions []SessionType `json:"sessions"` } type CmdUpdate struct { @@ -32,11 +31,6 @@ type CmdUpdate struct { Remove bool `json:"remove,omitempty"` } -type ScreenUpdate struct { - Screen CmdType `json:"screen"` - Remove bool `json:"remove,omitempty"` -} - type UpdateChannel struct { SessionId string ClientId string From 5b1eb383e3f7f0309e79f2c7e09fab0d1e799f97 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 15 Jul 2022 01:57:45 -0700 Subject: [PATCH 042/397] screen deletion working, bug fixes --- cmd/main-server.go | 24 ++++++++++--- pkg/scws/scws.go | 4 +++ pkg/sstore/dbops.go | 78 ++++++++++++++++++++++++++++++++++++----- pkg/sstore/updatebus.go | 13 ++++++- 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 3a9a8dfca..ca27f8f3d 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -9,6 +9,7 @@ import ( "io/fs" "net/http" "os" + "runtime/debug" "strconv" "strings" "sync" @@ -334,6 +335,15 @@ type runCommandResponse struct { } func HandleRunCommand(w http.ResponseWriter, r *http.Request) { + defer func() { + r := recover() + if r == nil { + return + } + fmt.Printf("[error] in run-command: %v\n", r) + debug.PrintStack() + return + }() w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") @@ -385,8 +395,7 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp } colonIdx := strings.Index(metaCmd, ":") if colonIdx != -1 { - metaCmd = metaCmd[0:colonIdx] - metaSubCmd = metaCmd[colonIdx+1:] + metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] } if metaCmd == "" { return nil, fmt.Errorf("invalid command, got bare '/', with no command") @@ -440,6 +449,13 @@ func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketTyp } func RunScreenCmd(ctx context.Context, sessionId string, screenId string, subCmd string, commandStr string) error { + if subCmd == "close" { + err := sstore.DeleteScreen(ctx, sessionId, screenId) + if err != nil { + return err + } + return nil + } if subCmd != "" { return fmt.Errorf("invalid /screen subcommand '%s'", subCmd) } @@ -458,11 +474,11 @@ func RunScreenCmd(ctx context.Context, sessionId string, screenId string, subCmd return sstore.SwitchScreenById(ctx, sessionId, screens[screenNum-1].ScreenId) } for _, screen := range screens { - if screen.Name == commandStr { + if screen.ScreenId == commandStr || screen.Name == commandStr { return sstore.SwitchScreenById(ctx, sessionId, screen.ScreenId) } } - return fmt.Errorf("could not switch to screen '%s' (name not found)", commandStr) + return fmt.Errorf("could not switch to screen '%s' (name/id not found)", commandStr) } // /api/start-session diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 6ad4cd980..759e8747c 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -143,6 +143,10 @@ func (ws *WSState) RunWSRead() { fmt.Printf("[error] invalid watchscreen sessionid: %v\n", err) continue } + if wsPk.ScreenId == "" { + ws.UnWatchScreen() + continue + } if _, err := uuid.Parse(wsPk.ScreenId); err != nil { fmt.Printf("[error] invalid watchscreen screenid: %v\n", err) continue diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a9e173f91..d18350c21 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -167,6 +167,7 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { tx.SelectWrap(&session.Screens, query, session.SessionId) query = `SELECT * FROM remote_instance WHERE sessionid = ?` tx.SelectWrap(&session.Remotes, query, session.SessionId) + session.Full = true return nil }) if err != nil { @@ -270,6 +271,16 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti } return nil }) + newScreen, err := GetScreenById(ctx, sessionId, newScreenId) + if err != nil { + return "", err + } + update, session := MakeSingleSessionUpdate(sessionId) + if activate { + session.ActiveScreenId = newScreenId + } + session.Screens = append(session.Screens, newScreen) + MainBus.SendUpdate("", update) return newScreenId, txErr } @@ -285,6 +296,7 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr rtnScreen = &screen query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ?` tx.SelectWrap(&screen.Windows, query, sessionId, screenId) + screen.Full = true return nil }) if txErr != nil { @@ -385,6 +397,31 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { }) } +func getNextId(ids []string, delId string) string { + fmt.Printf("getnextid %v | %v\n", ids, delId) + if len(ids) == 0 { + return "" + } + if len(ids) == 1 { + if ids[0] == delId { + return "" + } + return ids[0] + } + for idx := 0; idx < len(ids); idx++ { + if ids[idx] == delId { + var rtnIdx int + if idx == len(ids)-1 { + rtnIdx = idx - 1 + } else { + rtnIdx = idx + 1 + } + return ids[rtnIdx] + } + } + return ids[0] +} + func SwitchScreenById(ctx context.Context, sessionId string, screenId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` @@ -395,14 +432,39 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) er tx.ExecWrap(query, screenId, sessionId) return nil }) - sessionUpdate := SessionType{ - SessionId: sessionId, - ActiveScreenId: screenId, - NotifyNum: -1, - } - update := &SessionUpdate{ - Sessions: []SessionType{sessionUpdate}, - } + update, session := MakeSingleSessionUpdate(sessionId) + session.ActiveScreenId = screenId MainBus.SendUpdate("", update) return txErr } + +func CleanWindows() { +} + +func DeleteScreen(ctx context.Context, sessionId string, screenId string) error { + var newActiveScreenId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId) + fmt.Printf("delete-screen %s %s | %v\n", sessionId, screenId, isActive) + if isActive { + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? ORDER BY screenidx`, sessionId) + nextId := getNextId(screenIds, screenId) + tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + newActiveScreenId = nextId + } + query := `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, sessionId, screenId) + query = `DELETE FROM screen WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, sessionId, screenId) + return nil + }) + if txErr != nil { + return txErr + } + go CleanWindows() + update, session := MakeSingleSessionUpdate(sessionId) + session.ActiveScreenId = newActiveScreenId + session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) + MainBus.SendUpdate("", update) + return nil +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 1540149d3..b9e94298d 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -23,7 +23,18 @@ type WindowUpdate struct { } type SessionUpdate struct { - Sessions []SessionType `json:"sessions"` + Sessions []*SessionType `json:"sessions"` +} + +func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { + session := &SessionType{ + SessionId: sessionId, + NotifyNum: -1, + } + update := &SessionUpdate{ + Sessions: []*SessionType{session}, + } + return update, session } type CmdUpdate struct { From 6807547ff79adcc43cde1579392dc35e44440de4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 15 Jul 2022 17:37:32 -0700 Subject: [PATCH 043/397] updates, switch to new fecmd packet to run all UI commands through --- cmd/main-server.go | 192 +------------------ pkg/cmdrunner/cmdrunner.go | 367 +++++++++++++++++++++++++++++++++++++ pkg/remote/remote.go | 54 +++--- pkg/scpacket/scpacket.go | 12 +- pkg/sstore/dbops.go | 22 +++ pkg/sstore/updatebus.go | 33 +++- 6 files changed, 461 insertions(+), 219 deletions(-) create mode 100644 pkg/cmdrunner/cmdrunner.go diff --git a/cmd/main-server.go b/cmd/main-server.go index ca27f8f3d..409730ac5 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "runtime/debug" - "strconv" "strings" "sync" "time" @@ -18,7 +17,7 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/cmdrunner" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" @@ -153,69 +152,6 @@ func HandleGetAllSessions(w http.ResponseWriter, r *http.Request) { return } -// params: name -func HandleCreateSession(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - qvals := r.URL.Query() - name := qvals.Get("name") - sessionId, err := sstore.InsertSessionWithName(r.Context(), name) - if err != nil { - WriteJsonError(w, fmt.Errorf("inserting session: %w", err)) - return - } - session, err := sstore.GetSessionById(r.Context(), sessionId) - if err != nil { - WriteJsonError(w, fmt.Errorf("getting new session: %w", err)) - return - } - WriteJsonSuccess(w, session) - return -} - -// params: sessionid, name -func HandleCreateWindow(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") - if _, err := uuid.Parse(sessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) - return - } - name := qvals.Get("name") - fmt.Printf("insert-window %s\n", name) - return -} - -// params: sessionid, name, activate -func HandleCreateScreen(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") - if _, err := uuid.Parse(sessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) - return - } - name := qvals.Get("name") - activateStr := qvals.Get("activate") - activate := activateStr != "" - screenId, err := sstore.InsertScreen(r.Context(), sessionId, name, activate) - if err != nil { - WriteJsonError(w, fmt.Errorf("inserting screen: %w", err)) - return - } - WriteJsonSuccess(w, screenId) - return -} - // params: [none] func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -304,7 +240,7 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { func WriteJsonError(w http.ResponseWriter, errVal error) { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(200) errMap := make(map[string]interface{}) errMap["error"] = errVal.Error() barr, _ := json.Marshal(errMap) @@ -329,11 +265,6 @@ func WriteJsonSuccess(w http.ResponseWriter, data interface{}) { return } -type runCommandResponse struct { - Line *sstore.LineType `json:"line"` - Cmd *sstore.CmdType `json:"cmd"` -} - func HandleRunCommand(w http.ResponseWriter, r *http.Request) { defer func() { r := recover() @@ -360,127 +291,15 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) return } - if _, err = uuid.Parse(commandPk.SessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid '%s': %w", commandPk.SessionId, err)) - return - } - resp, err := ProcessFeCommandPacket(r.Context(), &commandPk) + update, err := cmdrunner.HandleCommand(r.Context(), &commandPk) if err != nil { WriteJsonError(w, err) return } - WriteJsonSuccess(w, resp) + WriteJsonSuccess(w, update) return } -func ProcessFeCommandPacket(ctx context.Context, pk *scpacket.FeCommandPacketType) (*runCommandResponse, error) { - // parse metacmd - commandStr := strings.TrimSpace(pk.CmdStr) - if commandStr == "" { - return nil, fmt.Errorf("invalid emtpty command") - } - metaCmd := "" - metaSubCmd := "" - if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { - metaCmd = "cd" - commandStr = strings.TrimSpace(commandStr[2:]) - } else if commandStr[0] == '/' { - spaceIdx := strings.Index(commandStr, " ") - if spaceIdx == -1 { - metaCmd = commandStr[1:] - commandStr = "" - } else { - metaCmd = commandStr[1:spaceIdx] - commandStr = strings.TrimSpace(commandStr[spaceIdx+1:]) - } - colonIdx := strings.Index(metaCmd, ":") - if colonIdx != -1 { - metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] - } - if metaCmd == "" { - return nil, fmt.Errorf("invalid command, got bare '/', with no command") - } - } - - // execute metacmd - if metaCmd == "comment" { - text := commandStr - rtnLine, err := sstore.AddCommentLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, text) - if err != nil { - return nil, err - } - return &runCommandResponse{Line: rtnLine}, nil - } else if metaCmd == "cd" { - newDir := commandStr - cdPacket := packet.MakeCdPacket() - cdPacket.ReqId = uuid.New().String() - cdPacket.Dir = newDir - localRemote := remote.GetRemoteById(pk.RemoteState.RemoteId) - if localRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot execute command") - } - resp, err := localRemote.PacketRpc(ctx, cdPacket) - if err != nil { - return nil, err - } - fmt.Printf("GOT cd RESP: %v\n", resp) - return nil, nil - } else if metaCmd == "s" || metaCmd == "screen" { - err := RunScreenCmd(ctx, pk.SessionId, pk.ScreenId, metaSubCmd, commandStr) - if err != nil { - return nil, err - } - return nil, nil - } else if metaCmd == "" { - cmdId := uuid.New().String() - cmd, err := remote.RunCommand(ctx, pk, cmdId) - if err != nil { - return nil, err - } - rtnLine, err := sstore.AddCmdLine(ctx, pk.SessionId, pk.WindowId, pk.UserId, cmd) - if err != nil { - return nil, err - } - return &runCommandResponse{Line: rtnLine, Cmd: cmd}, nil - } else { - fmt.Printf("INVALID metacmd '%s'\n", metaCmd) - return nil, fmt.Errorf("Invalid command type /%s", metaCmd) - } -} - -func RunScreenCmd(ctx context.Context, sessionId string, screenId string, subCmd string, commandStr string) error { - if subCmd == "close" { - err := sstore.DeleteScreen(ctx, sessionId, screenId) - if err != nil { - return err - } - return nil - } - if subCmd != "" { - return fmt.Errorf("invalid /screen subcommand '%s'", subCmd) - } - if commandStr == "" { - return fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") - } - screens, err := sstore.GetSessionScreens(ctx, sessionId) - if err != nil { - return fmt.Errorf("could not retreive screens for session=%s", sessionId) - } - screenNum, err := strconv.Atoi(commandStr) - if err == nil { - if screenNum < 1 || screenNum > len(screens) { - return fmt.Errorf("could not switch to screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) - } - return sstore.SwitchScreenById(ctx, sessionId, screens[screenNum-1].ScreenId) - } - for _, screen := range screens { - if screen.ScreenId == commandStr || screen.Name == commandStr { - return sstore.SwitchScreenById(ctx, sessionId, screen.ScreenId) - } - } - return fmt.Errorf("could not switch to screen '%s' (name/id not found)", commandStr) -} - // /api/start-session // returns: // * userid @@ -620,11 +439,8 @@ func main() { gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) - gr.HandleFunc("/api/create-session", HandleCreateSession) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/get-remotes", HandleGetRemotes) - gr.HandleFunc("/api/create-window", HandleCreateWindow) - gr.HandleFunc("/api/create-screen", HandleCreateScreen) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go new file mode 100644 index 000000000..7eae553bf --- /dev/null +++ b/pkg/cmdrunner/cmdrunner.go @@ -0,0 +1,367 @@ +package cmdrunner + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +const DefaultUserId = "sawka" + +const ( + R_Session = 1 + R_Screen = 2 + R_Window = 4 + R_Remote = 8 + R_SessionOpt = 16 + R_ScreenOpt = 32 + R_WindowOpt = 64 + R_RemoteOpt = 128 +) + +type resolvedIds struct { + SessionId string + ScreenId string + WindowId string + RemoteId string + RemoteState *sstore.RemoteState +} + +func SubMetaCmd(cmd string) string { + switch cmd { + case "s": + return "screen" + case "w": + return "window" + case "r": + return "run" + case "c": + return "comment" + case "e": + return "eval" + default: + return cmd + } +} + +func firstArg(pk *scpacket.FeCommandPacketType) string { + if len(pk.Args) == 0 { + return "" + } + return pk.Args[0] +} + +func resolveBool(arg string, def bool) bool { + if arg == "" { + return def + } + if arg == "0" || arg == "false" { + return false + } + return true +} + +func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string) (string, error) { + screens, err := sstore.GetSessionScreens(ctx, sessionId) + if err != nil { + return "", fmt.Errorf("could not retreive screens for session=%s", sessionId) + } + screenNum, err := strconv.Atoi(screenArg) + if err == nil { + if screenNum < 1 || screenNum > len(screens) { + return "", fmt.Errorf("could not resolve screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) + } + return screens[screenNum-1].ScreenId, nil + } + for _, screen := range screens { + if screen.ScreenId == screenArg || screen.Name == screenArg { + return screen.ScreenId, nil + } + } + return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) +} + +func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { + sessionId := pk.Kwargs["session"] + if sessionId == "" { + return "", nil + } + if _, err := uuid.Parse(sessionId); err != nil { + return "", fmt.Errorf("invalid sessionid '%s'", sessionId) + } + return sessionId, nil +} + +func resolveWindowId(pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { + windowId := pk.Kwargs["window"] + if windowId == "" { + return "", nil + } + if _, err := uuid.Parse(windowId); err != nil { + return "", fmt.Errorf("invalid windowid '%s'", windowId) + } + return windowId, nil +} + +func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { + screenArg := pk.Kwargs["screen"] + if screenArg == "" { + return "", nil + } + if _, err := uuid.Parse(screenArg); err == nil { + return screenArg, nil + } + if sessionId == "" { + return "", fmt.Errorf("cannot resolve screen without session") + } + return resolveSessionScreen(ctx, sessionId, screenArg) +} + +func resolveRemote(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string, windowId string) (string, *sstore.RemoteState, error) { + remoteArg := pk.Kwargs["remote"] + if remoteArg == "" { + return "", nil, nil + } + remoteId, state, err := sstore.GetRemoteState(ctx, remoteArg, sessionId, windowId) + if err != nil { + return "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteArg, err) + } + if state == nil { + state, err = remote.GetDefaultRemoteStateById(remoteId) + if err != nil { + return "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteArg, err) + } + } + return remoteId, state, nil +} + +func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { + rtn := resolvedIds{} + if rtype == 0 { + return rtn, nil + } + var err error + if (rtype&R_Session)+(rtype&R_SessionOpt) > 0 { + rtn.SessionId, err = resolveSessionId(pk) + if err != nil { + return rtn, err + } + if rtn.SessionId == "" && (rtype&R_Session) > 0 { + return rtn, fmt.Errorf("no session") + } + } + if (rtype&R_Window)+(rtype&R_WindowOpt) > 0 { + rtn.WindowId, err = resolveWindowId(pk, rtn.SessionId) + if err != nil { + return rtn, err + } + if rtn.WindowId == "" && (rtype&R_Window) > 0 { + return rtn, fmt.Errorf("no window") + } + + } + if (rtype&R_Screen)+(rtype&R_ScreenOpt) > 0 { + rtn.ScreenId, err = resolveScreenId(ctx, pk, rtn.SessionId) + if err != nil { + return rtn, err + } + if rtn.ScreenId == "" && (rtype&R_Screen) > 0 { + return rtn, fmt.Errorf("no screen") + } + } + if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { + rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk, rtn.SessionId, rtn.WindowId) + if err != nil { + return rtn, err + } + if rtn.RemoteId == "" && (rtype&R_Remote) > 0 { + return rtn, fmt.Errorf("no remote") + } + } + return rtn, nil +} + +func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + switch SubMetaCmd(pk.MetaCmd) { + case "run": + return RunCommand(ctx, pk) + + case "eval": + return EvalCommand(ctx, pk) + + case "screen": + return ScreenCommand(ctx, pk) + + case "comment": + return CommentCommand(ctx, pk) + + case "cd": + return CdCommand(ctx, pk) + + default: + return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) + } +} + +func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, fmt.Errorf("/run error: %w", err) + } + cmdId := uuid.New().String() + cmdStr := firstArg(pk) + runPacket := packet.MakeRunPacket() + runPacket.ReqId = uuid.New().String() + runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) + runPacket.Cwd = ids.RemoteState.Cwd + runPacket.Env = nil + runPacket.UsePty = true + runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} + runPacket.Command = strings.TrimSpace(cmdStr) + cmd, err := remote.RunCommand(ctx, cmdId, ids.RemoteId, ids.RemoteState, runPacket) + if err != nil { + return nil, err + } + rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd) + if err != nil { + return nil, err + } + return sstore.LineUpdate{Line: rtnLine, Cmd: cmd}, nil +} + +func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") + } + // parse metacmd + commandStr := strings.TrimSpace(pk.Args[0]) + if commandStr == "" { + return nil, fmt.Errorf("/eval, invalid emtpty command") + } + metaCmd := "" + metaSubCmd := "" + if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { + metaCmd = "cd" + commandStr = strings.TrimSpace(commandStr[2:]) + } else if commandStr[0] == '/' { + spaceIdx := strings.Index(commandStr, " ") + if spaceIdx == -1 { + metaCmd = commandStr[1:] + commandStr = "" + } else { + metaCmd = commandStr[1:spaceIdx] + commandStr = strings.TrimSpace(commandStr[spaceIdx+1:]) + } + colonIdx := strings.Index(metaCmd, ":") + if colonIdx != -1 { + metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] + } + if metaCmd == "" { + return nil, fmt.Errorf("invalid command, got bare '/', with no command") + } + } + if metaCmd == "" { + metaCmd = "run" + } + var args []string + if metaCmd == "run" || metaCmd == "comment" { + args = []string{commandStr} + } else { + args = strings.Fields(commandStr) + } + newPk := &scpacket.FeCommandPacketType{ + MetaCmd: metaCmd, + MetaSubCmd: metaSubCmd, + Args: args, + Kwargs: pk.Kwargs, + } + return HandleCommand(ctx, newPk) +} + +func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if pk.MetaSubCmd == "close" { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) + } + err = sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) + if err != nil { + return nil, err + } + return nil, nil + } + if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { + ids, err := resolveIds(ctx, pk, R_Session) + if err != nil { + return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) + } + activate := resolveBool(pk.Kwargs["activate"], true) + _, err = sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) + if err != nil { + return nil, err + } + return nil, nil + } + if pk.MetaSubCmd != "" { + return nil, fmt.Errorf("invalid /screen subcommand '%s'", pk.MetaSubCmd) + } + ids, err := resolveIds(ctx, pk, R_Session) + if err != nil { + return nil, fmt.Errorf("/screen cannot switch to screen: %w", err) + } + firstArg := firstArg(pk) + if firstArg == "" { + return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") + } + screenIdArg, err := resolveSessionScreen(ctx, ids.SessionId, firstArg) + if err != nil { + return nil, err + } + sstore.SwitchScreenById(ctx, ids.SessionId, screenIdArg) + return nil, nil +} + +func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, fmt.Errorf("/cd error: %w", err) + } + newDir := firstArg(pk) + cdPacket := packet.MakeCdPacket() + cdPacket.ReqId = uuid.New().String() + cdPacket.Dir = newDir + localRemote := remote.GetRemoteById(ids.RemoteId) + if localRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot execute command") + } + resp, err := localRemote.PacketRpc(ctx, cdPacket) + if err != nil { + return nil, err + } + fmt.Printf("GOT cd RESP: %v\n", resp) + return nil, nil +} + +func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window) + if err != nil { + return nil, fmt.Errorf("/comment error: %w", err) + } + text := firstArg(pk) + if strings.TrimSpace(text) == "" { + return nil, fmt.Errorf("cannot post empty comment") + } + rtnLine, err := sstore.AddCommentLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, text) + if err != nil { + return nil, err + } + return sstore.LineUpdate{Line: rtnLine}, nil +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e53ae02b3..faf0ab261 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -6,14 +6,11 @@ import ( "errors" "fmt" "os/exec" - "strings" "sync" - "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -123,6 +120,21 @@ func GetAllRemoteState() []RemoteState { return rtn } +func GetDefaultRemoteStateById(remoteId string) (*sstore.RemoteState, error) { + remote := GetRemoteById(remoteId) + if remote == nil { + return nil, fmt.Errorf("remote not found") + } + if !remote.IsConnected() { + return nil, fmt.Errorf("remote not connected") + } + state := remote.GetDefaultState() + if state == nil { + return nil, fmt.Errorf("could not get default remote state") + } + return state, nil +} + func MakeMShell(r *sstore.RemoteType) *MShellProc { rtn := &MShellProc{Lock: &sync.Mutex{}, Remote: r, Status: StatusInit} return rtn @@ -168,6 +180,15 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } +func (msh *MShellProc) GetDefaultState() *sstore.RemoteState { + msh.Lock.Lock() + defer msh.Lock.Unlock() + if msh.ServerProc == nil || msh.ServerProc.InitPk == nil { + return nil + } + return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir} +} + func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -193,30 +214,21 @@ func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { return msh.ServerProc.Input.SendPacket(dataPk) } -func convertRemoteState(rs scpacket.RemoteState) sstore.RemoteState { - return sstore.RemoteState{Cwd: rs.Cwd} -} - func makeTermOpts() sstore.TermOpts { return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true} } -func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId string) (*sstore.CmdType, error) { - msh := GetRemoteById(pk.RemoteState.RemoteId) +func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { + msh := GetRemoteById(remoteId) if msh == nil { - return nil, fmt.Errorf("no remote id=%s found", pk.RemoteState.RemoteId) + return nil, fmt.Errorf("no remote id=%s found", remoteId) } if !msh.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected", msh.Remote.RemoteName) + return nil, fmt.Errorf("remote '%s' is not connected", remoteId) + } + if remoteState == nil { + return nil, fmt.Errorf("no remote state passed to RunCommand") } - runPacket := packet.MakeRunPacket() - runPacket.ReqId = uuid.New().String() - runPacket.CK = base.MakeCommandKey(pk.SessionId, cmdId) - runPacket.Cwd = pk.RemoteState.Cwd - runPacket.Env = nil - runPacket.UsePty = true - runPacket.TermOpts = &packet.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, Term: DefaultTerm} - runPacket.Command = strings.TrimSpace(pk.CmdStr) fmt.Printf("RUN-CMD> %s reqid=%s (msh=%v)\n", runPacket.CK, runPacket.ReqId, msh.Remote) msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) @@ -240,11 +252,11 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdId str status = sstore.CmdStatusDetached } cmd := &sstore.CmdType{ - SessionId: pk.SessionId, + SessionId: runPacket.CK.GetSessionId(), CmdId: startPk.CK.GetCmdId(), CmdStr: runPacket.Command, RemoteId: msh.Remote.RemoteId, - RemoteState: convertRemoteState(pk.RemoteState), + RemoteState: *remoteState, TermOpts: makeTermOpts(), Status: status, StartPk: startPk, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 0c0b34599..b5e32e0a8 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -16,13 +16,11 @@ type RemoteState struct { } type FeCommandPacketType struct { - Type string `json:"type"` - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - UserId string `json:"userid"` - CmdStr string `json:"cmdstr"` - RemoteState RemoteState `json:"remotestate"` + Type string `json:"type"` + MetaCmd string `json:"metacmd"` + MetaSubCmd string `json:"metasubcmd,omitempty"` + Args []string `json:"args,omitempty"` + Kwargs map[string]string `json:"kwargs,omitempty"` } func init() { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d18350c21..1d475b53d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -468,3 +468,25 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) error MainBus.SendUpdate("", update) return nil } + +func GetRemoteState(ctx context.Context, rname string, sessionId string, windowId string) (string, *RemoteState, error) { + var remoteId string + var remoteState *RemoteState + txErr := WithTx(ctx, func(tx *TxWrap) error { + var ri RemoteInstance + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, rname) + if found { + remoteId = ri.RemoteId + remoteState = &ri.State + return nil + } + query = `SELECT remoteid FROM remote WHERE remotename = ?` + remoteId = tx.GetString(query, rname) + if remoteId == "" { + return fmt.Errorf("remote not found", rname) + } + return nil + }) + return remoteId, remoteState, txErr +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index b9e94298d..f36b76268 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -4,6 +4,16 @@ import "sync" var MainBus *UpdateBus = MakeUpdateBus() +const PtyDataUpdateStr = "pty" +const SessionUpdateStr = "session" +const WindowUpdateStr = "window" +const CmdUpdateStr = "cmd" +const LineCmdUpdateStr = "line+cmd" + +type UpdatePacket interface { + UpdateType() string +} + type UpdateCmd struct { CmdId string Status string @@ -17,15 +27,27 @@ type PtyDataUpdate struct { PtyDataLen int64 `json:"ptydatalen"` } +func (PtyDataUpdate) UpdateType() string { + return PtyDataUpdateStr +} + type WindowUpdate struct { Window WindowType `json:"window"` Remove bool `json:"remove,omitempty"` } +func (WindowUpdate) WindowUpdate() string { + return WindowUpdateStr +} + type SessionUpdate struct { Sessions []*SessionType `json:"sessions"` } +func (SessionUpdate) UpdateType() string { + return SessionUpdateStr +} + func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { session := &SessionType{ SessionId: sessionId, @@ -37,9 +59,14 @@ func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { return update, session } -type CmdUpdate struct { - Cmd CmdType `json:"cmd"` - Remove bool `json:"remove,omitempty"` +type LineUpdate struct { + Line *LineType `json:"line"` + Cmd *CmdType `json:"cmd,omitempty"` + Remove bool `json:"remove,omitempty"` +} + +func (LineUpdate) UpdateType() string { + return LineCmdUpdateStr } type UpdateChannel struct { From 4785253a10ddfb50f13a8074806c62ba6e657ebe Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 15 Jul 2022 17:53:23 -0700 Subject: [PATCH 044/397] return interactive update --- pkg/cmdrunner/cmdrunner.go | 22 ++++++++++++++++------ pkg/sstore/dbops.go | 19 ++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 7eae553bf..e8925ee93 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -200,6 +200,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "screen": return ScreenCommand(ctx, pk) + case "session": + return SessionCommand(ctx, pk) + case "comment": return CommentCommand(ctx, pk) @@ -292,11 +295,11 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) } - err = sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) + update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { return nil, err } - return nil, nil + return update, nil } if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { ids, err := resolveIds(ctx, pk, R_Session) @@ -304,11 +307,11 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) } activate := resolveBool(pk.Kwargs["activate"], true) - _, err = sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) + update, err := sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) if err != nil { return nil, err } - return nil, nil + return update, nil } if pk.MetaSubCmd != "" { return nil, fmt.Errorf("invalid /screen subcommand '%s'", pk.MetaSubCmd) @@ -325,8 +328,11 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, err } - sstore.SwitchScreenById(ctx, ids.SessionId, screenIdArg) - return nil, nil + update, err := sstore.SwitchScreenById(ctx, ids.SessionId, screenIdArg) + if err != nil { + return nil, err + } + return update, nil } func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -365,3 +371,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } return sstore.LineUpdate{Line: rtnLine}, nil } + +func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1d475b53d..a00e93ba9 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -248,7 +248,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin } } -func InsertScreen(ctx context.Context, sessionId string, screenName string, activate bool) (string, error) { +func InsertScreen(ctx context.Context, sessionId string, screenName string, activate bool) (UpdatePacket, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid FROM session WHERE sessionid = ?` @@ -273,15 +273,14 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti }) newScreen, err := GetScreenById(ctx, sessionId, newScreenId) if err != nil { - return "", err + return nil, err } update, session := MakeSingleSessionUpdate(sessionId) if activate { session.ActiveScreenId = newScreenId } session.Screens = append(session.Screens, newScreen) - MainBus.SendUpdate("", update) - return newScreenId, txErr + return update, txErr } func GetScreenById(ctx context.Context, sessionId string, screenId string) (*ScreenType, error) { @@ -422,7 +421,7 @@ func getNextId(ids []string, delId string) string { return ids[0] } -func SwitchScreenById(ctx context.Context, sessionId string, screenId string) error { +func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, sessionId, screenId) { @@ -434,14 +433,13 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) er }) update, session := MakeSingleSessionUpdate(sessionId) session.ActiveScreenId = screenId - MainBus.SendUpdate("", update) - return txErr + return update, txErr } func CleanWindows() { } -func DeleteScreen(ctx context.Context, sessionId string, screenId string) error { +func DeleteScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId) @@ -459,14 +457,13 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) error return nil }) if txErr != nil { - return txErr + return nil, txErr } go CleanWindows() update, session := MakeSingleSessionUpdate(sessionId) session.ActiveScreenId = newActiveScreenId session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) - MainBus.SendUpdate("", update) - return nil + return update, nil } func GetRemoteState(ctx context.Context, rname string, sessionId string, windowId string) (string, *RemoteState, error) { From 15c78c8934eb3e2d2995dbf22ce993c455ae5065 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 Aug 2022 08:34:59 -0700 Subject: [PATCH 045/397] sync schema to schema.db --- db/schema.sql | 60 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index d41f17587..be05a1688 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -2,25 +2,43 @@ CREATE TABLE schema_migrations (version uint64,dirty bool); CREATE UNIQUE INDEX version_unique ON schema_migrations (version); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, - name varchar(50) NOT NULL + name varchar(50) NOT NULL, + sessionidx int NOT NULL, + activescreenid varchar(36) NOT NULL, + notifynum int NOT NULL ); -CREATE UNIQUE INDEX session_name_unique ON session(name); CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - name varchar(50) NOT NULL, curremote varchar(50) NOT NULL, - version int NOT NULL, + winopts json NOT NULL, PRIMARY KEY (sessionid, windowid) ); -CREATE UNIQUE INDEX window_name_unique ON window(sessionid, name); -CREATE TABLE session_remote ( +CREATE TABLE screen ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + activewindowid varchar(36) NOT NULL, + screenidx int NOT NULL, + screenopts json NOT NULL, + PRIMARY KEY (sessionid, screenid) +); +CREATE TABLE screen_window ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + layout json NOT NULL, + PRIMARY KEY (sessionid, screenid, windowid) +); +CREATE TABLE remote_instance ( + riid varchar(36) PRIMARY KEY, + name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - remotename varchar(50) NOT NULL, remoteid varchar(36) NOT NULL, - cwd varchar(300) NOT NULL, - PRIMARY KEY (sessionid, windowid, remotename) + sessionscope boolean NOT NULL, + state json NOT NULL ); CREATE TABLE line ( sessionid varchar(36) NOT NULL, @@ -37,28 +55,30 @@ CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, remotetype varchar(10) NOT NULL, remotename varchar(50) NOT NULL, - connectopts varchar(300) NOT NULL, - ptyout BLOB NOT NULL + autoconnect boolean NOT NULL, + initpk json NOT NULL, + sshopts json NOT NULL, + lastconnectts bigint NOT NULL ); -CREATE TABLE session_cmd ( +CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, + cmdstr text NOT NULL, + remotestate json NOT NULL, + termopts json NOT NULL, status varchar(10) NOT NULL, - startts bigint NOT NULL, - pid int NOT NULL, - runnerpid int NOT NULL, - donets bigint NOT NULL, - exitcode int NOT NULL, - ptyout BLOB NOT NULL, - runout BLOB NOT NULL, + startpk json NOT NULL, + donepk json NOT NULL, + runout json NOT NULL, + usedrows int NOT NULL, PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, userid varchar(36) NOT NULL, - ts int64 NOT NULL, + ts bigint NOT NULL, lineid varchar(36) NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); From 66f547a6958067bc64070c9edb43ecc04c8b01e7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 Aug 2022 16:21:46 -0700 Subject: [PATCH 046/397] implement new session and switch session --- pkg/cmdrunner/cmdrunner.go | 34 ++++++++++++++++++++++- pkg/sstore/dbops.go | 56 ++++++++++++++++++++++++-------------- pkg/sstore/sstore.go | 3 +- pkg/sstore/updatebus.go | 3 +- 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e8925ee93..f459e35bd 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -89,6 +89,19 @@ func resolveSessionScreen(ctx context.Context, sessionId string, screenArg strin return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) } +func resolveSession(ctx context.Context, sessionArg string) (string, error) { + sessions, err := sstore.GetBareSessions(ctx) + if err != nil { + return "", fmt.Errorf("could not retrive bare sessions") + } + for _, session := range sessions { + if session.SessionId == sessionArg || session.Name == sessionArg { + return session.SessionId, nil + } + } + return "", fmt.Errorf("could not resolve sesssion '%s' (name/id not found)", sessionArg) +} + func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { sessionId := pk.Kwargs["session"] if sessionId == "" { @@ -373,5 +386,24 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { + activate := resolveBool(pk.Kwargs["activate"], true) + update, err := sstore.InsertSessionWithName(ctx, pk.Kwargs["name"], activate) + if err != nil { + return nil, err + } + return update, nil + } + if pk.MetaSubCmd != "" { + return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) + } + firstArg := firstArg(pk) + if firstArg == "" { + return nil, fmt.Errorf("usage /session [session-name|session-id], no param specified") + } + sessionId, err := resolveSession(ctx, firstArg) + if err != nil { + return nil, err + } + return sstore.SessionUpdate{ActiveSessionId: sessionId}, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a00e93ba9..6bb87f831 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -85,11 +85,27 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { return nil } +func GetBareSessions(ctx context.Context) ([]*SessionType, error) { + var rtn []*SessionType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM session` + tx.SelectWrap(&rtn, query) + return nil + }) + if err != nil { + return nil, err + } + return rtn, nil +} + func GetAllSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session` tx.SelectWrap(&rtn, query) + for _, session := range rtn { + session.Full = true + } var screens []*ScreenType query = `SELECT * FROM screen ORDER BY screenidx` tx.SelectWrap(&screens, query) @@ -154,26 +170,16 @@ func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, er } func GetSessionById(ctx context.Context, id string) (*SessionType, error) { - var rtnSession *SessionType - err := WithTx(ctx, func(tx *TxWrap) error { - var session SessionType - query := `SELECT * FROM session WHERE sessionid = ?` - found := tx.GetWrap(&session, query, id) - if !found { - return nil - } - rtnSession = &session - query = `SELECT * FROM screen WHERE sessionid = ? ORDER BY screenidx` - tx.SelectWrap(&session.Screens, query, session.SessionId) - query = `SELECT * FROM remote_instance WHERE sessionid = ?` - tx.SelectWrap(&session.Remotes, query, session.SessionId) - session.Full = true - return nil - }) + allSessions, err := GetAllSessions(ctx) if err != nil { return nil, err } - return rtnSession, nil + for _, session := range allSessions { + if session.SessionId == id { + return session, nil + } + } + return nil, nil } func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { @@ -195,7 +201,7 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { // also creates default window, returns sessionId // if sessionName == "", it will be generated -func InsertSessionWithName(ctx context.Context, sessionName string) (string, error) { +func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*SessionUpdate, error) { newSessionId := uuid.New().String() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) @@ -210,9 +216,19 @@ func InsertSessionWithName(ctx context.Context, sessionName string) (string, err return nil }) if txErr != nil { - return "", txErr + return nil, txErr } - return newSessionId, nil + session, err := GetSessionById(ctx, newSessionId) + if err != nil { + return nil, err + } + update := &SessionUpdate{ + Sessions: []*SessionType{session}, + } + if activate { + update.ActiveSessionId = newSessionId + } + return update, nil } func containsStr(strs []string, testStr string) bool { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d35148643..b8997fdb6 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -178,6 +178,7 @@ type TermOpts struct { Rows int64 `json:"rows"` Cols int64 `json:"cols"` FlexRows bool `json:"flexrows,omitempty"` + CmdSize int64 `json:"cmdsize,omitempty"` } func (opts *TermOpts) Scan(val interface{}) error { @@ -393,7 +394,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { if session != nil { return session, nil } - _, err = InsertSessionWithName(ctx, DefaultSessionName) + _, err = InsertSessionWithName(ctx, DefaultSessionName, true) if err != nil { return nil, err } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index f36b76268..70909e4cf 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -41,7 +41,8 @@ func (WindowUpdate) WindowUpdate() string { } type SessionUpdate struct { - Sessions []*SessionType `json:"sessions"` + Sessions []*SessionType `json:"sessions"` + ActiveSessionId string `json:"activesessionid,omitempty"` } func (SessionUpdate) UpdateType() string { From a175d236c1e02ed3ade86d1817e038a1d691e972 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 9 Aug 2022 14:24:57 -0700 Subject: [PATCH 047/397] implement /cd and /compgen --- pkg/cmdrunner/cmdrunner.go | 124 ++++++++++++++++++++++++++++++++----- pkg/sstore/dbops.go | 31 ++++++++++ pkg/sstore/updatebus.go | 12 +++- 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f459e35bd..11f6e6cc5 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3,6 +3,8 @@ package cmdrunner import ( "context" "fmt" + "path" + "path/filepath" "strconv" "strings" @@ -32,6 +34,7 @@ type resolvedIds struct { ScreenId string WindowId string RemoteId string + RemoteName string RemoteState *sstore.RemoteState } @@ -59,6 +62,13 @@ func firstArg(pk *scpacket.FeCommandPacketType) string { return pk.Args[0] } +func argN(pk *scpacket.FeCommandPacketType, n int) string { + if len(pk.Args) <= n { + return "" + } + return pk.Args[n] +} + func resolveBool(arg string, def bool) bool { if arg == "" { return def @@ -138,22 +148,22 @@ func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sess return resolveSessionScreen(ctx, sessionId, screenArg) } -func resolveRemote(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string, windowId string) (string, *sstore.RemoteState, error) { - remoteArg := pk.Kwargs["remote"] - if remoteArg == "" { - return "", nil, nil +func resolveRemote(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string, windowId string) (string, string, *sstore.RemoteState, error) { + remoteName := pk.Kwargs["remote"] + if remoteName == "" { + return "", "", nil, nil } - remoteId, state, err := sstore.GetRemoteState(ctx, remoteArg, sessionId, windowId) + remoteId, state, err := sstore.GetRemoteState(ctx, remoteName, sessionId, windowId) if err != nil { - return "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteArg, err) + return "", "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteName, err) } if state == nil { state, err = remote.GetDefaultRemoteStateById(remoteId) if err != nil { - return "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteArg, err) + return "", "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteName, err) } } - return remoteId, state, nil + return remoteName, remoteId, state, nil } func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { @@ -191,7 +201,7 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int } } if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk, rtn.SessionId, rtn.WindowId) + rtn.RemoteName, rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk, rtn.SessionId, rtn.WindowId) if err != nil { return rtn, err } @@ -222,6 +232,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "cd": return CdCommand(ctx, pk) + case "compgen": + return CompGenCommand(ctx, pk) + default: return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) } @@ -354,19 +367,102 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("/cd error: %w", err) } newDir := firstArg(pk) + if newDir == "" { + return nil, nil + } + if !strings.HasPrefix(newDir, "/") { + if ids.RemoteState == nil { + return nil, fmt.Errorf("/cd error: cannot get current remote directory (can only cd with absolute path)") + } + newDir = path.Join(ids.RemoteState.Cwd, newDir) + newDir, err = filepath.Abs(newDir) + if err != nil { + return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) + } + } + fmt.Printf("cd [%s] => [%s]\n", firstArg(pk), newDir) cdPacket := packet.MakeCdPacket() cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir - localRemote := remote.GetRemoteById(ids.RemoteId) - if localRemote == nil { + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot execute command") } - resp, err := localRemote.PacketRpc(ctx, cdPacket) + _, err = curRemote.PacketRpc(ctx, cdPacket) if err != nil { return nil, err } - fmt.Printf("GOT cd RESP: %v\n", resp) - return nil, nil + remote, err := sstore.UpdateRemoteCwd(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, newDir) + if err != nil { + return nil, err + } + update := sstore.WindowUpdate{ + Window: sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + Remotes: []*sstore.RemoteInstance{remote}, + }, + } + return update, nil +} + +func getStrArr(v interface{}, field string) []string { + if v == nil { + return nil + } + m, ok := v.(map[string]interface{}) + if !ok { + return nil + } + fieldVal := m[field] + if fieldVal == nil { + return nil + } + iarr, ok := fieldVal.([]interface{}) + if !ok { + return nil + } + var sarr []string + for _, iv := range iarr { + if sv, ok := iv.(string); ok { + sarr = append(sarr, sv) + } + } + return sarr +} + +func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, fmt.Errorf("/compgen error: %w", err) + } + compType := argN(pk, 0) + prefix := argN(pk, 1) + if !packet.IsValidCompGenType(compType) { + return nil, fmt.Errorf("/compgen invalid type '%s'", compType) + } + cgPacket := packet.MakeCompGenPacket() + cgPacket.ReqId = uuid.New().String() + cgPacket.CompType = compType + cgPacket.Prefix = prefix + if ids.RemoteState == nil { + return nil, fmt.Errorf("/compgen invalid remote state") + } + cgPacket.Cwd = ids.RemoteState.Cwd + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot execute command") + } + resp, err := curRemote.PacketRpc(ctx, cgPacket) + if err != nil { + return nil, err + } + comps := getStrArr(resp.Data, "comps") + update := sstore.InfoUpdate{ + InfoTitle: fmt.Sprintf("%s completions", compType), + InfoStrings: comps, + } + return update, nil } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 6bb87f831..8b763c9ed 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -503,3 +503,34 @@ func GetRemoteState(ctx context.Context, rname string, sessionId string, windowI }) return remoteId, remoteState, txErr } + +func UpdateRemoteCwd(ctx context.Context, rname string, sessionId string, windowId string, remoteId string, cwd string) (*RemoteInstance, error) { + var ri RemoteInstance + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("cannot update remote instance cwd, no window found") + } + query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, rname) + if !found { + ri = RemoteInstance{ + RIId: uuid.New().String(), + Name: rname, + SessionId: sessionId, + WindowId: windowId, + RemoteId: remoteId, + SessionScope: (windowId == ""), + State: RemoteState{Cwd: cwd}, + } + query = `INSERT INTO remote_instance (riid, name, sessionid, windowid, remoteid, sessionscope, state) VALUES (:riid, :name, :sessionid, :windowid, :remoteid, :sessionscope, :state)` + tx.NamedExecWrap(query, ri) + return nil + } + ri.State.Cwd = cwd + query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND name = ?` + tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, ri.Name) + return nil + }) + return &ri, txErr +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 70909e4cf..f6d552943 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -9,6 +9,7 @@ const SessionUpdateStr = "session" const WindowUpdateStr = "window" const CmdUpdateStr = "cmd" const LineCmdUpdateStr = "line+cmd" +const InfoUpdateStr = "info" type UpdatePacket interface { UpdateType() string @@ -36,7 +37,7 @@ type WindowUpdate struct { Remove bool `json:"remove,omitempty"` } -func (WindowUpdate) WindowUpdate() string { +func (WindowUpdate) UpdateType() string { return WindowUpdateStr } @@ -70,6 +71,15 @@ func (LineUpdate) UpdateType() string { return LineCmdUpdateStr } +type InfoUpdate struct { + InfoTitle string `json:"infotitle"` + InfoStrings []string `json:"infostrings"` +} + +func (InfoUpdate) UpdateType() string { + return InfoUpdateStr +} + type UpdateChannel struct { SessionId string ClientId string From 81c564aca93dd146e6f40899ef9561b6e8d6f08a Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 10 Aug 2022 18:33:32 -0700 Subject: [PATCH 048/397] implement simple command completions --- go.mod | 1 + go.sum | 1 + pkg/cmdrunner/cmdrunner.go | 250 +++++++++++++++++++++++++++++-------- pkg/sstore/dbops.go | 13 ++ pkg/sstore/sstore.go | 5 +- pkg/sstore/updatebus.go | 31 +++-- 6 files changed, 243 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 511d18ff9..567032ef7 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect ) diff --git a/go.sum b/go.sum index f5a32cd6f..809fecbb3 100644 --- a/go.sum +++ b/go.sum @@ -787,6 +787,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 11f6e6cc5..d5c3eed48 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -5,6 +5,8 @@ import ( "fmt" "path" "path/filepath" + "regexp" + "sort" "strconv" "strings" @@ -55,6 +57,44 @@ func SubMetaCmd(cmd string) string { } } +var ValidCommands = []string{ + "/run", + "/eval", + "/screen", "/screen:open", "/screen:close", + "/session", "/session:open", "/session:close", + "/comment", + "/cd", + "/compgen", +} + +func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + switch SubMetaCmd(pk.MetaCmd) { + case "run": + return RunCommand(ctx, pk) + + case "eval": + return EvalCommand(ctx, pk) + + case "screen": + return ScreenCommand(ctx, pk) + + case "session": + return SessionCommand(ctx, pk) + + case "comment": + return CommentCommand(ctx, pk) + + case "cd": + return CdCommand(ctx, pk) + + case "compgen": + return CompGenCommand(ctx, pk) + + default: + return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) + } +} + func firstArg(pk *scpacket.FeCommandPacketType) string { if len(pk.Args) == 0 { return "" @@ -212,34 +252,6 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int return rtn, nil } -func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - switch SubMetaCmd(pk.MetaCmd) { - case "run": - return RunCommand(ctx, pk) - - case "eval": - return EvalCommand(ctx, pk) - - case "screen": - return ScreenCommand(ctx, pk) - - case "session": - return SessionCommand(ctx, pk) - - case "comment": - return CommentCommand(ctx, pk) - - case "cd": - return CdCommand(ctx, pk) - - case "compgen": - return CompGenCommand(ctx, pk) - - default: - return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) - } -} - func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -300,18 +312,24 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if metaCmd == "" { metaCmd = "run" } - var args []string - if metaCmd == "run" || metaCmd == "comment" { - args = []string{commandStr} - } else { - args = strings.Fields(commandStr) - } newPk := &scpacket.FeCommandPacketType{ MetaCmd: metaCmd, MetaSubCmd: metaSubCmd, - Args: args, Kwargs: pk.Kwargs, } + if metaCmd == "run" || metaCmd == "comment" { + newPk.Args = []string{commandStr} + } else { + allArgs := strings.Fields(commandStr) + for _, arg := range allArgs { + if strings.Index(arg, "=") == -1 { + newPk.Args = append(newPk.Args, arg) + continue + } + fields := strings.SplitN(arg, "=", 2) + newPk.Kwargs[fields[0]] = fields[1] + } + } return HandleCommand(ctx, newPk) } @@ -388,10 +406,14 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot execute command") } - _, err = curRemote.PacketRpc(ctx, cdPacket) + resp, err := curRemote.PacketRpc(ctx, cdPacket) if err != nil { return nil, err } + if err = resp.Err(); err != nil { + return nil, err + } + fmt.Printf("cd-resp %#v\n", resp) remote, err := sstore.UpdateRemoteCwd(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, newDir) if err != nil { return nil, err @@ -402,6 +424,10 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up WindowId: ids.WindowId, Remotes: []*sstore.RemoteInstance{remote}, }, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, newDir), + TimeoutMs: 2000, + }, } return update, nil } @@ -431,38 +457,162 @@ func getStrArr(v interface{}, field string) []string { return sarr } -func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) - if err != nil { - return nil, fmt.Errorf("/compgen error: %w", err) +func getBool(v interface{}, field string) bool { + if v == nil { + return false + } + m, ok := v.(map[string]interface{}) + if !ok { + return false + } + fieldVal := m[field] + if fieldVal == nil { + return false + } + bval, ok := fieldVal.(bool) + if !ok { + return false + } + return bval +} + +func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { + sort.Strings(comps) + update := sstore.InfoUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("%s completions", compType), + InfoStrings: comps, + InfoStringsMore: hasMore, + }, + } + return update +} + +func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore bool) sstore.UpdatePacket { + if hasMore { + return nil + } + lcp := longestPrefix(prefix, comps) + if lcp == prefix || len(lcp) < len(prefix) || !strings.HasPrefix(lcp, prefix) { + return nil + } + insertChars := lcp[len(prefix):] + clu := &sstore.CmdLineType{InsertChars: insertChars, InsertPos: pos} + return sstore.InfoUpdate{CmdLine: clu} +} + +func longestPrefix(root string, comps []string) string { + if len(comps) == 0 { + return root + } + if len(comps) == 1 { + comp := comps[0] + if len(comp) >= len(root) && strings.HasPrefix(comp, root) { + return comps[0] + " " + } + } + lcp := comps[0] + for i := 1; i < len(comps); i++ { + s := comps[i] + for j := 0; j < len(lcp); j++ { + if j >= len(s) || lcp[j] != s[j] { + lcp = lcp[0:j] + break + } + } + } + if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) { + return root + } + return lcp +} + +var wsRe = regexp.MustCompile("\\s+") + +func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string) ([]string, bool, error) { + var comps []string + for _, cmd := range ValidCommands { + if strings.HasPrefix(cmd, prefix) { + comps = append(comps, cmd) + } + } + return comps, false, nil +} + +func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string) ([]string, bool, error) { + if compType == "metacommand" { + return doMetaCompGen(ctx, ids, prefix) } - compType := argN(pk, 0) - prefix := argN(pk, 1) if !packet.IsValidCompGenType(compType) { - return nil, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix if ids.RemoteState == nil { - return nil, fmt.Errorf("/compgen invalid remote state") + return nil, false, fmt.Errorf("/compgen invalid remote state") } cgPacket.Cwd = ids.RemoteState.Cwd curRemote := remote.GetRemoteById(ids.RemoteId) if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot execute command") + return nil, false, fmt.Errorf("invalid remote, cannot execute command") } resp, err := curRemote.PacketRpc(ctx, cgPacket) if err != nil { - return nil, err + return nil, false, err + } + if err = resp.Err(); err != nil { + return nil, false, err } comps := getStrArr(resp.Data, "comps") - update := sstore.InfoUpdate{ - InfoTitle: fmt.Sprintf("%s completions", compType), - InfoStrings: comps, + hasMore := getBool(resp.Data, "hasmore") + return comps, hasMore, nil +} + +func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, fmt.Errorf("/compgen error: %w", err) } - return update, nil + cmdLine := firstArg(pk) + pos := len(cmdLine) + if pk.Kwargs["comppos"] != "" { + posArg, err := strconv.Atoi(pk.Kwargs["comppos"]) + if err != nil { + return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) + } + pos = posArg + } + if pos < 0 { + pos = 0 + } + if pos > len(cmdLine) { + pos = len(cmdLine) + } + showComps := resolveBool(pk.Kwargs["compshow"], false) + prefix := cmdLine[:pos] + parts := strings.Split(prefix, " ") + compType := "file" + if len(parts) > 0 && strings.HasPrefix(parts[0], "/") { + compType = "metacommand" + } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { + compType = "directory" + } else if len(parts) <= 1 { + compType = "command" + } + lastPart := "" + if len(parts) > 0 { + lastPart = parts[len(parts)-1] + } + comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType) + if err != nil { + return nil, err + } + if showComps { + return makeInfoFromComps(compType, comps, hasMore), nil + } + return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8b763c9ed..a27b7acf0 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -103,7 +103,9 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session` tx.SelectWrap(&rtn, query) + sessionMap := make(map[string]*SessionType) for _, session := range rtn { + sessionMap[session.SessionId] = session session.Full = true } var screens []*ScreenType @@ -132,6 +134,15 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { } screen.Windows = append(screen.Windows, sw) } + query = `SELECT * FROM remote_instance WHERE sessionscope` + var ris []*RemoteInstance + tx.SelectWrap(&ris, query) + for _, ri := range ris { + s := sessionMap[ri.SessionId] + if s != nil { + s.Remotes = append(s.Remotes, ri) + } + } return nil }) return rtn, err @@ -154,6 +165,8 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win for _, m := range cmdMaps { window.Cmds = append(window.Cmds, CmdFromMap(m)) } + query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND NOT sessionscope` + tx.SelectWrap(&window.Remotes, query, sessionId, windowId) return nil }) return rtnWindow, err diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b8997fdb6..1ba2bd853 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -194,9 +194,12 @@ type RemoteInstance struct { Name string `json:"name"` SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - RemoteId string `json"remoteid"` + RemoteId string `json:"remoteid"` SessionScope bool `json:"sessionscope"` State RemoteState `json:"state"` + + // only for updates + Remove bool `json:"remove,omitempty"` } type LineType struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index f6d552943..9a969148b 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -10,6 +10,7 @@ const WindowUpdateStr = "window" const CmdUpdateStr = "cmd" const LineCmdUpdateStr = "line+cmd" const InfoUpdateStr = "info" +const CompGenUpdateStr = "compgen" type UpdatePacket interface { UpdateType() string @@ -33,8 +34,8 @@ func (PtyDataUpdate) UpdateType() string { } type WindowUpdate struct { - Window WindowType `json:"window"` - Remove bool `json:"remove,omitempty"` + Window WindowType `json:"window"` + Info *InfoMsgType `json:"info,omitempty"` } func (WindowUpdate) UpdateType() string { @@ -44,6 +45,7 @@ func (WindowUpdate) UpdateType() string { type SessionUpdate struct { Sessions []*SessionType `json:"sessions"` ActiveSessionId string `json:"activesessionid,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` } func (SessionUpdate) UpdateType() string { @@ -62,18 +64,33 @@ func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { } type LineUpdate struct { - Line *LineType `json:"line"` - Cmd *CmdType `json:"cmd,omitempty"` - Remove bool `json:"remove,omitempty"` + Line *LineType `json:"line"` + Cmd *CmdType `json:"cmd,omitempty"` + Remove bool `json:"remove,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` } func (LineUpdate) UpdateType() string { return LineCmdUpdateStr } +type InfoMsgType struct { + InfoTitle string `json:"infotitle"` + InfoError string `json:"infoerror,omitempty"` + InfoMsg string `json:"infomsg,omitempty"` + InfoStrings []string `json:"infostrings"` + InfoStringsMore bool `json:"infostringsmore"` + TimeoutMs int64 `json:"timeoutms,omitempty"` +} + +type CmdLineType struct { + InsertChars string `json:"insertchars"` + InsertPos int64 `json:"insertpos"` +} + type InfoUpdate struct { - InfoTitle string `json:"infotitle"` - InfoStrings []string `json:"infostrings"` + Info *InfoMsgType `json:"info,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` } func (InfoUpdate) UpdateType() string { From adca87e9db26d9da8020a5d84ebaa7e4f4dc730b Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 11 Aug 2022 10:21:45 -0700 Subject: [PATCH 049/397] better directory vs file completion --- pkg/cmdrunner/cmdrunner.go | 9 ++++++--- pkg/sstore/updatebus.go | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d5c3eed48..22cc3471d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -480,9 +480,9 @@ func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.Upd sort.Strings(comps) update := sstore.InfoUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("%s completions", compType), - InfoStrings: comps, - InfoStringsMore: hasMore, + InfoTitle: fmt.Sprintf("%s completions", compType), + InfoComps: comps, + InfoCompsMore: hasMore, }, } return update @@ -508,6 +508,9 @@ func longestPrefix(root string, comps []string) string { if len(comps) == 1 { comp := comps[0] if len(comp) >= len(root) && strings.HasPrefix(comp, root) { + if strings.HasSuffix(comp, "/") { + return comps[0] + } return comps[0] + " " } } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 9a969148b..62a1f4eac 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -75,12 +75,12 @@ func (LineUpdate) UpdateType() string { } type InfoMsgType struct { - InfoTitle string `json:"infotitle"` - InfoError string `json:"infoerror,omitempty"` - InfoMsg string `json:"infomsg,omitempty"` - InfoStrings []string `json:"infostrings"` - InfoStringsMore bool `json:"infostringsmore"` - TimeoutMs int64 `json:"timeoutms,omitempty"` + InfoTitle string `json:"infotitle"` + InfoError string `json:"infoerror,omitempty"` + InfoMsg string `json:"infomsg,omitempty"` + InfoComps []string `json:"infocomps"` + InfoCompsMore bool `json:"infocompssmore"` + TimeoutMs int64 `json:"timeoutms,omitempty"` } type CmdLineType struct { From d5cf15e946160097be4c1472865ee28938e44d2f Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 11 Aug 2022 12:07:41 -0700 Subject: [PATCH 050/397] update history table --- db/migrations/000001_init.up.sql | 13 ++++++++----- db/schema.sql | 13 ++++++++----- pkg/cmdrunner/cmdrunner.go | 8 ++++++++ pkg/sstore/dbops.go | 17 +++++++++++++++++ pkg/sstore/sstore.go | 29 +++++++++++++++++++---------- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 594b12f49..21ae524d8 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -81,10 +81,13 @@ CREATE TABLE cmd ( ); CREATE TABLE history ( - sessionid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, - userid varchar(36) NOT NULL, + historyid varchar(36) PRIMARY KEY, ts bigint NOT NULL, - lineid varchar(36) NOT NULL, - PRIMARY KEY (sessionid, windowid, lineid) + userid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + lineid int NOT NULL, + cmdid varchar(36) NOT NULL, + cmdstr text NOT NULL ); diff --git a/db/schema.sql b/db/schema.sql index be05a1688..1acc4fffc 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -75,10 +75,13 @@ CREATE TABLE cmd ( PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( - sessionid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, - userid varchar(36) NOT NULL, + historyid varchar(36) PRIMARY KEY, ts bigint NOT NULL, - lineid varchar(36) NOT NULL, - PRIMARY KEY (sessionid, windowid, lineid) + userid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + lineid int NOT NULL, + cmdid varchar(36) NOT NULL, + cmdstr text NOT NULL ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 22cc3471d..0b27579c9 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -278,6 +278,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return sstore.LineUpdate{Line: rtnLine, Cmd: cmd}, nil } +func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdStr string) error { + return nil +} + func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") @@ -287,6 +291,9 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if commandStr == "" { return nil, fmt.Errorf("/eval, invalid emtpty command") } + if !resolveBool(pk.Kwargs["nohist"], false) { + addToHistory(ctx, pk, pk.Args[0]) + } metaCmd := "" metaSubCmd := "" if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { @@ -330,6 +337,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. newPk.Kwargs[fields[0]] = fields[1] } } + return HandleCommand(ctx, newPk) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a27b7acf0..e88d6d971 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -85,6 +85,23 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { return nil } +func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { + if hitem == nil { + return fmt.Errorf("cannot insert nil history item") + } + db, err := GetDB(ctx) + if err != nil { + return err + } + query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, cmdstr) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:cmdstr)` + _, err = db.NamedExec(query, hitem) + if err != nil { + return err + } + return nil +} + func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1ba2bd853..265676c10 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -82,14 +82,13 @@ func (opts WindowOptsType) Value() (driver.Value, error) { } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - CurRemote string `json:"curremote"` - WinOpts WindowOptsType `json:"winopts"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` - History []*HistoryItemType `json:"history"` - Remotes []*RemoteInstance `json:"remotes"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + CurRemote string `json:"curremote"` + WinOpts WindowOptsType `json:"winopts"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` + Remotes []*RemoteInstance `json:"remotes"` // only for updates Remove bool `json:"remove,omitempty"` @@ -158,8 +157,18 @@ type ScreenWindowType struct { } type HistoryItemType struct { - CmdStr string `json:"cmdstr"` - Remove bool `json:"remove"` + HistoryId string `json:"historyid"` + Ts int64 `json:"ts"` + UserId string `json:"userid"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + LineId int64 `json:"lineid"` + CmdId string `json:"cmdid"` + CmdStr string `json:"cmdstr"` + + // only for updates + Remove bool `json:"remove"` } type RemoteState struct { From a67ae15b325c945d776676d0cd779c6ab73ef276 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 11 Aug 2022 23:45:15 -0700 Subject: [PATCH 051/397] implement history --- cmd/main-server.go | 33 +++++++++++++++++++++++++++ db/migrations/000001_init.up.sql | 1 + db/schema.sql | 1 + pkg/cmdrunner/cmdrunner.go | 38 +++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 17 ++++++++++++-- pkg/sstore/sstore.go | 1 + pkg/sstore/updatebus.go | 11 +++++++++ 7 files changed, 97 insertions(+), 5 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 409730ac5..9364fe5f2 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "runtime/debug" + "strconv" "strings" "sync" "time" @@ -163,6 +164,37 @@ func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { return } +// params: sessionid +func HandleGetHistory(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + if _, err := uuid.Parse(sessionId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) + return + } + numItems := 1000 + numStr := qvals.Get("num") + if numStr != "" { + parsedNum, err := strconv.Atoi(numStr) + if err == nil { + numItems = parsedNum + } + } + hitems, err := sstore.GetSessionHistoryItems(r.Context(), sessionId, numItems) + if err != nil { + WriteJsonError(w, err) + return + } + rtnMap := make(map[string]interface{}) + rtnMap["history"] = hitems + WriteJsonSuccess(w, rtnMap) + return +} + // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -441,6 +473,7 @@ func main() { gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/get-remotes", HandleGetRemotes) + gr.HandleFunc("/api/get-history", HandleGetHistory) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 21ae524d8..5aa14286e 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -88,6 +88,7 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, cmdstr text NOT NULL ); diff --git a/db/schema.sql b/db/schema.sql index 1acc4fffc..043cc3484 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -82,6 +82,7 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, cmdstr text NOT NULL ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0b27579c9..479ed4a3e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" @@ -278,7 +279,29 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return sstore.LineUpdate{Line: rtnLine, Cmd: cmd}, nil } -func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, cmdStr string) error { +func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, hadError bool) error { + cmdStr := firstArg(pk) + ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return err + } + lineId, cmdId := sstore.ReadLineCmdIdFromUpdate(update) + hitem := &sstore.HistoryItemType{ + HistoryId: uuid.New().String(), + Ts: time.Now().UnixMilli(), + UserId: DefaultUserId, + SessionId: ids.SessionId, + ScreenId: ids.ScreenId, + WindowId: ids.WindowId, + LineId: lineId, + HadError: hadError, + CmdId: cmdId, + CmdStr: cmdStr, + } + err = sstore.InsertHistoryItem(ctx, hitem) + if err != nil { + return err + } return nil } @@ -291,9 +314,19 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if commandStr == "" { return nil, fmt.Errorf("/eval, invalid emtpty command") } + update, err := evalCommandInternal(ctx, pk) if !resolveBool(pk.Kwargs["nohist"], false) { - addToHistory(ctx, pk, pk.Args[0]) + err := addToHistory(ctx, pk, update, (err != nil)) + if err != nil { + fmt.Printf("[error] adding to history: %w\n", err) + // continue... + } } + return update, err +} + +func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + commandStr := strings.TrimSpace(pk.Args[0]) metaCmd := "" metaSubCmd := "" if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { @@ -337,7 +370,6 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. newPk.Kwargs[fields[0]] = fields[1] } } - return HandleCommand(ctx, newPk) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e88d6d971..7218cc12c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -93,8 +93,8 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if err != nil { return err } - query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, cmdstr) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:cmdstr)` + query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr)` _, err = db.NamedExec(query, hitem) if err != nil { return err @@ -102,6 +102,19 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { return nil } +func GetSessionHistoryItems(ctx context.Context, sessionId string, maxItems int) ([]*HistoryItemType, error) { + var rtn []*HistoryItemType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC LIMIT ?` + tx.SelectWrap(&rtn, query, sessionId, maxItems) + return nil + }) + if err != nil { + return nil, err + } + return rtn, nil +} + func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 265676c10..479decf8c 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -164,6 +164,7 @@ type HistoryItemType struct { ScreenId string `json:"screenid"` WindowId string `json:"windowid"` LineId int64 `json:"lineid"` + HadError bool `json:"haderror"` CmdId string `json:"cmdid"` CmdStr string `json:"cmdstr"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 62a1f4eac..c633f9091 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -74,6 +74,17 @@ func (LineUpdate) UpdateType() string { return LineCmdUpdateStr } +func ReadLineCmdIdFromUpdate(update UpdatePacket) (int64, string) { + lineUpdate, ok := update.(LineUpdate) + if !ok { + return 0, "" + } + if lineUpdate.Line == nil { + return 0, "" + } + return lineUpdate.Line.LineId, lineUpdate.Line.CmdId +} + type InfoMsgType struct { InfoTitle string `json:"infotitle"` InfoError string `json:"infoerror,omitempty"` From 62bbe18171330d7ee0d63b52135129831b5592e7 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 12 Aug 2022 13:59:31 -0700 Subject: [PATCH 052/397] remove PosAppend weirdness and track filepos in remote --- pkg/remote/remote.go | 6 ++++-- pkg/sstore/fileops.go | 43 +++++++++++++------------------------------ 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index faf0ab261..4adf98642 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -263,7 +263,7 @@ func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState DonePk: nil, RunOut: nil, } - err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil, sstore.PosAppend) + err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil, 0) if err != nil { return nil, err } @@ -359,6 +359,7 @@ func (runner *MShellProc) ProcessPackets() { } runner.notifyHangups_nolock() }) + var dataPos int64 for pk := range runner.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) @@ -370,12 +371,13 @@ func (runner *MShellProc) ProcessPackets() { } var ack *packet.DataAckPacketType if len(realData) > 0 { - err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, sstore.PosAppend) + err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) } + dataPos += int64(len(realData)) } if ack != nil { runner.ServerProc.Input.SendPacket(ack) diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index e296129c2..ad89f4dee 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -9,41 +9,24 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -const PosAppend = -1 - -// when calling with PosAppend, this is not multithread safe (since file could be modified). -// we need to know the real position of the write to send a proper pty update to the frontends -// in practice this is fine since we only use PosAppend in non-detached mode where -// we are reading/writing a stream in order with a single goroutine func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) error { + if pos < 0 { + return fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) + } ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) if err != nil { return err } - var fd *os.File - var realPos int64 - if pos == PosAppend { - fd, err = os.OpenFile(ptyOutFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err - } - finfo, err := fd.Stat() - if err != nil { - return err - } - realPos = finfo.Size() - } else { - fd, err = os.OpenFile(ptyOutFileName, os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err - } - realPos, err = fd.Seek(pos, 0) - if err != nil { - return err - } - if realPos != pos { - return fmt.Errorf("could not seek to pos:%d (realpos=%d)", pos, realPos) - } + fd, err := os.OpenFile(ptyOutFileName, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + realPos, err := fd.Seek(pos, 0) + if err != nil { + return err + } + if realPos != pos { + return fmt.Errorf("could not seek to pos:%d (realpos=%d)", pos, realPos) } defer fd.Close() if len(data) == 0 { From 550fd89472fe1b96b56a9135f4e591ef315bf2d7 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 13 Aug 2022 10:27:22 -0700 Subject: [PATCH 053/397] fix datapos --- pkg/remote/remote.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 4adf98642..a43742c99 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -359,7 +359,7 @@ func (runner *MShellProc) ProcessPackets() { } runner.notifyHangups_nolock() }) - var dataPos int64 + dataPosMap := make(map[base.CommandKey]int64) for pk := range runner.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) @@ -371,13 +371,14 @@ func (runner *MShellProc) ProcessPackets() { } var ack *packet.DataAckPacketType if len(realData) > 0 { + dataPos := dataPosMap[dataPk.CK] err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) } - dataPos += int64(len(realData)) + dataPosMap[dataPk.CK] += int64(len(realData)) } if ack != nil { runner.ServerProc.Input.SendPacket(ack) From 8186e54cd2074575ed9160c944de2e1f3a0bb3eb Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Aug 2022 18:42:25 -0700 Subject: [PATCH 054/397] add client table, ensure client id / public / private key --- cmd/main-server.go | 6 ++ db/migrations/000001_init.up.sql | 6 ++ pkg/sstore/sstore.go | 99 ++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9364fe5f2..6c5d359fd 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -456,6 +456,12 @@ func main() { fmt.Printf("[error] ensuring default session: %v\n", err) return } + userData, err := sstore.EnsureUserData(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring user data: %v\n", err) + return + } + fmt.Printf("userid = %s\n", userData.UserId) err = remote.LoadRemotes(context.Background()) if err != nil { fmt.Printf("[error] loading remotes: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 5aa14286e..18db0089a 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -1,3 +1,9 @@ +CREATE TABLE client ( + userid varchar(36) NOT NULL, + userpublickeybytes blob NOT NULL, + userprivatekeybytes blob NOT NULL +); + CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 479decf8c..18e903834 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -2,6 +2,10 @@ package sstore import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" "database/sql/driver" "fmt" "log" @@ -10,6 +14,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -29,11 +34,21 @@ const DefaultScreenWindowName = "w1" const DefaultCwd = "~" -const CmdStatusRunning = "running" -const CmdStatusDetached = "detached" -const CmdStatusError = "error" -const CmdStatusDone = "done" -const CmdStatusHangup = "hangup" +const ( + CmdStatusRunning = "running" + CmdStatusDetached = "detached" + CmdStatusError = "error" + CmdStatusDone = "done" + CmdStatusHangup = "hangup" +) + +const ( + ShareModeLocal = "local" + ShareModePrivate = "private" + ShareModeView = "view" + ShareModeShared = "shared" + ShareModeSharedView = "shared-view" +) var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB @@ -56,6 +71,14 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { return globalDB, globalDBErr } +type UserData struct { + UserId string `json:"userid"` + UserPrivateKeyBytes []byte `json:"-"` + UserPublicKeyBytes []byte `json:"-"` + UserPrivateKey *ecdsa.PrivateKey + UserPublicKey *ecdsa.PublicKey +} + type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` @@ -413,3 +436,69 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { } return GetSessionByName(ctx, DefaultSessionName) } + +func createUserData(tx *TxWrap) error { + userId := uuid.New().String() + curve := elliptic.P384() + pkey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return fmt.Errorf("generating P-834 key: %w", err) + } + pkBytes, err := x509.MarshalECPrivateKey(pkey) + if err != nil { + return fmt.Errorf("marshaling (pkcs8) private key bytes: %w", err) + } + pubBytes, err := x509.MarshalPKIXPublicKey(&pkey.PublicKey) + if err != nil { + return fmt.Errorf("marshaling (pkix) public key bytes: %w", err) + } + query := `INSERT INTO client (userid, userpublickeybytes, userprivatekeybytes) VALUES (?, ?, ?)` + tx.ExecWrap(query, userId, pubBytes, pkBytes) + fmt.Printf("create new userid[%s] with public/private keypair\n", userId) + return nil +} + +func EnsureUserData(ctx context.Context) (*UserData, error) { + var rtn UserData + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT count(*) FROM client` + count := tx.GetInt(query) + if count > 1 { + return fmt.Errorf("invalid client database, multiple (%d) rows in client table", count) + } + if count == 0 { + createErr := createUserData(tx) + if createErr != nil { + return createErr + } + } + found := tx.GetWrap(&rtn, "SELECT * FROM client") + if !found { + return fmt.Errorf("invalid client data") + } + return nil + }) + if err != nil { + return nil, err + } + if rtn.UserId == "" { + return nil, fmt.Errorf("invalid client data (no userid)") + } + if len(rtn.UserPrivateKeyBytes) == 0 || len(rtn.UserPublicKeyBytes) == 0 { + return nil, fmt.Errorf("invalid client data (no public/private keypair)") + } + rtn.UserPrivateKey, err = x509.ParseECPrivateKey(rtn.UserPrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("invalid client data, cannot parse private key: %w", err) + } + pubKey, err := x509.ParsePKIXPublicKey(rtn.UserPublicKeyBytes) + if err != nil { + return nil, fmt.Errorf("invalid client data, cannot parse public key: %w", err) + } + var ok bool + rtn.UserPublicKey, ok = pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("invalid client data, wrong public key type: %T", pubKey) + } + return &rtn, nil +} From b26b9924f30533441c395ea256c15e7a02776f2e Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 16 Aug 2022 12:08:26 -0700 Subject: [PATCH 055/397] updates to tables, lineid, sharemode, owneruserid --- cmd/main-server.go | 4 +-- db/migrations/000001_init.down.sql | 1 + db/migrations/000001_init.up.sql | 12 +++++-- pkg/sstore/dbops.go | 16 ++++----- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 58 +++++++++++++++++++++--------- pkg/sstore/updatebus.go | 6 ++-- 7 files changed, 64 insertions(+), 35 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 6c5d359fd..320893be5 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -437,13 +437,13 @@ func main() { if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") { err := sstore.MigrateCommandOpts(os.Args[1:]) if err != nil { - fmt.Printf("[error] %v\n", err) + fmt.Printf("[error] migrate cmd: %v\n", err) } return } err = sstore.TryMigrateUp() if err != nil { - fmt.Printf("[error] %v\n", err) + fmt.Printf("[error] migrate up: %v\n", err) return } err = sstore.EnsureLocalRemote(context.Background()) diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql index fcb937708..8896331f6 100644 --- a/db/migrations/000001_init.down.sql +++ b/db/migrations/000001_init.down.sql @@ -1,3 +1,4 @@ +DROP TABLE client; DROP TABLE session; DROP TABLE window; DROP TABLE screen; diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 18db0089a..24c6de5c0 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -9,7 +9,10 @@ CREATE TABLE session ( name varchar(50) NOT NULL, sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, - notifynum int NOT NULL + notifynum int NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + accesskey varchar(36) NOT NULL ); CREATE TABLE window ( @@ -17,6 +20,9 @@ CREATE TABLE window ( windowid varchar(36) NOT NULL, curremote varchar(50) NOT NULL, winopts json NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + shareopts json NOT NULL, PRIMARY KEY (sessionid, windowid) ); @@ -27,6 +33,8 @@ CREATE TABLE screen ( activewindowid varchar(36) NOT NULL, screenidx int NOT NULL, screenopts json NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid) ); @@ -52,7 +60,7 @@ CREATE TABLE remote_instance ( CREATE TABLE line ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - lineid int NOT NULL, + lineid varchar(36) NOT NULL, userid varchar(36) NOT NULL, ts bigint NOT NULL, linetype varchar(10) NOT NULL, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 7218cc12c..e09a6285d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -250,7 +250,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) - query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum) VALUES (?, ?, '', ?, ?)` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, owneruserid, sharemode, accesskey) VALUES (?, ?, '', ?, ?, '', 'local', '')` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { @@ -319,7 +319,7 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) screenName = fmtUniqueName(screenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = uuid.New().String() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts) VALUES (?, ?, ?, ?, ?, ?)` + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, owneruserid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout) VALUES (?, ?, ?, ?, ?)` @@ -365,8 +365,8 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr func txCreateWindow(tx *TxWrap, sessionId string) string { windowId := uuid.New().String() - query := `INSERT INTO window (sessionid, windowid, curremote, winopts) VALUES (?, ?, ?, ?)` - tx.ExecWrap(query, sessionId, windowId, LocalRemoteName, WindowOptsType{}) + query := `INSERT INTO window (sessionid, windowid, curremote, winopts, shareopts, owneruserid, sharemode) VALUES (?, ?, ?, ?, ?, '', 'local')` + tx.ExecWrap(query, sessionId, windowId, LocalRemoteName, WindowOptsType{}, WindowShareOptsType{}) return windowId } @@ -374,8 +374,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line == nil { return fmt.Errorf("line cannot be nil") } - if line.LineId != 0 { - return fmt.Errorf("new line cannot have LineId set") + if line.LineId == "" { + return fmt.Errorf("line must have lineid set") } return WithTx(ctx, func(tx *TxWrap) error { var windowId string @@ -384,10 +384,6 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if !hasWindow { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } - var maxLineId int64 - query = `SELECT COALESCE(max(lineid), 0) FROM line WHERE sessionid = ? AND windowid = ?` - tx.GetWrap(&maxLineId, query, line.SessionId, line.WindowId) - line.LineId = maxLineId + 1 query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid) VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid)` tx.NamedExecWrap(query, line) diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 043222cb0..7e8151280 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -22,7 +22,7 @@ func MakeMigrate() (*migrate.Migrate, error) { dbUrl := fmt.Sprintf("sqlite3://%s", GetSessionDBName()) m, err := migrate.New(migrationPathUrl, dbUrl) if err != nil { - return nil, err + return nil, fmt.Errorf("making migration [%s] db[%s]: %w", migrationPathUrl, GetSessionDBName(), err) } return m, nil } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 18e903834..f2f10d036 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -43,11 +43,10 @@ const ( ) const ( - ShareModeLocal = "local" - ShareModePrivate = "private" - ShareModeView = "view" - ShareModeShared = "shared" - ShareModeSharedView = "shared-view" + ShareModeLocal = "local" + ShareModePrivate = "private" + ShareModeView = "view" + ShareModeShared = "shared" ) var globalDBLock = &sync.Mutex{} @@ -66,7 +65,11 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil && globalDBErr == nil { - globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", GetSessionDBName())) + dbName := GetSessionDBName() + globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) + if globalDBErr != nil { + globalDBErr = fmt.Errorf("opening db[%s]: %w", dbName, globalDBErr) + } } return globalDB, globalDBErr } @@ -84,6 +87,9 @@ type SessionType struct { Name string `json:"name"` SessionIdx int64 `json:"sessionidx"` ActiveScreenId string `json:"activescreenid"` + OwnerUserId string `json:"owneruserid"` + ShareMode string `json:"sharemode"` + AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` Screens []*ScreenType `json:"screens"` Remotes []*RemoteInstance `json:"remotes"` @@ -104,14 +110,28 @@ func (opts WindowOptsType) Value() (driver.Value, error) { return quickValueJson(opts) } +type WindowShareOptsType struct { +} + +func (opts *WindowShareOptsType) Scan(val interface{}) error { + return quickScanJson(opts, val) +} + +func (opts WindowShareOptsType) Value() (driver.Value, error) { + return quickValueJson(opts) +} + type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - CurRemote string `json:"curremote"` - WinOpts WindowOptsType `json:"winopts"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` - Remotes []*RemoteInstance `json:"remotes"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + CurRemote string `json:"curremote"` + WinOpts WindowOptsType `json:"winopts"` + OwnerUserId string `json:"owneruserid"` + ShareMode string `json:"sharemode"` + ShareOpts WindowShareOptsType `json:"shareopts"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` + Remotes []*RemoteInstance `json:"remotes"` // only for updates Remove bool `json:"remove,omitempty"` @@ -136,6 +156,8 @@ type ScreenType struct { Name string `json:"name"` ActiveWindowId string `json:"activewindowid"` ScreenOpts ScreenOptsType `json:"screenopts"` + OwnerUserId string `json:"owneruserid"` + ShareMode string `json:"sharemode"` Windows []*ScreenWindowType `json:"windows"` // only for updates @@ -186,7 +208,7 @@ type HistoryItemType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` WindowId string `json:"windowid"` - LineId int64 `json:"lineid"` + LineId string `json:"lineid"` HadError bool `json:"haderror"` CmdId string `json:"cmdid"` CmdStr string `json:"cmdstr"` @@ -238,7 +260,7 @@ type RemoteInstance struct { type LineType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - LineId int64 `json:"lineid"` + LineId string `json:"lineid"` Ts int64 `json:"ts"` UserId string `json:"userid"` LineType string `json:"linetype"` @@ -359,6 +381,7 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId + rtn.LineId = uuid.New().String() rtn.Ts = time.Now().UnixMilli() rtn.UserId = userId rtn.LineType = LineTypeCmd @@ -370,6 +393,7 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId + rtn.LineId = uuid.New().String() rtn.Ts = time.Now().UnixMilli() rtn.UserId = userId rtn.LineType = LineTypeText @@ -398,11 +422,11 @@ func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId s func EnsureLocalRemote(ctx context.Context) error { remoteId, err := base.GetRemoteId() if err != nil { - return err + return fmt.Errorf("getting local remoteid: %w", err) } remote, err := GetRemoteById(ctx, remoteId) if err != nil { - return err + return fmt.Errorf("getting remote[%s] from db: %w", remoteId, err) } if remote != nil { return nil diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index c633f9091..6915fa2e3 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -74,13 +74,13 @@ func (LineUpdate) UpdateType() string { return LineCmdUpdateStr } -func ReadLineCmdIdFromUpdate(update UpdatePacket) (int64, string) { +func ReadLineCmdIdFromUpdate(update UpdatePacket) (string, string) { lineUpdate, ok := update.(LineUpdate) if !ok { - return 0, "" + return "", "" } if lineUpdate.Line == nil { - return 0, "" + return "", "" } return lineUpdate.Line.LineId, lineUpdate.Line.CmdId } From f86de49e3123c6afb482e48e560fd71fb61304a6 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 16 Aug 2022 15:08:28 -0700 Subject: [PATCH 056/397] updates to remote --- db/migrations/000001_init.up.sql | 7 ++- pkg/remote/remote.go | 38 ++++++++++------ pkg/sstore/dbops.go | 20 ++++----- pkg/sstore/sstore.go | 74 +++++++++++++++++++++----------- 4 files changed, 91 insertions(+), 48 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 24c6de5c0..4854b7bf3 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -71,8 +71,13 @@ CREATE TABLE line ( CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, + physicalid varchar(36) NOT NULL, remotetype varchar(10) NOT NULL, - remotename varchar(50) NOT NULL, + remotealias varchar(50) NOT NULL, + remotecanonicalname varchar(200) NOT NULL, + remotesudo boolean NOT NULL, + remoteuser varchar(50) NOT NULL, + remotehost varchar(200) NOT NULL, autoconnect boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a43742c99..7d1687366 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -35,12 +35,14 @@ type Store struct { } type RemoteState struct { - RemoteType string `json:"remotetype"` - RemoteId string `json:"remoteid"` - RemoteName string `json:"remotename"` - RemoteVars map[string]string `json:"remotevars"` - Status string `json:"status"` - DefaultState *sstore.RemoteState `json:"defaultstate"` + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalremoteid"` + RemoteAlias string `json:"remotealias"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteVars map[string]string `json:"remotevars"` + Status string `json:"status"` + DefaultState *sstore.RemoteState `json:"defaultstate"` } type MShellProc struct { @@ -78,7 +80,7 @@ func GetRemoteByName(name string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() for _, msh := range GlobalStore.Map { - if msh.Remote.RemoteName == name { + if msh.Remote.RemoteAlias == name || msh.Remote.GetName() == name { return msh } } @@ -98,13 +100,25 @@ func GetAllRemoteState() []RemoteState { var rtn []RemoteState for _, proc := range GlobalStore.Map { state := RemoteState{ - RemoteType: proc.Remote.RemoteType, - RemoteId: proc.Remote.RemoteId, - RemoteName: proc.Remote.RemoteName, - Status: proc.Status, + RemoteType: proc.Remote.RemoteType, + RemoteId: proc.Remote.RemoteId, + RemoteAlias: proc.Remote.RemoteAlias, + RemoteCanonicalName: proc.Remote.RemoteCanonicalName, + PhysicalId: proc.Remote.PhysicalId, + Status: proc.Status, } vars := make(map[string]string) - vars["user"], vars["host"] = proc.Remote.GetUserHost() + vars["user"] = proc.Remote.RemoteUser + vars["host"] = proc.Remote.RemoteHost + if proc.Remote.RemoteSudo { + vars["sudo"] = "1" + } + vars["alias"] = proc.Remote.RemoteAlias + vars["cname"] = proc.Remote.RemoteCanonicalName + vars["physicalid"] = proc.Remote.PhysicalId + vars["remoteid"] = proc.Remote.RemoteId + vars["status"] = proc.Status + vars["type"] = proc.Remote.RemoteType if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{Cwd: proc.ServerProc.InitPk.HomeDir} vars["home"] = proc.ServerProc.InitPk.HomeDir diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e09a6285d..a9b5c434a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -40,11 +40,11 @@ func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { return rtn, nil } -func GetRemoteByName(ctx context.Context, remoteName string) (*RemoteType, error) { +func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote WHERE remotename = ?` - m := tx.GetMap(query, remoteName) + query := `SELECT * FROM remote WHERE remoteid = ?` + m := tx.GetMap(query, remoteId) remote = RemoteFromMap(m) return nil }) @@ -54,11 +54,11 @@ func GetRemoteByName(ctx context.Context, remoteName string) (*RemoteType, error return remote, nil } -func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { +func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, error) { var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote WHERE remoteid = ?` - m := tx.GetMap(query, remoteId) + query := `SELECT * FROM remote WHERE physicalid = ?` + m := tx.GetMap(query, physicalId) remote = RemoteFromMap(m) return nil }) @@ -76,8 +76,8 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote ( remoteid, remotetype, remotename, autoconnect, initpk, sshopts, lastconnectts) VALUES - (:remoteid,:remotetype,:remotename,:autoconnect,:initpk,:sshopts,:lastconnectts)` + query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, lastconnectts) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:lastconnectts)` _, err = db.NamedExec(query, remote.ToMap()) if err != nil { return err @@ -533,8 +533,8 @@ func GetRemoteState(ctx context.Context, rname string, sessionId string, windowI remoteState = &ri.State return nil } - query = `SELECT remoteid FROM remote WHERE remotename = ?` - remoteId = tx.GetString(query, rname) + query = `SELECT remoteid FROM remote WHERE remotealias = ? OR remotecanonicalname = ?` + remoteId = tx.GetString(query, rname, rname) if remoteId == "" { return fmt.Errorf("remote not found", rname) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index f2f10d036..47401eaa9 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -9,8 +9,9 @@ import ( "database/sql/driver" "fmt" "log" + "os" + "os/user" "path" - "strings" "sync" "time" @@ -277,27 +278,28 @@ type SSHOpts struct { } type RemoteType struct { - RemoteId string `json:"remoteid"` - RemoteType string `json:"remotetype"` - RemoteName string `json:"remotename"` - AutoConnect bool `json:"autoconnect"` - InitPk *packet.InitPacketType `json:"inipk"` - SSHOpts *SSHOpts `json:"sshopts"` - LastConnectTs int64 `json:"lastconnectts"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalid"` + RemoteType string `json:"remotetype"` + RemoteAlias string `json:"remotealias"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteSudo bool `json:"remotesudo"` + RemoteUser string `json:"remoteuser"` + RemoteHost string `json:"remotehost"` + AutoConnect bool `json:"autoconnect"` + InitPk *packet.InitPacketType `json:"inipk"` + SSHOpts *SSHOpts `json:"sshopts"` + LastConnectTs int64 `json:"lastconnectts"` } -func (r *RemoteType) GetUserHost() (string, string) { - if r.SSHOpts == nil { - return "", "" +func (r *RemoteType) GetName() string { + if r.RemoteAlias != "" { + return r.RemoteAlias } - if r.SSHOpts.SSHUser != "" { - return r.SSHOpts.SSHUser, r.SSHOpts.SSHHost + if r.RemoteUser == "" { + return r.RemoteHost } - atIdx := strings.Index(r.SSHOpts.SSHHost, "@") - if atIdx == -1 { - return "", r.SSHOpts.SSHHost - } - return r.SSHOpts.SSHHost[0:atIdx], r.SSHOpts.SSHHost[atIdx+1:] + return fmt.Sprintf("%s@%s", r.RemoteUser, r.RemoteHost) } type CmdType struct { @@ -318,8 +320,13 @@ type CmdType struct { func (r *RemoteType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["remoteid"] = r.RemoteId + rtn["physicalid"] = r.PhysicalId rtn["remotetype"] = r.RemoteType - rtn["remotename"] = r.RemoteName + rtn["remotealias"] = r.RemoteAlias + rtn["remotecanonicalname"] = r.RemoteCanonicalName + rtn["remotesudo"] = r.RemoteSudo + rtn["remoteuser"] = r.RemoteUser + rtn["remotehost"] = r.RemoteHost rtn["autoconnect"] = r.AutoConnect rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) @@ -333,8 +340,13 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { } var r RemoteType quickSetStr(&r.RemoteId, m, "remoteid") + quickSetStr(&r.PhysicalId, m, "physicalid") quickSetStr(&r.RemoteType, m, "remotetype") - quickSetStr(&r.RemoteName, m, "remotename") + quickSetStr(&r.RemoteAlias, m, "remotealias") + quickSetStr(&r.RemoteCanonicalName, m, "remotecanonicalname") + quickSetBool(&r.RemoteSudo, m, "remotesudo") + quickSetStr(&r.RemoteUser, m, "remoteuser") + quickSetStr(&r.RemoteHost, m, "remotehost") quickSetBool(&r.AutoConnect, m, "autoconnect") quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") @@ -431,18 +443,30 @@ func EnsureLocalRemote(ctx context.Context) error { if remote != nil { return nil } + hostName, err := os.Hostname() + if err != nil { + return fmt.Errorf("getting hostname: %w", err) + } + user, err := user.Current() + if err != nil { + return fmt.Errorf("getting user: %w", err) + } // create the local remote localRemote := &RemoteType{ - RemoteId: remoteId, - RemoteType: "ssh", - RemoteName: LocalRemoteName, - AutoConnect: true, + RemoteId: remoteId, + RemoteType: "ssh", + RemoteAlias: LocalRemoteName, + RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), + RemoteSudo: false, + RemoteUser: user.Username, + RemoteHost: hostName, + AutoConnect: true, } err = InsertRemote(ctx, localRemote) if err != nil { return err } - log.Printf("[db] added remote '%s', id=%s\n", localRemote.RemoteName, localRemote.RemoteId) + log.Printf("[db] added remote '%s', id=%s\n", localRemote.GetName(), localRemote.RemoteId) return nil } From 249bf88a4d1ca71ae8609f9ac8d3ca74a55f7e4b Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 17 Aug 2022 12:24:09 -0700 Subject: [PATCH 057/397] testing a 2nd remote with mshell --server --- cmd/main-server.go | 5 +++ db/schema.sql | 24 +++++++++++-- pkg/cmdrunner/cmdrunner.go | 71 +++++++++++++++++++++++++++++++++----- pkg/remote/remote.go | 53 +++++++++++++++++++++++----- pkg/sstore/dbops.go | 27 +++++++++++++++ pkg/sstore/sstore.go | 42 +++++++++++++++++++--- pkg/sstore/updatebus.go | 5 --- 7 files changed, 198 insertions(+), 29 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 320893be5..cfc23754d 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -451,6 +451,11 @@ func main() { fmt.Printf("[error] ensuring local remote: %v\n", err) return } + err = sstore.AddTest01Remote(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring test01 remote: %v\n", err) + return + } _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) diff --git a/db/schema.sql b/db/schema.sql index 043cc3484..7a32d70c2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,17 +1,28 @@ CREATE TABLE schema_migrations (version uint64,dirty bool); CREATE UNIQUE INDEX version_unique ON schema_migrations (version); +CREATE TABLE client ( + userid varchar(36) NOT NULL, + userpublickeybytes blob NOT NULL, + userprivatekeybytes blob NOT NULL +); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, - notifynum int NOT NULL + notifynum int NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + accesskey varchar(36) NOT NULL ); CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, curremote varchar(50) NOT NULL, winopts json NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + shareopts json NOT NULL, PRIMARY KEY (sessionid, windowid) ); CREATE TABLE screen ( @@ -21,6 +32,8 @@ CREATE TABLE screen ( activewindowid varchar(36) NOT NULL, screenidx int NOT NULL, screenopts json NOT NULL, + owneruserid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid) ); CREATE TABLE screen_window ( @@ -43,7 +56,7 @@ CREATE TABLE remote_instance ( CREATE TABLE line ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - lineid int NOT NULL, + lineid varchar(36) NOT NULL, userid varchar(36) NOT NULL, ts bigint NOT NULL, linetype varchar(10) NOT NULL, @@ -53,8 +66,13 @@ CREATE TABLE line ( ); CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, + physicalid varchar(36) NOT NULL, remotetype varchar(10) NOT NULL, - remotename varchar(50) NOT NULL, + remotealias varchar(50) NOT NULL, + remotecanonicalname varchar(200) NOT NULL, + remotesudo boolean NOT NULL, + remoteuser varchar(50) NOT NULL, + remotehost varchar(200) NOT NULL, autoconnect boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 479ed4a3e..03909ef07 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -88,6 +88,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "cd": return CdCommand(ctx, pk) + case "cr": + return CrCommand(ctx, pk) + case "compgen": return CompGenCommand(ctx, pk) @@ -189,8 +192,8 @@ func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sess return resolveSessionScreen(ctx, sessionId, screenArg) } -func resolveRemote(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string, windowId string) (string, string, *sstore.RemoteState, error) { - remoteName := pk.Kwargs["remote"] +// returns (remoteName, remoteId, state, err) +func resolveRemote(ctx context.Context, remoteName string, sessionId string, windowId string) (string, string, *sstore.RemoteState, error) { if remoteName == "" { return "", "", nil, nil } @@ -242,7 +245,7 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int } } if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rtn.RemoteName, rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk, rtn.SessionId, rtn.WindowId) + rtn.RemoteName, rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) if err != nil { return rtn, err } @@ -332,6 +335,9 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { metaCmd = "cd" commandStr = strings.TrimSpace(commandStr[2:]) + } else if commandStr == "cr" || strings.HasPrefix(commandStr, "cr ") { + metaCmd = "cr" + commandStr = strings.TrimSpace(commandStr[2:]) } else if commandStr[0] == '/' { spaceIdx := strings.Index(commandStr, " ") if spaceIdx == -1 { @@ -419,14 +425,67 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } +func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window) + if err != nil { + return nil, fmt.Errorf("/cr error: %w", err) + } + newRemote := firstArg(pk) + if newRemote == "" { + return nil, nil + } + remoteName, remoteId, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + fmt.Printf("found: name[%s] id[%s] err[%v]\n", remoteName, remoteId, err) + if err != nil { + return nil, err + } + if remoteId == "" { + return nil, fmt.Errorf("/cr error: remote not found") + } + err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, remoteName) + if err != nil { + return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) + } + update := sstore.WindowUpdate{ + Window: sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + CurRemote: remoteName, + }, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("current remote = %s", remoteName), + TimeoutMs: 2000, + }, + } + return update, nil +} + func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { return nil, fmt.Errorf("/cd error: %w", err) } newDir := firstArg(pk) + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot change directory") + } + if !curRemote.IsConnected() { + return nil, fmt.Errorf("remote is not connected, cannot change directory") + } if newDir == "" { - return nil, nil + if ids.RemoteState == nil { + return nil, fmt.Errorf("remote state is not available") + } + return sstore.InfoUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, ids.RemoteState.Cwd), + }, + }, nil + } + newDir, err = curRemote.ExpandHomeDir(newDir) + if err != nil { + return nil, err } if !strings.HasPrefix(newDir, "/") { if ids.RemoteState == nil { @@ -442,10 +501,6 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up cdPacket := packet.MakeCdPacket() cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir - curRemote := remote.GetRemoteById(ids.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot execute command") - } resp, err := curRemote.PacketRpc(ctx, cdPacket) if err != nil { return nil, err diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 7d1687366..c90ebd87f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -5,7 +5,8 @@ import ( "encoding/base64" "errors" "fmt" - "os/exec" + "path" + "strings" "sync" "github.com/scripthaus-dev/mshell/pkg/base" @@ -19,6 +20,17 @@ const DefaultTermRows = 25 const DefaultTermCols = 80 const DefaultTerm = "xterm-256color" +const MShellServerCommand = ` +PATH=$PATH:~/.mshell; +which mshell > /dev/null; +if [[ "$?" -ne 0 ]] +then + printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s | %s\"}\n" "$(uname -s)" "$(uname -m)" +else + mshell --server +fi +` + const ( StatusInit = "init" StatusConnected = "connected" @@ -154,17 +166,23 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { return rtn } +func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { + if opts == nil { + return shexec.SSHOpts{} + } + return shexec.SSHOpts{ + SSHHost: opts.SSHHost, + SSHOptsStr: opts.SSHOptsStr, + SSHIdentity: opts.SSHIdentity, + SSHUser: opts.SSHUser, + } +} + func (msh *MShellProc) Launch() { msh.Lock.Lock() defer msh.Lock.Unlock() - msPath, err := base.GetMShellPath() - if err != nil { - msh.Status = StatusError - msh.Err = err - return - } - ecmd := exec.Command(msPath, "--server") + ecmd := convertSSHOpts(msh.Remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) cproc, err := shexec.MakeClientProc(ecmd) if err != nil { msh.Status = StatusError @@ -203,6 +221,25 @@ func (msh *MShellProc) GetDefaultState() *sstore.RemoteState { return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir} } +func (msh *MShellProc) ExpandHomeDir(pathStr string) (string, error) { + if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") { + return pathStr, nil + } + msh.Lock.Lock() + defer msh.Lock.Unlock() + if msh.ServerProc.InitPk == nil { + return "", fmt.Errorf("remote not connected, does not have home directory set for ~ expansion") + } + homeDir := msh.ServerProc.InitPk.HomeDir + if homeDir == "" { + return "", fmt.Errorf("remote does not have HOME set, cannot do ~ expansion") + } + if pathStr == "~" { + return homeDir, nil + } + return path.Join(homeDir, pathStr[2:]), nil +} + func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { msh.Lock.Lock() defer msh.Lock.Unlock() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a9b5c434a..14fde7c0e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -40,6 +40,20 @@ func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { return rtn, nil } +func GetRemoteByAlias(ctx context.Context, alias string) (*RemoteType, error) { + var remote *RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote WHERE remotealias = ?` + m := tx.GetMap(query, alias) + remote = RemoteFromMap(m) + return nil + }) + if err != nil { + return nil, err + } + return remote, nil +} + func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { @@ -573,3 +587,16 @@ func UpdateRemoteCwd(ctx context.Context, rname string, sessionId string, window }) return &ri, txErr } + +func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, remoteName string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("cannot update curremote, no window found") + } + query = `UPDATE window SET curremote = ? WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, remoteName, sessionId, windowId) + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 47401eaa9..046569820 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -432,13 +432,13 @@ func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId s } func EnsureLocalRemote(ctx context.Context) error { - remoteId, err := base.GetRemoteId() + physicalId, err := base.GetRemoteId() if err != nil { - return fmt.Errorf("getting local remoteid: %w", err) + return fmt.Errorf("getting local physical remoteid: %w", err) } - remote, err := GetRemoteById(ctx, remoteId) + remote, err := GetRemoteByPhysicalId(ctx, physicalId) if err != nil { - return fmt.Errorf("getting remote[%s] from db: %w", remoteId, err) + return fmt.Errorf("getting remote[%s] from db: %w", physicalId, err) } if remote != nil { return nil @@ -453,7 +453,8 @@ func EnsureLocalRemote(ctx context.Context) error { } // create the local remote localRemote := &RemoteType{ - RemoteId: remoteId, + RemoteId: uuid.New().String(), + PhysicalId: physicalId, RemoteType: "ssh", RemoteAlias: LocalRemoteName, RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), @@ -470,6 +471,37 @@ func EnsureLocalRemote(ctx context.Context) error { return nil } +func AddTest01Remote(ctx context.Context) error { + remote, err := GetRemoteByAlias(ctx, "test01") + if err != nil { + return fmt.Errorf("getting remote[test01] from db: %w", err) + } + if remote != nil { + return nil + } + testRemote := &RemoteType{ + RemoteId: uuid.New().String(), + RemoteType: "ssh", + RemoteAlias: "test01", + RemoteCanonicalName: "ubuntu@test01.ec2", + RemoteSudo: false, + RemoteUser: "ubuntu", + RemoteHost: "test01.ec2", + SSHOpts: &SSHOpts{ + SSHHost: "test01.ec2", + SSHUser: "ubuntu", + SSHIdentity: "/Users/mike/aws/mfmt.pem", + }, + AutoConnect: true, + } + err = InsertRemote(ctx, testRemote) + if err != nil { + return err + } + log.Printf("[db] added remote '%s', id=%s\n", testRemote.GetName(), testRemote.RemoteId) + return nil +} + func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { session, err := GetSessionByName(ctx, DefaultSessionName) if err != nil { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 6915fa2e3..3ec9708c8 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -16,11 +16,6 @@ type UpdatePacket interface { UpdateType() string } -type UpdateCmd struct { - CmdId string - Status string -} - type PtyDataUpdate struct { SessionId string `json:"sessionid"` CmdId string `json:"cmdid"` From b142e350be28ba3ecf77e26d573c11b2a6f73e52 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 17 Aug 2022 12:54:40 -0700 Subject: [PATCH 058/397] capture uname --- pkg/remote/remote.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c90ebd87f..e993fedcf 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -64,6 +64,7 @@ type MShellProc struct { // runtime Status string ServerProc *shexec.ClientProc + UName string Err error RunningCmds []base.CommandKey @@ -183,12 +184,15 @@ func (msh *MShellProc) Launch() { defer msh.Lock.Unlock() ecmd := convertSSHOpts(msh.Remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) - cproc, err := shexec.MakeClientProc(ecmd) + cproc, uname, err := shexec.MakeClientProc(ecmd) + msh.UName = uname if err != nil { msh.Status = StatusError msh.Err = err + fmt.Printf("[error] connecting remote %s (%s): %w\n", msh.Remote.GetName(), msh.UName, err) return } + fmt.Printf("connected remote %s\n", msh.Remote.GetName()) msh.ServerProc = cproc msh.Status = StatusConnected go func() { From 03cfabd9b6f3df4361b463f84901e4f56f4c6699 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 19 Aug 2022 13:23:00 -0700 Subject: [PATCH 059/397] convert ptyout files to CF files (fixed size circular buffer files). connect to remotes with their own controlling terminal and capture that terminal output. POC to send password to controlling terminal to login. --- cmd/main-server.go | 35 +++---------- db/migrations/000001_init.up.sql | 1 + pkg/remote/remote.go | 84 ++++++++++++++++++++++++++++---- pkg/scbase/scbase.go | 4 +- pkg/sstore/dbops.go | 4 +- pkg/sstore/fileops.go | 45 +++++++++++------ pkg/sstore/sstore.go | 53 ++++++++++++++++++-- 7 files changed, 167 insertions(+), 59 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index cfc23754d..e723e6895 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "io/fs" "net/http" "os" @@ -221,11 +220,6 @@ func HandleGetWindow(w http.ResponseWriter, r *http.Request) { return } -func GetPtyOutFile(sessionId string, cmdId string) string { - pathStr := fmt.Sprintf("/Users/mike/scripthaus/.sessions/%s/%s.ptyout", sessionId, cmdId) - return pathStr -} - func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") @@ -249,25 +243,18 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) return } - pathStr, err := scbase.PtyOutFile(sessionId, cmdId) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("cannot get ptyout file name: %v", err))) - return - } - fd, err := os.Open(pathStr) + _, data, err := sstore.ReadFullPtyOutFile(r.Context(), sessionId, cmdId) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusOK) return } w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("cannot open file '%s': %v", pathStr, err))) + w.Write([]byte(fmt.Sprintf("error reading ptyout file: %v", err))) return } - defer fd.Close() w.WriteHeader(http.StatusOK) - io.Copy(w, fd) + w.Write(data) } func WriteJsonError(w http.ResponseWriter, errVal error) { @@ -398,17 +385,6 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { // cmd-type = comment // cmd-type = command, commandid=ABC -// how to know if command is still executing? is command done? - -// local -- .ptyout, .stdin -// remote -- transfer controller program -// controller-startcmd -- start command (with options) => returns cmdid -// controller-watchsession [sessionid] -// transfer [cmdid:pos] pairs. streams back anything new written to ptyout on stdout -// stdin-packet [cmdid:user:data] -// startcmd will figure out the correct -// - func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) @@ -456,6 +432,11 @@ func main() { fmt.Printf("[error] ensuring test01 remote: %v\n", err) return } + err = sstore.AddTest02Remote(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring test02 remote: %v\n", err) + return + } _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 4854b7bf3..eedc7bf41 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -81,6 +81,7 @@ CREATE TABLE remote ( autoconnect boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, + remoteopts json NOT NULL, lastconnectts bigint NOT NULL ); diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e993fedcf..3bd5ac215 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -5,10 +5,16 @@ import ( "encoding/base64" "errors" "fmt" + "io" + "os" + "os/exec" "path" "strings" "sync" + "syscall" + "time" + "github.com/creack/pty" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" @@ -19,6 +25,7 @@ const RemoteTypeMShell = "mshell" const DefaultTermRows = 25 const DefaultTermCols = 80 const DefaultTerm = "xterm-256color" +const DefaultMaxPtySize = 1024 * 1024 const MShellServerCommand = ` PATH=$PATH:~/.mshell; @@ -50,11 +57,13 @@ type RemoteState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalremoteid"` - RemoteAlias string `json:"remotealias"` + RemoteAlias string `json:"remotealias,omitempty"` RemoteCanonicalName string `json:"remotecanonicalname"` RemoteVars map[string]string `json:"remotevars"` Status string `json:"status"` + ErrorStr string `json:"errorstr,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` + AutoConnect bool `json:"autoconnect"` } type MShellProc struct { @@ -62,10 +71,11 @@ type MShellProc struct { Remote *sstore.RemoteType // runtime - Status string - ServerProc *shexec.ClientProc - UName string - Err error + Status string + ServerProc *shexec.ClientProc + UName string + Err error + ControllingPty *os.File RunningCmds []base.CommandKey } @@ -119,6 +129,10 @@ func GetAllRemoteState() []RemoteState { RemoteCanonicalName: proc.Remote.RemoteCanonicalName, PhysicalId: proc.Remote.PhysicalId, Status: proc.Status, + AutoConnect: proc.Remote.AutoConnect, + } + if proc.Err != nil { + state.ErrorStr = proc.Err.Error() } vars := make(map[string]string) vars["user"] = proc.Remote.RemoteUser @@ -179,17 +193,70 @@ func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { } } +func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) error { + cmdPty, cmdTty, err := pty.Open() + if err != nil { + return err + } + msh.ControllingPty = cmdPty + ecmd.ExtraFiles = append(ecmd.ExtraFiles, cmdTty) + if ecmd.SysProcAttr == nil { + ecmd.SysProcAttr = &syscall.SysProcAttr{} + } + ecmd.SysProcAttr.Setsid = true + ecmd.SysProcAttr.Setctty = true + ecmd.SysProcAttr.Ctty = len(ecmd.ExtraFiles) + 3 - 1 + return nil +} + func (msh *MShellProc) Launch() { msh.Lock.Lock() defer msh.Lock.Unlock() ecmd := convertSSHOpts(msh.Remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) + err := msh.addControllingTty(ecmd) + if err != nil { + msh.Status = StatusError + msh.Err = fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) + return + } + defer func() { + if len(ecmd.ExtraFiles) > 0 { + ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() + } + }() + remoteName := msh.Remote.GetName() + go func() { + fmt.Printf("[c-pty %s] starting...\n", msh.Remote.GetName()) + buf := make([]byte, 100) + for { + n, readErr := msh.ControllingPty.Read(buf) + if readErr == io.EOF { + break + } + if readErr != nil { + fmt.Printf("[error] read from controlling-pty [%s]: %v\n", remoteName, readErr) + break + } + readStr := string(buf[0:n]) + readStr = strings.ReplaceAll(readStr, "\r", "") + readStr = strings.ReplaceAll(readStr, "\n", "\\n") + fmt.Printf("[c-pty %s] %d '%s'\n", remoteName, n, readStr) + } + }() + if remoteName == "test2" { + go func() { + time.Sleep(2 * time.Second) + msh.ControllingPty.Write([]byte(Test2Pw)) + fmt.Printf("[c-pty %s] wrote password!\n", remoteName) + }() + } cproc, uname, err := shexec.MakeClientProc(ecmd) msh.UName = uname if err != nil { msh.Status = StatusError msh.Err = err - fmt.Printf("[error] connecting remote %s (%s): %w\n", msh.Remote.GetName(), msh.UName, err) + fmt.Printf("[error] connecting remote %s (%s): %v\n", msh.Remote.GetName(), msh.UName, err) return } fmt.Printf("connected remote %s\n", msh.Remote.GetName()) @@ -203,7 +270,6 @@ func (msh *MShellProc) Launch() { msh.Status = StatusDisconnected } }) - fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) }() go msh.ProcessPackets() @@ -270,7 +336,7 @@ func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { } func makeTermOpts() sstore.TermOpts { - return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true} + return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true, MaxPtySize: DefaultMaxPtySize} } func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { @@ -318,7 +384,7 @@ func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState DonePk: nil, RunOut: nil, } - err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, nil, 0) + err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { return nil, err } diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index f30b53092..a64c39bdc 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -90,7 +90,7 @@ func PtyOutFile(sessionId string, cmdId string) (string, error) { if err != nil { return "", err } - return fmt.Sprintf("%s/%s.ptyout", sdir, cmdId), nil + return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } func RunOutFile(sessionId string, cmdId string) (string, error) { @@ -108,7 +108,7 @@ func RemotePtyOut(remoteId string) (string, error) { if err != nil { return "", err } - return fmt.Sprintf("%s/%s.ptyout", rdir, remoteId), nil + return fmt.Sprintf("%s/%s.ptyout.cf", rdir, remoteId), nil } type ScFileNameGenerator struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 14fde7c0e..954d6d08e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -90,8 +90,8 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, lastconnectts) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:lastconnectts)` + query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, remoteopts, lastconnectts) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:remoteopts,:lastconnectts)` _, err = db.NamedExec(query, remote.ToMap()) if err != nil { return err diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index ad89f4dee..e55002600 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -4,11 +4,23 @@ import ( "context" "encoding/base64" "fmt" - "os" + "github.com/scripthaus-dev/mshell/pkg/cirfile" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) +func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSize int64) error { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return err + } + f, err := cirfile.CreateCirFile(ptyOutFileName, maxSize) + if err != nil { + return err + } + return f.Close() +} + func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) error { if pos < 0 { return fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) @@ -17,22 +29,12 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat if err != nil { return err } - fd, err := os.OpenFile(ptyOutFileName, os.O_WRONLY|os.O_CREATE, 0600) + f, err := cirfile.OpenCirFile(ptyOutFileName) if err != nil { return err } - realPos, err := fd.Seek(pos, 0) - if err != nil { - return err - } - if realPos != pos { - return fmt.Errorf("could not seek to pos:%d (realpos=%d)", pos, realPos) - } - defer fd.Close() - if len(data) == 0 { - return nil - } - _, err = fd.Write(data) + defer f.Close() + err = f.WriteAt(ctx, data, pos) if err != nil { return err } @@ -40,10 +42,23 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat update := &PtyDataUpdate{ SessionId: sessionId, CmdId: cmdId, - PtyPos: realPos, + PtyPos: pos, PtyData64: data64, PtyDataLen: int64(len(data)), } MainBus.SendUpdate(sessionId, update) return nil } + +func ReadFullPtyOutFile(ctx context.Context, sessionId string, cmdId string) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, 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.ReadAll(ctx) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 046569820..c189f8860 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -231,10 +231,10 @@ func (s RemoteState) Value() (driver.Value, error) { } type TermOpts struct { - Rows int64 `json:"rows"` - Cols int64 `json:"cols"` - FlexRows bool `json:"flexrows,omitempty"` - CmdSize int64 `json:"cmdsize,omitempty"` + Rows int64 `json:"rows"` + Cols int64 `json:"cols"` + FlexRows bool `json:"flexrows,omitempty"` + MaxPtySize int64 `json:"maxptysize,omitempty"` } func (opts *TermOpts) Scan(val interface{}) error { @@ -277,6 +277,18 @@ type SSHOpts struct { SSHUser string `json:"sshuser"` } +type RemoteOptsType struct { + Color string `json:"color"` +} + +func (opts *RemoteOptsType) Scan(val interface{}) error { + return quickScanJson(opts, val) +} + +func (opts RemoteOptsType) Value() (driver.Value, error) { + return quickValueJson(opts) +} + type RemoteType struct { RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalid"` @@ -289,6 +301,7 @@ type RemoteType struct { AutoConnect bool `json:"autoconnect"` InitPk *packet.InitPacketType `json:"inipk"` SSHOpts *SSHOpts `json:"sshopts"` + RemoteOpts *RemoteOptsType `json:"remoteopts"` LastConnectTs int64 `json:"lastconnectts"` } @@ -330,6 +343,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["autoconnect"] = r.AutoConnect rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) + rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs return rtn } @@ -350,6 +364,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetBool(&r.AutoConnect, m, "autoconnect") quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") + quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") return &r } @@ -502,6 +517,36 @@ func AddTest01Remote(ctx context.Context) error { return nil } +func AddTest02Remote(ctx context.Context) error { + remote, err := GetRemoteByAlias(ctx, "test2") + if err != nil { + return fmt.Errorf("getting remote[test01] from db: %w", err) + } + if remote != nil { + return nil + } + testRemote := &RemoteType{ + RemoteId: uuid.New().String(), + RemoteType: "ssh", + RemoteAlias: "test2", + RemoteCanonicalName: "test2@test01.ec2", + RemoteSudo: false, + RemoteUser: "test2", + RemoteHost: "test01.ec2", + SSHOpts: &SSHOpts{ + SSHHost: "test01.ec2", + SSHUser: "test2", + }, + AutoConnect: true, + } + err = InsertRemote(ctx, testRemote) + if err != nil { + return err + } + log.Printf("[db] added remote '%s', id=%s\n", testRemote.GetName(), testRemote.RemoteId) + return nil +} + func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { session, err := GetSessionByName(ctx, DefaultSessionName) if err != nil { From b2a2b6252d161afa1fd3c9cc3e2dc61eb920ed6d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 19 Aug 2022 17:14:53 -0700 Subject: [PATCH 060/397] send cmd updates for donepk --- pkg/remote/remote.go | 24 +++++++++++++++--------- pkg/sstore/dbops.go | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 3bd5ac215..410ba2198 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -48,9 +48,8 @@ const ( var GlobalStore *Store type Store struct { - Lock *sync.Mutex - Map map[string]*MShellProc // key=remoteid - CmdStatusCallback func(ck base.CommandKey, status string) + Lock *sync.Mutex + Map map[string]*MShellProc // key=remoteid } type RemoteState struct { @@ -440,13 +439,17 @@ func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *pa } func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { - err := sstore.UpdateCmdDonePk(context.Background(), donePk) + update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) if err != nil { fmt.Printf("[error] updating cmddone: %v\n", err) return } - if GlobalStore.CmdStatusCallback != nil { - GlobalStore.CmdStatusCallback(donePk.CK, sstore.CmdStatusDone) + if update != nil { + // TODO fix timing issue (this update gets to the FE before run-command returns for short lived commands) + go func() { + time.Sleep(10 * time.Millisecond) + sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) + }() } return } @@ -461,10 +464,13 @@ func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { } func (msh *MShellProc) notifyHangups_nolock() { - if GlobalStore.CmdStatusCallback != nil { - for _, ck := range msh.RunningCmds { - GlobalStore.CmdStatusCallback(ck, sstore.CmdStatusHangup) + for _, ck := range msh.RunningCmds { + cmd, err := sstore.GetCmdById(context.Background(), ck.GetSessionId(), ck.GetCmdId()) + if err != nil { + continue } + update := sstore.LineUpdate{Cmd: cmd} + sstore.MainBus.SendUpdate(ck.GetSessionId(), update) } msh.RunningCmds = nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 954d6d08e..8df780cde 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -427,15 +427,28 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, return cmd, nil } -func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) error { +func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (UpdatePacket, error) { if donePk == nil || donePk.CK.IsEmpty() { - return fmt.Errorf("invalid cmddone packet (no ck)") + return nil, fmt.Errorf("invalid cmddone packet (no ck)") } - return WithTx(ctx, func(tx *TxWrap) error { + var rtnCmd *CmdType + txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ?, donepk = ? WHERE sessionid = ? AND cmdid = ?` tx.ExecWrap(query, CmdStatusDone, quickJson(donePk), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + var err error + rtnCmd, err = GetCmdById(tx.Context(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + if err != nil { + return err + } return nil }) + if txErr != nil { + return nil, txErr + } + if rtnCmd == nil { + return nil, fmt.Errorf("cmd data not found for ck[%s]", donePk.CK) + } + return LineUpdate{Cmd: rtnCmd}, nil } func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { From 9d150dc7e375c25dd18bfda11f5a417accd204d9 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 21 Aug 2022 12:31:29 -0700 Subject: [PATCH 061/397] connectmode, add stubs for export/setenv command --- db/migrations/000001_init.up.sql | 2 +- db/schema.sql | 3 ++- pkg/cmdrunner/cmdrunner.go | 16 ++++++++++++++++ pkg/remote/remote.go | 6 +++--- pkg/sstore/dbops.go | 4 ++-- pkg/sstore/sstore.go | 28 +++++++++++++++++++--------- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index eedc7bf41..1c6d6bf6d 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -78,7 +78,7 @@ CREATE TABLE remote ( remotesudo boolean NOT NULL, remoteuser varchar(50) NOT NULL, remotehost varchar(200) NOT NULL, - autoconnect boolean NOT NULL, + connectmode varchar(20) NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, diff --git a/db/schema.sql b/db/schema.sql index 7a32d70c2..eedef254e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -73,9 +73,10 @@ CREATE TABLE remote ( remotesudo boolean NOT NULL, remoteuser varchar(50) NOT NULL, remotehost varchar(200) NOT NULL, - autoconnect boolean NOT NULL, + connectmode varchar(20) NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, + remoteopts json NOT NULL, lastconnectts bigint NOT NULL ); CREATE TABLE cmd ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 03909ef07..d2a8b038b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -53,6 +53,8 @@ func SubMetaCmd(cmd string) string { return "comment" case "e": return "eval" + case "export": + return "setenv" default: return cmd } @@ -66,6 +68,7 @@ var ValidCommands = []string{ "/comment", "/cd", "/compgen", + "/setenv", } func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -94,6 +97,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "compgen": return CompGenCommand(ctx, pk) + case "setenv": + return SetEnvCommand(ctx, pk) + default: return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) } @@ -338,6 +344,12 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) } else if commandStr == "cr" || strings.HasPrefix(commandStr, "cr ") { metaCmd = "cr" commandStr = strings.TrimSpace(commandStr[2:]) + } else if commandStr == "export" || strings.HasPrefix(commandStr, "export ") { + metaCmd = "setenv" + commandStr = strings.TrimSpace(commandStr[6:]) + } else if commandStr == "setenv" || strings.HasPrefix(commandStr, "setenv ") { + metaCmd = "setenv" + commandStr = strings.TrimSpace(commandStr[6:]) } else if commandStr[0] == '/' { spaceIdx := strings.Index(commandStr, " ") if spaceIdx == -1 { @@ -425,6 +437,10 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } +func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 410ba2198..89593cad2 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -62,7 +62,7 @@ type RemoteState struct { Status string `json:"status"` ErrorStr string `json:"errorstr,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` - AutoConnect bool `json:"autoconnect"` + ConnectMode string `json:"connectmode"` } type MShellProc struct { @@ -91,7 +91,7 @@ func LoadRemotes(ctx context.Context) error { for _, remote := range allRemotes { msh := MakeMShell(remote) GlobalStore.Map[remote.RemoteId] = msh - if remote.AutoConnect { + if remote.ConnectMode == sstore.ConnectModeStartup { go msh.Launch() } } @@ -128,7 +128,7 @@ func GetAllRemoteState() []RemoteState { RemoteCanonicalName: proc.Remote.RemoteCanonicalName, PhysicalId: proc.Remote.PhysicalId, Status: proc.Status, - AutoConnect: proc.Remote.AutoConnect, + ConnectMode: proc.Remote.ConnectMode, } if proc.Err != nil { state.ErrorStr = proc.Err.Error() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8df780cde..8d105757a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -90,8 +90,8 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if err != nil { return err } - query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, autoconnect, initpk, sshopts, remoteopts, lastconnectts) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:autoconnect,:initpk,:sshopts,:remoteopts,:lastconnectts)` + query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts)` _, err = db.NamedExec(query, remote.ToMap()) if err != nil { return err diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c189f8860..b814ec4d3 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -50,6 +50,16 @@ const ( ShareModeShared = "shared" ) +const ( + ConnectModeStartup = "startup" + ConnectModeAuto = "auto" + ConnectModeManual = "manual" +) + +const ( + RemoteTypeSsh = "ssh" +) + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error @@ -298,7 +308,7 @@ type RemoteType struct { RemoteSudo bool `json:"remotesudo"` RemoteUser string `json:"remoteuser"` RemoteHost string `json:"remotehost"` - AutoConnect bool `json:"autoconnect"` + ConnectMode string `json:"connectmode"` InitPk *packet.InitPacketType `json:"inipk"` SSHOpts *SSHOpts `json:"sshopts"` RemoteOpts *RemoteOptsType `json:"remoteopts"` @@ -340,7 +350,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["remotesudo"] = r.RemoteSudo rtn["remoteuser"] = r.RemoteUser rtn["remotehost"] = r.RemoteHost - rtn["autoconnect"] = r.AutoConnect + rtn["connectmode"] = r.ConnectMode rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) rtn["remoteopts"] = quickJson(r.RemoteOpts) @@ -361,7 +371,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetBool(&r.RemoteSudo, m, "remotesudo") quickSetStr(&r.RemoteUser, m, "remoteuser") quickSetStr(&r.RemoteHost, m, "remotehost") - quickSetBool(&r.AutoConnect, m, "autoconnect") + quickSetStr(&r.ConnectMode, m, "connectmode") quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") quickSetJson(&r.RemoteOpts, m, "remoteopts") @@ -470,13 +480,13 @@ func EnsureLocalRemote(ctx context.Context) error { localRemote := &RemoteType{ RemoteId: uuid.New().String(), PhysicalId: physicalId, - RemoteType: "ssh", + RemoteType: RemoteTypeSsh, RemoteAlias: LocalRemoteName, RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), RemoteSudo: false, RemoteUser: user.Username, RemoteHost: hostName, - AutoConnect: true, + ConnectMode: ConnectModeStartup, } err = InsertRemote(ctx, localRemote) if err != nil { @@ -496,7 +506,7 @@ func AddTest01Remote(ctx context.Context) error { } testRemote := &RemoteType{ RemoteId: uuid.New().String(), - RemoteType: "ssh", + RemoteType: RemoteTypeSsh, RemoteAlias: "test01", RemoteCanonicalName: "ubuntu@test01.ec2", RemoteSudo: false, @@ -507,7 +517,7 @@ func AddTest01Remote(ctx context.Context) error { SSHUser: "ubuntu", SSHIdentity: "/Users/mike/aws/mfmt.pem", }, - AutoConnect: true, + ConnectMode: ConnectModeStartup, } err = InsertRemote(ctx, testRemote) if err != nil { @@ -527,7 +537,7 @@ func AddTest02Remote(ctx context.Context) error { } testRemote := &RemoteType{ RemoteId: uuid.New().String(), - RemoteType: "ssh", + RemoteType: RemoteTypeSsh, RemoteAlias: "test2", RemoteCanonicalName: "test2@test01.ec2", RemoteSudo: false, @@ -537,7 +547,7 @@ func AddTest02Remote(ctx context.Context) error { SSHHost: "test01.ec2", SSHUser: "test2", }, - AutoConnect: true, + ConnectMode: ConnectModeStartup, } err = InsertRemote(ctx, testRemote) if err != nil { From 525fe77a5f0e586f4cbe1eb165f5e8c8b944d6fe Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 22 Aug 2022 16:00:25 -0700 Subject: [PATCH 062/397] send initpk.env in remotestate --- pkg/cmdrunner/cmdrunner.go | 12 ++++++++++- pkg/remote/remote.go | 41 +++++++++++++++++++++++++++++++++++++- pkg/sstore/sstore.go | 1 + 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d2a8b038b..01b986394 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -68,7 +68,7 @@ var ValidCommands = []string{ "/comment", "/cd", "/compgen", - "/setenv", + "/setenv", "/unset", } func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -100,6 +100,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "setenv": return SetEnvCommand(ctx, pk) + case "unset": + return UnSetCommand(ctx, pk) + default: return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) } @@ -350,6 +353,9 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) } else if commandStr == "setenv" || strings.HasPrefix(commandStr, "setenv ") { metaCmd = "setenv" commandStr = strings.TrimSpace(commandStr[6:]) + } else if commandStr == "unset" || strings.HasPrefix(commandStr, "unset ") { + metaCmd = "unset" + commandStr = strings.TrimSpace(commandStr[5:]) } else if commandStr[0] == '/' { spaceIdx := strings.Index(commandStr, " ") if spaceIdx == -1 { @@ -437,6 +443,10 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } +func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { return nil, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 89593cad2..0ffec94ab 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -115,6 +115,42 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } +func unquoteDQBashString(str string) (string, bool) { + if len(str) < 2 { + return str, false + } + if str[0] != '"' || str[len(str)-1] != '"' { + return str, false + } + rtn := make([]byte, 0, len(str)-2) + for idx := 1; idx < len(str)-1; idx++ { + ch := str[idx] + if ch == '"' { + return str, false + } + if ch == '\\' { + if idx == len(str)-2 { + return str, false + } + nextCh := str[idx+1] + if nextCh == '\n' { + idx++ + continue + } + if nextCh == '$' || nextCh == '"' || nextCh == '\\' || nextCh == '`' { + idx++ + rtn = append(rtn, nextCh) + continue + } + rtn = append(rtn, '\\') + continue + } else { + rtn = append(rtn, ch) + } + } + return string(rtn), true +} + func GetAllRemoteState() []RemoteState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -146,7 +182,10 @@ func GetAllRemoteState() []RemoteState { vars["status"] = proc.Status vars["type"] = proc.Remote.RemoteType if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { - state.DefaultState = &sstore.RemoteState{Cwd: proc.ServerProc.InitPk.HomeDir} + state.DefaultState = &sstore.RemoteState{ + Cwd: proc.ServerProc.InitPk.Cwd, + Env: proc.ServerProc.InitPk.Env, + } vars["home"] = proc.ServerProc.InitPk.HomeDir vars["remoteuser"] = proc.ServerProc.InitPk.User vars["remotehost"] = proc.ServerProc.InitPk.HostName diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b814ec4d3..52e2c81f5 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -230,6 +230,7 @@ type HistoryItemType struct { type RemoteState struct { Cwd string `json:"cwd"` + Env []byte `json:"env"` } func (s *RemoteState) Scan(val interface{}) error { From dca199b492c87bfc96738b0fa7145daad5408d9e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 22 Aug 2022 16:26:44 -0700 Subject: [PATCH 063/397] update to use env0 --- pkg/cmdrunner/cmdrunner.go | 3 ++- pkg/remote/remote.go | 6 +++--- pkg/sstore/sstore.go | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 01b986394..2a103cf6e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -276,7 +276,8 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) runPacket.Cwd = ids.RemoteState.Cwd - runPacket.Env = nil + runPacket.Env0 = ids.RemoteState.Env0 + runPacket.EnvComplete = true runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} runPacket.Command = strings.TrimSpace(cmdStr) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 0ffec94ab..745dbfee8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -183,8 +183,8 @@ func GetAllRemoteState() []RemoteState { vars["type"] = proc.Remote.RemoteType if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{ - Cwd: proc.ServerProc.InitPk.Cwd, - Env: proc.ServerProc.InitPk.Env, + Cwd: proc.ServerProc.InitPk.Cwd, + Env0: proc.ServerProc.InitPk.Env0, } vars["home"] = proc.ServerProc.InitPk.HomeDir vars["remoteuser"] = proc.ServerProc.InitPk.User @@ -326,7 +326,7 @@ func (msh *MShellProc) GetDefaultState() *sstore.RemoteState { if msh.ServerProc == nil || msh.ServerProc.InitPk == nil { return nil } - return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir} + return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir, Env0: msh.ServerProc.InitPk.Env0} } func (msh *MShellProc) ExpandHomeDir(pathStr string) (string, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 52e2c81f5..32d49180e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -229,8 +229,8 @@ type HistoryItemType struct { } type RemoteState struct { - Cwd string `json:"cwd"` - Env []byte `json:"env"` + Cwd string `json:"cwd"` + Env0 []byte `json:"env0"` // "env -0" format } func (s *RemoteState) Scan(val interface{}) error { From 292c76808ae1264b0dedb90e4acfde5e40f4c737 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 22 Aug 2022 18:38:52 -0700 Subject: [PATCH 064/397] setenv and unset working --- pkg/cmdrunner/cmdrunner.go | 121 ++++++++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 6 +- 2 files changed, 115 insertions(+), 12 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2a103cf6e..8ade405ed 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -277,6 +278,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) runPacket.Cwd = ids.RemoteState.Cwd runPacket.Env0 = ids.RemoteState.Env0 + fmt.Printf("run-command FOO [%s]\n", shexec.ParseEnv0(ids.RemoteState.Env0)["FOO"]) runPacket.EnvComplete = true runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} @@ -331,7 +333,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if !resolveBool(pk.Kwargs["nohist"], false) { err := addToHistory(ctx, pk, update, (err != nil)) if err != nil { - fmt.Printf("[error] adding to history: %w\n", err) + fmt.Printf("[error] adding to history: %v\n", err) // continue... } } @@ -377,6 +379,7 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) if metaCmd == "" { metaCmd = "run" } + metaCmd = SubMetaCmd(metaCmd) newPk := &scpacket.FeCommandPacketType{ MetaCmd: metaCmd, MetaSubCmd: metaSubCmd, @@ -384,6 +387,8 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) } if metaCmd == "run" || metaCmd == "comment" { newPk.Args = []string{commandStr} + } else if (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" { + newPk.Args = strings.Fields(commandStr) } else { allArgs := strings.Fields(commandStr) for _, arg := range allArgs { @@ -445,11 +450,109 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + if pk.MetaSubCmd != "" { + return nil, fmt.Errorf("invalid /unset subcommand '%s'", pk.MetaSubCmd) + } + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot unset") + } + if !curRemote.IsConnected() { + return nil, fmt.Errorf("remote is not connected, cannot unset") + } + if ids.RemoteState == nil { + return nil, fmt.Errorf("remote state is not available") + } + envMap := shexec.ParseEnv0(ids.RemoteState.Env0) + unsetVars := make(map[string]bool) + for _, argStr := range pk.Args { + eqIdx := strings.Index(argStr, "=") + if eqIdx != -1 { + return nil, fmt.Errorf("invalid argument to setenv, '%s' (cannot contain equal sign)", argStr) + } + delete(envMap, argStr) + unsetVars[argStr] = true + } + state := *ids.RemoteState + state.Env0 = shexec.MakeEnv0(envMap) + remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) + if err != nil { + return nil, err + } + update := sstore.WindowUpdate{ + Window: sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + Remotes: []*sstore.RemoteInstance{remote}, + }, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteName, makeSetVarsStr(unsetVars)), + TimeoutMs: 2000, + }, + } + return update, nil +} + +func makeSetVarsStr(setVars map[string]bool) string { + varArr := make([]string, 0, len(setVars)) + for varName, _ := range setVars { + varArr = append(varArr, varName) + } + return strings.Join(varArr, ", ") } func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + if pk.MetaSubCmd != "" { + return nil, fmt.Errorf("invalid /setenv subcommand '%s'", pk.MetaSubCmd) + } + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote, cannot setenv") + } + if !curRemote.IsConnected() { + return nil, fmt.Errorf("remote is not connected, cannot setenv") + } + if ids.RemoteState == nil { + return nil, fmt.Errorf("remote state is not available") + } + envMap := shexec.ParseEnv0(ids.RemoteState.Env0) + setVars := make(map[string]bool) + for _, argStr := range pk.Args { + eqIdx := strings.Index(argStr, "=") + if eqIdx == -1 { + return nil, fmt.Errorf("invalid argument to setenv, '%s' (no equal sign)", argStr) + } + envName := argStr[:eqIdx] + envVal := argStr[eqIdx+1:] + envMap[envName] = envVal + setVars[envName] = true + } + state := *ids.RemoteState + state.Env0 = shexec.MakeEnv0(envMap) + remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) + if err != nil { + return nil, err + } + update := sstore.WindowUpdate{ + Window: sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + Remotes: []*sstore.RemoteInstance{remote}, + }, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteName, makeSetVarsStr(setVars)), + TimeoutMs: 2000, + }, + } + return update, nil } func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -500,10 +603,10 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if !curRemote.IsConnected() { return nil, fmt.Errorf("remote is not connected, cannot change directory") } + if ids.RemoteState == nil { + return nil, fmt.Errorf("remote state is not available") + } if newDir == "" { - if ids.RemoteState == nil { - return nil, fmt.Errorf("remote state is not available") - } return sstore.InfoUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, ids.RemoteState.Cwd), @@ -524,7 +627,6 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) } } - fmt.Printf("cd [%s] => [%s]\n", firstArg(pk), newDir) cdPacket := packet.MakeCdPacket() cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir @@ -535,8 +637,9 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if err = resp.Err(); err != nil { return nil, err } - fmt.Printf("cd-resp %#v\n", resp) - remote, err := sstore.UpdateRemoteCwd(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, newDir) + state := *ids.RemoteState + state.Cwd = newDir + remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) if err != nil { return nil, err } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8d105757a..d8a3c0acb 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -570,7 +570,7 @@ func GetRemoteState(ctx context.Context, rname string, sessionId string, windowI return remoteId, remoteState, txErr } -func UpdateRemoteCwd(ctx context.Context, rname string, sessionId string, windowId string, remoteId string, cwd string) (*RemoteInstance, error) { +func UpdateRemoteState(ctx context.Context, rname string, sessionId string, windowId string, remoteId string, state RemoteState) (*RemoteInstance, error) { var ri RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` @@ -587,14 +587,14 @@ func UpdateRemoteCwd(ctx context.Context, rname string, sessionId string, window WindowId: windowId, RemoteId: remoteId, SessionScope: (windowId == ""), - State: RemoteState{Cwd: cwd}, + State: state, } query = `INSERT INTO remote_instance (riid, name, sessionid, windowid, remoteid, sessionscope, state) VALUES (:riid, :name, :sessionid, :windowid, :remoteid, :sessionscope, :state)` tx.NamedExecWrap(query, ri) return nil } - ri.State.Cwd = cwd query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND name = ?` + ri.State = state tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, ri.Name) return nil }) From 429c41cfd0198b02124fc96d35f485027e4b6b0d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 22 Aug 2022 18:53:38 -0700 Subject: [PATCH 065/397] show environment --- pkg/cmdrunner/cmdrunner.go | 15 +++++++++++++++ pkg/sstore/updatebus.go | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8ade405ed..b8bef9085 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/alessio/shellescape" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -524,6 +525,20 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("remote state is not available") } envMap := shexec.ParseEnv0(ids.RemoteState.Env0) + if len(pk.Args) == 0 { + var infoLines []string + for varName, varVal := range envMap { + line := fmt.Sprintf("%s=%s", varName, shellescape.Quote(varVal)) + infoLines = append(infoLines, line) + } + update := sstore.InfoUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("[%s] environment", ids.RemoteName), + InfoLines: infoLines, + }, + } + return update, nil + } setVars := make(map[string]bool) for _, argStr := range pk.Args { eqIdx := strings.Index(argStr, "=") diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 3ec9708c8..155c491cf 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -84,8 +84,9 @@ type InfoMsgType struct { InfoTitle string `json:"infotitle"` InfoError string `json:"infoerror,omitempty"` InfoMsg string `json:"infomsg,omitempty"` - InfoComps []string `json:"infocomps"` - InfoCompsMore bool `json:"infocompssmore"` + InfoComps []string `json:"infocomps,omitempty"` + InfoCompsMore bool `json:"infocompssmore,omitempty"` + InfoLines []string `json:"infolines,omitempty"` TimeoutMs int64 `json:"timeoutms,omitempty"` } From 709920ad8effb1955533860fec7a081c1d778847 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 23 Aug 2022 13:14:14 -0700 Subject: [PATCH 066/397] add ephemeral to line --- db/migrations/000001_init.up.sql | 1 + pkg/cmdrunner/cmdrunner.go | 4 ++++ pkg/sstore/dbops.go | 4 ++-- pkg/sstore/sstore.go | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 1c6d6bf6d..b428e2cb9 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -66,6 +66,7 @@ CREATE TABLE line ( linetype varchar(10) NOT NULL, text text NOT NULL, cmdid varchar(36) NOT NULL, + ephemeral boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b8bef9085..3ee5e1f3f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -386,6 +386,10 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) MetaSubCmd: metaSubCmd, Kwargs: pk.Kwargs, } + if strings.HasSuffix(commandStr, " ?") { + newPk.Kwargs["ephemeral"] = "1" + commandStr = commandStr[0 : len(commandStr)-2] + } if metaCmd == "run" || metaCmd == "comment" { newPk.Args = []string{commandStr} } else if (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d8a3c0acb..a0de382df 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -398,8 +398,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if !hasWindow { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } - query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid) - VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid)` + query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid, ephemeral) + VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid,:ephemeral)` tx.NamedExecWrap(query, line) if cmd != nil { cmdMap := cmd.ToMap() diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 32d49180e..f82d38d2e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -278,6 +278,7 @@ type LineType struct { LineType string `json:"linetype"` Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` Remove bool `json:"remove,omitempty"` } From 946f31988c1d17a3d9d3d3e81518c2c2ce0b0470 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 23 Aug 2022 13:37:08 -0700 Subject: [PATCH 067/397] switch from '/' to '@' as metacommand character --- pkg/cmdrunner/cmdrunner.go | 82 ++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3ee5e1f3f..3f1d4b11b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -63,14 +63,14 @@ func SubMetaCmd(cmd string) string { } var ValidCommands = []string{ - "/run", - "/eval", - "/screen", "/screen:open", "/screen:close", - "/session", "/session:open", "/session:close", - "/comment", - "/cd", - "/compgen", - "/setenv", "/unset", + "@run", + "@eval", + "@screen", "@screen:open", "@screen:close", + "@session", "@session:open", "@session:close", + "@comment", + "@cd", + "@compgen", + "@setenv", "@unset", } func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -106,7 +106,7 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return UnSetCommand(ctx, pk) default: - return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) + return nil, fmt.Errorf("invalid command '@%s', no handler", pk.MetaCmd) } } @@ -270,7 +270,7 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("/run error: %w", err) + return nil, fmt.Errorf("@run error: %w", err) } cmdId := uuid.New().String() cmdStr := firstArg(pk) @@ -323,12 +323,12 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { - return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") + return nil, fmt.Errorf("usage: @eval [command], no command passed to eval") } // parse metacmd commandStr := strings.TrimSpace(pk.Args[0]) if commandStr == "" { - return nil, fmt.Errorf("/eval, invalid emtpty command") + return nil, fmt.Errorf("@eval, invalid emtpty command") } update, err := evalCommandInternal(ctx, pk) if !resolveBool(pk.Kwargs["nohist"], false) { @@ -360,7 +360,7 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) } else if commandStr == "unset" || strings.HasPrefix(commandStr, "unset ") { metaCmd = "unset" commandStr = strings.TrimSpace(commandStr[5:]) - } else if commandStr[0] == '/' { + } else if commandStr[0] == '@' { spaceIdx := strings.Index(commandStr, " ") if spaceIdx == -1 { metaCmd = commandStr[1:] @@ -374,7 +374,7 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] } if metaCmd == "" { - return nil, fmt.Errorf("invalid command, got bare '/', with no command") + return nil, fmt.Errorf("invalid command, got bare '@', with no command") } } if metaCmd == "" { @@ -412,7 +412,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if pk.MetaSubCmd == "close" { ids, err := resolveIds(ctx, pk, R_Session|R_Screen) if err != nil { - return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) + return nil, fmt.Errorf("@screen:close cannot close screen: %w", err) } update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { @@ -423,7 +423,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { ids, err := resolveIds(ctx, pk, R_Session) if err != nil { - return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) + return nil, fmt.Errorf("@screen:open cannot open screen: %w", err) } activate := resolveBool(pk.Kwargs["activate"], true) update, err := sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) @@ -433,15 +433,15 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /screen subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid @screen subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session) if err != nil { - return nil, fmt.Errorf("/screen cannot switch to screen: %w", err) + return nil, fmt.Errorf("@screen cannot switch to screen: %w", err) } firstArg := firstArg(pk) if firstArg == "" { - return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") + return nil, fmt.Errorf("usage @screen [screen-name|screen-index|screen-id], no param specified") } screenIdArg, err := resolveSessionScreen(ctx, ids.SessionId, firstArg) if err != nil { @@ -456,7 +456,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /unset subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid @unset subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -512,7 +512,7 @@ func makeSetVarsStr(setVars map[string]bool) string { func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /setenv subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid @setenv subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -577,7 +577,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window) if err != nil { - return nil, fmt.Errorf("/cr error: %w", err) + return nil, fmt.Errorf("@cr error: %w", err) } newRemote := firstArg(pk) if newRemote == "" { @@ -589,11 +589,11 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, err } if remoteId == "" { - return nil, fmt.Errorf("/cr error: remote not found") + return nil, fmt.Errorf("@cr error: remote not found") } err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, remoteName) if err != nil { - return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) + return nil, fmt.Errorf("@cr error: cannot update curremote: %w", err) } update := sstore.WindowUpdate{ Window: sstore.WindowType{ @@ -612,7 +612,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("/cd error: %w", err) + return nil, fmt.Errorf("@cd error: %w", err) } newDir := firstArg(pk) curRemote := remote.GetRemoteById(ids.RemoteId) @@ -638,12 +638,12 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up } if !strings.HasPrefix(newDir, "/") { if ids.RemoteState == nil { - return nil, fmt.Errorf("/cd error: cannot get current remote directory (can only cd with absolute path)") + return nil, fmt.Errorf("@cd error: cannot get current remote directory (can only cd with absolute path)") } newDir = path.Join(ids.RemoteState.Cwd, newDir) newDir, err = filepath.Abs(newDir) if err != nil { - return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) + return nil, fmt.Errorf("@cd error: error canonicalizing new directory: %w", err) } } cdPacket := packet.MakeCdPacket() @@ -722,6 +722,9 @@ func getBool(v interface{}, field string) bool { func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { sort.Strings(comps) + if len(comps) == 0 { + comps = []string{"(no completions)"} + } update := sstore.InfoUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("%s completions", compType), @@ -777,13 +780,16 @@ func longestPrefix(root string, comps []string) string { var wsRe = regexp.MustCompile("\\s+") func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string) ([]string, bool, error) { - var comps []string + comps, hasMore, err := doCompGen(ctx, ids, prefix, "file") + if err != nil { + return nil, false, err + } for _, cmd := range ValidCommands { if strings.HasPrefix(cmd, prefix) { comps = append(comps, cmd) } } - return comps, false, nil + return comps, hasMore, nil } func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string) ([]string, bool, error) { @@ -791,14 +797,14 @@ func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType str return doMetaCompGen(ctx, ids, prefix) } if !packet.IsValidCompGenType(compType) { - return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, false, fmt.Errorf("@compgen invalid type '%s'", compType) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix if ids.RemoteState == nil { - return nil, false, fmt.Errorf("/compgen invalid remote state") + return nil, false, fmt.Errorf("@compgen invalid remote state") } cgPacket.Cwd = ids.RemoteState.Cwd curRemote := remote.GetRemoteById(ids.RemoteId) @@ -820,14 +826,14 @@ func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType str func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("/compgen error: %w", err) + return nil, fmt.Errorf("@compgen error: %w", err) } cmdLine := firstArg(pk) pos := len(cmdLine) if pk.Kwargs["comppos"] != "" { posArg, err := strconv.Atoi(pk.Kwargs["comppos"]) if err != nil { - return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) + return nil, fmt.Errorf("@compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) } pos = posArg } @@ -841,9 +847,9 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto prefix := cmdLine[:pos] parts := strings.Split(prefix, " ") compType := "file" - if len(parts) > 0 && strings.HasPrefix(parts[0], "/") { + if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "@") { compType = "metacommand" - } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { + } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "@cd") { compType = "directory" } else if len(parts) <= 1 { compType = "command" @@ -865,7 +871,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window) if err != nil { - return nil, fmt.Errorf("/comment error: %w", err) + return nil, fmt.Errorf("@comment error: %w", err) } text := firstArg(pk) if strings.TrimSpace(text) == "" { @@ -888,11 +894,11 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return update, nil } if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid @session subcommand '%s'", pk.MetaSubCmd) } firstArg := firstArg(pk) if firstArg == "" { - return nil, fmt.Errorf("usage /session [session-name|session-id], no param specified") + return nil, fmt.Errorf("usage @session [session-name|session-id], no param specified") } sessionId, err := resolveSession(ctx, firstArg) if err != nil { From dd5e6259bddd521d57da73f1d516b81fa8ea4f87 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 23 Aug 2022 14:01:52 -0700 Subject: [PATCH 068/397] add remote:show command --- pkg/cmdrunner/cmdrunner.go | 46 +++++++++++++++++++++- pkg/remote/remote.go | 81 +++++++++++++++++++++----------------- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3f1d4b11b..551e82b5f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1,6 +1,7 @@ package cmdrunner import ( + "bytes" "context" "fmt" "path" @@ -71,6 +72,7 @@ var ValidCommands = []string{ "@cd", "@compgen", "@setenv", "@unset", + "@remote:show", } func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -105,6 +107,9 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor case "unset": return UnSetCommand(ctx, pk) + case "remote": + return RemoteCommand(ctx, pk) + default: return nil, fmt.Errorf("invalid command '@%s', no handler", pk.MetaCmd) } @@ -502,6 +507,45 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if pk.MetaSubCmd == "show" { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + curRemote := remote.GetRemoteById(ids.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote [%s] (not found)", ids.RemoteName) + } + state := curRemote.GetRemoteState() + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", state.RemoteType)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remoteid", state.RemoteId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "physicalid", state.PhysicalId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "alias", state.RemoteAlias)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) + if ids.RemoteState != nil { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) + } + output := buf.String() + if strings.HasSuffix(output, "\n") { + output = output[0 : len(output)-1] + } + return sstore.InfoUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteName), + InfoLines: strings.Split(output, "\n"), + }, + }, nil + } + if pk.MetaSubCmd != "" { + return nil, fmt.Errorf("invalid @remote subcommand: '%s'", pk.MetaSubCmd) + } + return nil, fmt.Errorf("@remote requires a subcommand: 'show'") +} + func makeSetVarsStr(setVars map[string]bool) string { varArr := make([]string, 0, len(setVars)) for varName, _ := range setVars { @@ -537,7 +581,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } update := sstore.InfoUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("[%s] environment", ids.RemoteName), + InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.RemoteName), InfoLines: infoLines, }, } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 745dbfee8..a84c068cc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -151,49 +151,56 @@ func unquoteDQBashString(str string) (string, bool) { return string(rtn), true } +func (proc *MShellProc) GetRemoteState() RemoteState { + proc.Lock.Lock() + defer proc.Lock.Unlock() + state := RemoteState{ + RemoteType: proc.Remote.RemoteType, + RemoteId: proc.Remote.RemoteId, + RemoteAlias: proc.Remote.RemoteAlias, + RemoteCanonicalName: proc.Remote.RemoteCanonicalName, + PhysicalId: proc.Remote.PhysicalId, + Status: proc.Status, + ConnectMode: proc.Remote.ConnectMode, + } + if proc.Err != nil { + state.ErrorStr = proc.Err.Error() + } + vars := make(map[string]string) + vars["user"] = proc.Remote.RemoteUser + vars["host"] = proc.Remote.RemoteHost + if proc.Remote.RemoteSudo { + vars["sudo"] = "1" + } + vars["alias"] = proc.Remote.RemoteAlias + vars["cname"] = proc.Remote.RemoteCanonicalName + vars["physicalid"] = proc.Remote.PhysicalId + vars["remoteid"] = proc.Remote.RemoteId + vars["status"] = proc.Status + vars["type"] = proc.Remote.RemoteType + if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { + state.DefaultState = &sstore.RemoteState{ + Cwd: proc.ServerProc.InitPk.Cwd, + Env0: proc.ServerProc.InitPk.Env0, + } + vars["home"] = proc.ServerProc.InitPk.HomeDir + vars["remoteuser"] = proc.ServerProc.InitPk.User + vars["remotehost"] = proc.ServerProc.InitPk.HostName + if proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.SSHHost == "" { + vars["local"] = "1" + } + } + state.RemoteVars = vars + return state +} + func GetAllRemoteState() []RemoteState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() var rtn []RemoteState for _, proc := range GlobalStore.Map { - state := RemoteState{ - RemoteType: proc.Remote.RemoteType, - RemoteId: proc.Remote.RemoteId, - RemoteAlias: proc.Remote.RemoteAlias, - RemoteCanonicalName: proc.Remote.RemoteCanonicalName, - PhysicalId: proc.Remote.PhysicalId, - Status: proc.Status, - ConnectMode: proc.Remote.ConnectMode, - } - if proc.Err != nil { - state.ErrorStr = proc.Err.Error() - } - vars := make(map[string]string) - vars["user"] = proc.Remote.RemoteUser - vars["host"] = proc.Remote.RemoteHost - if proc.Remote.RemoteSudo { - vars["sudo"] = "1" - } - vars["alias"] = proc.Remote.RemoteAlias - vars["cname"] = proc.Remote.RemoteCanonicalName - vars["physicalid"] = proc.Remote.PhysicalId - vars["remoteid"] = proc.Remote.RemoteId - vars["status"] = proc.Status - vars["type"] = proc.Remote.RemoteType - if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { - state.DefaultState = &sstore.RemoteState{ - Cwd: proc.ServerProc.InitPk.Cwd, - Env0: proc.ServerProc.InitPk.Env0, - } - vars["home"] = proc.ServerProc.InitPk.HomeDir - vars["remoteuser"] = proc.ServerProc.InitPk.User - vars["remotehost"] = proc.ServerProc.InitPk.HostName - if proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.SSHHost == "" { - vars["local"] = "1" - } - } - state.RemoteVars = vars + state := proc.GetRemoteState() rtn = append(rtn, state) } return rtn From 3423b63213b280b0e268aefbec3dc1bda12e7559 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 23 Aug 2022 17:26:42 -0700 Subject: [PATCH 069/397] switch back to slash, combine file and metacmd completions --- pkg/cmdrunner/cmdrunner.go | 121 ++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 551e82b5f..d8801e4f7 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -64,15 +64,15 @@ func SubMetaCmd(cmd string) string { } var ValidCommands = []string{ - "@run", - "@eval", - "@screen", "@screen:open", "@screen:close", - "@session", "@session:open", "@session:close", - "@comment", - "@cd", - "@compgen", - "@setenv", "@unset", - "@remote:show", + "/run", + "/eval", + "/screen", "/screen:open", "/screen:close", + "/session", "/session:open", "/session:close", + "/comment", + "/cd", + "/compgen", + "/setenv", "/unset", + "/remote:show", } func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -111,7 +111,7 @@ func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return RemoteCommand(ctx, pk) default: - return nil, fmt.Errorf("invalid command '@%s', no handler", pk.MetaCmd) + return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) } } @@ -275,7 +275,7 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("@run error: %w", err) + return nil, fmt.Errorf("/run error: %w", err) } cmdId := uuid.New().String() cmdStr := firstArg(pk) @@ -328,12 +328,12 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { - return nil, fmt.Errorf("usage: @eval [command], no command passed to eval") + return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") } // parse metacmd commandStr := strings.TrimSpace(pk.Args[0]) if commandStr == "" { - return nil, fmt.Errorf("@eval, invalid emtpty command") + return nil, fmt.Errorf("/eval, invalid emtpty command") } update, err := evalCommandInternal(ctx, pk) if !resolveBool(pk.Kwargs["nohist"], false) { @@ -365,7 +365,7 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) } else if commandStr == "unset" || strings.HasPrefix(commandStr, "unset ") { metaCmd = "unset" commandStr = strings.TrimSpace(commandStr[5:]) - } else if commandStr[0] == '@' { + } else if commandStr[0] == '/' { spaceIdx := strings.Index(commandStr, " ") if spaceIdx == -1 { metaCmd = commandStr[1:] @@ -379,7 +379,7 @@ func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] } if metaCmd == "" { - return nil, fmt.Errorf("invalid command, got bare '@', with no command") + return nil, fmt.Errorf("invalid command, got bare '/', with no command") } } if metaCmd == "" { @@ -417,7 +417,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if pk.MetaSubCmd == "close" { ids, err := resolveIds(ctx, pk, R_Session|R_Screen) if err != nil { - return nil, fmt.Errorf("@screen:close cannot close screen: %w", err) + return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) } update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { @@ -428,7 +428,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { ids, err := resolveIds(ctx, pk, R_Session) if err != nil { - return nil, fmt.Errorf("@screen:open cannot open screen: %w", err) + return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) } activate := resolveBool(pk.Kwargs["activate"], true) update, err := sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) @@ -438,15 +438,15 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid @screen subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid /screen subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session) if err != nil { - return nil, fmt.Errorf("@screen cannot switch to screen: %w", err) + return nil, fmt.Errorf("/screen cannot switch to screen: %w", err) } firstArg := firstArg(pk) if firstArg == "" { - return nil, fmt.Errorf("usage @screen [screen-name|screen-index|screen-id], no param specified") + return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") } screenIdArg, err := resolveSessionScreen(ctx, ids.SessionId, firstArg) if err != nil { @@ -461,7 +461,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid @unset subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid /unset subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -530,20 +530,17 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) } output := buf.String() - if strings.HasSuffix(output, "\n") { - output = output[0 : len(output)-1] - } return sstore.InfoUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteName), - InfoLines: strings.Split(output, "\n"), + InfoLines: splitLinesForInfo(output), }, }, nil } if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid @remote subcommand: '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid /remote subcommand: '%s'", pk.MetaSubCmd) } - return nil, fmt.Errorf("@remote requires a subcommand: 'show'") + return nil, fmt.Errorf("/remote requires a subcommand: 'show'") } func makeSetVarsStr(setVars map[string]bool) string { @@ -556,7 +553,7 @@ func makeSetVarsStr(setVars map[string]bool) string { func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid @setenv subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid /setenv subcommand '%s'", pk.MetaSubCmd) } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -621,7 +618,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window) if err != nil { - return nil, fmt.Errorf("@cr error: %w", err) + return nil, fmt.Errorf("/cr error: %w", err) } newRemote := firstArg(pk) if newRemote == "" { @@ -633,11 +630,11 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, err } if remoteId == "" { - return nil, fmt.Errorf("@cr error: remote not found") + return nil, fmt.Errorf("/cr error: remote not found") } err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, remoteName) if err != nil { - return nil, fmt.Errorf("@cr error: cannot update curremote: %w", err) + return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) } update := sstore.WindowUpdate{ Window: sstore.WindowType{ @@ -656,7 +653,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("@cd error: %w", err) + return nil, fmt.Errorf("/cd error: %w", err) } newDir := firstArg(pk) curRemote := remote.GetRemoteById(ids.RemoteId) @@ -682,12 +679,12 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up } if !strings.HasPrefix(newDir, "/") { if ids.RemoteState == nil { - return nil, fmt.Errorf("@cd error: cannot get current remote directory (can only cd with absolute path)") + return nil, fmt.Errorf("/cd error: cannot get current remote directory (can only cd with absolute path)") } newDir = path.Join(ids.RemoteState.Cwd, newDir) newDir, err = filepath.Abs(newDir) if err != nil { - return nil, fmt.Errorf("@cd error: error canonicalizing new directory: %w", err) + return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) } } cdPacket := packet.MakeCdPacket() @@ -765,7 +762,19 @@ func getBool(v interface{}, field string) bool { } func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { - sort.Strings(comps) + sort.Slice(comps, func(i int, j int) bool { + c1 := comps[i] + c2 := comps[j] + c1mc := strings.HasPrefix(c1, "^") + c2mc := strings.HasPrefix(c2, "^") + if c1mc && !c2mc { + return true + } + if !c1mc && c2mc { + return false + } + return c1 < c2 + }) if len(comps) == 0 { comps = []string{"(no completions)"} } @@ -823,32 +832,36 @@ func longestPrefix(root string, comps []string) string { var wsRe = regexp.MustCompile("\\s+") -func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string) ([]string, bool, error) { - comps, hasMore, err := doCompGen(ctx, ids, prefix, "file") +func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string, forDisplay bool) ([]string, bool, error) { + comps, hasMore, err := doCompGen(ctx, ids, prefix, "file", forDisplay) if err != nil { return nil, false, err } for _, cmd := range ValidCommands { if strings.HasPrefix(cmd, prefix) { - comps = append(comps, cmd) + if forDisplay { + comps = append(comps, "^"+cmd) + } else { + comps = append(comps, cmd) + } } } return comps, hasMore, nil } -func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string) ([]string, bool, error) { +func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string, forDisplay bool) ([]string, bool, error) { if compType == "metacommand" { - return doMetaCompGen(ctx, ids, prefix) + return doMetaCompGen(ctx, ids, prefix, forDisplay) } if !packet.IsValidCompGenType(compType) { - return nil, false, fmt.Errorf("@compgen invalid type '%s'", compType) + return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix if ids.RemoteState == nil { - return nil, false, fmt.Errorf("@compgen invalid remote state") + return nil, false, fmt.Errorf("/compgen invalid remote state") } cgPacket.Cwd = ids.RemoteState.Cwd curRemote := remote.GetRemoteById(ids.RemoteId) @@ -870,14 +883,14 @@ func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType str func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { - return nil, fmt.Errorf("@compgen error: %w", err) + return nil, fmt.Errorf("/compgen error: %w", err) } cmdLine := firstArg(pk) pos := len(cmdLine) if pk.Kwargs["comppos"] != "" { posArg, err := strconv.Atoi(pk.Kwargs["comppos"]) if err != nil { - return nil, fmt.Errorf("@compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) + return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) } pos = posArg } @@ -891,9 +904,9 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto prefix := cmdLine[:pos] parts := strings.Split(prefix, " ") compType := "file" - if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "@") { + if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "/") { compType = "metacommand" - } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "@cd") { + } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { compType = "directory" } else if len(parts) <= 1 { compType = "command" @@ -902,7 +915,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if len(parts) > 0 { lastPart = parts[len(parts)-1] } - comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType) + comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType, showComps) if err != nil { return nil, err } @@ -915,7 +928,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window) if err != nil { - return nil, fmt.Errorf("@comment error: %w", err) + return nil, fmt.Errorf("/comment error: %w", err) } text := firstArg(pk) if strings.TrimSpace(text) == "" { @@ -938,11 +951,11 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return update, nil } if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid @session subcommand '%s'", pk.MetaSubCmd) + return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) } firstArg := firstArg(pk) if firstArg == "" { - return nil, fmt.Errorf("usage @session [session-name|session-id], no param specified") + return nil, fmt.Errorf("usage /session [session-name|session-id], no param specified") } sessionId, err := resolveSession(ctx, firstArg) if err != nil { @@ -950,3 +963,11 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } return sstore.SessionUpdate{ActiveSessionId: sessionId}, nil } + +func splitLinesForInfo(str string) []string { + rtn := strings.Split(str, "\n") + if rtn[len(rtn)-1] == "" { + return rtn[:len(rtn)-1] + } + return rtn +} From 51f7b0798bcac25bee3ffec407414152693d03d0 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 23 Aug 2022 21:05:49 -0700 Subject: [PATCH 070/397] add prompt to cmd --- db/migrations/000001_init.up.sql | 1 + db/schema.sql | 2 + pkg/remote/remote.go | 112 +++++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 4 +- pkg/sstore/sstore.go | 8 ++- 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index b428e2cb9..630af038e 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -98,6 +98,7 @@ CREATE TABLE cmd ( donepk json NOT NULL, runout json NOT NULL, usedrows int NOT NULL, + prompt text NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/db/schema.sql b/db/schema.sql index eedef254e..9f4a2d58b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -62,6 +62,7 @@ CREATE TABLE line ( linetype varchar(10) NOT NULL, text text NOT NULL, cmdid varchar(36) NOT NULL, + ephemeral boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -91,6 +92,7 @@ CREATE TABLE cmd ( donepk json NOT NULL, runout json NOT NULL, usedrows int NOT NULL, + prompt text NOT NULL, PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a84c068cc..b0308d17b 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path" + "strconv" "strings" "sync" "syscall" @@ -151,6 +152,14 @@ func unquoteDQBashString(str string) (string, bool) { return string(rtn), true } +func makeShortHost(host string) string { + dotIdx := strings.Index(host, ".") + if dotIdx == -1 { + return host + } + return host[0:dotIdx] +} + func (proc *MShellProc) GetRemoteState() RemoteState { proc.Lock.Lock() defer proc.Lock.Unlock() @@ -169,6 +178,7 @@ func (proc *MShellProc) GetRemoteState() RemoteState { vars := make(map[string]string) vars["user"] = proc.Remote.RemoteUser vars["host"] = proc.Remote.RemoteHost + vars["shorthost"] = makeShortHost(proc.Remote.RemoteHost) if proc.Remote.RemoteSudo { vars["sudo"] = "1" } @@ -186,6 +196,7 @@ func (proc *MShellProc) GetRemoteState() RemoteState { vars["home"] = proc.ServerProc.InitPk.HomeDir vars["remoteuser"] = proc.ServerProc.InitPk.User vars["remotehost"] = proc.ServerProc.InitPk.HostName + vars["remoteshorthost"] = makeShortHost(proc.ServerProc.InitPk.HostName) if proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.SSHHost == "" { vars["local"] = "1" } @@ -590,3 +601,104 @@ func (runner *MShellProc) ProcessPackets() { fmt.Printf("MSH> %s\n", packet.AsString(pk)) } } + +func EvalPromptEsc(escCode string, vars map[string]string, state sstore.RemoteState) string { + if escCode == "d" { + now := time.Now() + return now.Format("Mon Jan 02") + } + if strings.HasPrefix(escCode, "x{") && strings.HasSuffix(escCode, "}") { + varName := escCode[2 : len(escCode)-1] + return vars[varName] + } + if strings.HasPrefix(escCode, "y{") && strings.HasSuffix(escCode, "}") { + varName := escCode[2 : len(escCode)-1] + varMap := shexec.ParseEnv0(state.Env0) + return varMap[varName] + } + if escCode == "h" { + return vars["remoteshorthost"] + } + if escCode == "H" { + return vars["remotehost"] + } + if escCode == "j" { + return "0" + } + if escCode == "l" { + return "(l)" + } + if escCode == "s" { + return "mshell" + } + if escCode == "t" { + now := time.Now() + return now.Format("15:04:05") + } + if escCode == "T" { + now := time.Now() + return now.Format("03:04:05") + } + if escCode == "@" { + now := time.Now() + return now.Format("03:04:05PM") + } + if escCode == "u" { + return vars["remoteuser"] + } + if escCode == "v" { + return "0.1" + } + if escCode == "V" { + return "0" + } + if escCode == "w" { + return state.Cwd + } + if escCode == "W" { + return path.Base(state.Cwd) + } + if escCode == "!" { + return "(!)" + } + if escCode == "#" { + return "(#)" + } + if escCode == "$" { + if vars["remoteuser"] == "root" { + return "#" + } else { + return "$" + } + } + if len(escCode) == 3 { + // \nnn escape + ival, err := strconv.ParseInt(escCode, 8, 32) + if err != nil { + return escCode + } + return string([]byte{byte(ival)}) + } + if escCode == "e" { + return "\033" + } + if escCode == "n" { + return "\n" + } + if escCode == "r" { + return "\r" + } + if escCode == "a" { + return "\007" + } + if escCode == "\\" { + return "\\" + } + if escCode == "[" { + return "" + } + if escCode == "]" { + return "" + } + return "(" + escCode + ")" +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a0de382df..2de2b72de 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -404,8 +404,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil { cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) - VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) +INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows, prompt) + VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows,:prompt) ` tx.NamedExecWrap(query, cmdMap) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index f82d38d2e..b07243acf 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -283,6 +283,7 @@ type LineType struct { } type SSHOpts struct { + Local bool `json:"local"` SSHHost string `json:"sshhost"` SSHOptsStr string `json:"sshopts"` SSHIdentity string `json:"sshidentity"` @@ -290,7 +291,8 @@ type SSHOpts struct { } type RemoteOptsType struct { - Color string `json:"color"` + Color string `json:"color"` + Prompt string `json:"prompt"` } func (opts *RemoteOptsType) Scan(val interface{}) error { @@ -339,6 +341,7 @@ type CmdType struct { DonePk *packet.CmdDonePacketType `json:"donepk"` UsedRows int64 `json:"usedrows"` RunOut []packet.PacketType `json:"runout"` + Prompt string `json:"prompt"` Remove bool `json:"remove"` } @@ -394,6 +397,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["donepk"] = quickJson(cmd.DonePk) rtn["runout"] = quickJson(cmd.RunOut) rtn["usedrows"] = cmd.UsedRows + rtn["prompt"] = cmd.Prompt return rtn } @@ -413,6 +417,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetJson(&cmd.DonePk, m, "donepk") quickSetJson(&cmd.RunOut, m, "runout") quickSetInt64(&cmd.UsedRows, m, "usedrows") + quickSetStr(&cmd.Prompt, m, "prompt") return &cmd } @@ -489,6 +494,7 @@ func EnsureLocalRemote(ctx context.Context) error { RemoteUser: user.Username, RemoteHost: hostName, ConnectMode: ConnectModeStartup, + SSHOpts: &SSHOpts{Local: true}, } err = InsertRemote(ctx, localRemote) if err != nil { From 4f686e891b5caea7b320b347302f3413d02e3111 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 24 Aug 2022 02:14:16 -0700 Subject: [PATCH 071/397] big updates to remoteinstance, push changes through stack --- db/migrations/000001_init.up.sql | 7 +- db/schema.sql | 7 +- pkg/cmdrunner/cmdrunner.go | 163 ++++++++++++++++----------- pkg/remote/remote.go | 187 +++++++++++++++++++++++-------- pkg/sstore/dbops.go | 125 ++++++++++++--------- pkg/sstore/sstore.go | 87 +++++++++++--- pkg/sstore/updatebus.go | 65 ++++------- 7 files changed, 411 insertions(+), 230 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 630af038e..71fc09b9a 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -18,7 +18,9 @@ CREATE TABLE session ( CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - curremote varchar(50) NOT NULL, + curremoteowneruserid varchar(36) NOT NULL, + curremoteid varchar(36) NOT NULL, + curremotename varchar(50) NOT NULL, winopts json NOT NULL, owneruserid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, @@ -52,8 +54,8 @@ CREATE TABLE remote_instance ( name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, + remoteowneruserid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, - sessionscope boolean NOT NULL, state json NOT NULL ); @@ -98,7 +100,6 @@ CREATE TABLE cmd ( donepk json NOT NULL, runout json NOT NULL, usedrows int NOT NULL, - prompt text NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/db/schema.sql b/db/schema.sql index 9f4a2d58b..94f91b6d8 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -18,7 +18,9 @@ CREATE TABLE session ( CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - curremote varchar(50) NOT NULL, + curremoteowneruserid varchar(36) NOT NULL, + curremoteid varchar(36) NOT NULL, + curremotename varchar(50) NOT NULL, winopts json NOT NULL, owneruserid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, @@ -49,8 +51,8 @@ CREATE TABLE remote_instance ( name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, + remoteowneruserid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, - sessionscope boolean NOT NULL, state json NOT NULL ); CREATE TABLE line ( @@ -92,7 +94,6 @@ CREATE TABLE cmd ( donepk json NOT NULL, runout json NOT NULL, usedrows int NOT NULL, - prompt text NOT NULL, PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d8801e4f7..0914af4a1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -36,12 +36,12 @@ const ( ) type resolvedIds struct { - SessionId string - ScreenId string - WindowId string - RemoteId string - RemoteName string - RemoteState *sstore.RemoteState + SessionId string + ScreenId string + WindowId string + RemotePtr sstore.RemotePtrType + RemoteState *sstore.RemoteState + RemoteDisplayName string } func SubMetaCmd(cmd string) string { @@ -208,22 +208,59 @@ func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sess return resolveSessionScreen(ctx, sessionId, screenArg) } -// returns (remoteName, remoteId, state, err) -func resolveRemote(ctx context.Context, remoteName string, sessionId string, windowId string) (string, string, *sstore.RemoteState, error) { - if remoteName == "" { - return "", "", nil, nil +// returns (remoteuserref, remoteref, name, error) +func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { + if strings.HasPrefix(fullRemoteRef, "[") && strings.HasSuffix(fullRemoteRef, "]") { + fullRemoteRef = fullRemoteRef[1 : len(fullRemoteRef)-1] } - remoteId, state, err := sstore.GetRemoteState(ctx, remoteName, sessionId, windowId) + fields := strings.Split(fullRemoteRef, ":") + if len(fields) > 3 { + return "", "", "", fmt.Errorf("invalid remote format '%s'", fullRemoteRef) + } + if len(fields) == 1 { + return "", fields[0], "", nil + } + if len(fields) == 2 { + if strings.HasPrefix(fields[0], "@") { + return fields[0], fields[1], "", nil + } + return "", fields[0], fields[1], nil + } + return fields[0], fields[1], fields[2], nil +} + +// returns (remoteDisplayName, remoteptr, state, err) +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, error) { + if fullRemoteRef == "" { + return "", nil, nil, nil + } + userRef, remoteRef, remoteName, err := parseFullRemoteRef(fullRemoteRef) if err != nil { - return "", "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteName, err) + return "", nil, nil, err + } + if userRef != "" { + return "", nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) + } + rstate := remote.ResolveRemoteRef(remoteRef) + if rstate == nil { + return "", nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) + } + rptr := sstore.RemotePtrType{RemoteId: rstate.RemoteId, Name: remoteName} + state, err := sstore.GetRemoteState(ctx, sessionId, windowId, rptr) + if err != nil { + return "", nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) + } + rname := rstate.RemoteCanonicalName + if rstate.RemoteAlias != "" { + rname = rstate.RemoteAlias + } + if rptr.Name != "" { + rname = fmt.Sprintf("%s:%s", rname, rptr.Name) } if state == nil { - state, err = remote.GetDefaultRemoteStateById(remoteId) - if err != nil { - return "", "", nil, fmt.Errorf("cannot resolve remote '%s': %w", remoteName, err) - } + return rname, &rptr, rstate.DefaultState, nil } - return remoteName, remoteId, state, nil + return rname, &rptr, state, nil } func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { @@ -261,13 +298,16 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int } } if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rtn.RemoteName, rtn.RemoteId, rtn.RemoteState, err = resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) + rname, rptr, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) if err != nil { return rtn, err } - if rtn.RemoteId == "" && (rtype&R_Remote) > 0 { + if rptr == nil && (rtype&R_Remote) > 0 { return rtn, fmt.Errorf("no remote") } + rtn.RemoteDisplayName = rname + rtn.RemotePtr = *rptr + rtn.RemoteState = rstate } return rtn, nil } @@ -289,7 +329,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} runPacket.Command = strings.TrimSpace(cmdStr) - cmd, err := remote.RunCommand(ctx, cmdId, ids.RemoteId, ids.RemoteState, runPacket) + cmd, err := remote.RunCommand(ctx, cmdId, ids.RemotePtr.RemoteId, ids.RemoteState, runPacket) if err != nil { return nil, err } @@ -297,7 +337,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } - return sstore.LineUpdate{Line: rtnLine, Cmd: cmd}, nil + return sstore.ModelUpdate{Line: rtnLine, Cmd: cmd}, nil } func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, hadError bool) error { @@ -467,7 +507,7 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemoteId) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot unset") } @@ -489,18 +529,14 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } state := *ids.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) + remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) if err != nil { return nil, err } - update := sstore.WindowUpdate{ - Window: sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - Remotes: []*sstore.RemoteInstance{remote}, - }, + update := sstore.ModelUpdate{ + Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteName, makeSetVarsStr(unsetVars)), + InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteDisplayName, makeSetVarsStr(unsetVars)), TimeoutMs: 2000, }, } @@ -513,9 +549,9 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemoteId) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { - return nil, fmt.Errorf("invalid remote [%s] (not found)", ids.RemoteName) + return nil, fmt.Errorf("invalid remote '%s' (not found)", ids.RemoteDisplayName) } state := curRemote.GetRemoteState() var buf bytes.Buffer @@ -530,9 +566,9 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) } output := buf.String() - return sstore.InfoUpdate{ + return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteName), + InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), InfoLines: splitLinesForInfo(output), }, }, nil @@ -559,7 +595,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemoteId) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot setenv") } @@ -576,9 +612,9 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor line := fmt.Sprintf("%s=%s", varName, shellescape.Quote(varVal)) infoLines = append(infoLines, line) } - update := sstore.InfoUpdate{ + update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.RemoteName), + InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.RemoteDisplayName), InfoLines: infoLines, }, } @@ -597,18 +633,14 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } state := *ids.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) + remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) if err != nil { return nil, err } - update := sstore.WindowUpdate{ - Window: sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - Remotes: []*sstore.RemoteInstance{remote}, - }, + update := sstore.ModelUpdate{ + Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteName, makeSetVarsStr(setVars)), + InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteDisplayName, makeSetVarsStr(setVars)), TimeoutMs: 2000, }, } @@ -624,23 +656,22 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if newRemote == "" { return nil, nil } - remoteName, remoteId, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) - fmt.Printf("found: name[%s] id[%s] err[%v]\n", remoteName, remoteId, err) + remoteName, rptr, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { return nil, err } - if remoteId == "" { - return nil, fmt.Errorf("/cr error: remote not found") + if rptr == nil { + return nil, fmt.Errorf("/cr error: remote '%s' not found", newRemote) } - err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, remoteName) + err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, *rptr) if err != nil { return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) } - update := sstore.WindowUpdate{ + update := sstore.ModelUpdate{ Window: sstore.WindowType{ SessionId: ids.SessionId, WindowId: ids.WindowId, - CurRemote: remoteName, + CurRemote: *rptr, }, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("current remote = %s", remoteName), @@ -656,7 +687,7 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("/cd error: %w", err) } newDir := firstArg(pk) - curRemote := remote.GetRemoteById(ids.RemoteId) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot change directory") } @@ -667,9 +698,9 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("remote state is not available") } if newDir == "" { - return sstore.InfoUpdate{ + return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, ids.RemoteState.Cwd), + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteDisplayName, ids.RemoteState.Cwd), }, }, nil } @@ -699,18 +730,14 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up } state := *ids.RemoteState state.Cwd = newDir - remote, err := sstore.UpdateRemoteState(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, state) + remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) if err != nil { return nil, err } - update := sstore.WindowUpdate{ - Window: sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - Remotes: []*sstore.RemoteInstance{remote}, - }, + update := sstore.ModelUpdate{ + Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, newDir), + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteDisplayName, newDir), TimeoutMs: 2000, }, } @@ -778,7 +805,7 @@ func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.Upd if len(comps) == 0 { comps = []string{"(no completions)"} } - update := sstore.InfoUpdate{ + update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("%s completions", compType), InfoComps: comps, @@ -798,7 +825,7 @@ func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore } insertChars := lcp[len(prefix):] clu := &sstore.CmdLineType{InsertChars: insertChars, InsertPos: pos} - return sstore.InfoUpdate{CmdLine: clu} + return sstore.ModelUpdate{CmdLine: clu} } func longestPrefix(root string, comps []string) string { @@ -864,7 +891,7 @@ func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType str return nil, false, fmt.Errorf("/compgen invalid remote state") } cgPacket.Cwd = ids.RemoteState.Cwd - curRemote := remote.GetRemoteById(ids.RemoteId) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { return nil, false, fmt.Errorf("invalid remote, cannot execute command") } @@ -938,7 +965,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - return sstore.LineUpdate{Line: rtnLine}, nil + return sstore.ModelUpdate{Line: rtnLine}, nil } func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -961,7 +988,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - return sstore.SessionUpdate{ActiveSessionId: sessionId}, nil + return sstore.ModelUpdate{ActiveSessionId: sessionId}, nil } func splitLinesForInfo(str string) []string { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index b0308d17b..2d8cd6f79 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1,6 +1,7 @@ package remote import ( + "bytes" "context" "encoding/base64" "errors" @@ -16,6 +17,7 @@ import ( "time" "github.com/creack/pty" + "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" @@ -116,6 +118,28 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } +func ResolveRemoteRef(remoteRef string) *RemoteState { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + + _, err := uuid.Parse(remoteRef) + if err == nil { + msh := GlobalStore.Map[remoteRef] + if msh != nil { + state := msh.GetRemoteState() + return &state + } + return nil + } + for _, msh := range GlobalStore.Map { + if msh.Remote.RemoteAlias == remoteRef || msh.Remote.RemoteCanonicalName == remoteRef { + state := msh.GetRemoteState() + return &state + } + } + return nil +} + func unquoteDQBashString(str string) (string, bool) { if len(str) < 2 { return str, false @@ -175,19 +199,24 @@ func (proc *MShellProc) GetRemoteState() RemoteState { if proc.Err != nil { state.ErrorStr = proc.Err.Error() } + local := (proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.Local) vars := make(map[string]string) vars["user"] = proc.Remote.RemoteUser + vars["bestuser"] = vars["user"] vars["host"] = proc.Remote.RemoteHost vars["shorthost"] = makeShortHost(proc.Remote.RemoteHost) - if proc.Remote.RemoteSudo { - vars["sudo"] = "1" - } vars["alias"] = proc.Remote.RemoteAlias vars["cname"] = proc.Remote.RemoteCanonicalName vars["physicalid"] = proc.Remote.PhysicalId vars["remoteid"] = proc.Remote.RemoteId vars["status"] = proc.Status vars["type"] = proc.Remote.RemoteType + if proc.Remote.RemoteSudo { + vars["sudo"] = "1" + } + if local { + vars["local"] = "1" + } if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{ Cwd: proc.ServerProc.InitPk.Cwd, @@ -195,11 +224,23 @@ func (proc *MShellProc) GetRemoteState() RemoteState { } vars["home"] = proc.ServerProc.InitPk.HomeDir vars["remoteuser"] = proc.ServerProc.InitPk.User + vars["bestuser"] = vars["remoteuser"] vars["remotehost"] = proc.ServerProc.InitPk.HostName vars["remoteshorthost"] = makeShortHost(proc.ServerProc.InitPk.HostName) - if proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.SSHHost == "" { - vars["local"] = "1" - } + vars["besthost"] = vars["remotehost"] + vars["bestshorthost"] = vars["remoteshorthost"] + } + if local && proc.Remote.RemoteSudo { + vars["bestuser"] = "sudo" + } else if proc.Remote.RemoteSudo { + vars["bestuser"] = "sudo@" + vars["bestuser"] + } + if local { + vars["bestname"] = vars["bestuser"] + "@local" + vars["bestshortname"] = vars["bestuser"] + "@local" + } else { + vars["bestname"] = vars["bestuser"] + "@" + vars["besthost"] + vars["bestshortname"] = vars["bestuser"] + "@" + vars["bestshorthost"] } state.RemoteVars = vars return state @@ -238,8 +279,8 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { } func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { - if opts == nil { - return shexec.SSHOpts{} + if opts == nil || opts.Local { + opts = &sstore.SSHOpts{} } return shexec.SSHOpts{ SSHHost: opts.SSHHost, @@ -347,6 +388,19 @@ func (msh *MShellProc) GetDefaultState() *sstore.RemoteState { return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir, Env0: msh.ServerProc.InitPk.Env0} } +func replaceHomePath(pathStr string, homeDir string) string { + if homeDir == "" { + return pathStr + } + if pathStr == homeDir { + return "~" + } + if strings.HasPrefix(pathStr, homeDir+"/") { + return "~" + pathStr[len(homeDir):] + } + return pathStr +} + func (msh *MShellProc) ExpandHomeDir(pathStr string) (string, error) { if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") { return pathStr, nil @@ -526,7 +580,7 @@ func (msh *MShellProc) notifyHangups_nolock() { if err != nil { continue } - update := sstore.LineUpdate{Cmd: cmd} + update := sstore.ModelUpdate{Cmd: cmd} sstore.MainBus.SendUpdate(ck.GetSessionId(), update) } msh.RunningCmds = nil @@ -602,16 +656,79 @@ func (runner *MShellProc) ProcessPackets() { } } -func EvalPromptEsc(escCode string, vars map[string]string, state sstore.RemoteState) string { - if escCode == "d" { - now := time.Now() - return now.Format("Mon Jan 02") +// returns number of chars (including braces) for brace-expr +func getBracedStr(runeStr []rune) int { + if len(runeStr) < 3 { + return 0 } + if runeStr[0] != '{' { + return 0 + } + for i := 1; i < len(runeStr); i++ { + if runeStr[i] == '}' { + if i == 1 { // cannot have {} + return 0 + } + return i + 1 + } + } + return 0 +} + +func isDigit(r rune) bool { + return r >= '0' && r <= '9' // just check ascii digits (not unicode) +} + +func EvalPrompt(promptFmt string, vars map[string]string, state *sstore.RemoteState) string { + var buf bytes.Buffer + promptRunes := []rune(promptFmt) + for i := 0; i < len(promptRunes); i++ { + ch := promptRunes[i] + if ch == '\\' && i != len(promptRunes)-1 { + nextCh := promptRunes[i+1] + if nextCh == 'x' || nextCh == 'y' { + nr := getBracedStr(promptRunes[i+2:]) + if nr > 0 { + escCode := string(promptRunes[i+1 : i+1+nr+1]) // start at "x" or "y", extend nr+1 runes + escStr := evalPromptEsc(escCode, vars, state) + buf.WriteString(escStr) + i += nr + 1 + continue + } else { + buf.WriteRune(ch) // invalid escape, so just write ch and move on + continue + } + } else if isDigit(nextCh) { + if len(promptRunes) >= i+4 && isDigit(promptRunes[i+2]) && isDigit(promptRunes[i+3]) { + i += 3 + escStr := evalPromptEsc(string(promptRunes[i+1:i+4]), vars, state) + buf.WriteString(escStr) + continue + } else { + buf.WriteRune(ch) // invalid escape, so just write ch and move on + continue + } + } else { + i += 1 + escStr := evalPromptEsc(string(nextCh), vars, state) + buf.WriteString(escStr) + continue + } + } + buf.WriteRune(ch) + } + return buf.String() +} + +func evalPromptEsc(escCode string, vars map[string]string, state *sstore.RemoteState) string { if strings.HasPrefix(escCode, "x{") && strings.HasSuffix(escCode, "}") { varName := escCode[2 : len(escCode)-1] return vars[varName] } if strings.HasPrefix(escCode, "y{") && strings.HasSuffix(escCode, "}") { + if state == nil { + return "" + } varName := escCode[2 : len(escCode)-1] varMap := shexec.ParseEnv0(state.Env0) return varMap[varName] @@ -622,50 +739,26 @@ func EvalPromptEsc(escCode string, vars map[string]string, state sstore.RemoteSt if escCode == "H" { return vars["remotehost"] } - if escCode == "j" { - return "0" - } - if escCode == "l" { - return "(l)" - } if escCode == "s" { return "mshell" } - if escCode == "t" { - now := time.Now() - return now.Format("15:04:05") - } - if escCode == "T" { - now := time.Now() - return now.Format("03:04:05") - } - if escCode == "@" { - now := time.Now() - return now.Format("03:04:05PM") - } if escCode == "u" { return vars["remoteuser"] } - if escCode == "v" { - return "0.1" - } - if escCode == "V" { - return "0" - } if escCode == "w" { - return state.Cwd + if state == nil { + return "?" + } + return replaceHomePath(state.Cwd, vars["home"]) } if escCode == "W" { - return path.Base(state.Cwd) - } - if escCode == "!" { - return "(!)" - } - if escCode == "#" { - return "(#)" + if state == nil { + return "?" + } + return path.Base(replaceHomePath(state.Cwd, vars["home"])) } if escCode == "$" { - if vars["remoteuser"] == "root" { + if vars["remoteuser"] == "root" || vars["sudo"] == "1" { return "#" } else { return "$" @@ -700,5 +793,7 @@ func EvalPromptEsc(escCode string, vars map[string]string, state sstore.RemoteSt if escCode == "]" { return "" } + + // we don't support date/time escapes (d, t, T, @), version escapes (v, V), cmd number (#, !), terminal device (l), jobs (j) return "(" + escCode + ")" } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 2de2b72de..ac5772c38 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -178,7 +178,7 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { } screen.Windows = append(screen.Windows, sw) } - query = `SELECT * FROM remote_instance WHERE sessionscope` + query = `SELECT * FROM remote_instance` var ris []*RemoteInstance tx.SelectWrap(&ris, query) for _, ri := range ris { @@ -195,22 +195,19 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { var rtnWindow *WindowType err := WithTx(ctx, func(tx *TxWrap) error { - var window WindowType query := `SELECT * FROM window WHERE sessionid = ? AND windowid = ?` - found := tx.GetWrap(&window, query, sessionId, windowId) - if !found { + m := tx.GetMap(query, sessionId, windowId) + if m == nil { return nil } - rtnWindow = &window + rtnWindow = WindowFromMap(m) query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` - tx.SelectWrap(&window.Lines, query, sessionId, windowId) + tx.SelectWrap(&rtnWindow.Lines, query, sessionId, windowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` cmdMaps := tx.SelectMaps(query, sessionId, windowId) for _, m := range cmdMaps { - window.Cmds = append(window.Cmds, CmdFromMap(m)) + rtnWindow.Cmds = append(rtnWindow.Cmds, CmdFromMap(m)) } - query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND NOT sessionscope` - tx.SelectWrap(&window.Remotes, query, sessionId, windowId) return nil }) return rtnWindow, err @@ -258,7 +255,7 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { // also creates default window, returns sessionId // if sessionName == "", it will be generated -func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*SessionUpdate, error) { +func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (UpdatePacket, error) { newSessionId := uuid.New().String() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) @@ -279,7 +276,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo if err != nil { return nil, err } - update := &SessionUpdate{ + update := ModelUpdate{ Sessions: []*SessionType{session}, } if activate { @@ -328,7 +325,11 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot create screen, no session found") } - newWindowId := txCreateWindow(tx, sessionId) + remoteId := tx.GetString(`SELECT remoteid FROM remote WHERE remotealias = ?`, LocalRemoteAlias) + if remoteId == "" { + return fmt.Errorf("cannot create screen, no local remote found") + } + newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) screenName = fmtUniqueName(screenName, "s%d", maxScreenIdx+1, screenNames) @@ -377,11 +378,20 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr return rtnScreen, nil } -func txCreateWindow(tx *TxWrap, sessionId string) string { - windowId := uuid.New().String() - query := `INSERT INTO window (sessionid, windowid, curremote, winopts, shareopts, owneruserid, sharemode) VALUES (?, ?, ?, ?, ?, '', 'local')` - tx.ExecWrap(query, sessionId, windowId, LocalRemoteName, WindowOptsType{}, WindowShareOptsType{}) - return windowId +func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) string { + w := &WindowType{ + SessionId: sessionId, + WindowId: uuid.New().String(), + CurRemote: curRemote, + WinOpts: WindowOptsType{}, + ShareMode: ShareModeLocal, + ShareOpts: WindowShareOptsType{}, + } + wmap := w.ToMap() + query := `INSERT INTO window ( sessionid, windowid, curremoteowneruserid, curremoteid, curremotename, winopts, owneruserid, sharemode, shareopts) + VALUES (:sessionid,:windowid,:curremoteowneruserid,:curremoteid,:curremotename,:winopts,:owneruserid,:sharemode,:shareopts)` + tx.NamedExecWrap(query, wmap) + return w.WindowId } func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { @@ -404,8 +414,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil { cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows, prompt) - VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows,:prompt) +INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) + VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) ` tx.NamedExecWrap(query, cmdMap) } @@ -448,7 +458,7 @@ func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (Upd if rtnCmd == nil { return nil, fmt.Errorf("cmd data not found for ck[%s]", donePk.CK) } - return LineUpdate{Cmd: rtnCmd}, nil + return ModelUpdate{Cmd: rtnCmd}, nil } func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { @@ -548,67 +558,80 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat return update, nil } -func GetRemoteState(ctx context.Context, rname string, sessionId string, windowId string) (string, *RemoteState, error) { - var remoteId string +func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*RemoteState, error) { var remoteState *RemoteState txErr := WithTx(ctx, func(tx *TxWrap) error { var ri RemoteInstance - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, rname) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) if found { - remoteId = ri.RemoteId remoteState = &ri.State return nil } - query = `SELECT remoteid FROM remote WHERE remotealias = ? OR remotecanonicalname = ?` - remoteId = tx.GetString(query, rname, rname) - if remoteId == "" { - return fmt.Errorf("remote not found", rname) - } return nil }) - return remoteId, remoteState, txErr + return remoteState, txErr } -func UpdateRemoteState(ctx context.Context, rname string, sessionId string, windowId string, remoteId string, state RemoteState) (*RemoteInstance, error) { - var ri RemoteInstance - txErr := WithTx(ctx, func(tx *TxWrap) error { +func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error { + if windowId == "" { + query := `SELECT sessionid FROM session WHERE sessionid = ?` + if !tx.Exists(query, sessionId) { + return fmt.Errorf("no session found") + } + return nil + } else { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("cannot update remote instance cwd, no window found") + return fmt.Errorf("no window found") } - query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, rname) + return nil + } +} + +func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, state RemoteState) (*RemoteInstance, error) { + if remotePtr.IsSessionScope() { + windowId = "" + } + var ri RemoteInstance + txErr := WithTx(ctx, func(tx *TxWrap) error { + err := validateSessionWindow(tx, sessionId, windowId) + if err != nil { + return fmt.Errorf("cannot update remote instance cwd: %w", err) + } + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) if !found { ri = RemoteInstance{ - RIId: uuid.New().String(), - Name: rname, - SessionId: sessionId, - WindowId: windowId, - RemoteId: remoteId, - SessionScope: (windowId == ""), - State: state, + RIId: uuid.New().String(), + Name: remotePtr.Name, + SessionId: sessionId, + WindowId: windowId, + RemoteOwnerUserId: remotePtr.OwnerUserId, + RemoteId: remotePtr.RemoteId, + State: state, } - query = `INSERT INTO remote_instance (riid, name, sessionid, windowid, remoteid, sessionscope, state) VALUES (:riid, :name, :sessionid, :windowid, :remoteid, :sessionscope, :state)` + query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteowneruserid, remoteid, state) + VALUES (:riid,:name,:sessionid,:windowid,:remoteowneruserid,:remoteid,:state)` tx.NamedExecWrap(query, ri) return nil } - query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND name = ?` + query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` ri.State = state - tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, ri.Name) + tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) return nil }) return &ri, txErr } -func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, remoteName string) error { +func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("cannot update curremote, no window found") + return fmt.Errorf("cannot update curremote: no window found") } - query = `UPDATE window SET curremote = ? WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, remoteName, sessionId, windowId) + query = `UPDATE window SET curremoteowneruserid = ?, curremoteid = ?, curremotename = ? WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) return nil }) return txErr diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b07243acf..83b46cca0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -12,6 +12,7 @@ import ( "os" "os/user" "path" + "strings" "sync" "time" @@ -30,7 +31,7 @@ const DBFileName = "sh2.db" const DefaultSessionName = "default" const DefaultWindowName = "default" -const LocalRemoteName = "local" +const LocalRemoteAlias = "local" const DefaultScreenWindowName = "w1" const DefaultCwd = "~" @@ -132,22 +133,78 @@ func (opts WindowShareOptsType) Value() (driver.Value, error) { return quickValueJson(opts) } +type RemotePtrType struct { + OwnerUserId string `json:"owneruserid"` + RemoteId string `json:"remoteid"` + Name string `json:"name"` +} + +func (r RemotePtrType) IsSessionScope() bool { + return strings.HasPrefix(r.Name, "*") +} + +func (r RemotePtrType) MakeFullRemoteRef() string { + if r.RemoteId == "" { + return "" + } + if r.OwnerUserId == "" && r.Name == "" { + return r.RemoteId + } + if r.OwnerUserId != "" && r.Name == "" { + return fmt.Sprintf("@%s:%s", r.OwnerUserId, r.RemoteId) + } + if r.OwnerUserId == "" && r.Name != "" { + return fmt.Sprintf("%s:%s", r.RemoteId, r.Name) + } + return fmt.Sprintf("@%s:%s:%s", r.OwnerUserId, r.RemoteId, r.Name) +} + type WindowType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - CurRemote string `json:"curremote"` + CurRemote RemotePtrType `json:"curremote"` WinOpts WindowOptsType `json:"winopts"` OwnerUserId string `json:"owneruserid"` ShareMode string `json:"sharemode"` ShareOpts WindowShareOptsType `json:"shareopts"` Lines []*LineType `json:"lines"` Cmds []*CmdType `json:"cmds"` - Remotes []*RemoteInstance `json:"remotes"` // only for updates Remove bool `json:"remove,omitempty"` } +func (w *WindowType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["sessionid"] = w.SessionId + rtn["windowid"] = w.WindowId + rtn["curremoteowneruserid"] = w.CurRemote.OwnerUserId + rtn["curremoteid"] = w.CurRemote.RemoteId + rtn["curremotename"] = w.CurRemote.Name + rtn["winopts"] = quickJson(w.WinOpts) + rtn["owneruserid"] = w.OwnerUserId + rtn["sharemode"] = w.ShareMode + rtn["shareopts"] = quickJson(w.ShareOpts) + return rtn +} + +func WindowFromMap(m map[string]interface{}) *WindowType { + if len(m) == 0 { + return nil + } + var w WindowType + quickSetStr(&w.SessionId, m, "sessionid") + quickSetStr(&w.WindowId, m, "windowid") + quickSetStr(&w.CurRemote.OwnerUserId, m, "curremoteowneruserid") + quickSetStr(&w.CurRemote.RemoteId, m, "curremoteid") + quickSetStr(&w.CurRemote.Name, m, "curremotename") + quickSetJson(&w.WinOpts, m, "winopts") + quickSetStr(&w.OwnerUserId, m, "owneruserid") + quickSetStr(&w.ShareMode, m, "sharemode") + quickSetJson(&w.ShareOpts, m, "shareopts") + return &w +} + type ScreenOptsType struct { TabColor string `json:"tabcolor"` } @@ -257,13 +314,13 @@ func (opts TermOpts) Value() (driver.Value, error) { } type RemoteInstance struct { - RIId string `json:"riid"` - Name string `json:"name"` - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - RemoteId string `json:"remoteid"` - SessionScope bool `json:"sessionscope"` - State RemoteState `json:"state"` + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteOwnerUserId string `json:"remoteowneruserid"` + RemoteId string `json:"remoteid"` + State RemoteState `json:"state"` // only for updates Remove bool `json:"remove,omitempty"` @@ -291,8 +348,7 @@ type SSHOpts struct { } type RemoteOptsType struct { - Color string `json:"color"` - Prompt string `json:"prompt"` + Color string `json:"color"` } func (opts *RemoteOptsType) Scan(val interface{}) error { @@ -341,7 +397,6 @@ type CmdType struct { DonePk *packet.CmdDonePacketType `json:"donepk"` UsedRows int64 `json:"usedrows"` RunOut []packet.PacketType `json:"runout"` - Prompt string `json:"prompt"` Remove bool `json:"remove"` } @@ -397,7 +452,6 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["donepk"] = quickJson(cmd.DonePk) rtn["runout"] = quickJson(cmd.RunOut) rtn["usedrows"] = cmd.UsedRows - rtn["prompt"] = cmd.Prompt return rtn } @@ -417,7 +471,6 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetJson(&cmd.DonePk, m, "donepk") quickSetJson(&cmd.RunOut, m, "runout") quickSetInt64(&cmd.UsedRows, m, "usedrows") - quickSetStr(&cmd.Prompt, m, "prompt") return &cmd } @@ -488,7 +541,7 @@ func EnsureLocalRemote(ctx context.Context) error { RemoteId: uuid.New().String(), PhysicalId: physicalId, RemoteType: RemoteTypeSsh, - RemoteAlias: LocalRemoteName, + RemoteAlias: LocalRemoteAlias, RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), RemoteSudo: false, RemoteUser: user.Username, @@ -521,6 +574,7 @@ func AddTest01Remote(ctx context.Context) error { RemoteUser: "ubuntu", RemoteHost: "test01.ec2", SSHOpts: &SSHOpts{ + Local: false, SSHHost: "test01.ec2", SSHUser: "ubuntu", SSHIdentity: "/Users/mike/aws/mfmt.pem", @@ -552,6 +606,7 @@ func AddTest02Remote(ctx context.Context) error { RemoteUser: "test2", RemoteHost: "test01.ec2", SSHOpts: &SSHOpts{ + Local: false, SSHHost: "test01.ec2", SSHUser: "test2", }, diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 155c491cf..155db0122 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -5,12 +5,7 @@ import "sync" var MainBus *UpdateBus = MakeUpdateBus() const PtyDataUpdateStr = "pty" -const SessionUpdateStr = "session" -const WindowUpdateStr = "window" -const CmdUpdateStr = "cmd" -const LineCmdUpdateStr = "line+cmd" -const InfoUpdateStr = "info" -const CompGenUpdateStr = "compgen" +const ModelUpdateStr = "model" type UpdatePacket interface { UpdateType() string @@ -28,56 +23,40 @@ func (PtyDataUpdate) UpdateType() string { return PtyDataUpdateStr } -type WindowUpdate struct { - Window WindowType `json:"window"` - Info *InfoMsgType `json:"info,omitempty"` -} - -func (WindowUpdate) UpdateType() string { - return WindowUpdateStr -} - -type SessionUpdate struct { +type ModelUpdate struct { Sessions []*SessionType `json:"sessions"` ActiveSessionId string `json:"activesessionid,omitempty"` + Window WindowType `json:"window"` + Line *LineType `json:"line"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` Info *InfoMsgType `json:"info,omitempty"` } -func (SessionUpdate) UpdateType() string { - return SessionUpdateStr +func (ModelUpdate) UpdateType() string { + return ModelUpdateStr } -func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { +func MakeSingleSessionUpdate(sessionId string) (ModelUpdate, *SessionType) { session := &SessionType{ SessionId: sessionId, NotifyNum: -1, } - update := &SessionUpdate{ + update := ModelUpdate{ Sessions: []*SessionType{session}, } return update, session } -type LineUpdate struct { - Line *LineType `json:"line"` - Cmd *CmdType `json:"cmd,omitempty"` - Remove bool `json:"remove,omitempty"` - Info *InfoMsgType `json:"info,omitempty"` -} - -func (LineUpdate) UpdateType() string { - return LineCmdUpdateStr -} - func ReadLineCmdIdFromUpdate(update UpdatePacket) (string, string) { - lineUpdate, ok := update.(LineUpdate) + modelUpdate, ok := update.(ModelUpdate) if !ok { return "", "" } - if lineUpdate.Line == nil { + if modelUpdate.Line == nil { return "", "" } - return lineUpdate.Line.LineId, lineUpdate.Line.CmdId + return modelUpdate.Line.LineId, modelUpdate.Line.CmdId } type InfoMsgType struct { @@ -95,15 +74,6 @@ type CmdLineType struct { InsertPos int64 `json:"insertpos"` } -type InfoUpdate struct { - Info *InfoMsgType `json:"info,omitempty"` - CmdLine *CmdLineType `json:"cmdline,omitempty"` -} - -func (InfoUpdate) UpdateType() string { - return InfoUpdateStr -} - type UpdateChannel struct { SessionId string ClientId string @@ -167,3 +137,12 @@ func (bus *UpdateBus) SendUpdate(sessionId string, update interface{}) { } } } + +func MakeSessionsUpdateForRemote(sessionId string, ri *RemoteInstance) []*SessionType { + return []*SessionType{ + &SessionType{ + SessionId: sessionId, + Remotes: []*RemoteInstance{ri}, + }, + } +} From 0d314000ac806a8dfcf3ec0b4a26c05a7df7f6e0 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 24 Aug 2022 13:21:54 -0700 Subject: [PATCH 072/397] fully support new remoteptr --- cmd/main-server.go | 1 + db/migrations/000001_init.up.sql | 12 +++--- db/schema.sql | 12 +++--- pkg/cmdrunner/cmdrunner.go | 64 ++++++++++++++--------------- pkg/remote/remote.go | 26 ++++++------ pkg/scbase/scbase.go | 18 ++++++++ pkg/scpacket/scpacket.go | 39 +++++++++++++++--- pkg/scws/scws.go | 14 ++++++- pkg/sstore/dbops.go | 46 ++++++++++----------- pkg/sstore/sstore.go | 70 +++++++++++++++++--------------- 10 files changed, 184 insertions(+), 118 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index e723e6895..e706568c4 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -292,6 +292,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { } fmt.Printf("[error] in run-command: %v\n", r) debug.PrintStack() + WriteJsonError(w, fmt.Errorf("panic: %v", r)) return }() w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 71fc09b9a..2022e65f9 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -10,7 +10,7 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL ); @@ -18,11 +18,11 @@ CREATE TABLE session ( CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - curremoteowneruserid varchar(36) NOT NULL, + curremoteownerid varchar(36) NOT NULL, curremoteid varchar(36) NOT NULL, curremotename varchar(50) NOT NULL, winopts json NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, shareopts json NOT NULL, PRIMARY KEY (sessionid, windowid) @@ -35,7 +35,7 @@ CREATE TABLE screen ( activewindowid varchar(36) NOT NULL, screenidx int NOT NULL, screenopts json NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid) ); @@ -54,7 +54,7 @@ CREATE TABLE remote_instance ( name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - remoteowneruserid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, state json NOT NULL ); @@ -91,7 +91,9 @@ CREATE TABLE remote ( CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, cmdstr text NOT NULL, remotestate json NOT NULL, termopts json NOT NULL, diff --git a/db/schema.sql b/db/schema.sql index 94f91b6d8..fc98b13fa 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -11,18 +11,18 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL ); CREATE TABLE window ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - curremoteowneruserid varchar(36) NOT NULL, + curremoteownerid varchar(36) NOT NULL, curremoteid varchar(36) NOT NULL, curremotename varchar(50) NOT NULL, winopts json NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, shareopts json NOT NULL, PRIMARY KEY (sessionid, windowid) @@ -34,7 +34,7 @@ CREATE TABLE screen ( activewindowid varchar(36) NOT NULL, screenidx int NOT NULL, screenopts json NOT NULL, - owneruserid varchar(36) NOT NULL, + ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid) ); @@ -51,7 +51,7 @@ CREATE TABLE remote_instance ( name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - remoteowneruserid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, state json NOT NULL ); @@ -85,7 +85,9 @@ CREATE TABLE remote ( CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, cmdstr text NOT NULL, remotestate json NOT NULL, termopts json NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0914af4a1..d68c29fc7 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -42,6 +42,7 @@ type resolvedIds struct { RemotePtr sstore.RemotePtrType RemoteState *sstore.RemoteState RemoteDisplayName string + RState remote.RemoteState } func SubMetaCmd(cmd string) string { @@ -229,26 +230,26 @@ func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { return fields[0], fields[1], fields[2], nil } -// returns (remoteDisplayName, remoteptr, state, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, error) { +// returns (remoteDisplayName, remoteptr, state, rstate, err) +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteState, error) { if fullRemoteRef == "" { - return "", nil, nil, nil + return "", nil, nil, nil, nil } userRef, remoteRef, remoteName, err := parseFullRemoteRef(fullRemoteRef) if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } if userRef != "" { - return "", nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) + return "", nil, nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) } rstate := remote.ResolveRemoteRef(remoteRef) if rstate == nil { - return "", nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) + return "", nil, nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) } rptr := sstore.RemotePtrType{RemoteId: rstate.RemoteId, Name: remoteName} state, err := sstore.GetRemoteState(ctx, sessionId, windowId, rptr) if err != nil { - return "", nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) + return "", nil, nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) } rname := rstate.RemoteCanonicalName if rstate.RemoteAlias != "" { @@ -258,9 +259,9 @@ func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, rname = fmt.Sprintf("%s:%s", rname, rptr.Name) } if state == nil { - return rname, &rptr, rstate.DefaultState, nil + return rname, &rptr, rstate.DefaultState, rstate, nil } - return rname, &rptr, state, nil + return rname, &rptr, state, rstate, nil } func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { @@ -298,7 +299,7 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int } } if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rname, rptr, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) + rname, rptr, state, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) if err != nil { return rtn, err } @@ -307,7 +308,8 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int } rtn.RemoteDisplayName = rname rtn.RemotePtr = *rptr - rtn.RemoteState = rstate + rtn.RemoteState = state + rtn.RState = *rstate } return rtn, nil } @@ -317,6 +319,12 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, fmt.Errorf("/run error: %w", err) } + if !ids.RState.IsConnected() { + return nil, fmt.Errorf("cannot run command, remote '%s' not connected", ids.RemoteDisplayName) + } + if ids.RemoteState == nil { + return nil, fmt.Errorf("cannot run command, remote '%s' has no state", ids.RemoteDisplayName) + } cmdId := uuid.New().String() cmdStr := firstArg(pk) runPacket := packet.MakeRunPacket() @@ -329,7 +337,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} runPacket.Command = strings.TrimSpace(cmdStr) - cmd, err := remote.RunCommand(ctx, cmdId, ids.RemotePtr.RemoteId, ids.RemoteState, runPacket) + cmd, err := remote.RunCommand(ctx, cmdId, ids.RemotePtr, ids.RemoteState, runPacket) if err != nil { return nil, err } @@ -507,15 +515,11 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot unset") - } - if !curRemote.IsConnected() { - return nil, fmt.Errorf("remote is not connected, cannot unset") + if !ids.RState.IsConnected() { + return nil, fmt.Errorf("remote '%s' is not connected, cannot unset", ids.RemoteDisplayName) } if ids.RemoteState == nil { - return nil, fmt.Errorf("remote state is not available") + return nil, fmt.Errorf("remote '%s' state is not available, cannot unset", ids.RemoteDisplayName) } envMap := shexec.ParseEnv0(ids.RemoteState.Env0) unsetVars := make(map[string]bool) @@ -595,15 +599,11 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot setenv") - } - if !curRemote.IsConnected() { - return nil, fmt.Errorf("remote is not connected, cannot setenv") + if !ids.RState.IsConnected() { + return nil, fmt.Errorf("remote '%s' is not connected, cannot setenv", ids.RemoteDisplayName) } if ids.RemoteState == nil { - return nil, fmt.Errorf("remote state is not available") + return nil, fmt.Errorf("remote '%s' state is not available, cannot setenv", ids.RemoteDisplayName) } envMap := shexec.ParseEnv0(ids.RemoteState.Env0) if len(pk.Args) == 0 { @@ -656,7 +656,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if newRemote == "" { return nil, nil } - remoteName, rptr, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + remoteName, rptr, _, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { return nil, err } @@ -689,13 +689,13 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up newDir := firstArg(pk) curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot change directory") + return nil, fmt.Errorf("remote '%s' not found, cannot change directory", ids.RemoteDisplayName) } - if !curRemote.IsConnected() { - return nil, fmt.Errorf("remote is not connected, cannot change directory") + if !ids.RState.IsConnected() { + return nil, fmt.Errorf("remote '%s' is not connected, cannot change directory", ids.RemoteDisplayName) } if ids.RemoteState == nil { - return nil, fmt.Errorf("remote state is not available") + return nil, fmt.Errorf("remote '%s' state is not available, cannot change directory", ids.RemoteDisplayName) } if newDir == "" { return sstore.ModelUpdate{ @@ -704,7 +704,7 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up }, }, nil } - newDir, err = curRemote.ExpandHomeDir(newDir) + newDir, err = ids.RState.ExpandHomeDir(newDir) if err != nil { return nil, err } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 2d8cd6f79..eb1dcca12 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -68,6 +68,10 @@ type RemoteState struct { ConnectMode string `json:"connectmode"` } +func (state RemoteState) IsConnected() bool { + return state.Status == StatusConnected +} + type MShellProc struct { Lock *sync.Mutex Remote *sstore.RemoteType @@ -401,16 +405,11 @@ func replaceHomePath(pathStr string, homeDir string) string { return pathStr } -func (msh *MShellProc) ExpandHomeDir(pathStr string) (string, error) { +func (state RemoteState) ExpandHomeDir(pathStr string) (string, error) { if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") { return pathStr, nil } - msh.Lock.Lock() - defer msh.Lock.Unlock() - if msh.ServerProc.InitPk == nil { - return "", fmt.Errorf("remote not connected, does not have home directory set for ~ expansion") - } - homeDir := msh.ServerProc.InitPk.HomeDir + homeDir := state.RemoteVars["home"] if homeDir == "" { return "", fmt.Errorf("remote does not have HOME set, cannot do ~ expansion") } @@ -449,13 +448,16 @@ func makeTermOpts() sstore.TermOpts { return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true, MaxPtySize: DefaultMaxPtySize} } -func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { - msh := GetRemoteById(remoteId) +func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { + if remotePtr.OwnerId != "" { + return nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) + } + msh := GetRemoteById(remotePtr.RemoteId) if msh == nil { - return nil, fmt.Errorf("no remote id=%s found", remoteId) + return nil, fmt.Errorf("no remote id=%s found", remotePtr.RemoteId) } if !msh.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected", remoteId) + return nil, fmt.Errorf("remote '%s' is not connected", remotePtr.RemoteId) } if remoteState == nil { return nil, fmt.Errorf("no remote state passed to RunCommand") @@ -486,7 +488,7 @@ func RunCommand(ctx context.Context, cmdId string, remoteId string, remoteState SessionId: runPacket.CK.GetSessionId(), CmdId: startPk.CK.GetCmdId(), CmdStr: runPacket.Command, - RemoteId: msh.Remote.RemoteId, + Remote: remotePtr, RemoteState: *remoteState, TermOpts: makeTermOpts(), Status: status, diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index a64c39bdc..094d7b389 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -49,6 +49,9 @@ func AcquireSCLock() (*os.File, error) { } func EnsureSessionDir(sessionId string) (string, error) { + if sessionId == "" { + return "", fmt.Errorf("cannot get session dir for blank sessionid") + } BaseLock.Lock() sdir, ok := SessionDirCache[sessionId] BaseLock.Unlock() @@ -90,6 +93,12 @@ func PtyOutFile(sessionId string, cmdId string) (string, error) { if err != nil { return "", err } + if sessionId == "" { + return "", fmt.Errorf("cannot get ptyout file for blank sessionid") + } + if cmdId == "" { + return "", fmt.Errorf("cannot get ptyout file for blank cmdid") + } return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } @@ -98,10 +107,19 @@ func RunOutFile(sessionId string, cmdId string) (string, error) { if err != nil { return "", err } + if sessionId == "" { + return "", fmt.Errorf("cannot get runout file for blank sessionid") + } + if cmdId == "" { + return "", fmt.Errorf("cannot get runout file for blank cmdid") + } return fmt.Sprintf("%s/%s.runout", sdir, cmdId), nil } func RemotePtyOut(remoteId string) (string, error) { + if remoteId == "" { + return "", fmt.Errorf("cannot get remote ptyout file for blank remoteid") + } scHome := GetScHomeDir() rdir := path.Join(scHome, RemotesDirBaseName) err := ensureDir(rdir) diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index b5e32e0a8..8010873f2 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -3,17 +3,14 @@ package scpacket import ( "reflect" + "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) const FeCommandPacketStr = "fecmd" const WatchScreenPacketStr = "watchscreen" - -type RemoteState struct { - RemoteId string `json:"remoteid"` - RemoteName string `json:"remotename"` - Cwd string `json:"cwd"` -} +const FeInputPacketStr = "feinput" type FeCommandPacketType struct { Type string `json:"type"` @@ -23,9 +20,20 @@ type FeCommandPacketType struct { Kwargs map[string]string `json:"kwargs,omitempty"` } +type FeInputPacketType struct { + Type string `json:"type"` + CK base.CommandKey `json:"ck"` + Remote sstore.RemotePtrType `json:"remote"` + InputData64 string `json:"inputdata"` + SigNum int `json:"signum,omitempty"` + WinSizeRows int `json:"winsizerows"` + WinSizeCols int `json:"winsizecols"` +} + func init() { packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{})) packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{})) + packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{})) } func (*FeCommandPacketType) GetType() string { @@ -36,6 +44,25 @@ func MakeFeCommandPacket() *FeCommandPacketType { return &FeCommandPacketType{Type: FeCommandPacketStr} } +func (*FeInputPacketType) GetType() string { + return FeInputPacketStr +} + +func MakeFeInputPacket() *FeInputPacketType { + return &FeInputPacketType{Type: FeInputPacketStr} +} + +func (p *FeInputPacketType) ConvertToInputPacket() *packet.InputPacketType { + rtn := packet.MakeInputPacket() + rtn.CK = p.CK + rtn.RemoteId = p.Remote.RemoteId + rtn.InputData64 = p.InputData64 + rtn.SigNum = p.SigNum + rtn.WinSizeRows = p.WinSizeRows + rtn.WinSizeCols = p.WinSizeCols + return rtn +} + type WatchScreenPacketType struct { Type string `json:"type"` SessionId string `json:"sessionid"` diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 759e8747c..d8a810bcb 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -128,9 +128,19 @@ func (ws *WSState) RunWSRead() { fmt.Printf("error unmarshalling ws message: %v\n", err) continue } - if pk.GetType() == "input" { + if pk.GetType() == "feinput" { + feInputPk := pk.(*scpacket.FeInputPacketType) + if feInputPk.Remote.OwnerId != "" { + fmt.Printf("[error] cannot send input to remote with ownerid\n") + continue + } + if feInputPk.Remote.RemoteId == "" { + fmt.Printf("[error] invalid input packet, remoteid is not set\n") + continue + } + inputPk := feInputPk.ConvertToInputPacket() go func() { - err = sendCmdInput(pk.(*packet.InputPacketType)) + err = sendCmdInput(inputPk) if err != nil { fmt.Printf("[error] sending command input: %v\n", err) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index ac5772c38..735c92757 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -261,7 +261,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) - query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, owneruserid, sharemode, accesskey) VALUES (?, ?, '', ?, ?, '', 'local', '')` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, ownerid, sharemode, accesskey) VALUES (?, ?, '', ?, ?, '', 'local', '')` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { @@ -334,7 +334,7 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) screenName = fmtUniqueName(screenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = uuid.New().String() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, owneruserid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout) VALUES (?, ?, ?, ?, ?)` @@ -388,8 +388,8 @@ func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) strin ShareOpts: WindowShareOptsType{}, } wmap := w.ToMap() - query := `INSERT INTO window ( sessionid, windowid, curremoteowneruserid, curremoteid, curremotename, winopts, owneruserid, sharemode, shareopts) - VALUES (:sessionid,:windowid,:curremoteowneruserid,:curremoteid,:curremotename,:winopts,:owneruserid,:sharemode,:shareopts)` + query := `INSERT INTO window ( sessionid, windowid, curremoteownerid, curremoteid, curremotename, winopts, ownerid, sharemode, shareopts) + VALUES (:sessionid,:windowid,:curremoteownerid,:curremoteid,:curremotename,:winopts,:ownerid,:sharemode,:shareopts)` tx.NamedExecWrap(query, wmap) return w.WindowId } @@ -414,8 +414,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil { cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteid, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) - VALUES (:sessionid,:cmdid,:remoteid,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) +INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) + VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) ` tx.NamedExecWrap(query, cmdMap) } @@ -562,8 +562,8 @@ func GetRemoteState(ctx context.Context, sessionId string, windowId string, remo var remoteState *RemoteState txErr := WithTx(ctx, func(tx *TxWrap) error { var ri RemoteInstance - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if found { remoteState = &ri.State return nil @@ -599,26 +599,26 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r if err != nil { return fmt.Errorf("cannot update remote instance cwd: %w", err) } - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if !found { ri = RemoteInstance{ - RIId: uuid.New().String(), - Name: remotePtr.Name, - SessionId: sessionId, - WindowId: windowId, - RemoteOwnerUserId: remotePtr.OwnerUserId, - RemoteId: remotePtr.RemoteId, - State: state, + RIId: uuid.New().String(), + Name: remotePtr.Name, + SessionId: sessionId, + WindowId: windowId, + RemoteOwnerId: remotePtr.OwnerId, + RemoteId: remotePtr.RemoteId, + State: state, } - query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteowneruserid, remoteid, state) - VALUES (:riid,:name,:sessionid,:windowid,:remoteowneruserid,:remoteid,:state)` + query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, state) + VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:state)` tx.NamedExecWrap(query, ri) return nil } - query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND remoteowneruserid = ? AND remoteid = ? AND name = ?` + query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` ri.State = state - tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name) + tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) return nil }) return &ri, txErr @@ -630,8 +630,8 @@ func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, rem if !tx.Exists(query, sessionId, windowId) { return fmt.Errorf("cannot update curremote: no window found") } - query = `UPDATE window SET curremoteowneruserid = ?, curremoteid = ?, curremotename = ? WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, remotePtr.OwnerUserId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) + query = `UPDATE window SET curremoteownerid = ?, curremoteid = ?, curremotename = ? WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) return nil }) return txErr diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 83b46cca0..0afb84412 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -99,7 +99,7 @@ type SessionType struct { Name string `json:"name"` SessionIdx int64 `json:"sessionidx"` ActiveScreenId string `json:"activescreenid"` - OwnerUserId string `json:"owneruserid"` + OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` @@ -134,9 +134,9 @@ func (opts WindowShareOptsType) Value() (driver.Value, error) { } type RemotePtrType struct { - OwnerUserId string `json:"owneruserid"` - RemoteId string `json:"remoteid"` - Name string `json:"name"` + OwnerId string `json:"ownerid"` + RemoteId string `json:"remoteid"` + Name string `json:"name"` } func (r RemotePtrType) IsSessionScope() bool { @@ -147,28 +147,28 @@ func (r RemotePtrType) MakeFullRemoteRef() string { if r.RemoteId == "" { return "" } - if r.OwnerUserId == "" && r.Name == "" { + if r.OwnerId == "" && r.Name == "" { return r.RemoteId } - if r.OwnerUserId != "" && r.Name == "" { - return fmt.Sprintf("@%s:%s", r.OwnerUserId, r.RemoteId) + if r.OwnerId != "" && r.Name == "" { + return fmt.Sprintf("@%s:%s", r.OwnerId, r.RemoteId) } - if r.OwnerUserId == "" && r.Name != "" { + if r.OwnerId == "" && r.Name != "" { return fmt.Sprintf("%s:%s", r.RemoteId, r.Name) } - return fmt.Sprintf("@%s:%s:%s", r.OwnerUserId, r.RemoteId, r.Name) + return fmt.Sprintf("@%s:%s:%s", r.OwnerId, r.RemoteId, r.Name) } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - CurRemote RemotePtrType `json:"curremote"` - WinOpts WindowOptsType `json:"winopts"` - OwnerUserId string `json:"owneruserid"` - ShareMode string `json:"sharemode"` - ShareOpts WindowShareOptsType `json:"shareopts"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + CurRemote RemotePtrType `json:"curremote"` + WinOpts WindowOptsType `json:"winopts"` + OwnerId string `json:"ownerid"` + ShareMode string `json:"sharemode"` + ShareOpts WindowShareOptsType `json:"shareopts"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` // only for updates Remove bool `json:"remove,omitempty"` @@ -178,11 +178,11 @@ func (w *WindowType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = w.SessionId rtn["windowid"] = w.WindowId - rtn["curremoteowneruserid"] = w.CurRemote.OwnerUserId + rtn["curremoteownerid"] = w.CurRemote.OwnerId rtn["curremoteid"] = w.CurRemote.RemoteId rtn["curremotename"] = w.CurRemote.Name rtn["winopts"] = quickJson(w.WinOpts) - rtn["owneruserid"] = w.OwnerUserId + rtn["ownerid"] = w.OwnerId rtn["sharemode"] = w.ShareMode rtn["shareopts"] = quickJson(w.ShareOpts) return rtn @@ -195,11 +195,11 @@ func WindowFromMap(m map[string]interface{}) *WindowType { var w WindowType quickSetStr(&w.SessionId, m, "sessionid") quickSetStr(&w.WindowId, m, "windowid") - quickSetStr(&w.CurRemote.OwnerUserId, m, "curremoteowneruserid") + quickSetStr(&w.CurRemote.OwnerId, m, "curremoteownerid") quickSetStr(&w.CurRemote.RemoteId, m, "curremoteid") quickSetStr(&w.CurRemote.Name, m, "curremotename") quickSetJson(&w.WinOpts, m, "winopts") - quickSetStr(&w.OwnerUserId, m, "owneruserid") + quickSetStr(&w.OwnerId, m, "ownerid") quickSetStr(&w.ShareMode, m, "sharemode") quickSetJson(&w.ShareOpts, m, "shareopts") return &w @@ -224,7 +224,7 @@ type ScreenType struct { Name string `json:"name"` ActiveWindowId string `json:"activewindowid"` ScreenOpts ScreenOptsType `json:"screenopts"` - OwnerUserId string `json:"owneruserid"` + OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` Windows []*ScreenWindowType `json:"windows"` @@ -314,13 +314,13 @@ func (opts TermOpts) Value() (driver.Value, error) { } type RemoteInstance struct { - RIId string `json:"riid"` - Name string `json:"name"` - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - RemoteOwnerUserId string `json:"remoteowneruserid"` - RemoteId string `json:"remoteid"` - State RemoteState `json:"state"` + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteOwnerId string `json:"remoteownerid"` + RemoteId string `json:"remoteid"` + State RemoteState `json:"state"` // only for updates Remove bool `json:"remove,omitempty"` @@ -388,7 +388,7 @@ func (r *RemoteType) GetName() string { type CmdType struct { SessionId string `json:"sessionid"` CmdId string `json:"cmdid"` - RemoteId string `json:"remoteid"` + Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` RemoteState RemoteState `json:"remotestate"` TermOpts TermOpts `json:"termopts"` @@ -443,7 +443,9 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = cmd.SessionId rtn["cmdid"] = cmd.CmdId - rtn["remoteid"] = cmd.RemoteId + rtn["remoteownerid"] = cmd.Remote.OwnerId + rtn["remoteid"] = cmd.Remote.RemoteId + rtn["remotename"] = cmd.Remote.Name rtn["cmdstr"] = cmd.CmdStr rtn["remotestate"] = quickJson(cmd.RemoteState) rtn["termopts"] = quickJson(cmd.TermOpts) @@ -462,7 +464,9 @@ func CmdFromMap(m map[string]interface{}) *CmdType { var cmd CmdType quickSetStr(&cmd.SessionId, m, "sessionid") quickSetStr(&cmd.CmdId, m, "cmdid") - quickSetStr(&cmd.RemoteId, m, "remoteid") + quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") + quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") + quickSetStr(&cmd.Remote.Name, m, "remotename") quickSetStr(&cmd.CmdStr, m, "cmdstr") quickSetJson(&cmd.RemoteState, m, "remotestate") quickSetJson(&cmd.TermOpts, m, "termopts") From 5b1b67cf55d5696e5c00fdcd5104b84cdf0879dd Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 24 Aug 2022 18:56:50 -0700 Subject: [PATCH 073/397] quick fixes --- pkg/cmdrunner/cmdrunner.go | 3 +-- pkg/sstore/updatebus.go | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d68c29fc7..86a3b6522 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -332,7 +332,6 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) runPacket.Cwd = ids.RemoteState.Cwd runPacket.Env0 = ids.RemoteState.Env0 - fmt.Printf("run-command FOO [%s]\n", shexec.ParseEnv0(ids.RemoteState.Env0)["FOO"]) runPacket.EnvComplete = true runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} @@ -668,7 +667,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) } update := sstore.ModelUpdate{ - Window: sstore.WindowType{ + Window: &sstore.WindowType{ SessionId: ids.SessionId, WindowId: ids.WindowId, CurRemote: *rptr, diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 155db0122..911dd1384 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -24,10 +24,10 @@ func (PtyDataUpdate) UpdateType() string { } type ModelUpdate struct { - Sessions []*SessionType `json:"sessions"` + Sessions []*SessionType `json:"sessions,omitempty"` ActiveSessionId string `json:"activesessionid,omitempty"` - Window WindowType `json:"window"` - Line *LineType `json:"line"` + Window *WindowType `json:"window,omitempty"` + Line *LineType `json:"line,omitempty"` Cmd *CmdType `json:"cmd,omitempty"` CmdLine *CmdLineType `json:"cmdline,omitempty"` Info *InfoMsgType `json:"info,omitempty"` From d0806bbd634c3cb4b772a4c9ca6df4d2e4dfb9be Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 24 Aug 2022 22:57:41 -0700 Subject: [PATCH 074/397] remove lock around connecting remote --- pkg/remote/remote.go | 54 ++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index eb1dcca12..3f94c0b4f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -294,10 +294,13 @@ func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { } } -func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) error { +func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) (*os.File, error) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + cmdPty, cmdTty, err := pty.Open() if err != nil { - return err + return nil, err } msh.ControllingPty = cmdPty ecmd.ExtraFiles = append(ecmd.ExtraFiles, cmdTty) @@ -307,18 +310,28 @@ func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) error { ecmd.SysProcAttr.Setsid = true ecmd.SysProcAttr.Setctty = true ecmd.SysProcAttr.Ctty = len(ecmd.ExtraFiles) + 3 - 1 - return nil + return cmdPty, nil +} + +func (msh *MShellProc) setErrorStatus(err error) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.Status = StatusError + msh.Err = err +} + +func (msh *MShellProc) getRemoteCopy() sstore.RemoteType { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return *msh.Remote } func (msh *MShellProc) Launch() { - msh.Lock.Lock() - defer msh.Lock.Unlock() - - ecmd := convertSSHOpts(msh.Remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) - err := msh.addControllingTty(ecmd) + remote := msh.getRemoteCopy() + ecmd := convertSSHOpts(remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) + cmdPty, err := msh.addControllingTty(ecmd) if err != nil { - msh.Status = StatusError - msh.Err = fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) + msh.setErrorStatus(fmt.Errorf("cannot attach controlling tty to mshell command: %w", err)) return } defer func() { @@ -326,12 +339,12 @@ func (msh *MShellProc) Launch() { ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() } }() - remoteName := msh.Remote.GetName() + remoteName := remote.GetName() go func() { - fmt.Printf("[c-pty %s] starting...\n", msh.Remote.GetName()) + fmt.Printf("[c-pty %s] starting...\n", remote.GetName()) buf := make([]byte, 100) for { - n, readErr := msh.ControllingPty.Read(buf) + n, readErr := cmdPty.Read(buf) if readErr == io.EOF { break } @@ -348,21 +361,24 @@ func (msh *MShellProc) Launch() { if remoteName == "test2" { go func() { time.Sleep(2 * time.Second) - msh.ControllingPty.Write([]byte(Test2Pw)) + cmdPty.Write([]byte(Test2Pw)) fmt.Printf("[c-pty %s] wrote password!\n", remoteName) }() } cproc, uname, err := shexec.MakeClientProc(ecmd) - msh.UName = uname + msh.WithLock(func() { + msh.UName = uname + }) if err != nil { - msh.Status = StatusError - msh.Err = err + msh.setErrorStatus(err) fmt.Printf("[error] connecting remote %s (%s): %v\n", msh.Remote.GetName(), msh.UName, err) return } fmt.Printf("connected remote %s\n", msh.Remote.GetName()) - msh.ServerProc = cproc - msh.Status = StatusConnected + msh.WithLock(func() { + msh.ServerProc = cproc + msh.Status = StatusConnected + }) go func() { exitErr := cproc.Cmd.Wait() exitCode := shexec.GetExitCode(exitErr) From dc8cba79da486ce1935be385411ae5e0139da8d7 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 13:12:17 -0700 Subject: [PATCH 075/397] add quoting/shell-parsing for commands --- go.mod | 1 + go.sum | 2 + pkg/cmdrunner/cmdrunner.go | 116 +++++--------------- pkg/cmdrunner/shparse.go | 210 +++++++++++++++++++++++++++++++++++++ pkg/remote/remote.go | 6 ++ pkg/sstore/dbops.go | 67 ++++++++++++ pkg/sstore/updatebus.go | 1 + 7 files changed, 312 insertions(+), 91 deletions(-) create mode 100644 pkg/cmdrunner/shparse.go diff --git a/go.mod b/go.mod index 567032ef7..3fe97fe9b 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/mattn/go-shellwords v1.0.12 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + mvdan.cc/sh/v3 v3.5.1 // indirect ) replace github.com/scripthaus-dev/mshell v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ diff --git a/go.sum b/go.sum index 809fecbb3..1e458fc35 100644 --- a/go.sum +++ b/go.sum @@ -1798,6 +1798,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= +mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 86a3b6522..ef042544e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -45,25 +45,6 @@ type resolvedIds struct { RState remote.RemoteState } -func SubMetaCmd(cmd string) string { - switch cmd { - case "s": - return "screen" - case "w": - return "window" - case "r": - return "run" - case "c": - return "comment" - case "e": - return "eval" - case "export": - return "setenv" - default: - return cmd - } -} - var ValidCommands = []string{ "/run", "/eval", @@ -377,12 +358,11 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") } - // parse metacmd - commandStr := strings.TrimSpace(pk.Args[0]) - if commandStr == "" { - return nil, fmt.Errorf("/eval, invalid emtpty command") + newPk, err := EvalMetaCommand(ctx, pk) + if err != nil { + return nil, err } - update, err := evalCommandInternal(ctx, pk) + update, err := HandleCommand(ctx, newPk) if !resolveBool(pk.Kwargs["nohist"], false) { err := addToHistory(ctx, pk, update, (err != nil)) if err != nil { @@ -393,73 +373,6 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return update, err } -func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - commandStr := strings.TrimSpace(pk.Args[0]) - metaCmd := "" - metaSubCmd := "" - if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") { - metaCmd = "cd" - commandStr = strings.TrimSpace(commandStr[2:]) - } else if commandStr == "cr" || strings.HasPrefix(commandStr, "cr ") { - metaCmd = "cr" - commandStr = strings.TrimSpace(commandStr[2:]) - } else if commandStr == "export" || strings.HasPrefix(commandStr, "export ") { - metaCmd = "setenv" - commandStr = strings.TrimSpace(commandStr[6:]) - } else if commandStr == "setenv" || strings.HasPrefix(commandStr, "setenv ") { - metaCmd = "setenv" - commandStr = strings.TrimSpace(commandStr[6:]) - } else if commandStr == "unset" || strings.HasPrefix(commandStr, "unset ") { - metaCmd = "unset" - commandStr = strings.TrimSpace(commandStr[5:]) - } else if commandStr[0] == '/' { - spaceIdx := strings.Index(commandStr, " ") - if spaceIdx == -1 { - metaCmd = commandStr[1:] - commandStr = "" - } else { - metaCmd = commandStr[1:spaceIdx] - commandStr = strings.TrimSpace(commandStr[spaceIdx+1:]) - } - colonIdx := strings.Index(metaCmd, ":") - if colonIdx != -1 { - metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:] - } - if metaCmd == "" { - return nil, fmt.Errorf("invalid command, got bare '/', with no command") - } - } - if metaCmd == "" { - metaCmd = "run" - } - metaCmd = SubMetaCmd(metaCmd) - newPk := &scpacket.FeCommandPacketType{ - MetaCmd: metaCmd, - MetaSubCmd: metaSubCmd, - Kwargs: pk.Kwargs, - } - if strings.HasSuffix(commandStr, " ?") { - newPk.Kwargs["ephemeral"] = "1" - commandStr = commandStr[0 : len(commandStr)-2] - } - if metaCmd == "run" || metaCmd == "comment" { - newPk.Args = []string{commandStr} - } else if (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" { - newPk.Args = strings.Fields(commandStr) - } else { - allArgs := strings.Fields(commandStr) - for _, arg := range allArgs { - if strings.Index(arg, "=") == -1 { - newPk.Args = append(newPk.Args, arg) - continue - } - fields := strings.SplitN(arg, "=", 2) - newPk.Kwargs[fields[0]] = fields[1] - } - } - return HandleCommand(ctx, newPk) -} - func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd == "close" { ids, err := resolveIds(ctx, pk, R_Session|R_Screen) @@ -976,6 +889,27 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } return update, nil } + if pk.MetaSubCmd == "set" { + ids, err := resolveIds(ctx, pk, R_Session) + if err != nil { + return nil, err + } + bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) + if err != nil { + return nil, err + } + if bareSession == nil { + return nil, fmt.Errorf("session '%s' not found", ids.SessionId) + } + update := sstore.ModelUpdate{ + Sessions: nil, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s]: update", bareSession.Name), + TimeoutMs: 2000, + }, + } + return update, nil + } if pk.MetaSubCmd != "" { return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go new file mode 100644 index 000000000..96e84852e --- /dev/null +++ b/pkg/cmdrunner/shparse.go @@ -0,0 +1,210 @@ +package cmdrunner + +import ( + "context" + "fmt" + "io" + "regexp" + "strings" + + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/syntax" +) + +type parseEnviron struct { + Env map[string]string +} + +func (e *parseEnviron) Get(name string) expand.Variable { + val, ok := e.Env[name] + if !ok { + return expand.Variable{} + } + return expand.Variable{ + Exported: true, + Kind: expand.String, + Str: val, + } +} + +func (e *parseEnviron) Each(fn func(name string, vr expand.Variable) bool) { + for key, _ := range e.Env { + rtn := fn(key, e.Get(key)) + if !rtn { + break + } + } +} + +func DumpPacket(pk *scpacket.FeCommandPacketType) { + if pk == nil || pk.MetaCmd == "" { + fmt.Printf("[no metacmd]\n") + return + } + if pk.MetaSubCmd == "" { + fmt.Printf("/%s\n", pk.MetaCmd) + } else { + fmt.Printf("/%s:%s\n", pk.MetaCmd, pk.MetaSubCmd) + } + for _, arg := range pk.Args { + fmt.Printf(" %q\n", arg) + } + for key, val := range pk.Kwargs { + fmt.Printf(" [%s]=%q\n", key, val) + } +} + +func doCmdSubst(commandStr string, w io.Writer, word *syntax.CmdSubst) error { + return nil +} + +func doProcSubst(w *syntax.ProcSubst) (string, error) { + return "", nil +} + +func isQuoted(source string, w *syntax.Word) bool { + if w == nil { + return false + } + offset := w.Pos().Offset() + if int(offset) >= len(source) { + return false + } + return source[offset] == '"' || source[offset] == '\'' +} + +func getSourceStr(source string, w *syntax.Word) string { + if w == nil { + return "" + } + offset := w.Pos().Offset() + end := w.End().Offset() + return source[offset:end] +} + +var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(:[a-z][a-z0-9_-]*)?$") + +type BareMetaCmdDecl struct { + CmdStr string + MetaCmd string +} + +var BareMetaCmds = []BareMetaCmdDecl{ + BareMetaCmdDecl{"cd", "cd"}, + BareMetaCmdDecl{"cr", "cr"}, + BareMetaCmdDecl{"setenv", "setenv"}, + BareMetaCmdDecl{"export", "setenv"}, + BareMetaCmdDecl{"unset", "unset"}, +} + +func SubMetaCmd(cmd string) string { + switch cmd { + case "s": + return "screen" + case "w": + return "window" + case "r": + return "run" + case "c": + return "comment" + case "e": + return "eval" + case "export": + return "setenv" + default: + return cmd + } +} + +// returns (metaCmd, metaSubCmd, rest) +// if metaCmd is "" then this isn't a valid metacmd string +func parseMetaCmd(origCommandStr string) (string, string, string) { + commandStr := strings.TrimSpace(origCommandStr) + if len(commandStr) < 2 { + return "run", "", origCommandStr + } + fields := strings.SplitN(commandStr, " ", 2) + firstArg := fields[0] + rest := "" + if len(fields) > 1 { + rest = strings.TrimSpace(fields[1]) + } + for _, decl := range BareMetaCmds { + if firstArg == decl.CmdStr { + return decl.MetaCmd, "", rest + } + } + m := ValidMetaCmdRe.FindStringSubmatch(firstArg) + if m == nil { + return "run", "", origCommandStr + } + return SubMetaCmd(m[1]), m[2], rest +} + +func onlyPositionalArgs(metaCmd string, metaSubCmd string) bool { + return (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" +} + +func onlyRawArgs(metaCmd string, metaSubCmd string) bool { + return metaCmd == "run" || metaCmd == "comment" +} + +func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) (*scpacket.FeCommandPacketType, error) { + if len(origPk.Args) == 0 { + return nil, fmt.Errorf("empty command (no fields)") + } + metaCmd, metaSubCmd, commandArgs := parseMetaCmd(origPk.Args[0]) + rtnPk := scpacket.MakeFeCommandPacket() + rtnPk.MetaCmd = metaCmd + rtnPk.MetaSubCmd = metaSubCmd + rtnPk.Kwargs = make(map[string]string) + for key, val := range origPk.Kwargs { + rtnPk.Kwargs[key] = val + } + if onlyRawArgs(metaCmd, metaSubCmd) { + // don't evaluate arguments for /run or /comment + rtnPk.Args = []string{commandArgs} + return rtnPk, nil + } + commandReader := strings.NewReader(commandArgs) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + var words []*syntax.Word + err := parser.Words(commandReader, func(w *syntax.Word) bool { + words = append(words, w) + return true + }) + if err != nil { + return nil, fmt.Errorf("parsing metacmd, position %v", err) + } + envMap := make(map[string]string) // later we can add vars like session, window, screen, remote, and user + cfg := &expand.Config{ + Env: &parseEnviron{Env: envMap}, + GlobStar: false, + NullGlob: false, + NoUnset: false, + CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst(commandArgs, w, word) }, + ProcSubst: doProcSubst, + ReadDir: nil, + } + // process arguments + for idx, w := range words { + literalVal, err := expand.Literal(cfg, w) + if err != nil { + return nil, fmt.Errorf("error evaluating metacmd argument %d [%s]: %v", idx+1, getSourceStr(commandArgs, w), err) + } + if isQuoted(commandArgs, w) || onlyPositionalArgs(metaCmd, metaSubCmd) { + rtnPk.Args = append(rtnPk.Args, literalVal) + continue + } + eqIdx := strings.Index(literalVal, "=") + if eqIdx != -1 && eqIdx != 0 { + varName := literalVal[:eqIdx] + varVal := literalVal[eqIdx+1:] + rtnPk.Kwargs[varName] = varVal + continue + } + rtnPk.Args = append(rtnPk.Args, literalVal) + } + return rtnPk, nil +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 3f94c0b4f..0c31ad8d6 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -250,6 +250,12 @@ func (proc *MShellProc) GetRemoteState() RemoteState { return state } +func (msh *MShellProc) NotifyUpdate() { + rstate := msh.GetRemoteState() + update := &sstore.ModelUpdate{Remote: rstate} + sstore.MainBus.SendUpdate("", update) +} + func GetAllRemoteState() []RemoteState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 735c92757..7fd0d8c11 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -142,6 +142,22 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { return rtn, nil } +func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, error) { + var rtn SessionType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM session WHERE sessionid = ?` + tx.GetWrap(&rtn, query, sessionId) + return nil + }) + if txErr != nil { + return nil, txErr + } + if rtn.SessionId == "" { + return nil, nil + } + return &rtn, nil +} + func GetAllSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { @@ -636,3 +652,54 @@ func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, rem }) return txErr } + +func reorderStrings(strs []string, toMove string, newIndex int) []string { + if toMove == "" { + return strs + } + var newStrs []string + if newIndex < 0 { + newStrs = append(newStrs, toMove) + } + for _, sval := range strs { + if len(newStrs) == newIndex { + newStrs = append(newStrs, toMove) + } + if sval != toMove { + newStrs = append(newStrs, sval) + } + } + if newIndex >= len(newStrs) { + newStrs = append(newStrs, toMove) + } + return newStrs +} + +func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM sessions ORDER BY sessionidx, name, sessionid` + ids := tx.SelectStrings(query) + if sessionId != "" { + ids = reorderStrings(ids, sessionId, newIndex) + } + query = `UPDATE sessions SET sessionid = ? WHERE sessionid = ?` + for idx, id := range ids { + tx.ExecWrap(query, id, idx+1) + } + return nil + }) + return txErr +} + +func SetSessionName(ctx context.Context, sessionId string, name string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM sessions WHERE sessionid = ?` + if !tx.Exists(query, sessionId) { + return fmt.Errorf("session does not exist") + } + query = `UPDATE sessions SET name = ? WHERE sessionid = ?` + tx.ExecWrap(query, name, sessionId) + return nil + }) + return txErr +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 911dd1384..47057778a 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -31,6 +31,7 @@ type ModelUpdate struct { Cmd *CmdType `json:"cmd,omitempty"` CmdLine *CmdLineType `json:"cmdline,omitempty"` Info *InfoMsgType `json:"info,omitempty"` + Remote interface{} `json:"remote,omitempty"` // *remote.RemoteState } func (ModelUpdate) UpdateType() string { From 00b88f7f131487e57b4ded2a812bb5150ad105ee Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 13:18:26 -0700 Subject: [PATCH 076/397] fix regexp, add dump argument to show parsed command --- pkg/cmdrunner/shparse.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 96e84852e..6d463be13 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -83,7 +83,7 @@ func getSourceStr(source string, w *syntax.Word) string { return source[offset:end] } -var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(:[a-z][a-z0-9_-]*)?$") +var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(?::([a-z][a-z0-9_-]*))?$") type BareMetaCmdDecl struct { CmdStr string @@ -206,5 +206,8 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) } rtnPk.Args = append(rtnPk.Args, literalVal) } + if resolveBool(rtnPk.Kwargs["dump"], false) { + DumpPacket(rtnPk) + } return rtnPk, nil } From 46ba21030bef4c625ad74c657ed9556b8db7969a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 16:21:19 -0700 Subject: [PATCH 077/397] save/restore activesessionid, set session name, much more sophisticated session switching logic --- cmd/main-server.go | 12 +- db/migrations/000001_init.up.sql | 1 + db/schema.sql | 1 + pkg/cmdrunner/cmdrunner.go | 194 ++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 42 +++++-- pkg/sstore/sstore.go | 13 ++- 6 files changed, 228 insertions(+), 35 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index e706568c4..b61627b4d 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -423,6 +423,12 @@ func main() { fmt.Printf("[error] migrate up: %v\n", err) return } + userData, err := sstore.EnsureClientData(context.Background()) + if err != nil { + fmt.Printf("[error] ensuring user data: %v\n", err) + return + } + fmt.Printf("userid = %s\n", userData.UserId) err = sstore.EnsureLocalRemote(context.Background()) if err != nil { fmt.Printf("[error] ensuring local remote: %v\n", err) @@ -443,12 +449,6 @@ func main() { fmt.Printf("[error] ensuring default session: %v\n", err) return } - userData, err := sstore.EnsureUserData(context.Background()) - if err != nil { - fmt.Printf("[error] ensuring user data: %v\n", err) - return - } - fmt.Printf("userid = %s\n", userData.UserId) err = remote.LoadRemotes(context.Background()) if err != nil { fmt.Printf("[error] loading remotes: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 2022e65f9..af75bb6bf 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -1,5 +1,6 @@ CREATE TABLE client ( userid varchar(36) NOT NULL, + activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, userprivatekeybytes blob NOT NULL ); diff --git a/db/schema.sql b/db/schema.sql index fc98b13fa..470f8e367 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -2,6 +2,7 @@ CREATE TABLE schema_migrations (version uint64,dirty bool); CREATE UNIQUE INDEX version_unique ON schema_migrations (version); CREATE TABLE client ( userid varchar(36) NOT NULL, + activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, userprivatekeybytes blob NOT NULL ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ef042544e..d22193f14 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -35,6 +35,10 @@ const ( R_RemoteOpt = 128 ) +const MaxNameLen = 50 + +var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") + type resolvedIds struct { SessionId string ScreenId string @@ -57,6 +61,52 @@ var ValidCommands = []string{ "/remote:show", } +var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") + +func resolveByPosition(ids []string, curId string, posStr string) string { + if len(ids) == 0 { + return "" + } + if !positionRe.MatchString(posStr) { + return "" + } + curIdx := 1 // if no match, curIdx will be first item + for idx, id := range ids { + if id == curId { + curIdx = idx + 1 + break + } + } + isRelative := strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") + isWrap := posStr == "+" || posStr == "-" + var pos int + if isWrap && posStr == "+" { + pos = 1 + } else if isWrap && posStr == "-" { + pos = -1 + } else { + pos, _ = strconv.Atoi(posStr) + } + if isRelative { + pos = curIdx + pos + } + if pos < 1 { + if isWrap { + pos = len(ids) + } else { + pos = 1 + } + } + if pos > len(ids) { + if isWrap { + pos = 1 + } else { + pos = len(ids) + } + } + return ids[pos-1] +} + func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { switch SubMetaCmd(pk.MetaCmd) { case "run": @@ -137,21 +187,61 @@ func resolveSessionScreen(ctx context.Context, sessionId string, screenArg strin if screen.ScreenId == screenArg || screen.Name == screenArg { return screen.ScreenId, nil } + } return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) } -func resolveSession(ctx context.Context, sessionArg string) (string, error) { - sessions, err := sstore.GetBareSessions(ctx) - if err != nil { - return "", fmt.Errorf("could not retrive bare sessions") +func getSessionIds(sarr []*sstore.SessionType) []string { + rtn := make([]string, len(sarr)) + for idx, s := range sarr { + rtn[idx] = s.SessionId } - for _, session := range sessions { - if session.SessionId == sessionArg || session.Name == sessionArg { - return session.SessionId, nil + return rtn +} + +var partialUUIDRe = regexp.MustCompile("^[0-9a-f]{8}$") + +func isPartialUUID(s string) bool { + return partialUUIDRe.MatchString(s) +} + +func resolveSession(ctx context.Context, sessionArg string, curSession string, bareSessions []*sstore.SessionType) (string, error) { + if bareSessions == nil { + var err error + bareSessions, err = sstore.GetBareSessions(ctx) + if err != nil { + return "", fmt.Errorf("could not retrive bare sessions") } } - return "", fmt.Errorf("could not resolve sesssion '%s' (name/id not found)", sessionArg) + var curSessionId string + if curSession != "" { + curSessionId, _ = resolveSession(ctx, curSession, "", bareSessions) + } + sids := getSessionIds(bareSessions) + rtnId := resolveByPosition(sids, curSessionId, sessionArg) + if rtnId != "" { + return rtnId, nil + } + tryPuid := isPartialUUID(sessionArg) + var prefixMatches []string + var lastPrefixMatchId string + for _, session := range bareSessions { + if session.SessionId == sessionArg || session.Name == sessionArg || (tryPuid && strings.HasPrefix(session.SessionId, sessionArg)) { + return session.SessionId, nil + } + if strings.HasPrefix(session.Name, sessionArg) { + prefixMatches = append(prefixMatches, session.Name) + lastPrefixMatchId = session.SessionId + } + } + if len(prefixMatches) == 1 { + return lastPrefixMatchId, nil + } + if len(prefixMatches) > 1 { + return "", fmt.Errorf("could not resolve session '%s', ambiguious prefix matched multiple sessions: %s", sessionArg, formatStrs(prefixMatches, "and", true)) + } + return "", fmt.Errorf("could not resolve sesssion '%s' (name/id/pos not found)", sessionArg) } func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { @@ -880,10 +970,55 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return sstore.ModelUpdate{Line: rtnLine}, nil } +func maybeQuote(s string, quote bool) string { + if quote { + return fmt.Sprintf("%q", s) + } + return s +} + +func formatStrs(strs []string, conj string, quote bool) string { + if len(strs) == 0 { + return "(none)" + } + if len(strs) == 1 { + return maybeQuote(strs[0], quote) + } + if len(strs) == 2 { + return fmt.Sprintf("%s %s %s", maybeQuote(strs[0], quote), conj, maybeQuote(strs[1], quote)) + } + var buf bytes.Buffer + for idx := 0; idx < len(strs)-1; idx++ { + buf.WriteString(maybeQuote(strs[idx], quote)) + buf.WriteString(", ") + } + buf.WriteString(conj) + buf.WriteString(" ") + buf.WriteString(maybeQuote(strs[len(strs)-1], quote)) + return buf.String() +} + +func validateName(name string, typeStr string) error { + if len(name) > MaxNameLen { + return fmt.Errorf("%s name too long, max length is %d", typeStr, MaxNameLen) + } + if !genericNameRe.MatchString(name) { + return fmt.Errorf("invalid %s name", typeStr) + } + return nil +} + func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { activate := resolveBool(pk.Kwargs["activate"], true) - update, err := sstore.InsertSessionWithName(ctx, pk.Kwargs["name"], activate) + newName := pk.Kwargs["name"] + if newName != "" { + err := validateName(newName, "session") + if err != nil { + return nil, err + } + } + update, err := sstore.InsertSessionWithName(ctx, newName, activate) if err != nil { return nil, err } @@ -901,10 +1036,30 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if bareSession == nil { return nil, fmt.Errorf("session '%s' not found", ids.SessionId) } + var varsUpdated []string + if pk.Kwargs["name"] != "" { + newName := pk.Kwargs["name"] + err = validateName(newName, "session") + if err != nil { + return nil, err + } + err = sstore.SetSessionName(ctx, ids.SessionId, newName) + if err != nil { + return nil, fmt.Errorf("setting session name: %v", err) + } + varsUpdated = append(varsUpdated, "name") + } + if pk.Kwargs["pos"] != "" { + + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) + } + bareSession, err = sstore.GetBareSessionById(ctx, ids.SessionId) update := sstore.ModelUpdate{ - Sessions: nil, + Sessions: []*sstore.SessionType{bareSession}, Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s]: update", bareSession.Name), + InfoMsg: fmt.Sprintf("[%s]: updated %s", bareSession.Name, formatStrs(varsUpdated, "and", false)), TimeoutMs: 2000, }, } @@ -915,13 +1070,24 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } firstArg := firstArg(pk) if firstArg == "" { - return nil, fmt.Errorf("usage /session [session-name|session-id], no param specified") + return nil, fmt.Errorf("usage /session [name|id|pos], no param specified") } - sessionId, err := resolveSession(ctx, firstArg) + sessionId, err := resolveSession(ctx, firstArg, pk.Kwargs["session"], nil) if err != nil { return nil, err } - return sstore.ModelUpdate{ActiveSessionId: sessionId}, nil + bareSession, err := sstore.GetSessionById(ctx, sessionId) + if err != nil { + return nil, fmt.Errorf("could not find session '%s': %v", sessionId, err) + } + update := sstore.ModelUpdate{ + ActiveSessionId: sessionId, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("switched to session %q", bareSession.Name), + TimeoutMs: 2000, + }, + } + return update, nil } func splitLinesForInfo(str string) []string { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 7fd0d8c11..a0f30dc60 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -132,7 +132,7 @@ func GetSessionHistoryItems(ctx context.Context, sessionId string, maxItems int) func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM session` + query := `SELECT * FROM session ORDER BY sessionidx` tx.SelectWrap(&rtn, query) return nil }) @@ -142,6 +142,19 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { return rtn, nil } +func GetAllSessionIds(ctx context.Context) ([]string, error) { + var rtn []string + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid from session ORDER by sessionidx` + rtn = tx.SelectStrings(query) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, error) { var rtn SessionType txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -158,9 +171,10 @@ func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, er return &rtn, nil } -func GetAllSessions(ctx context.Context) ([]*SessionType, error) { +func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { var rtn []*SessionType - err := WithTx(ctx, func(tx *TxWrap) error { + var activeSessionId string + txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session` tx.SelectWrap(&rtn, query) sessionMap := make(map[string]*SessionType) @@ -203,9 +217,14 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { s.Remotes = append(s.Remotes, ri) } } + query = `SELECT activesessionid FROM client` + activeSessionId = tx.GetString(query) return nil }) - return rtn, err + if txErr != nil { + return nil, txErr + } + return &ModelUpdate{Sessions: rtn, ActiveSessionId: activeSessionId}, nil } func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { @@ -240,10 +259,11 @@ func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, er } func GetSessionById(ctx context.Context, id string) (*SessionType, error) { - allSessions, err := GetAllSessions(ctx) + allSessionsUpdate, err := GetAllSessions(ctx) if err != nil { return nil, err } + allSessions := allSessionsUpdate.Sessions for _, session := range allSessions { if session.SessionId == id { return session, nil @@ -283,6 +303,10 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo if err != nil { return err } + if activate { + query = `UPDATE client SET activesessionid = ?` + tx.ExecWrap(query, newSessionId) + } return nil }) if txErr != nil { @@ -677,12 +701,12 @@ func reorderStrings(strs []string, toMove string, newIndex int) []string { func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM sessions ORDER BY sessionidx, name, sessionid` + query := `SELECT sessionid FROM session ORDER BY sessionidx, name, sessionid` ids := tx.SelectStrings(query) if sessionId != "" { ids = reorderStrings(ids, sessionId, newIndex) } - query = `UPDATE sessions SET sessionid = ? WHERE sessionid = ?` + query = `UPDATE session SET sessionid = ? WHERE sessionid = ?` for idx, id := range ids { tx.ExecWrap(query, id, idx+1) } @@ -693,11 +717,11 @@ func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error func SetSessionName(ctx context.Context, sessionId string, name string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM sessions WHERE sessionid = ?` + query := `SELECT sessionid FROM session WHERE sessionid = ?` if !tx.Exists(query, sessionId) { return fmt.Errorf("session does not exist") } - query = `UPDATE sessions SET name = ? WHERE sessionid = ?` + query = `UPDATE session SET name = ? WHERE sessionid = ?` tx.ExecWrap(query, name, sessionId) return nil }) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 0afb84412..dadbb4ad0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -86,12 +86,13 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { return globalDB, globalDBErr } -type UserData struct { +type ClientData struct { UserId string `json:"userid"` UserPrivateKeyBytes []byte `json:"-"` UserPublicKeyBytes []byte `json:"-"` UserPrivateKey *ecdsa.PrivateKey UserPublicKey *ecdsa.PublicKey + ActiveSessionId string `json:"activesessionid"` } type SessionType struct { @@ -639,7 +640,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { return GetSessionByName(ctx, DefaultSessionName) } -func createUserData(tx *TxWrap) error { +func createClientData(tx *TxWrap) error { userId := uuid.New().String() curve := elliptic.P384() pkey, err := ecdsa.GenerateKey(curve, rand.Reader) @@ -654,14 +655,14 @@ func createUserData(tx *TxWrap) error { if err != nil { return fmt.Errorf("marshaling (pkix) public key bytes: %w", err) } - query := `INSERT INTO client (userid, userpublickeybytes, userprivatekeybytes) VALUES (?, ?, ?)` + query := `INSERT INTO client (userid, activesessionid, userpublickeybytes, userprivatekeybytes) VALUES (?, '', ?, ?)` tx.ExecWrap(query, userId, pubBytes, pkBytes) fmt.Printf("create new userid[%s] with public/private keypair\n", userId) return nil } -func EnsureUserData(ctx context.Context) (*UserData, error) { - var rtn UserData +func EnsureClientData(ctx context.Context) (*ClientData, error) { + var rtn ClientData err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT count(*) FROM client` count := tx.GetInt(query) @@ -669,7 +670,7 @@ func EnsureUserData(ctx context.Context) (*UserData, error) { return fmt.Errorf("invalid client database, multiple (%d) rows in client table", count) } if count == 0 { - createErr := createUserData(tx) + createErr := createClientData(tx) if createErr != nil { return createErr } From 773e881bf6edac7bd6af09b0507eaec4cec380af Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 16:24:07 -0700 Subject: [PATCH 078/397] move resolver into its own file --- pkg/cmdrunner/cmdrunner.go | 287 +----------------------------------- pkg/cmdrunner/resolver.go | 293 +++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+), 285 deletions(-) create mode 100644 pkg/cmdrunner/resolver.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d22193f14..b245f7b7e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -23,32 +23,8 @@ import ( ) const DefaultUserId = "sawka" - -const ( - R_Session = 1 - R_Screen = 2 - R_Window = 4 - R_Remote = 8 - R_SessionOpt = 16 - R_ScreenOpt = 32 - R_WindowOpt = 64 - R_RemoteOpt = 128 -) - const MaxNameLen = 50 -var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") - -type resolvedIds struct { - SessionId string - ScreenId string - WindowId string - RemotePtr sstore.RemotePtrType - RemoteState *sstore.RemoteState - RemoteDisplayName string - RState remote.RemoteState -} - var ValidCommands = []string{ "/run", "/eval", @@ -60,52 +36,9 @@ var ValidCommands = []string{ "/setenv", "/unset", "/remote:show", } - +var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") - -func resolveByPosition(ids []string, curId string, posStr string) string { - if len(ids) == 0 { - return "" - } - if !positionRe.MatchString(posStr) { - return "" - } - curIdx := 1 // if no match, curIdx will be first item - for idx, id := range ids { - if id == curId { - curIdx = idx + 1 - break - } - } - isRelative := strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") - isWrap := posStr == "+" || posStr == "-" - var pos int - if isWrap && posStr == "+" { - pos = 1 - } else if isWrap && posStr == "-" { - pos = -1 - } else { - pos, _ = strconv.Atoi(posStr) - } - if isRelative { - pos = curIdx + pos - } - if pos < 1 { - if isWrap { - pos = len(ids) - } else { - pos = 1 - } - } - if pos > len(ids) { - if isWrap { - pos = 1 - } else { - pos = len(ids) - } - } - return ids[pos-1] -} +var wsRe = regexp.MustCompile("\\s+") func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { switch SubMetaCmd(pk.MetaCmd) { @@ -171,220 +104,6 @@ func resolveBool(arg string, def bool) bool { return true } -func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string) (string, error) { - screens, err := sstore.GetSessionScreens(ctx, sessionId) - if err != nil { - return "", fmt.Errorf("could not retreive screens for session=%s", sessionId) - } - screenNum, err := strconv.Atoi(screenArg) - if err == nil { - if screenNum < 1 || screenNum > len(screens) { - return "", fmt.Errorf("could not resolve screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) - } - return screens[screenNum-1].ScreenId, nil - } - for _, screen := range screens { - if screen.ScreenId == screenArg || screen.Name == screenArg { - return screen.ScreenId, nil - } - - } - return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) -} - -func getSessionIds(sarr []*sstore.SessionType) []string { - rtn := make([]string, len(sarr)) - for idx, s := range sarr { - rtn[idx] = s.SessionId - } - return rtn -} - -var partialUUIDRe = regexp.MustCompile("^[0-9a-f]{8}$") - -func isPartialUUID(s string) bool { - return partialUUIDRe.MatchString(s) -} - -func resolveSession(ctx context.Context, sessionArg string, curSession string, bareSessions []*sstore.SessionType) (string, error) { - if bareSessions == nil { - var err error - bareSessions, err = sstore.GetBareSessions(ctx) - if err != nil { - return "", fmt.Errorf("could not retrive bare sessions") - } - } - var curSessionId string - if curSession != "" { - curSessionId, _ = resolveSession(ctx, curSession, "", bareSessions) - } - sids := getSessionIds(bareSessions) - rtnId := resolveByPosition(sids, curSessionId, sessionArg) - if rtnId != "" { - return rtnId, nil - } - tryPuid := isPartialUUID(sessionArg) - var prefixMatches []string - var lastPrefixMatchId string - for _, session := range bareSessions { - if session.SessionId == sessionArg || session.Name == sessionArg || (tryPuid && strings.HasPrefix(session.SessionId, sessionArg)) { - return session.SessionId, nil - } - if strings.HasPrefix(session.Name, sessionArg) { - prefixMatches = append(prefixMatches, session.Name) - lastPrefixMatchId = session.SessionId - } - } - if len(prefixMatches) == 1 { - return lastPrefixMatchId, nil - } - if len(prefixMatches) > 1 { - return "", fmt.Errorf("could not resolve session '%s', ambiguious prefix matched multiple sessions: %s", sessionArg, formatStrs(prefixMatches, "and", true)) - } - return "", fmt.Errorf("could not resolve sesssion '%s' (name/id/pos not found)", sessionArg) -} - -func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { - sessionId := pk.Kwargs["session"] - if sessionId == "" { - return "", nil - } - if _, err := uuid.Parse(sessionId); err != nil { - return "", fmt.Errorf("invalid sessionid '%s'", sessionId) - } - return sessionId, nil -} - -func resolveWindowId(pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { - windowId := pk.Kwargs["window"] - if windowId == "" { - return "", nil - } - if _, err := uuid.Parse(windowId); err != nil { - return "", fmt.Errorf("invalid windowid '%s'", windowId) - } - return windowId, nil -} - -func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { - screenArg := pk.Kwargs["screen"] - if screenArg == "" { - return "", nil - } - if _, err := uuid.Parse(screenArg); err == nil { - return screenArg, nil - } - if sessionId == "" { - return "", fmt.Errorf("cannot resolve screen without session") - } - return resolveSessionScreen(ctx, sessionId, screenArg) -} - -// returns (remoteuserref, remoteref, name, error) -func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { - if strings.HasPrefix(fullRemoteRef, "[") && strings.HasSuffix(fullRemoteRef, "]") { - fullRemoteRef = fullRemoteRef[1 : len(fullRemoteRef)-1] - } - fields := strings.Split(fullRemoteRef, ":") - if len(fields) > 3 { - return "", "", "", fmt.Errorf("invalid remote format '%s'", fullRemoteRef) - } - if len(fields) == 1 { - return "", fields[0], "", nil - } - if len(fields) == 2 { - if strings.HasPrefix(fields[0], "@") { - return fields[0], fields[1], "", nil - } - return "", fields[0], fields[1], nil - } - return fields[0], fields[1], fields[2], nil -} - -// returns (remoteDisplayName, remoteptr, state, rstate, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteState, error) { - if fullRemoteRef == "" { - return "", nil, nil, nil, nil - } - userRef, remoteRef, remoteName, err := parseFullRemoteRef(fullRemoteRef) - if err != nil { - return "", nil, nil, nil, err - } - if userRef != "" { - return "", nil, nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) - } - rstate := remote.ResolveRemoteRef(remoteRef) - if rstate == nil { - return "", nil, nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) - } - rptr := sstore.RemotePtrType{RemoteId: rstate.RemoteId, Name: remoteName} - state, err := sstore.GetRemoteState(ctx, sessionId, windowId, rptr) - if err != nil { - return "", nil, nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) - } - rname := rstate.RemoteCanonicalName - if rstate.RemoteAlias != "" { - rname = rstate.RemoteAlias - } - if rptr.Name != "" { - rname = fmt.Sprintf("%s:%s", rname, rptr.Name) - } - if state == nil { - return rname, &rptr, rstate.DefaultState, rstate, nil - } - return rname, &rptr, state, rstate, nil -} - -func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { - rtn := resolvedIds{} - if rtype == 0 { - return rtn, nil - } - var err error - if (rtype&R_Session)+(rtype&R_SessionOpt) > 0 { - rtn.SessionId, err = resolveSessionId(pk) - if err != nil { - return rtn, err - } - if rtn.SessionId == "" && (rtype&R_Session) > 0 { - return rtn, fmt.Errorf("no session") - } - } - if (rtype&R_Window)+(rtype&R_WindowOpt) > 0 { - rtn.WindowId, err = resolveWindowId(pk, rtn.SessionId) - if err != nil { - return rtn, err - } - if rtn.WindowId == "" && (rtype&R_Window) > 0 { - return rtn, fmt.Errorf("no window") - } - - } - if (rtype&R_Screen)+(rtype&R_ScreenOpt) > 0 { - rtn.ScreenId, err = resolveScreenId(ctx, pk, rtn.SessionId) - if err != nil { - return rtn, err - } - if rtn.ScreenId == "" && (rtype&R_Screen) > 0 { - return rtn, fmt.Errorf("no screen") - } - } - if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rname, rptr, state, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) - if err != nil { - return rtn, err - } - if rptr == nil && (rtype&R_Remote) > 0 { - return rtn, fmt.Errorf("no remote") - } - rtn.RemoteDisplayName = rname - rtn.RemotePtr = *rptr - rtn.RemoteState = state - rtn.RState = *rstate - } - return rtn, nil -} - func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -859,8 +578,6 @@ func longestPrefix(root string, comps []string) string { return lcp } -var wsRe = regexp.MustCompile("\\s+") - func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string, forDisplay bool) ([]string, bool, error) { comps, hasMore, err := doCompGen(ctx, ids, prefix, "file", forDisplay) if err != nil { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go new file mode 100644 index 000000000..1e85d1be2 --- /dev/null +++ b/pkg/cmdrunner/resolver.go @@ -0,0 +1,293 @@ +package cmdrunner + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +const ( + R_Session = 1 + R_Screen = 2 + R_Window = 4 + R_Remote = 8 + R_SessionOpt = 16 + R_ScreenOpt = 32 + R_WindowOpt = 64 + R_RemoteOpt = 128 +) + +type resolvedIds struct { + SessionId string + ScreenId string + WindowId string + RemotePtr sstore.RemotePtrType + RemoteState *sstore.RemoteState + RemoteDisplayName string + RState remote.RemoteState +} + +func resolveByPosition(ids []string, curId string, posStr string) string { + if len(ids) == 0 { + return "" + } + if !positionRe.MatchString(posStr) { + return "" + } + curIdx := 1 // if no match, curIdx will be first item + for idx, id := range ids { + if id == curId { + curIdx = idx + 1 + break + } + } + isRelative := strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") + isWrap := posStr == "+" || posStr == "-" + var pos int + if isWrap && posStr == "+" { + pos = 1 + } else if isWrap && posStr == "-" { + pos = -1 + } else { + pos, _ = strconv.Atoi(posStr) + } + if isRelative { + pos = curIdx + pos + } + if pos < 1 { + if isWrap { + pos = len(ids) + } else { + pos = 1 + } + } + if pos > len(ids) { + if isWrap { + pos = 1 + } else { + pos = len(ids) + } + } + return ids[pos-1] +} + +func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { + rtn := resolvedIds{} + if rtype == 0 { + return rtn, nil + } + var err error + if (rtype&R_Session)+(rtype&R_SessionOpt) > 0 { + rtn.SessionId, err = resolveSessionId(pk) + if err != nil { + return rtn, err + } + if rtn.SessionId == "" && (rtype&R_Session) > 0 { + return rtn, fmt.Errorf("no session") + } + } + if (rtype&R_Window)+(rtype&R_WindowOpt) > 0 { + rtn.WindowId, err = resolveWindowId(pk, rtn.SessionId) + if err != nil { + return rtn, err + } + if rtn.WindowId == "" && (rtype&R_Window) > 0 { + return rtn, fmt.Errorf("no window") + } + + } + if (rtype&R_Screen)+(rtype&R_ScreenOpt) > 0 { + rtn.ScreenId, err = resolveScreenId(ctx, pk, rtn.SessionId) + if err != nil { + return rtn, err + } + if rtn.ScreenId == "" && (rtype&R_Screen) > 0 { + return rtn, fmt.Errorf("no screen") + } + } + if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { + rname, rptr, state, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) + if err != nil { + return rtn, err + } + if rptr == nil && (rtype&R_Remote) > 0 { + return rtn, fmt.Errorf("no remote") + } + rtn.RemoteDisplayName = rname + rtn.RemotePtr = *rptr + rtn.RemoteState = state + rtn.RState = *rstate + } + return rtn, nil +} + +func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string) (string, error) { + screens, err := sstore.GetSessionScreens(ctx, sessionId) + if err != nil { + return "", fmt.Errorf("could not retreive screens for session=%s", sessionId) + } + screenNum, err := strconv.Atoi(screenArg) + if err == nil { + if screenNum < 1 || screenNum > len(screens) { + return "", fmt.Errorf("could not resolve screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) + } + return screens[screenNum-1].ScreenId, nil + } + for _, screen := range screens { + if screen.ScreenId == screenArg || screen.Name == screenArg { + return screen.ScreenId, nil + } + + } + return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) +} + +func getSessionIds(sarr []*sstore.SessionType) []string { + rtn := make([]string, len(sarr)) + for idx, s := range sarr { + rtn[idx] = s.SessionId + } + return rtn +} + +var partialUUIDRe = regexp.MustCompile("^[0-9a-f]{8}$") + +func isPartialUUID(s string) bool { + return partialUUIDRe.MatchString(s) +} + +func resolveSession(ctx context.Context, sessionArg string, curSession string, bareSessions []*sstore.SessionType) (string, error) { + if bareSessions == nil { + var err error + bareSessions, err = sstore.GetBareSessions(ctx) + if err != nil { + return "", fmt.Errorf("could not retrive bare sessions") + } + } + var curSessionId string + if curSession != "" { + curSessionId, _ = resolveSession(ctx, curSession, "", bareSessions) + } + sids := getSessionIds(bareSessions) + rtnId := resolveByPosition(sids, curSessionId, sessionArg) + if rtnId != "" { + return rtnId, nil + } + tryPuid := isPartialUUID(sessionArg) + var prefixMatches []string + var lastPrefixMatchId string + for _, session := range bareSessions { + if session.SessionId == sessionArg || session.Name == sessionArg || (tryPuid && strings.HasPrefix(session.SessionId, sessionArg)) { + return session.SessionId, nil + } + if strings.HasPrefix(session.Name, sessionArg) { + prefixMatches = append(prefixMatches, session.Name) + lastPrefixMatchId = session.SessionId + } + } + if len(prefixMatches) == 1 { + return lastPrefixMatchId, nil + } + if len(prefixMatches) > 1 { + return "", fmt.Errorf("could not resolve session '%s', ambiguious prefix matched multiple sessions: %s", sessionArg, formatStrs(prefixMatches, "and", true)) + } + return "", fmt.Errorf("could not resolve sesssion '%s' (name/id/pos not found)", sessionArg) +} + +func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { + sessionId := pk.Kwargs["session"] + if sessionId == "" { + return "", nil + } + if _, err := uuid.Parse(sessionId); err != nil { + return "", fmt.Errorf("invalid sessionid '%s'", sessionId) + } + return sessionId, nil +} + +func resolveWindowId(pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { + windowId := pk.Kwargs["window"] + if windowId == "" { + return "", nil + } + if _, err := uuid.Parse(windowId); err != nil { + return "", fmt.Errorf("invalid windowid '%s'", windowId) + } + return windowId, nil +} + +func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { + screenArg := pk.Kwargs["screen"] + if screenArg == "" { + return "", nil + } + if _, err := uuid.Parse(screenArg); err == nil { + return screenArg, nil + } + if sessionId == "" { + return "", fmt.Errorf("cannot resolve screen without session") + } + return resolveSessionScreen(ctx, sessionId, screenArg) +} + +// returns (remoteuserref, remoteref, name, error) +func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { + if strings.HasPrefix(fullRemoteRef, "[") && strings.HasSuffix(fullRemoteRef, "]") { + fullRemoteRef = fullRemoteRef[1 : len(fullRemoteRef)-1] + } + fields := strings.Split(fullRemoteRef, ":") + if len(fields) > 3 { + return "", "", "", fmt.Errorf("invalid remote format '%s'", fullRemoteRef) + } + if len(fields) == 1 { + return "", fields[0], "", nil + } + if len(fields) == 2 { + if strings.HasPrefix(fields[0], "@") { + return fields[0], fields[1], "", nil + } + return "", fields[0], fields[1], nil + } + return fields[0], fields[1], fields[2], nil +} + +// returns (remoteDisplayName, remoteptr, state, rstate, err) +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteState, error) { + if fullRemoteRef == "" { + return "", nil, nil, nil, nil + } + userRef, remoteRef, remoteName, err := parseFullRemoteRef(fullRemoteRef) + if err != nil { + return "", nil, nil, nil, err + } + if userRef != "" { + return "", nil, nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) + } + rstate := remote.ResolveRemoteRef(remoteRef) + if rstate == nil { + return "", nil, nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) + } + rptr := sstore.RemotePtrType{RemoteId: rstate.RemoteId, Name: remoteName} + state, err := sstore.GetRemoteState(ctx, sessionId, windowId, rptr) + if err != nil { + return "", nil, nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) + } + rname := rstate.RemoteCanonicalName + if rstate.RemoteAlias != "" { + rname = rstate.RemoteAlias + } + if rptr.Name != "" { + rname = fmt.Sprintf("%s:%s", rname, rptr.Name) + } + if state == nil { + return rname, &rptr, rstate.DefaultState, rstate, nil + } + return rname, &rptr, state, rstate, nil +} From ca29e28c85643d7d012ac3f2ac011b538743e944 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 17:17:33 -0700 Subject: [PATCH 079/397] register handlers instead of a switch statement. make resolve more generic --- pkg/cmdrunner/cmdrunner.go | 365 +++++++++++++++++++------------------ pkg/cmdrunner/resolver.go | 92 ++++++---- pkg/sstore/dbops.go | 12 +- 3 files changed, 258 insertions(+), 211 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b245f7b7e..4a4998646 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -25,59 +25,77 @@ import ( const DefaultUserId = "sawka" const MaxNameLen = 50 -var ValidCommands = []string{ - "/run", - "/eval", - "/screen", "/screen:open", "/screen:close", - "/session", "/session:open", "/session:close", - "/comment", - "/cd", - "/compgen", - "/setenv", "/unset", - "/remote:show", -} var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") var wsRe = regexp.MustCompile("\\s+") -func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - switch SubMetaCmd(pk.MetaCmd) { - case "run": - return RunCommand(ctx, pk) +type MetaCmdFnType = func(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) +type MetaCmdEntryType struct { + IsAlias bool + Fn MetaCmdFnType +} - case "eval": - return EvalCommand(ctx, pk) +var MetaCmdFnMap = make(map[string]MetaCmdEntryType) - case "screen": - return ScreenCommand(ctx, pk) +func init() { + registerCmdFn("run", RunCommand) + registerCmdFn("eval", EvalCommand) + registerCmdFn("comment", CommentCommand) + registerCmdFn("cd", CdCommand) + registerCmdFn("cr", CrCommand) + registerCmdFn("compgen", CompGenCommand) + registerCmdFn("setenv", SetEnvCommand) + registerCmdFn("unset", UnSetCommand) - case "session": - return SessionCommand(ctx, pk) + registerCmdFn("session", SessionCommand) + registerCmdFn("session:open", SessionOpenCommand) + registerCmdAlias("session:new", SessionOpenCommand) + registerCmdFn("session:set", SessionSetCommand) - case "comment": - return CommentCommand(ctx, pk) + registerCmdFn("screen", ScreenCommand) + registerCmdFn("screen:close", ScreenCloseCommand) + registerCmdFn("screen:open", ScreenOpenCommand) + registerCmdAlias("screen:new", ScreenOpenCommand) - case "cd": - return CdCommand(ctx, pk) + registerCmdAlias("remote", RemoteCommand) + registerCmdFn("remote:show", RemoteShowCommand) +} - case "cr": - return CrCommand(ctx, pk) - - case "compgen": - return CompGenCommand(ctx, pk) - - case "setenv": - return SetEnvCommand(ctx, pk) - - case "unset": - return UnSetCommand(ctx, pk) - - case "remote": - return RemoteCommand(ctx, pk) - - default: - return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) +func getValidCommands() []string { + var rtn []string + for key, val := range MetaCmdFnMap { + if val.IsAlias { + continue + } + rtn = append(rtn, key) } + return rtn +} + +func registerCmdFn(cmdName string, fn MetaCmdFnType) { + MetaCmdFnMap[cmdName] = MetaCmdEntryType{Fn: fn} +} + +func registerCmdAlias(cmdName string, fn MetaCmdFnType) { + MetaCmdFnMap[cmdName] = MetaCmdEntryType{IsAlias: true, Fn: fn} +} + +func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + metaCmd := SubMetaCmd(pk.MetaCmd) + var cmdName string + if pk.MetaSubCmd == "" { + cmdName = metaCmd + } else { + cmdName = fmt.Sprintf("%s:%s", pk.MetaCmd, pk.MetaSubCmd) + } + entry := MetaCmdFnMap[cmdName] + if entry.Fn == nil { + if MetaCmdFnMap[metaCmd].Fn != nil { + return nil, fmt.Errorf("invalid /%s subcommand '%s'", metaCmd, pk.MetaSubCmd) + } + return nil, fmt.Errorf("invalid command '/%s', no handler", cmdName) + } + return entry.Fn(ctx, pk) } func firstArg(pk *scpacket.FeCommandPacketType) string { @@ -182,33 +200,39 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return update, err } +func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) + } + update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) + if err != nil { + return nil, err + } + return update, nil +} + +func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session) + if err != nil { + return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) + } + activate := resolveBool(pk.Kwargs["activate"], true) + newName := pk.Kwargs["name"] + if newName != "" { + err := validateName(newName, "screen") + if err != nil { + return nil, err + } + } + update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, activate) + if err != nil { + return nil, err + } + return update, nil +} + func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if pk.MetaSubCmd == "close" { - ids, err := resolveIds(ctx, pk, R_Session|R_Screen) - if err != nil { - return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) - } - update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) - if err != nil { - return nil, err - } - return update, nil - } - if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { - ids, err := resolveIds(ctx, pk, R_Session) - if err != nil { - return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) - } - activate := resolveBool(pk.Kwargs["activate"], true) - update, err := sstore.InsertScreen(ctx, ids.SessionId, pk.Kwargs["name"], activate) - if err != nil { - return nil, err - } - return update, nil - } - if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /screen subcommand '%s'", pk.MetaSubCmd) - } ids, err := resolveIds(ctx, pk, R_Session) if err != nil { return nil, fmt.Errorf("/screen cannot switch to screen: %w", err) @@ -229,9 +253,6 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /unset subcommand '%s'", pk.MetaSubCmd) - } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { return nil, err @@ -261,61 +282,48 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore update := sstore.ModelUpdate{ Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteDisplayName, makeSetVarsStr(unsetVars)), + InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteDisplayName, formatStrs(mapToStrs(unsetVars), "and", false)), TimeoutMs: 2000, }, } return update, nil } -func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if pk.MetaSubCmd == "show" { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) - if err != nil { - return nil, err - } - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("invalid remote '%s' (not found)", ids.RemoteDisplayName) - } - state := curRemote.GetRemoteState() - var buf bytes.Buffer - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", state.RemoteType)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remoteid", state.RemoteId)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "physicalid", state.PhysicalId)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "alias", state.RemoteAlias)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) - if ids.RemoteState != nil { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) - } - output := buf.String() - return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), - InfoLines: splitLinesForInfo(output), - }, - }, nil +func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err } - if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /remote subcommand: '%s'", pk.MetaSubCmd) + curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) + if curRemote == nil { + return nil, fmt.Errorf("invalid remote '%s' (not found)", ids.RemoteDisplayName) } - return nil, fmt.Errorf("/remote requires a subcommand: 'show'") + state := curRemote.GetRemoteState() + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", state.RemoteType)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remoteid", state.RemoteId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "physicalid", state.PhysicalId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "alias", state.RemoteAlias)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) + if ids.RemoteState != nil { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) + } + output := buf.String() + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), + InfoLines: splitLinesForInfo(output), + }, + }, nil } -func makeSetVarsStr(setVars map[string]bool) string { - varArr := make([]string, 0, len(setVars)) - for varName, _ := range setVars { - varArr = append(varArr, varName) - } - return strings.Join(varArr, ", ") +func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, fmt.Errorf("/remote requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /setenv subcommand '%s'", pk.MetaSubCmd) - } ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { return nil, err @@ -361,7 +369,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor update := sstore.ModelUpdate{ Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteDisplayName, makeSetVarsStr(setVars)), + InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteDisplayName, formatStrs(mapToStrs(setVars), "and", false)), TimeoutMs: 2000, }, } @@ -583,7 +591,8 @@ func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string, forDispl if err != nil { return nil, false, err } - for _, cmd := range ValidCommands { + validCommands := getValidCommands() + for _, cmd := range validCommands { if strings.HasPrefix(cmd, prefix) { if forDisplay { comps = append(comps, "^"+cmd) @@ -694,6 +703,16 @@ func maybeQuote(s string, quote bool) string { return s } +func mapToStrs(m map[string]bool) []string { + var rtn []string + for key, val := range m { + if val { + rtn = append(rtn, key) + } + } + return rtn +} + func formatStrs(strs []string, conj string, quote bool) string { if len(strs) == 0 { return "(none)" @@ -725,82 +744,82 @@ func validateName(name string, typeStr string) error { return nil } -func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if pk.MetaSubCmd == "open" || pk.MetaSubCmd == "new" { - activate := resolveBool(pk.Kwargs["activate"], true) - newName := pk.Kwargs["name"] - if newName != "" { - err := validateName(newName, "session") - if err != nil { - return nil, err - } - } - update, err := sstore.InsertSessionWithName(ctx, newName, activate) +func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + activate := resolveBool(pk.Kwargs["activate"], true) + newName := pk.Kwargs["name"] + if newName != "" { + err := validateName(newName, "session") if err != nil { return nil, err } - return update, nil } - if pk.MetaSubCmd == "set" { - ids, err := resolveIds(ctx, pk, R_Session) - if err != nil { - return nil, err - } - bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) - if err != nil { - return nil, err - } - if bareSession == nil { - return nil, fmt.Errorf("session '%s' not found", ids.SessionId) - } - var varsUpdated []string - if pk.Kwargs["name"] != "" { - newName := pk.Kwargs["name"] - err = validateName(newName, "session") - if err != nil { - return nil, err - } - err = sstore.SetSessionName(ctx, ids.SessionId, newName) - if err != nil { - return nil, fmt.Errorf("setting session name: %v", err) - } - varsUpdated = append(varsUpdated, "name") - } - if pk.Kwargs["pos"] != "" { + update, err := sstore.InsertSessionWithName(ctx, newName, activate) + if err != nil { + return nil, err + } + return update, nil +} - } - if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) - } - bareSession, err = sstore.GetBareSessionById(ctx, ids.SessionId) - update := sstore.ModelUpdate{ - Sessions: []*sstore.SessionType{bareSession}, - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s]: updated %s", bareSession.Name, formatStrs(varsUpdated, "and", false)), - TimeoutMs: 2000, - }, - } - return update, nil +func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session) + if err != nil { + return nil, err } - if pk.MetaSubCmd != "" { - return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) + bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) + if err != nil { + return nil, err } + if bareSession == nil { + return nil, fmt.Errorf("session '%s' not found", ids.SessionId) + } + var varsUpdated []string + if pk.Kwargs["name"] != "" { + newName := pk.Kwargs["name"] + err = validateName(newName, "session") + if err != nil { + return nil, err + } + err = sstore.SetSessionName(ctx, ids.SessionId, newName) + if err != nil { + return nil, fmt.Errorf("setting session name: %v", err) + } + varsUpdated = append(varsUpdated, "name") + } + if pk.Kwargs["pos"] != "" { + + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) + } + bareSession, err = sstore.GetBareSessionById(ctx, ids.SessionId) + update := sstore.ModelUpdate{ + Sessions: []*sstore.SessionType{bareSession}, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s]: session updated %s", bareSession.Name, formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, + } + return update, nil +} + +func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { firstArg := firstArg(pk) if firstArg == "" { return nil, fmt.Errorf("usage /session [name|id|pos], no param specified") } - sessionId, err := resolveSession(ctx, firstArg, pk.Kwargs["session"], nil) + bareSessions, err := sstore.GetBareSessions(ctx) if err != nil { return nil, err } - bareSession, err := sstore.GetSessionById(ctx, sessionId) + ritems := sessionsToResolveItems(bareSessions) + ritem, err := genericResolve(firstArg, pk.Kwargs["session"], ritems, "session") if err != nil { - return nil, fmt.Errorf("could not find session '%s': %v", sessionId, err) + return nil, err } update := sstore.ModelUpdate{ - ActiveSessionId: sessionId, + ActiveSessionId: ritem.Id, Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("switched to session %q", bareSession.Name), + InfoMsg: fmt.Sprintf("switched to session %q", ritem.Name), TimeoutMs: 2000, }, } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 1e85d1be2..5aaa8e66a 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -34,16 +34,43 @@ type resolvedIds struct { RState remote.RemoteState } -func resolveByPosition(ids []string, curId string, posStr string) string { - if len(ids) == 0 { - return "" +type ResolveItem struct { + Name string + Id string +} + +func itemNames(items []ResolveItem) []string { + if len(items) == 0 { + return nil + } + rtn := make([]string, len(items)) + for idx, item := range items { + rtn[idx] = item.Name + } + return rtn +} + +func sessionsToResolveItems(sessions []*sstore.SessionType) []ResolveItem { + if len(sessions) == 0 { + return nil + } + rtn := make([]ResolveItem, len(sessions)) + for idx, session := range sessions { + rtn[idx] = ResolveItem{Name: session.Name, Id: session.SessionId} + } + return rtn +} + +func resolveByPosition(items []ResolveItem, curId string, posStr string) *ResolveItem { + if len(items) == 0 { + return nil } if !positionRe.MatchString(posStr) { - return "" + return nil } curIdx := 1 // if no match, curIdx will be first item - for idx, id := range ids { - if id == curId { + for idx, item := range items { + if item.Id == curId { curIdx = idx + 1 break } @@ -63,19 +90,19 @@ func resolveByPosition(ids []string, curId string, posStr string) string { } if pos < 1 { if isWrap { - pos = len(ids) + pos = len(items) } else { pos = 1 } } - if pos > len(ids) { + if pos > len(items) { if isWrap { pos = 1 } else { - pos = len(ids) + pos = len(items) } } - return ids[pos-1] + return &items[pos-1] } func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { @@ -163,42 +190,35 @@ func isPartialUUID(s string) bool { return partialUUIDRe.MatchString(s) } -func resolveSession(ctx context.Context, sessionArg string, curSession string, bareSessions []*sstore.SessionType) (string, error) { - if bareSessions == nil { - var err error - bareSessions, err = sstore.GetBareSessions(ctx) - if err != nil { - return "", fmt.Errorf("could not retrive bare sessions") +func genericResolve(arg string, curArg string, items []ResolveItem, typeStr string) (*ResolveItem, error) { + var curId string + if curArg != "" { + curItem, _ := genericResolve(curArg, "", items, typeStr) + if curItem != nil { + curId = curItem.Id } } - var curSessionId string - if curSession != "" { - curSessionId, _ = resolveSession(ctx, curSession, "", bareSessions) + rtnItem := resolveByPosition(items, curId, arg) + if rtnItem != nil { + return rtnItem, nil } - sids := getSessionIds(bareSessions) - rtnId := resolveByPosition(sids, curSessionId, sessionArg) - if rtnId != "" { - return rtnId, nil - } - tryPuid := isPartialUUID(sessionArg) - var prefixMatches []string - var lastPrefixMatchId string - for _, session := range bareSessions { - if session.SessionId == sessionArg || session.Name == sessionArg || (tryPuid && strings.HasPrefix(session.SessionId, sessionArg)) { - return session.SessionId, nil + tryPuid := isPartialUUID(arg) + var prefixMatches []ResolveItem + for _, item := range items { + if item.Id == arg || item.Name == arg || (tryPuid && strings.HasPrefix(item.Id, arg)) { + return &item, nil } - if strings.HasPrefix(session.Name, sessionArg) { - prefixMatches = append(prefixMatches, session.Name) - lastPrefixMatchId = session.SessionId + if strings.HasPrefix(item.Name, arg) { + prefixMatches = append(prefixMatches, item) } } if len(prefixMatches) == 1 { - return lastPrefixMatchId, nil + return &prefixMatches[0], nil } if len(prefixMatches) > 1 { - return "", fmt.Errorf("could not resolve session '%s', ambiguious prefix matched multiple sessions: %s", sessionArg, formatStrs(prefixMatches, "and", true)) + return nil, fmt.Errorf("could not resolve %s '%s', ambiguious prefix matched multiple %ss: %s", typeStr, arg, typeStr, formatStrs(itemNames(prefixMatches), "and", true)) } - return "", fmt.Errorf("could not resolve sesssion '%s' (name/id/pos not found)", sessionArg) + return nil, fmt.Errorf("could not resolve %s '%s' (name/id/pos not found)", typeStr, arg) } func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a0f30dc60..d8b6aca33 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -358,7 +358,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin } } -func InsertScreen(ctx context.Context, sessionId string, screenName string, activate bool) (UpdatePacket, error) { +func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (UpdatePacket, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid FROM session WHERE sessionid = ?` @@ -372,7 +372,7 @@ func InsertScreen(ctx context.Context, sessionId string, screenName string, acti newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) - screenName = fmtUniqueName(screenName, "s%d", maxScreenIdx+1, screenNames) + screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = uuid.New().String() query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) @@ -721,6 +721,14 @@ func SetSessionName(ctx context.Context, sessionId string, name string) error { if !tx.Exists(query, sessionId) { return fmt.Errorf("session does not exist") } + query = `SELECT sessionid FROM session WHERE name = ?` + dupSessionId := tx.GetString(query, name) + if dupSessionId == sessionId { + return nil + } + if tx.Exists(query, name) { + return fmt.Errorf("invalid duplicate session name '%s'", name) + } query = `UPDATE session SET name = ? WHERE sessionid = ?` tx.ExecWrap(query, name, sessionId) return nil From 9b6d4e928dc42fa940603c022972b8ec0fc46db9 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 17:29:32 -0700 Subject: [PATCH 080/397] use generic resolver for screens --- pkg/cmdrunner/cmdrunner.go | 4 ++-- pkg/cmdrunner/resolver.go | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 4a4998646..68b0b63da 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -241,11 +241,11 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if firstArg == "" { return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") } - screenIdArg, err := resolveSessionScreen(ctx, ids.SessionId, firstArg) + ritem, err := resolveSessionScreen(ctx, ids.SessionId, firstArg, pk.Kwargs["screen"]) if err != nil { return nil, err } - update, err := sstore.SwitchScreenById(ctx, ids.SessionId, screenIdArg) + update, err := sstore.SwitchScreenById(ctx, ids.SessionId, ritem.Id) if err != nil { return nil, err } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 5aaa8e66a..845140dfe 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -61,6 +61,17 @@ func sessionsToResolveItems(sessions []*sstore.SessionType) []ResolveItem { return rtn } +func screensToResolveItems(screens []*sstore.ScreenType) []ResolveItem { + if len(screens) == 0 { + return nil + } + rtn := make([]ResolveItem, len(screens)) + for idx, screen := range screens { + rtn[idx] = ResolveItem{Name: screen.Name, Id: screen.ScreenId} + } + return rtn +} + func resolveByPosition(items []ResolveItem, curId string, posStr string) *ResolveItem { if len(items) == 0 { return nil @@ -155,25 +166,13 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int return rtn, nil } -func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string) (string, error) { +func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string, curScreenArg string) (*ResolveItem, error) { screens, err := sstore.GetSessionScreens(ctx, sessionId) if err != nil { - return "", fmt.Errorf("could not retreive screens for session=%s", sessionId) + return nil, fmt.Errorf("could not retreive screens for session=%s", sessionId) } - screenNum, err := strconv.Atoi(screenArg) - if err == nil { - if screenNum < 1 || screenNum > len(screens) { - return "", fmt.Errorf("could not resolve screen #%d (out of range), valid screens 1-%d", screenNum, len(screens)) - } - return screens[screenNum-1].ScreenId, nil - } - for _, screen := range screens { - if screen.ScreenId == screenArg || screen.Name == screenArg { - return screen.ScreenId, nil - } - - } - return "", fmt.Errorf("could not resolve screen '%s' (name/id not found)", screenArg) + ritems := screensToResolveItems(screens) + return genericResolve(screenArg, curScreenArg, ritems, "screen") } func getSessionIds(sarr []*sstore.SessionType) []string { @@ -254,7 +253,11 @@ func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sess if sessionId == "" { return "", fmt.Errorf("cannot resolve screen without session") } - return resolveSessionScreen(ctx, sessionId, screenArg) + ritem, err := resolveSessionScreen(ctx, sessionId, screenArg, "") + if err != nil { + return "", err + } + return ritem.Id, nil } // returns (remoteuserref, remoteref, name, error) From 1997b9ea44360a6ecf1860d927898cadbeaa1ea9 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 17:51:28 -0700 Subject: [PATCH 081/397] implement screen:set name --- pkg/cmdrunner/cmdrunner.go | 46 ++++++++++++++++++++++++++++++-------- pkg/sstore/dbops.go | 23 ++++++++++++++++++- pkg/sstore/sstore.go | 2 +- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 68b0b63da..b555b2e4f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -56,6 +56,7 @@ func init() { registerCmdFn("screen:close", ScreenCloseCommand) registerCmdFn("screen:open", ScreenOpenCommand) registerCmdAlias("screen:new", ScreenOpenCommand) + registerCmdFn("screen:set", ScreenSetCommand) registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) @@ -232,6 +233,40 @@ func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return update, nil } +func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, err + } + var varsUpdated []string + if pk.Kwargs["name"] != "" { + newName := pk.Kwargs["name"] + err = validateName(newName, "screen") + if err != nil { + return nil, err + } + err = sstore.SetScreenName(ctx, ids.SessionId, ids.ScreenId, newName) + if err != nil { + return nil, fmt.Errorf("setting screen name: %v", err) + } + varsUpdated = append(varsUpdated, "name") + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) + } + screenObj, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) + if err != nil { + return nil, err + } + update, session := sstore.MakeSingleSessionUpdate(ids.SessionId) + session.Screens = append(session.Screens, screenObj) + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + } + return update, nil +} + func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session) if err != nil { @@ -765,13 +800,6 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if err != nil { return nil, err } - bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) - if err != nil { - return nil, err - } - if bareSession == nil { - return nil, fmt.Errorf("session '%s' not found", ids.SessionId) - } var varsUpdated []string if pk.Kwargs["name"] != "" { newName := pk.Kwargs["name"] @@ -791,11 +819,11 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if len(varsUpdated) == 0 { return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) } - bareSession, err = sstore.GetBareSessionById(ctx, ids.SessionId) + bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) update := sstore.ModelUpdate{ Sessions: []*sstore.SessionType{bareSession}, Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s]: session updated %s", bareSession.Name, formatStrs(varsUpdated, "and", false)), + InfoMsg: fmt.Sprintf("session updated %s", formatStrs(varsUpdated, "and", false)), TimeoutMs: 2000, }, } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d8b6aca33..4ea5bfa47 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -726,7 +726,7 @@ func SetSessionName(ctx context.Context, sessionId string, name string) error { if dupSessionId == sessionId { return nil } - if tx.Exists(query, name) { + if dupSessionId != "" { return fmt.Errorf("invalid duplicate session name '%s'", name) } query = `UPDATE session SET name = ? WHERE sessionid = ?` @@ -735,3 +735,24 @@ func SetSessionName(ctx context.Context, sessionId string, name string) error { }) return txErr } + +func SetScreenName(ctx context.Context, sessionId string, screenId string, name string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + if !tx.Exists(query, sessionId, screenId) { + return fmt.Errorf("screen does not exist") + } + query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ?` + dupScreenId := tx.GetString(query, sessionId, name) + if dupScreenId == screenId { + return nil + } + if dupScreenId != "" { + return fmt.Errorf("invalid duplicate screen name '%s'", name) + } + query = `UPDATE screen SET name = ? WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, name, sessionId, screenId) + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index dadbb4ad0..0eea99bdc 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -224,7 +224,7 @@ type ScreenType struct { ScreenIdx int64 `json:"screenidx"` Name string `json:"name"` ActiveWindowId string `json:"activewindowid"` - ScreenOpts ScreenOptsType `json:"screenopts"` + ScreenOpts *ScreenOptsType `json:"screenopts"` OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` Windows []*ScreenWindowType `json:"windows"` From aff174fa80a18c1598030f955a9d46daed91b345 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 21:44:18 -0700 Subject: [PATCH 082/397] tab colors --- pkg/cmdrunner/cmdrunner.go | 34 +++++++++++++++++++++++++++++++++- pkg/sstore/dbops.go | 16 ++++++++++++++++ pkg/sstore/sstore.go | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b555b2e4f..718c20e5d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -25,6 +25,8 @@ import ( const DefaultUserId = "sawka" const MaxNameLen = 50 +var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} + var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") var wsRe = regexp.MustCompile("\\s+") @@ -251,8 +253,29 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } varsUpdated = append(varsUpdated, "name") } + if pk.Kwargs["tabcolor"] != "" { + color := pk.Kwargs["tabcolor"] + err = validateColor(color, "screen tabcolor") + if err != nil { + return nil, err + } + screenObj, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) + if err != nil { + return nil, err + } + opts := screenObj.ScreenOpts + if opts == nil { + opts = &sstore.ScreenOptsType{} + } + opts.TabColor = color + err = sstore.SetScreenOpts(ctx, ids.SessionId, ids.ScreenId, opts) + if err != nil { + return nil, fmt.Errorf("setting screen opts: %v", err) + } + varsUpdated = append(varsUpdated, "tabcolor") + } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) + return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor"}, "or", false)) } screenObj, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) if err != nil { @@ -779,6 +802,15 @@ func validateName(name string, typeStr string) error { return nil } +func validateColor(color string, typeStr string) error { + for _, c := range ColorNames { + if color == c { + return nil + } + } + return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false)) +} + func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { activate := resolveBool(pk.Kwargs["activate"], true) newName := pk.Kwargs["name"] diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 4ea5bfa47..f5f55c1f4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -756,3 +756,19 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name }) return txErr } + +func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts *ScreenOptsType) error { + if opts == nil { + return fmt.Errorf("invalid screen opts cannot be nil") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + if !tx.Exists(query, sessionId, screenId) { + return fmt.Errorf("screen does not exist") + } + query = `UPDATE screen SET screenopts = ? WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, opts, sessionId, screenId) + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 0eea99bdc..075cf85ff 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -207,7 +207,7 @@ func WindowFromMap(m map[string]interface{}) *WindowType { } type ScreenOptsType struct { - TabColor string `json:"tabcolor"` + TabColor string `json:"tabcolor,omitempty"` } func (opts *ScreenOptsType) Scan(val interface{}) error { From 9d6cc1f67a3ec16bcc9f081cb1a7b75c43a44189 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 26 Aug 2022 22:01:29 -0700 Subject: [PATCH 083/397] clear window --- pkg/cmdrunner/cmdrunner.go | 17 +++++++++++++++++ pkg/cmdrunner/shparse.go | 1 + pkg/sstore/dbops.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 718c20e5d..352bd59ac 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -48,6 +48,7 @@ func init() { registerCmdFn("compgen", CompGenCommand) registerCmdFn("setenv", SetEnvCommand) registerCmdFn("unset", UnSetCommand) + registerCmdFn("clear", ClearCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -886,6 +887,22 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return update, nil } +func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + update, err := sstore.ClearWindow(ctx, ids.SessionId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("clearing window: %v", err) + } + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("window cleared"), + TimeoutMs: 2000, + } + return update, nil +} + func splitLinesForInfo(str string) []string { rtn := strings.Split(str, "\n") if rtn[len(rtn)-1] == "" { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 6d463be13..bd28184d5 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -96,6 +96,7 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"setenv", "setenv"}, BareMetaCmdDecl{"export", "setenv"}, BareMetaCmdDecl{"unset", "unset"}, + BareMetaCmdDecl{"clear", "clear"}, } func SubMetaCmd(cmd string) string { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f5f55c1f4..3a073b0a4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -772,3 +772,35 @@ func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts }) return txErr } + +func ClearWindow(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { + var lineIds []string + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("window does not exist") + } + query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` + lineIds = tx.SelectStrings(query, sessionId, windowId) + query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + return nil + }) + if txErr != nil { + return nil, txErr + } + win, err := GetWindowById(ctx, sessionId, windowId) + if err != nil { + return nil, err + } + for _, lineId := range lineIds { + line := &LineType{ + SessionId: sessionId, + WindowId: windowId, + LineId: lineId, + Remove: true, + } + win.Lines = append(win.Lines, line) + } + return &ModelUpdate{Window: win}, nil +} From f2a5985349572c976c4ed604d2cddafbd05b4fe4 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 28 Aug 2022 14:24:05 -0700 Subject: [PATCH 084/397] working on history command, remote:showall --- db/migrations/000001_init.up.sql | 6 +- db/schema.sql | 6 +- pkg/cmdrunner/cmdrunner.go | 96 ++++++++++++++++++++++++++++++-- pkg/cmdrunner/shparse.go | 3 + pkg/sstore/dbops.go | 19 +++++-- pkg/sstore/sstore.go | 66 ++++++++++++++++++---- pkg/sstore/updatebus.go | 12 ++-- 7 files changed, 182 insertions(+), 26 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index af75bb6bf..281b7bb09 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -114,7 +114,11 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, - cmdstr text NOT NULL + cmdstr text NOT NULL, + ismetacmd boolean ); diff --git a/db/schema.sql b/db/schema.sql index 470f8e367..503a72463 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -107,7 +107,11 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, - cmdstr text NOT NULL + cmdstr text NOT NULL, + ismetacmd boolean ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 352bd59ac..c6825cb3e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -63,6 +63,9 @@ func init() { registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) + registerCmdFn("remote:showall", RemoteShowAllCommand) + + registerCmdFn("history", HistoryCommand) } func getValidCommands() []string { @@ -126,6 +129,17 @@ func resolveBool(arg string, def bool) bool { return true } +func resolveInt(arg string, def int) (int, error) { + if arg == "" { + return def, nil + } + ival, err := strconv.Atoi(arg) + if err != nil { + return 0, err + } + return ival, nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -159,13 +173,13 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return sstore.ModelUpdate{Line: rtnLine, Cmd: cmd}, nil } -func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, hadError bool) error { +func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return err } - lineId, cmdId := sstore.ReadLineCmdIdFromUpdate(update) + lineId, cmdId, rptr := sstore.ReadHistoryDataFromUpdate(update) hitem := &sstore.HistoryItemType{ HistoryId: uuid.New().String(), Ts: time.Now().UnixMilli(), @@ -177,6 +191,10 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update HadError: hadError, CmdId: cmdId, CmdStr: cmdStr, + IsMetaCmd: isMetaCmd, + } + if !isMetaCmd && rptr != nil { + hitem.Remote = *rptr } err = sstore.InsertHistoryItem(ctx, hitem) if err != nil { @@ -195,7 +213,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } update, err := HandleCommand(ctx, newPk) if !resolveBool(pk.Kwargs["nohist"], false) { - err := addToHistory(ctx, pk, update, (err != nil)) + err := addToHistory(ctx, pk, update, (newPk.MetaCmd != "run"), (err != nil)) if err != nil { fmt.Printf("[error] adding to history: %v\n", err) // continue... @@ -369,11 +387,30 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if ids.RemoteState != nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) } - output := buf.String() return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), - InfoLines: splitLinesForInfo(output), + InfoLines: splitLinesForInfo(buf.String()), + }, + }, nil +} + +func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + stateArr := remote.GetAllRemoteState() + var buf bytes.Buffer + for _, rstate := range stateArr { + var name string + if rstate.RemoteAlias == "" { + name = rstate.RemoteCanonicalName + } else { + name = fmt.Sprintf("%s (%s)", rstate.RemoteCanonicalName, rstate.RemoteAlias) + } + buf.WriteString(fmt.Sprintf("%-12s %-5s %8s %s\n", rstate.Status, rstate.RemoteType, rstate.RemoteId[0:8], name)) + } + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show all remote info"), + InfoLines: splitLinesForInfo(buf.String()), }, }, nil } @@ -903,6 +940,55 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +const DefaultMaxHistoryItems = 10000 + +func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + if err != nil { + return nil, err + } + maxItems, err := resolveInt(pk.Kwargs["maxitems"], DefaultMaxHistoryItems) + if err != nil { + return nil, fmt.Errorf("invalid maxitems value '%s' (must be a number): %v", pk.Kwargs["maxitems"], err) + } + if maxItems < 0 { + return nil, fmt.Errorf("invalid maxitems value '%s' (cannot be negative)", maxItems) + } + if maxItems == 0 { + maxItems = DefaultMaxHistoryItems + } + hitems, err := sstore.GetSessionHistoryItems(ctx, ids.SessionId, maxItems) + if err != nil { + return nil, err + } + var filteredItems []*sstore.HistoryItemType + for _, hitem := range hitems { + if hitem.ScreenId == ids.ScreenId && hitem.WindowId == ids.WindowId && (hitem.Remote == ids.RemotePtr || hitem.IsMetaCmd) { + filteredItems = append(filteredItems, hitem) + } + } + var buf bytes.Buffer + if len(filteredItems) == 0 { + buf.WriteString("(no history)") + } else { + for idx := len(filteredItems) - 1; idx >= 0; idx-- { + hitem := filteredItems[idx] + hnumStr := hitem.HistoryNum + if hitem.IsMetaCmd { + hnumStr = "*" + hnumStr + } + hstr := fmt.Sprintf("%6s %s\n", hnumStr, hitem.CmdStr) + buf.WriteString(hstr) + } + } + update := &sstore.ModelUpdate{} + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("history, limited to current session, screen, window, and remote (maxitems=%d)", maxItems), + InfoLines: splitLinesForInfo(buf.String()), + } + return update, nil +} + func splitLinesForInfo(str string) []string { rtn := strings.Split(str, "\n") if rtn[len(rtn)-1] == "" { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index bd28184d5..d0a6386c3 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -155,6 +155,9 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) if len(origPk.Args) == 0 { return nil, fmt.Errorf("empty command (no fields)") } + if strings.TrimSpace(origPk.Args[0]) == "" { + return nil, fmt.Errorf("empty command") + } metaCmd, metaSubCmd, commandArgs := parseMetaCmd(origPk.Args[0]) rtnPk := scpacket.MakeFeCommandPacket() rtnPk.MetaCmd = metaCmd diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3a073b0a4..3c9931574 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strconv" "strings" "github.com/google/uuid" @@ -107,9 +108,9 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if err != nil { return err } - query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr)` - _, err = db.NamedExec(query, hitem) + query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd)` + _, err = db.NamedExec(query, hitem.ToMap()) if err != nil { return err } @@ -119,8 +120,16 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { func GetSessionHistoryItems(ctx context.Context, sessionId string, maxItems int) ([]*HistoryItemType, error) { var rtn []*HistoryItemType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC LIMIT ?` - tx.SelectWrap(&rtn, query, sessionId, maxItems) + query := `SELECT count(*) FROM history WHERE sessionid = ?` + totalNum := tx.GetInt(query, sessionId) + query = `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC, historyid LIMIT ?` + marr := tx.SelectMaps(query, sessionId, maxItems) + for idx, m := range marr { + hnum := totalNum - idx + hitem := HistoryItemFromMap(m) + hitem.HistoryNum = strconv.Itoa(hnum) + rtn = append(rtn, hitem) + } return nil }) if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 075cf85ff..12606d564 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -206,6 +206,47 @@ func WindowFromMap(m map[string]interface{}) *WindowType { return &w } +func (h *HistoryItemType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["historyid"] = h.HistoryId + rtn["ts"] = h.Ts + rtn["userid"] = h.UserId + rtn["sessionid"] = h.SessionId + rtn["screenid"] = h.ScreenId + rtn["windowid"] = h.WindowId + rtn["lineid"] = h.LineId + rtn["haderror"] = h.HadError + rtn["cmdid"] = h.CmdId + rtn["cmdstr"] = h.CmdStr + rtn["remoteownerid"] = h.Remote.OwnerId + rtn["remoteid"] = h.Remote.RemoteId + rtn["remotename"] = h.Remote.Name + rtn["ismetacmd"] = h.IsMetaCmd + return rtn +} + +func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { + if len(m) == 0 { + return nil + } + var h HistoryItemType + quickSetStr(&h.HistoryId, m, "historyid") + quickSetInt64(&h.Ts, m, "ts") + quickSetStr(&h.UserId, m, "userid") + quickSetStr(&h.SessionId, m, "sessionid") + quickSetStr(&h.ScreenId, m, "screenid") + quickSetStr(&h.WindowId, m, "windowid") + quickSetStr(&h.LineId, m, "lineid") + quickSetBool(&h.HadError, m, "haderror") + quickSetStr(&h.CmdId, m, "cmdid") + quickSetStr(&h.CmdStr, m, "cmdstr") + quickSetStr(&h.Remote.OwnerId, m, "remoteownerid") + quickSetStr(&h.Remote.RemoteId, m, "remoteid") + quickSetStr(&h.Remote.Name, m, "remotename") + quickSetBool(&h.IsMetaCmd, m, "ismetacmd") + return &h +} + type ScreenOptsType struct { TabColor string `json:"tabcolor,omitempty"` } @@ -271,19 +312,24 @@ type ScreenWindowType struct { } type HistoryItemType struct { - HistoryId string `json:"historyid"` - Ts int64 `json:"ts"` - UserId string `json:"userid"` - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - LineId string `json:"lineid"` - HadError bool `json:"haderror"` - CmdId string `json:"cmdid"` - CmdStr string `json:"cmdstr"` + HistoryId string `json:"historyid"` + Ts int64 `json:"ts"` + UserId string `json:"userid"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + LineId string `json:"lineid"` + HadError bool `json:"haderror"` + CmdId string `json:"cmdid"` + CmdStr string `json:"cmdstr"` + Remote RemotePtrType `json:"remote"` + IsMetaCmd bool `json:"ismetacmd"` // only for updates Remove bool `json:"remove"` + + // transient (string because of different history orderings) + HistoryNum string `json:"historynum"` } type RemoteState struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 47057778a..f9e6a461c 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -49,15 +49,19 @@ func MakeSingleSessionUpdate(sessionId string) (ModelUpdate, *SessionType) { return update, session } -func ReadLineCmdIdFromUpdate(update UpdatePacket) (string, string) { +func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { modelUpdate, ok := update.(ModelUpdate) if !ok { - return "", "" + return "", "", nil } if modelUpdate.Line == nil { - return "", "" + return "", "", nil } - return modelUpdate.Line.LineId, modelUpdate.Line.CmdId + var rptr *RemotePtrType + if modelUpdate.Cmd != nil { + rptr = &modelUpdate.Cmd.Remote + } + return modelUpdate.Line.LineId, modelUpdate.Line.CmdId, rptr } type InfoMsgType struct { From 03dd6b1a7e179e8e565a7b0b0bd3ef4d2756db28 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 29 Aug 2022 16:31:06 -0700 Subject: [PATCH 085/397] pass 'uicontext' with fecmd, use that to resolve ids instead of kwargs. other bug fixes --- cmd/main-server.go | 2 +- pkg/cmdrunner/cmdrunner.go | 167 ++++++++++++++++--------------------- pkg/cmdrunner/resolver.go | 131 +++++++++++++++++------------ pkg/cmdrunner/shparse.go | 1 + pkg/remote/remote.go | 46 +++++++--- pkg/scpacket/scpacket.go | 8 ++ pkg/sstore/dbops.go | 13 +++ pkg/sstore/updatebus.go | 21 +++-- 8 files changed, 215 insertions(+), 174 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index b61627b4d..9ff852255 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -158,7 +158,7 @@ func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") w.Header().Set("Cache-Control", "no-cache") - remotes := remote.GetAllRemoteState() + remotes := remote.GetAllRemoteRuntimeState() WriteJsonSuccess(w, remotes) return } diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c6825cb3e..718669278 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -141,28 +141,22 @@ func resolveInt(arg string, def int) (int, error) { } func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { return nil, fmt.Errorf("/run error: %w", err) } - if !ids.RState.IsConnected() { - return nil, fmt.Errorf("cannot run command, remote '%s' not connected", ids.RemoteDisplayName) - } - if ids.RemoteState == nil { - return nil, fmt.Errorf("cannot run command, remote '%s' has no state", ids.RemoteDisplayName) - } cmdId := uuid.New().String() cmdStr := firstArg(pk) runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) - runPacket.Cwd = ids.RemoteState.Cwd - runPacket.Env0 = ids.RemoteState.Env0 + runPacket.Cwd = ids.Remote.RemoteState.Cwd + runPacket.Env0 = ids.Remote.RemoteState.Env0 runPacket.EnvComplete = true runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} runPacket.Command = strings.TrimSpace(cmdStr) - cmd, err := remote.RunCommand(ctx, cmdId, ids.RemotePtr, ids.RemoteState, runPacket) + cmd, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) if err != nil { return nil, err } @@ -175,7 +169,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) - ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return err } @@ -194,7 +188,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update IsMetaCmd: isMetaCmd, } if !isMetaCmd && rptr != nil { - hitem.Remote = *rptr + hitem.Remote = ids.Remote.RemotePtr } err = sstore.InsertHistoryItem(ctx, hitem) if err != nil { @@ -223,7 +217,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Screen) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, fmt.Errorf("/screen:close cannot close screen: %w", err) } @@ -235,7 +229,7 @@ func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( } func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session) + ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { return nil, fmt.Errorf("/screen:open cannot open screen: %w", err) } @@ -255,7 +249,7 @@ func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Screen) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -310,7 +304,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session) + ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { return nil, fmt.Errorf("/screen cannot switch to screen: %w", err) } @@ -330,17 +324,11 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot unset: %v", err) } - if !ids.RState.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected, cannot unset", ids.RemoteDisplayName) - } - if ids.RemoteState == nil { - return nil, fmt.Errorf("remote '%s' state is not available, cannot unset", ids.RemoteDisplayName) - } - envMap := shexec.ParseEnv0(ids.RemoteState.Env0) + envMap := shexec.ParseEnv0(ids.Remote.RemoteState.Env0) unsetVars := make(map[string]bool) for _, argStr := range pk.Args { eqIdx := strings.Index(argStr, "=") @@ -350,16 +338,16 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore delete(envMap, argStr) unsetVars[argStr] = true } - state := *ids.RemoteState + state := *ids.Remote.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) + remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err } update := sstore.ModelUpdate{ Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.RemoteDisplayName, formatStrs(mapToStrs(unsetVars), "and", false)), + InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.Remote.DisplayName, formatStrs(mapToStrs(unsetVars), "and", false)), TimeoutMs: 2000, }, } @@ -367,15 +355,11 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { return nil, err } - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("invalid remote '%s' (not found)", ids.RemoteDisplayName) - } - state := curRemote.GetRemoteState() + state := ids.Remote.RState var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", state.RemoteType)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remoteid", state.RemoteId)) @@ -384,19 +368,19 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) - if ids.RemoteState != nil { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) + if ids.Remote.RemoteState != nil { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.Remote.RemoteState.Cwd)) } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), + InfoTitle: fmt.Sprintf("show remote '%s' info", ids.Remote.DisplayName), InfoLines: splitLinesForInfo(buf.String()), }, }, nil } func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - stateArr := remote.GetAllRemoteState() + stateArr := remote.GetAllRemoteRuntimeState() var buf bytes.Buffer for _, rstate := range stateArr { var name string @@ -420,17 +404,11 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot setenv: %v", err) } - if !ids.RState.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected, cannot setenv", ids.RemoteDisplayName) - } - if ids.RemoteState == nil { - return nil, fmt.Errorf("remote '%s' state is not available, cannot setenv", ids.RemoteDisplayName) - } - envMap := shexec.ParseEnv0(ids.RemoteState.Env0) + envMap := shexec.ParseEnv0(ids.Remote.RemoteState.Env0) if len(pk.Args) == 0 { var infoLines []string for varName, varVal := range envMap { @@ -439,7 +417,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.RemoteDisplayName), + InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.Remote.DisplayName), InfoLines: infoLines, }, } @@ -456,16 +434,16 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor envMap[envName] = envVal setVars[envName] = true } - state := *ids.RemoteState + state := *ids.Remote.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) + remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err } update := sstore.ModelUpdate{ Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.RemoteDisplayName, formatStrs(mapToStrs(setVars), "and", false)), + InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.Remote.DisplayName, formatStrs(mapToStrs(setVars), "and", false)), TimeoutMs: 2000, }, } @@ -473,7 +451,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) if err != nil { return nil, fmt.Errorf("/cr error: %w", err) } @@ -507,37 +485,27 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up } func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { return nil, fmt.Errorf("/cd error: %w", err) } newDir := firstArg(pk) - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, fmt.Errorf("remote '%s' not found, cannot change directory", ids.RemoteDisplayName) - } - if !ids.RState.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected, cannot change directory", ids.RemoteDisplayName) - } - if ids.RemoteState == nil { - return nil, fmt.Errorf("remote '%s' state is not available, cannot change directory", ids.RemoteDisplayName) - } if newDir == "" { return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteDisplayName, ids.RemoteState.Cwd), + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, ids.Remote.RemoteState.Cwd), }, }, nil } - newDir, err = ids.RState.ExpandHomeDir(newDir) + newDir, err = ids.Remote.RState.ExpandHomeDir(newDir) if err != nil { return nil, err } if !strings.HasPrefix(newDir, "/") { - if ids.RemoteState == nil { + if ids.Remote.RemoteState == nil { return nil, fmt.Errorf("/cd error: cannot get current remote directory (can only cd with absolute path)") } - newDir = path.Join(ids.RemoteState.Cwd, newDir) + newDir = path.Join(ids.Remote.RemoteState.Cwd, newDir) newDir, err = filepath.Abs(newDir) if err != nil { return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) @@ -546,23 +514,23 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up cdPacket := packet.MakeCdPacket() cdPacket.ReqId = uuid.New().String() cdPacket.Dir = newDir - resp, err := curRemote.PacketRpc(ctx, cdPacket) + resp, err := ids.Remote.MShell.PacketRpc(ctx, cdPacket) if err != nil { return nil, err } if err = resp.Err(); err != nil { return nil, err } - state := *ids.RemoteState + state := *ids.Remote.RemoteState state.Cwd = newDir - remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.RemotePtr, state) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err } update := sstore.ModelUpdate{ - Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), + Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst), Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteDisplayName, newDir), + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, newDir), TimeoutMs: 2000, }, } @@ -682,10 +650,15 @@ func longestPrefix(root string, comps []string) string { return lcp } -func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string, forDisplay bool) ([]string, bool, error) { - comps, hasMore, err := doCompGen(ctx, ids, prefix, "file", forDisplay) - if err != nil { - return nil, false, err +func doMetaCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, forDisplay bool) ([]string, bool, error) { + ids, err := resolveUiIds(ctx, pk, 0) // best effort + var comps []string + var hasMore bool + if ids.Remote != nil && ids.Remote.RState.IsConnected() { + comps, hasMore, err = doCompGen(ctx, pk, prefix, "file", forDisplay) + if err != nil { + return nil, false, err + } } validCommands := getValidCommands() for _, cmd := range validCommands { @@ -700,26 +673,23 @@ func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string, forDispl return comps, hasMore, nil } -func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string, forDisplay bool) ([]string, bool, error) { +func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, compType string, forDisplay bool) ([]string, bool, error) { if compType == "metacommand" { - return doMetaCompGen(ctx, ids, prefix, forDisplay) + return doMetaCompGen(ctx, pk, prefix, forDisplay) } if !packet.IsValidCompGenType(compType) { return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) + if err != nil { + return nil, false, fmt.Errorf("compgen error: %w", err) + } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix - if ids.RemoteState == nil { - return nil, false, fmt.Errorf("/compgen invalid remote state") - } - cgPacket.Cwd = ids.RemoteState.Cwd - curRemote := remote.GetRemoteById(ids.RemotePtr.RemoteId) - if curRemote == nil { - return nil, false, fmt.Errorf("invalid remote, cannot execute command") - } - resp, err := curRemote.PacketRpc(ctx, cgPacket) + cgPacket.Cwd = ids.Remote.RemoteState.Cwd + resp, err := ids.Remote.MShell.PacketRpc(ctx, cgPacket) if err != nil { return nil, false, err } @@ -732,10 +702,6 @@ func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType str } func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) - if err != nil { - return nil, fmt.Errorf("/compgen error: %w", err) - } cmdLine := firstArg(pk) pos := len(cmdLine) if pk.Kwargs["comppos"] != "" { @@ -766,7 +732,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if len(parts) > 0 { lastPart = parts[len(parts)-1] } - comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType, showComps) + comps, hasMore, err := doCompGen(ctx, pk, lastPart, compType, showComps) if err != nil { return nil, err } @@ -777,7 +743,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) if err != nil { return nil, fmt.Errorf("/comment error: %w", err) } @@ -866,7 +832,7 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( } func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session) + ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { return nil, err } @@ -914,6 +880,10 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } + err = sstore.SetActiveSessionId(ctx, ritem.Id) + if err != nil { + return nil, err + } update := sstore.ModelUpdate{ ActiveSessionId: ritem.Id, Info: &sstore.InfoMsgType{ @@ -925,7 +895,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, err } @@ -943,7 +913,7 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore const DefaultMaxHistoryItems = 10000 func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -963,7 +933,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } var filteredItems []*sstore.HistoryItemType for _, hitem := range hitems { - if hitem.ScreenId == ids.ScreenId && hitem.WindowId == ids.WindowId && (hitem.Remote == ids.RemotePtr || hitem.IsMetaCmd) { + if hitem.ScreenId == ids.ScreenId && hitem.WindowId == ids.WindowId && (hitem.Remote == ids.Remote.RemotePtr || hitem.IsMetaCmd) { filteredItems = append(filteredItems, hitem) } } @@ -986,6 +956,9 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto InfoMsg: fmt.Sprintf("history, limited to current session, screen, window, and remote (maxitems=%d)", maxItems), InfoLines: splitLinesForInfo(buf.String()), } + update.History = &sstore.HistoryInfoType{ + Items: filteredItems, + } return update, nil } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 845140dfe..f1a92e4bd 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -14,24 +14,26 @@ import ( ) const ( - R_Session = 1 - R_Screen = 2 - R_Window = 4 - R_Remote = 8 - R_SessionOpt = 16 - R_ScreenOpt = 32 - R_WindowOpt = 64 - R_RemoteOpt = 128 + R_Session = 1 + R_Screen = 2 + R_Window = 4 + R_Remote = 8 + R_RemoteConnected = 8 + 16 ) type resolvedIds struct { - SessionId string - ScreenId string - WindowId string - RemotePtr sstore.RemotePtrType - RemoteState *sstore.RemoteState - RemoteDisplayName string - RState remote.RemoteState + SessionId string + ScreenId string + WindowId string + Remote *ResolvedRemote +} + +type ResolvedRemote struct { + DisplayName string + RemotePtr sstore.RemotePtrType + RemoteState *sstore.RemoteState + RState remote.RemoteRuntimeState + MShell *remote.MShellProc } type ResolveItem struct { @@ -116,52 +118,45 @@ func resolveByPosition(items []ResolveItem, curId string, posStr string) *Resolv return &items[pos-1] } -func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { +func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { + fmt.Printf("resolve-ui-ids: %#v\n", pk) rtn := resolvedIds{} - if rtype == 0 { - return rtn, nil - } - var err error - if (rtype&R_Session)+(rtype&R_SessionOpt) > 0 { - rtn.SessionId, err = resolveSessionId(pk) - if err != nil { - return rtn, err - } - if rtn.SessionId == "" && (rtype&R_Session) > 0 { - return rtn, fmt.Errorf("no session") + uictx := pk.UIContext + if uictx != nil { + rtn.SessionId = uictx.SessionId + rtn.ScreenId = uictx.ScreenId + rtn.WindowId = uictx.WindowId + if uictx.Remote != nil && rtn.SessionId != "" && rtn.WindowId != "" { + rr, err := resolveRemoteFromPtr(ctx, uictx.Remote, rtn.SessionId, rtn.WindowId) + if err != nil { + if rtype&R_Remote > 0 { + return rtn, err + } + // otherwise just don't set uictx.Remote + } else { + rtn.Remote = rr + } } } - if (rtype&R_Window)+(rtype&R_WindowOpt) > 0 { - rtn.WindowId, err = resolveWindowId(pk, rtn.SessionId) - if err != nil { - return rtn, err - } - if rtn.WindowId == "" && (rtype&R_Window) > 0 { - return rtn, fmt.Errorf("no window") - } - + if rtype&R_Session > 0 && rtn.SessionId == "" { + return rtn, fmt.Errorf("no session") } - if (rtype&R_Screen)+(rtype&R_ScreenOpt) > 0 { - rtn.ScreenId, err = resolveScreenId(ctx, pk, rtn.SessionId) - if err != nil { - return rtn, err - } - if rtn.ScreenId == "" && (rtype&R_Screen) > 0 { - return rtn, fmt.Errorf("no screen") - } + if rtype&R_Screen > 0 && rtn.ScreenId == "" { + return rtn, fmt.Errorf("no screen") } - if (rtype&R_Remote)+(rtype&R_RemoteOpt) > 0 { - rname, rptr, state, rstate, err := resolveRemote(ctx, pk.Kwargs["remote"], rtn.SessionId, rtn.WindowId) - if err != nil { - return rtn, err + if rtype&R_Window > 0 && rtn.WindowId == "" { + return rtn, fmt.Errorf("no window") + } + if rtype&R_Remote > 0 && rtn.Remote == nil { + return rtn, fmt.Errorf("no remote") + } + if rtype&R_RemoteConnected > 0 { + if !rtn.Remote.RState.IsConnected() { + return rtn, fmt.Errorf("remote '%s' is not connected", rtn.Remote.DisplayName) } - if rptr == nil && (rtype&R_Remote) > 0 { - return rtn, fmt.Errorf("no remote") + if rtn.Remote.RemoteState == nil { + return rtn, fmt.Errorf("remote '%s' state is not available", rtn.Remote.DisplayName) } - rtn.RemoteDisplayName = rname - rtn.RemotePtr = *rptr - rtn.RemoteState = state - rtn.RState = *rstate } return rtn, nil } @@ -281,8 +276,34 @@ func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { return fields[0], fields[1], fields[2], nil } +func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, windowId string) (*ResolvedRemote, error) { + if rptr == nil || rptr.RemoteId == "" { + return nil, fmt.Errorf("no remote to resolve") + } + msh := remote.GetRemoteById(rptr.RemoteId) + if msh == nil { + return nil, fmt.Errorf("invalid remote '%s', not found", rptr.RemoteId) + } + rstate := msh.GetRemoteRuntimeState() + displayName := rstate.GetDisplayName(rptr) + state, err := sstore.GetRemoteState(ctx, sessionId, windowId, *rptr) + if err != nil { + return nil, fmt.Errorf("cannot resolve remote state '%s': %w", displayName, err) + } + if state == nil { + state = rstate.DefaultState + } + return &ResolvedRemote{ + DisplayName: displayName, + RemotePtr: *rptr, + RemoteState: state, + RState: rstate, + MShell: msh, + }, nil +} + // returns (remoteDisplayName, remoteptr, state, rstate, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteState, error) { +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteRuntimeState, error) { if fullRemoteRef == "" { return "", nil, nil, nil, nil } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index d0a6386c3..aa8592f08 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -163,6 +163,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) rtnPk.MetaCmd = metaCmd rtnPk.MetaSubCmd = metaSubCmd rtnPk.Kwargs = make(map[string]string) + rtnPk.UIContext = origPk.UIContext for key, val := range origPk.Kwargs { rtnPk.Kwargs[key] = val } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 0c31ad8d6..6cac6dffc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -55,7 +55,7 @@ type Store struct { Map map[string]*MShellProc // key=remoteid } -type RemoteState struct { +type RemoteRuntimeState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` PhysicalId string `json:"physicalremoteid"` @@ -68,10 +68,31 @@ type RemoteState struct { ConnectMode string `json:"connectmode"` } -func (state RemoteState) IsConnected() bool { +func (state RemoteRuntimeState) IsConnected() bool { return state.Status == StatusConnected } +func (state RemoteRuntimeState) GetBaseDisplayName() string { + if state.RemoteAlias != "" { + return state.RemoteAlias + } + return state.RemoteCanonicalName +} + +func (state RemoteRuntimeState) GetDisplayName(rptr *sstore.RemotePtrType) string { + name := state.GetBaseDisplayName() + if rptr == nil { + return name + } + if rptr.Name != "" { + name = name + ":" + rptr.Name + } + if rptr.OwnerId != "" { + name = "@" + rptr.OwnerId + ":" + name + } + return name +} + type MShellProc struct { Lock *sync.Mutex Remote *sstore.RemoteType @@ -122,7 +143,7 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } -func ResolveRemoteRef(remoteRef string) *RemoteState { +func ResolveRemoteRef(remoteRef string) *RemoteRuntimeState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -130,14 +151,14 @@ func ResolveRemoteRef(remoteRef string) *RemoteState { if err == nil { msh := GlobalStore.Map[remoteRef] if msh != nil { - state := msh.GetRemoteState() + state := msh.GetRemoteRuntimeState() return &state } return nil } for _, msh := range GlobalStore.Map { if msh.Remote.RemoteAlias == remoteRef || msh.Remote.RemoteCanonicalName == remoteRef { - state := msh.GetRemoteState() + state := msh.GetRemoteRuntimeState() return &state } } @@ -188,10 +209,10 @@ func makeShortHost(host string) string { return host[0:dotIdx] } -func (proc *MShellProc) GetRemoteState() RemoteState { +func (proc *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { proc.Lock.Lock() defer proc.Lock.Unlock() - state := RemoteState{ + state := RemoteRuntimeState{ RemoteType: proc.Remote.RemoteType, RemoteId: proc.Remote.RemoteId, RemoteAlias: proc.Remote.RemoteAlias, @@ -251,18 +272,18 @@ func (proc *MShellProc) GetRemoteState() RemoteState { } func (msh *MShellProc) NotifyUpdate() { - rstate := msh.GetRemoteState() + rstate := msh.GetRemoteRuntimeState() update := &sstore.ModelUpdate{Remote: rstate} sstore.MainBus.SendUpdate("", update) } -func GetAllRemoteState() []RemoteState { +func GetAllRemoteRuntimeState() []RemoteRuntimeState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() - var rtn []RemoteState + var rtn []RemoteRuntimeState for _, proc := range GlobalStore.Map { - state := proc.GetRemoteState() + state := proc.GetRemoteRuntimeState() rtn = append(rtn, state) } return rtn @@ -427,7 +448,7 @@ func replaceHomePath(pathStr string, homeDir string) string { return pathStr } -func (state RemoteState) ExpandHomeDir(pathStr string) (string, error) { +func (state RemoteRuntimeState) ExpandHomeDir(pathStr string) (string, error) { if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") { return pathStr, nil } @@ -484,7 +505,6 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp if remoteState == nil { return nil, fmt.Errorf("no remote state passed to RunCommand") } - fmt.Printf("RUN-CMD> %s reqid=%s (msh=%v)\n", runPacket.CK, runPacket.ReqId, msh.Remote) msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 8010873f2..f1a932a7e 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -18,6 +18,14 @@ type FeCommandPacketType struct { MetaSubCmd string `json:"metasubcmd,omitempty"` Args []string `json:"args,omitempty"` Kwargs map[string]string `json:"kwargs,omitempty"` + UIContext *UIContextType `json:"uicontext,omitempty"` +} + +type UIContextType struct { + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Remote *sstore.RemotePtrType `json:"remote,omitempty"` } type FeInputPacketType struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3c9931574..a7fbec4ad 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -334,6 +334,19 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo return update, nil } +func SetActiveSessionId(ctx context.Context, sessionId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM session WHERE sessionid = ?` + if !tx.Exists(query, sessionId) { + return fmt.Errorf("cannot switch to session, not found") + } + query = `UPDATE client SET activesessionid = ?` + tx.ExecWrap(query, sessionId) + return nil + }) + return txErr +} + func containsStr(strs []string, testStr string) bool { for _, s := range strs { if s == testStr { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index f9e6a461c..3b687bf72 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -24,14 +24,15 @@ func (PtyDataUpdate) UpdateType() string { } type ModelUpdate struct { - Sessions []*SessionType `json:"sessions,omitempty"` - ActiveSessionId string `json:"activesessionid,omitempty"` - Window *WindowType `json:"window,omitempty"` - Line *LineType `json:"line,omitempty"` - Cmd *CmdType `json:"cmd,omitempty"` - CmdLine *CmdLineType `json:"cmdline,omitempty"` - Info *InfoMsgType `json:"info,omitempty"` - Remote interface{} `json:"remote,omitempty"` // *remote.RemoteState + Sessions []*SessionType `json:"sessions,omitempty"` + ActiveSessionId string `json:"activesessionid,omitempty"` + Window *WindowType `json:"window,omitempty"` + Line *LineType `json:"line,omitempty"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` + Remote interface{} `json:"remote,omitempty"` // *remote.RemoteState + History *HistoryInfoType `json:"history,omitempty"` } func (ModelUpdate) UpdateType() string { @@ -74,6 +75,10 @@ type InfoMsgType struct { TimeoutMs int64 `json:"timeoutms,omitempty"` } +type HistoryInfoType struct { + Items []*HistoryItemType `json:"items"` +} + type CmdLineType struct { InsertChars string `json:"insertchars"` InsertPos int64 `json:"insertpos"` From c03bbe87150c8820d0cc2b428084de501ca1b928 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 29 Aug 2022 19:18:02 -0700 Subject: [PATCH 086/397] working on history queries --- cmd/main-server.go | 2 +- pkg/cmdrunner/cmdrunner.go | 8 ++--- pkg/cmdrunner/resolver.go | 1 - pkg/sstore/dbops.go | 67 ++++++++++++++++++++++++++++++-------- pkg/sstore/quick.go | 6 ++++ pkg/sstore/sstore.go | 6 ++++ 6 files changed, 68 insertions(+), 22 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9ff852255..a1f709062 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -183,7 +183,7 @@ func HandleGetHistory(w http.ResponseWriter, r *http.Request) { numItems = parsedNum } } - hitems, err := sstore.GetSessionHistoryItems(r.Context(), sessionId, numItems) + hitems, err := sstore.GetHistoryItems(r.Context(), sessionId, "", sstore.HistoryQueryOpts{MaxItems: numItems}) if err != nil { WriteJsonError(w, err) return diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 718669278..54d6a681f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -927,13 +927,13 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if maxItems == 0 { maxItems = DefaultMaxHistoryItems } - hitems, err := sstore.GetSessionHistoryItems(ctx, ids.SessionId, maxItems) + hitems, err := sstore.GetHistoryItems(ctx, ids.SessionId, ids.WindowId, sstore.HistoryQueryOpts{MaxItems: maxItems}) if err != nil { return nil, err } var filteredItems []*sstore.HistoryItemType for _, hitem := range hitems { - if hitem.ScreenId == ids.ScreenId && hitem.WindowId == ids.WindowId && (hitem.Remote == ids.Remote.RemotePtr || hitem.IsMetaCmd) { + if hitem.Remote == ids.Remote.RemotePtr || hitem.IsMetaCmd { filteredItems = append(filteredItems, hitem) } } @@ -952,10 +952,6 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } } update := &sstore.ModelUpdate{} - update.Info = &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("history, limited to current session, screen, window, and remote (maxitems=%d)", maxItems), - InfoLines: splitLinesForInfo(buf.String()), - } update.History = &sstore.HistoryInfoType{ Items: filteredItems, } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index f1a92e4bd..8a1c5c795 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -119,7 +119,6 @@ func resolveByPosition(items []ResolveItem, curId string, posStr string) *Resolv } func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { - fmt.Printf("resolve-ui-ids: %#v\n", pk) rtn := resolvedIds{} uictx := pk.UIContext if uictx != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a7fbec4ad..776734376 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,13 +4,15 @@ import ( "context" "database/sql" "fmt" - "strconv" "strings" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/packet" ) +const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd" +const DefaultMaxHistoryItems = 1000 + func NumSessions(ctx context.Context) (int, error) { db, err := GetDB(ctx) if err != nil { @@ -117,23 +119,60 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { return nil } -func GetSessionHistoryItems(ctx context.Context, sessionId string, maxItems int) ([]*HistoryItemType, error) { +func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { + // check sessionid/windowid format because we are directly inserting them into the SQL + if sessionId != "" { + _, err := uuid.Parse(sessionId) + if err != nil { + return nil, fmt.Errorf("malformed sessionid") + } + } + if windowId != "" { + _, err := uuid.Parse(windowId) + if err != nil { + return nil, fmt.Errorf("malformed windowid") + } + } + hnumStr := "" + whereClause := "" + if sessionId != "" && windowId != "" { + whereClause = fmt.Sprintf("WHERE sessionid = '%s' AND windowid = '%s'", sessionId, windowId) + hnumStr = "w" + } else if sessionId != "" { + whereClause = fmt.Sprintf("WHERE sessionid = '%s'", sessionId) + hnumStr = "s" + } else { + hnumStr = "g" + } + maxItems := opts.MaxItems + if maxItems == 0 { + maxItems = DefaultMaxHistoryItems + } + query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d", HistoryCols, hnumStr, whereClause, maxItems) + if opts.FromTs > 0 { + query = fmt.Sprintf("SELECT * FROM (%s) WHERE ts >= %d", query, opts.FromTs) + } + marr := tx.SelectMaps(query) + rtn := make([]*HistoryItemType, len(marr)) + for idx, m := range marr { + hitem := HistoryItemFromMap(m) + rtn[idx] = hitem + } + return rtn, nil +} + +func GetHistoryItems(ctx context.Context, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { var rtn []*HistoryItemType - err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT count(*) FROM history WHERE sessionid = ?` - totalNum := tx.GetInt(query, sessionId) - query = `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC, historyid LIMIT ?` - marr := tx.SelectMaps(query, sessionId, maxItems) - for idx, m := range marr { - hnum := totalNum - idx - hitem := HistoryItemFromMap(m) - hitem.HistoryNum = strconv.Itoa(hnum) - rtn = append(rtn, hitem) + txErr := WithTx(ctx, func(tx *TxWrap) error { + var err error + rtn, err = runHistoryQuery(tx, sessionId, windowId, opts) + if err != nil { + return err } return nil }) - if err != nil { - return nil, err + if txErr != nil { + return nil, txErr } return rtn, nil } diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index dcc3e789c..df7a26a71 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strconv" ) func quickSetStr(strVal *string, m map[string]interface{}, name string) { @@ -11,6 +12,11 @@ func quickSetStr(strVal *string, m map[string]interface{}, name string) { if !ok { return } + ival, ok := v.(int64) + if ok { + *strVal = strconv.FormatInt(ival, 10) + return + } str, ok := v.(string) if !ok { return diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 12606d564..dafdf9660 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -244,6 +244,7 @@ func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { quickSetStr(&h.Remote.RemoteId, m, "remoteid") quickSetStr(&h.Remote.Name, m, "remotename") quickSetBool(&h.IsMetaCmd, m, "ismetacmd") + quickSetStr(&h.HistoryNum, m, "historynum") return &h } @@ -332,6 +333,11 @@ type HistoryItemType struct { HistoryNum string `json:"historynum"` } +type HistoryQueryOpts struct { + MaxItems int + FromTs int64 +} + type RemoteState struct { Cwd string `json:"cwd"` Env0 []byte `json:"env0"` // "env -0" format From 76854b607910b8aa1b44e1296c058a0bc9f9ba61 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 30 Aug 2022 23:11:06 -0700 Subject: [PATCH 087/397] remote get-history-items, add show param and session/window ids to history command --- cmd/main-server.go | 33 --------------------------------- pkg/cmdrunner/cmdrunner.go | 6 +++++- pkg/sstore/updatebus.go | 5 ++++- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index a1f709062..a7abf5036 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "runtime/debug" - "strconv" "strings" "sync" "time" @@ -163,37 +162,6 @@ func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { return } -// params: sessionid -func HandleGetHistory(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") - if _, err := uuid.Parse(sessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) - return - } - numItems := 1000 - numStr := qvals.Get("num") - if numStr != "" { - parsedNum, err := strconv.Atoi(numStr) - if err == nil { - numItems = parsedNum - } - } - hitems, err := sstore.GetHistoryItems(r.Context(), sessionId, "", sstore.HistoryQueryOpts{MaxItems: numItems}) - if err != nil { - WriteJsonError(w, err) - return - } - rtnMap := make(map[string]interface{}) - rtnMap["history"] = hitems - WriteJsonSuccess(w, rtnMap) - return -} - // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -466,7 +434,6 @@ func main() { gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/get-remotes", HandleGetRemotes) - gr.HandleFunc("/api/get-history", HandleGetHistory) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 54d6a681f..09cb9b2e4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -951,9 +951,13 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto buf.WriteString(hstr) } } + show := !resolveBool(pk.Kwargs["noshow"], false) update := &sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ - Items: filteredItems, + SessionId: ids.SessionId, + WindowId: ids.WindowId, + Items: filteredItems, + Show: show, } return update, nil } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 3b687bf72..f1c8ef87c 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -76,7 +76,10 @@ type InfoMsgType struct { } type HistoryInfoType struct { - Items []*HistoryItemType `json:"items"` + SessionId string `json:"sessionid,omitempty"` + WindowId string `json:"windowid,omitempty"` + Items []*HistoryItemType `json:"items"` + Show bool `json:"show"` } type CmdLineType struct { From 8b11af682227ab6eec8e192b5dccf6ac4c4d45d2 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 31 Aug 2022 00:01:42 -0700 Subject: [PATCH 088/397] add non-parsing commands to history --- pkg/cmdrunner/cmdrunner.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 09cb9b2e4..1d42ccb77 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -201,19 +201,19 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") } - newPk, err := EvalMetaCommand(ctx, pk) - if err != nil { - return nil, err + var update sstore.UpdatePacket + newPk, rtnErr := EvalMetaCommand(ctx, pk) + if rtnErr == nil { + update, rtnErr = HandleCommand(ctx, newPk) } - update, err := HandleCommand(ctx, newPk) if !resolveBool(pk.Kwargs["nohist"], false) { - err := addToHistory(ctx, pk, update, (newPk.MetaCmd != "run"), (err != nil)) + err := addToHistory(ctx, pk, update, (newPk.MetaCmd != "run"), (rtnErr != nil)) if err != nil { fmt.Printf("[error] adding to history: %v\n", err) // continue... } } - return update, err + return update, rtnErr } func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { From 86f3eabf5e2668024c9333eac1f1cb2cfd4dcf92 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 31 Aug 2022 12:01:16 -0700 Subject: [PATCH 089/397] remote bugfix and filter remotes on client side --- pkg/cmdrunner/cmdrunner.go | 22 +--------------------- pkg/remote/remote.go | 3 +++ 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1d42ccb77..332a2c541 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -931,32 +931,12 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - var filteredItems []*sstore.HistoryItemType - for _, hitem := range hitems { - if hitem.Remote == ids.Remote.RemotePtr || hitem.IsMetaCmd { - filteredItems = append(filteredItems, hitem) - } - } - var buf bytes.Buffer - if len(filteredItems) == 0 { - buf.WriteString("(no history)") - } else { - for idx := len(filteredItems) - 1; idx >= 0; idx-- { - hitem := filteredItems[idx] - hnumStr := hitem.HistoryNum - if hitem.IsMetaCmd { - hnumStr = "*" + hnumStr - } - hstr := fmt.Sprintf("%6s %s\n", hnumStr, hitem.CmdStr) - buf.WriteString(hstr) - } - } show := !resolveBool(pk.Kwargs["noshow"], false) update := &sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ SessionId: ids.SessionId, WindowId: ids.WindowId, - Items: filteredItems, + Items: hitems, Show: show, } return update, nil diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 6cac6dffc..baa3eaaaa 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -511,6 +511,9 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp return nil, fmt.Errorf("sending run packet to remote: %w", err) } rtnPk := msh.ServerProc.Output.WaitForResponse(ctx, runPacket.ReqId) + if rtnPk == nil { + return nil, ctx.Err() + } startPk, ok := rtnPk.(*packet.CmdStartPacketType) if !ok { respPk, ok := rtnPk.(*packet.ResponsePacketType) From 1a88d564bb674b6574a94404fea331c9110ec058 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 31 Aug 2022 13:28:52 -0700 Subject: [PATCH 090/397] return different history types --- pkg/cmdrunner/cmdrunner.go | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 332a2c541..1e0ceea8e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -22,6 +22,12 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) +const ( + HistoryTypeWindow = "window" + HistoryTypeSession = "session" + HistoryTypeGlobal = "global" +) + const DefaultUserId = "sawka" const MaxNameLen = 50 @@ -927,17 +933,33 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if maxItems == 0 { maxItems = DefaultMaxHistoryItems } - hitems, err := sstore.GetHistoryItems(ctx, ids.SessionId, ids.WindowId, sstore.HistoryQueryOpts{MaxItems: maxItems}) + htype := HistoryTypeWindow + hSessionId := ids.SessionId + hWindowId := ids.WindowId + if pk.Kwargs["type"] != "" { + htype = pk.Kwargs["type"] + if htype != HistoryTypeWindow && htype != HistoryTypeSession && htype != HistoryTypeGlobal { + return nil, fmt.Errorf("invalid history type '%s', valid types: %s", htype, formatStrs([]string{HistoryTypeWindow, HistoryTypeSession, HistoryTypeGlobal}, "or", false)) + } + } + if htype == HistoryTypeGlobal { + hSessionId = "" + hWindowId = "" + } else if htype == HistoryTypeSession { + hWindowId = "" + } + hitems, err := sstore.GetHistoryItems(ctx, hSessionId, hWindowId, sstore.HistoryQueryOpts{MaxItems: maxItems}) if err != nil { return nil, err } show := !resolveBool(pk.Kwargs["noshow"], false) update := &sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - Items: hitems, - Show: show, + HistoryType: htype, + SessionId: ids.SessionId, + WindowId: ids.WindowId, + Items: hitems, + Show: show, } return update, nil } From d55bb8812b5311d76c92e3fac6eee0f9794ce877 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 31 Aug 2022 23:12:26 -0700 Subject: [PATCH 091/397] finishing up historytype and making remotes update --- cmd/main-server.go | 7 ++++++- pkg/cmdrunner/cmdrunner.go | 5 +++++ pkg/remote/remote.go | 2 +- pkg/sstore/updatebus.go | 11 ++++++----- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index a7abf5036..65cbbe222 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -158,7 +158,12 @@ func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Origin") w.Header().Set("Cache-Control", "no-cache") remotes := remote.GetAllRemoteRuntimeState() - WriteJsonSuccess(w, remotes) + ifarr := make([]interface{}, len(remotes)) + for idx, r := range remotes { + ifarr[idx] = r + } + update := sstore.ModelUpdate{Remotes: ifarr} + WriteJsonSuccess(w, update) return } diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1e0ceea8e..d265f600f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -70,6 +70,7 @@ func init() { registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) registerCmdFn("remote:showall", RemoteShowAllCommand) + registerCmdFn("remote:new", RemoteNewCommand) registerCmdFn("history", HistoryCommand) } @@ -360,6 +361,10 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index baa3eaaaa..050ded8a1 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -273,7 +273,7 @@ func (proc *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { func (msh *MShellProc) NotifyUpdate() { rstate := msh.GetRemoteRuntimeState() - update := &sstore.ModelUpdate{Remote: rstate} + update := &sstore.ModelUpdate{Remotes: []interface{}{rstate}} sstore.MainBus.SendUpdate("", update) } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index f1c8ef87c..0e09578db 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -31,7 +31,7 @@ type ModelUpdate struct { Cmd *CmdType `json:"cmd,omitempty"` CmdLine *CmdLineType `json:"cmdline,omitempty"` Info *InfoMsgType `json:"info,omitempty"` - Remote interface{} `json:"remote,omitempty"` // *remote.RemoteState + Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState History *HistoryInfoType `json:"history,omitempty"` } @@ -76,10 +76,11 @@ type InfoMsgType struct { } type HistoryInfoType struct { - SessionId string `json:"sessionid,omitempty"` - WindowId string `json:"windowid,omitempty"` - Items []*HistoryItemType `json:"items"` - Show bool `json:"show"` + HistoryType string `json:"historytype"` + SessionId string `json:"sessionid,omitempty"` + WindowId string `json:"windowid,omitempty"` + Items []*HistoryItemType `json:"items"` + Show bool `json:"show"` } type CmdLineType struct { From 247647cea81e9ba23f08e25d2cc22f07390f41aa Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 1 Sep 2022 12:47:10 -0700 Subject: [PATCH 092/397] remote connect/disconnect working. fix issue with remoteconnected in resolver. working on remote:new --- pkg/cmdrunner/cmdrunner.go | 135 ++++++++++++++++++++++++++++++++++++- pkg/cmdrunner/resolver.go | 6 +- pkg/remote/remote.go | 75 +++++++++++++++------ pkg/sstore/dbops.go | 38 ++++++++--- pkg/sstore/sstore.go | 4 ++ pkg/sstore/updatebus.go | 12 +++- 6 files changed, 236 insertions(+), 34 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d265f600f..b989bc4f1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "os" "path" "path/filepath" "regexp" @@ -30,9 +31,14 @@ const ( const DefaultUserId = "sawka" const MaxNameLen = 50 +const MaxRemoteAliasLen = 50 var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} +var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} +var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") +var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)$") +var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") var wsRe = regexp.MustCompile("\\s+") @@ -71,6 +77,8 @@ func init() { registerCmdFn("remote:show", RemoteShowCommand) registerCmdFn("remote:showall", RemoteShowAllCommand) registerCmdFn("remote:new", RemoteNewCommand) + registerCmdFn("remote:disconnect", RemoteDisconnectCommand) + registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("history", HistoryCommand) } @@ -361,8 +369,124 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + if ids.Remote.RState.IsConnected() { + return sstore.InfoMsgUpdate("remote %q already connected (no action taken)", ids.Remote.DisplayName), nil + } + go ids.Remote.MShell.Launch() + return sstore.InfoMsgUpdate("remote %q reconnecting", ids.Remote.DisplayName), nil +} + +func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + force := resolveBool(pk.Kwargs["force"], false) + if !ids.Remote.RState.IsConnected() && !force { + return sstore.InfoMsgUpdate("remote %q already disconnected (no action taken)", ids.Remote.DisplayName), nil + } + numCommands := ids.Remote.MShell.GetNumRunningCommands() + if numCommands > 0 && !force { + return nil, fmt.Errorf("remote not disconnected, %q has %d running commands. use 'force=1' to force disconnection", ids.Remote.DisplayName) + } + ids.Remote.MShell.Disconnect() + return sstore.InfoMsgUpdate("remote %q disconnected", ids.Remote.DisplayName), nil +} + func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + if len(pk.Args) == 0 || pk.Args[0] == "" { + return nil, fmt.Errorf("/remote:new requires one positional argument of 'user@host'") + } + userHost := pk.Args[0] + m := userHostRe.FindStringSubmatch(userHost) + if m == nil { + return nil, fmt.Errorf("/remote:new invalid format of user@host argument") + } + sudoStr, remoteUser, remoteHost := m[1], m[2], m[3] + alias := pk.Kwargs["alias"] + if alias != "" { + if len(alias) > MaxRemoteAliasLen { + return nil, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen) + } + if !remoteAliasRe.MatchString(alias) { + return nil, fmt.Errorf("invalid alias format") + } + } + connectMode := sstore.ConnectModeStartup + if pk.Kwargs["connectmode"] != "" { + connectMode = pk.Kwargs["connectmode"] + } + if !sstore.IsValidConnectMode(connectMode) { + return nil, fmt.Errorf("/remote:new invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) + } + var isSudo bool + if sudoStr != "" { + isSudo = true + } + if pk.Kwargs["sudo"] != "" { + sudoArg := resolveBool(pk.Kwargs["sudo"], false) + if isSudo && !sudoArg { + return nil, fmt.Errorf("/remote:new invalid 'sudo@' argument, with sudo kw arg set to false") + } + if !isSudo && sudoArg { + isSudo = true + userHost = "sudo@" + userHost + } + } + sshOpts := &sstore.SSHOpts{ + Local: false, + SSHHost: remoteHost, + SSHUser: remoteUser, + } + if pk.Kwargs["key"] != "" { + keyFile := pk.Kwargs["key"] + fd, err := os.Open(keyFile) + if fd != nil { + fd.Close() + } + if err != nil { + return nil, fmt.Errorf("/remote:new invalid key %q (cannot read): %v", keyFile, err) + } + sshOpts.SSHIdentity = keyFile + } + remoteOpts := &sstore.RemoteOptsType{} + if pk.Kwargs["color"] != "" { + color := pk.Kwargs["color"] + err := validateRemoteColor(color, "remote color") + if err != nil { + return nil, err + } + remoteOpts.Color = color + } + r := &sstore.RemoteType{ + RemoteId: uuid.New().String(), + PhysicalId: "", + RemoteType: sstore.RemoteTypeSsh, + RemoteAlias: alias, + RemoteCanonicalName: userHost, + RemoteSudo: isSudo, + RemoteUser: remoteUser, + RemoteHost: remoteHost, + ConnectMode: connectMode, + SSHOpts: sshOpts, + RemoteOpts: remoteOpts, + } + err := sstore.InsertRemote(ctx, r) + if err != nil { + return nil, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err) + } + update := &sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("remote %q created", r.RemoteCanonicalName), + TimeoutMs: 2000, + }, + } + return update, nil } func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -826,6 +950,15 @@ func validateColor(color string, typeStr string) error { return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false)) } +func validateRemoteColor(color string, typeStr string) error { + for _, c := range RemoteColorNames { + if color == c { + return nil + } + } + return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(RemoteColorNames, "or", false)) +} + func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { activate := resolveBool(pk.Kwargs["activate"], true) newName := pk.Kwargs["name"] diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 8a1c5c795..ca3e89fc8 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -18,7 +18,7 @@ const ( R_Screen = 2 R_Window = 4 R_Remote = 8 - R_RemoteConnected = 8 + 16 + R_RemoteConnected = 16 ) type resolvedIds struct { @@ -128,7 +128,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if uictx.Remote != nil && rtn.SessionId != "" && rtn.WindowId != "" { rr, err := resolveRemoteFromPtr(ctx, uictx.Remote, rtn.SessionId, rtn.WindowId) if err != nil { - if rtype&R_Remote > 0 { + if rtype&R_Remote > 0 || rtype&R_RemoteConnected > 0 { return rtn, err } // otherwise just don't set uictx.Remote @@ -146,7 +146,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if rtype&R_Window > 0 && rtn.WindowId == "" { return rtn, fmt.Errorf("no window") } - if rtype&R_Remote > 0 && rtn.Remote == nil { + if (rtype&R_Remote > 0 || rtype&R_RemoteConnected > 0) && rtn.Remote == nil { return rtn, fmt.Errorf("no remote") } if rtype&R_RemoteConnected > 0 { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 050ded8a1..32293816e 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -55,6 +55,20 @@ type Store struct { Map map[string]*MShellProc // key=remoteid } +type MShellProc struct { + Lock *sync.Mutex + Remote *sstore.RemoteType + + // runtime + Status string + ServerProc *shexec.ClientProc + UName string + Err error + ControllingPty *os.File + + RunningCmds []base.CommandKey +} + type RemoteRuntimeState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` @@ -93,20 +107,6 @@ func (state RemoteRuntimeState) GetDisplayName(rptr *sstore.RemotePtrType) strin return name } -type MShellProc struct { - Lock *sync.Mutex - Remote *sstore.RemoteType - - // runtime - Status string - ServerProc *shexec.ClientProc - UName string - Err error - ControllingPty *os.File - - RunningCmds []base.CommandKey -} - func LoadRemotes(ctx context.Context) error { GlobalStore = &Store{ Lock: &sync.Mutex{}, @@ -126,6 +126,28 @@ func LoadRemotes(ctx context.Context) error { return nil } +func LoadRemoteById(ctx context.Context, remoteId string) error { + r, err := sstore.GetRemoteById(ctx, remoteId) + if err != nil { + return err + } + if r == nil { + return fmt.Errorf("remote %s not found", remoteId) + } + msh := MakeMShell(r) + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + existingRemote := GlobalStore.Map[remoteId] + if existingRemote != nil { + return fmt.Errorf("cannot add remote %d, already in global map", remoteId) + } + GlobalStore.Map[r.RemoteId] = msh + if r.ConnectMode == sstore.ConnectModeStartup { + go msh.Launch() + } + return nil +} + func GetRemoteByName(name string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -353,9 +375,21 @@ func (msh *MShellProc) getRemoteCopy() sstore.RemoteType { return *msh.Remote } +func (msh *MShellProc) GetNumRunningCommands() int { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return len(msh.RunningCmds) +} + +func (msh *MShellProc) Disconnect() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.ServerProc.Close() +} + func (msh *MShellProc) Launch() { - remote := msh.getRemoteCopy() - ecmd := convertSSHOpts(remote.SSHOpts).MakeSSHExecCmd(MShellServerCommand) + remoteCopy := msh.getRemoteCopy() + ecmd := convertSSHOpts(remoteCopy.SSHOpts).MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { msh.setErrorStatus(fmt.Errorf("cannot attach controlling tty to mshell command: %w", err)) @@ -366,9 +400,9 @@ func (msh *MShellProc) Launch() { ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() } }() - remoteName := remote.GetName() + remoteName := remoteCopy.GetName() go func() { - fmt.Printf("[c-pty %s] starting...\n", remote.GetName()) + fmt.Printf("[c-pty %s] starting...\n", remoteCopy.GetName()) buf := make([]byte, 100) for { n, readErr := cmdPty.Read(buf) @@ -398,10 +432,10 @@ func (msh *MShellProc) Launch() { }) if err != nil { msh.setErrorStatus(err) - fmt.Printf("[error] connecting remote %s (%s): %v\n", msh.Remote.GetName(), msh.UName, err) + fmt.Printf("[error] connecting remote %s (%s): %v\n", remoteCopy.GetName(), msh.UName, err) return } - fmt.Printf("connected remote %s\n", msh.Remote.GetName()) + fmt.Printf("connected remote %s\n", remoteCopy.GetName()) msh.WithLock(func() { msh.ServerProc = cproc msh.Status = StatusConnected @@ -643,6 +677,7 @@ func (runner *MShellProc) ProcessPackets() { fmt.Printf("[error] calling HUP on remoteid=%d cmds\n", runner.Remote.RemoteId) } runner.notifyHangups_nolock() + go runner.NotifyUpdate() }) dataPosMap := make(map[base.CommandKey]int64) for pk := range runner.ServerProc.Output.MainCh { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 776734376..e747fe544 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -89,17 +89,37 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { if remote == nil { return fmt.Errorf("cannot insert nil remote") } - db, err := GetDB(ctx) - if err != nil { - return err + if remote.RemoteId == "" { + return fmt.Errorf("cannot insert remote without id") } - query := `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts)` - _, err = db.NamedExec(query, remote.ToMap()) - if err != nil { - return err + if remote.RemoteCanonicalName == "" { + return fmt.Errorf("cannot insert remote with canonicalname") } - return nil + if remote.RemoteType == "" { + return fmt.Errorf("cannot insert remote without type") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT remoteid FROM remote WHERE remoteid = ?` + if tx.Exists(query, remote.RemoteId) { + return fmt.Errorf("duplicate remoteid, cannot create") + } + if remote.RemoteAlias != "" { + query = `SELECT remoteid FROM remote WHERE alias = ?` + if tx.Exists(query, remote.RemoteAlias) { + return fmt.Errorf("remote has duplicate alias '%s', cannot create", remote.RemoteAlias) + } + } + query = `SELECT remoteid FROM remote WHERE remotecanonicaname = ?` + if tx.Exists(query, remote.RemoteCanonicalName) { + return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", remote.RemoteCanonicalName) + } + query = `INSERT INTO remote + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts)` + tx.NamedExecWrap(query, remote.ToMap()) + return nil + }) + return txErr } func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index dafdf9660..f67db53d1 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -70,6 +70,10 @@ func GetSessionDBName() string { return path.Join(scHome, DBFileName) } +func IsValidConnectMode(mode string) bool { + return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual +} + func GetDB(ctx context.Context) (*sqlx.DB, error) { if IsTxWrapContext(ctx) { return nil, fmt.Errorf("cannot call GetDB from within a running transaction") diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 0e09578db..56569d32f 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -1,6 +1,9 @@ package sstore -import "sync" +import ( + "fmt" + "sync" +) var MainBus *UpdateBus = MakeUpdateBus() @@ -65,6 +68,13 @@ func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrT return modelUpdate.Line.LineId, modelUpdate.Line.CmdId, rptr } +func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { + msg := fmt.Sprintf(infoMsgFmt, args...) + return &ModelUpdate{ + Info: &InfoMsgType{InfoMsg: msg}, + } +} + type InfoMsgType struct { InfoTitle string `json:"infotitle"` InfoError string `json:"infoerror,omitempty"` From 3639e2454d49b908b2847e0bb7f603d2624af245 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 3 Sep 2022 23:36:15 -0700 Subject: [PATCH 093/397] send termopts.cols to cmdrunner --- pkg/cmdrunner/cmdrunner.go | 11 +++++++- pkg/remote/circlelog.go | 53 ++++++++++++++++++++++++++++++++++++++ pkg/remote/remote.go | 36 +++++++++++++++++++------- pkg/scpacket/scpacket.go | 1 + 4 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 pkg/remote/circlelog.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b989bc4f1..fe1e7ae81 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -169,7 +169,16 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.Env0 = ids.Remote.RemoteState.Env0 runPacket.EnvComplete = true runPacket.UsePty = true - runPacket.TermOpts = &packet.TermOpts{Rows: remote.DefaultTermRows, Cols: remote.DefaultTermCols, Term: remote.DefaultTerm} + runPacket.TermOpts = &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} + if pk.UIContext != nil && pk.UIContext.TermOpts != nil { + pkOpts := pk.UIContext.TermOpts + if pkOpts.Cols > 0 { + runPacket.TermOpts.Cols = base.BoundInt(pkOpts.Cols, shexec.MinTermCols, shexec.MaxTermCols) + } + if pkOpts.MaxPtySize > 0 { + runPacket.TermOpts.MaxPtySize = base.BoundInt64(pkOpts.MaxPtySize, shexec.MinMaxPtySize, shexec.MaxMaxPtySize) + } + } runPacket.Command = strings.TrimSpace(cmdStr) cmd, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) if err != nil { diff --git a/pkg/remote/circlelog.go b/pkg/remote/circlelog.go new file mode 100644 index 000000000..b5d3eb298 --- /dev/null +++ b/pkg/remote/circlelog.go @@ -0,0 +1,53 @@ +package remote + +import ( + "fmt" + "sync" +) + +type CircleLog struct { + Lock *sync.Mutex + StartPos int + Log []string + MaxSize int +} + +func MakeCircleLog(maxSize int) *CircleLog { + if maxSize <= 0 { + panic("invalid maxsize, must be >= 0") + } + rtn := &CircleLog{ + Lock: &sync.Mutex{}, + StartPos: 0, + Log: make([]string, 0, maxSize), + MaxSize: maxSize, + } + return rtn +} + +func (l *CircleLog) Add(s string) { + l.Lock.Lock() + defer l.Lock.Unlock() + if len(l.Log) < l.MaxSize { + l.Log = append(l.Log, s) + return + } + l.Log[l.StartPos] = s + l.StartPos = (l.StartPos + 1) % l.MaxSize +} + +func (l *CircleLog) Addf(sfmt string, args ...interface{}) { + // no lock here, since l.Add() is synchronized + s := fmt.Sprintf(sfmt, args...) + l.Add(s) +} + +func (l *CircleLog) GetEntries() []string { + l.Lock.Lock() + defer l.Lock.Unlock() + rtn := make([]string, len(l.Log)) + for i := 0; i < len(l.Log); i++ { + rtn[i] = l.Log[(l.StartPos+i)%l.MaxSize] + } + return rtn +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 32293816e..09b4bac2f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -25,8 +25,6 @@ import ( ) const RemoteTypeMShell = "mshell" -const DefaultTermRows = 25 -const DefaultTermCols = 80 const DefaultTerm = "xterm-256color" const DefaultMaxPtySize = 1024 * 1024 @@ -53,6 +51,7 @@ var GlobalStore *Store type Store struct { Lock *sync.Mutex Map map[string]*MShellProc // key=remoteid + Log *CircleLog } type MShellProc struct { @@ -82,6 +81,21 @@ type RemoteRuntimeState struct { ConnectMode string `json:"connectmode"` } +func logf(rem *sstore.RemoteType, fmtStr string, args ...interface{}) { + rname := rem.GetName() + str := fmt.Sprintf(fmtStr, args...) + fullStr := fmt.Sprintf("[remote %s] %s", rname, str) + fmt.Printf("%s\n", fullStr) + GlobalStore.Log.Add(fullStr) +} + +func logError(rem *sstore.RemoteType, err error) { + rname := rem.GetName() + fullStr := fmt.Sprintf("[remote %s] error: %v", rname, err) + fmt.Printf("%s\n", fullStr) + GlobalStore.Log.Add(fullStr) +} + func (state RemoteRuntimeState) IsConnected() bool { return state.Status == StatusConnected } @@ -111,6 +125,7 @@ func LoadRemotes(ctx context.Context) error { GlobalStore = &Store{ Lock: &sync.Mutex{}, Map: make(map[string]*MShellProc), + Log: MakeCircleLog(100), } allRemotes, err := sstore.GetAllRemotes(ctx) if err != nil { @@ -389,10 +404,13 @@ func (msh *MShellProc) Disconnect() { func (msh *MShellProc) Launch() { remoteCopy := msh.getRemoteCopy() + logf(&remoteCopy, "starting launch") ecmd := convertSSHOpts(remoteCopy.SSHOpts).MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { - msh.setErrorStatus(fmt.Errorf("cannot attach controlling tty to mshell command: %w", err)) + statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) + logError(&remoteCopy, statusErr) + msh.setErrorStatus(statusErr) return } defer func() { @@ -402,7 +420,6 @@ func (msh *MShellProc) Launch() { }() remoteName := remoteCopy.GetName() go func() { - fmt.Printf("[c-pty %s] starting...\n", remoteCopy.GetName()) buf := make([]byte, 100) for { n, readErr := cmdPty.Read(buf) @@ -410,7 +427,7 @@ func (msh *MShellProc) Launch() { break } if readErr != nil { - fmt.Printf("[error] read from controlling-pty [%s]: %v\n", remoteName, readErr) + logf(&remoteCopy, "error: reading from controlling-pty: %v", readErr) break } readStr := string(buf[0:n]) @@ -432,7 +449,7 @@ func (msh *MShellProc) Launch() { }) if err != nil { msh.setErrorStatus(err) - fmt.Printf("[error] connecting remote %s (%s): %v\n", remoteCopy.GetName(), msh.UName, err) + logf(&remoteCopy, "error connecting remote (%s): %v", msh.UName, err) return } fmt.Printf("connected remote %s\n", remoteCopy.GetName()) @@ -448,6 +465,7 @@ func (msh *MShellProc) Launch() { msh.Status = StatusDisconnected } }) + logf(&remoteCopy, "remote disconnected exitcode=%d", exitCode) fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) }() go msh.ProcessPackets() @@ -521,8 +539,8 @@ func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { return msh.ServerProc.Input.SendPacket(dataPk) } -func makeTermOpts() sstore.TermOpts { - return sstore.TermOpts{Rows: DefaultTermRows, Cols: DefaultTermCols, FlexRows: true, MaxPtySize: DefaultMaxPtySize} +func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { + return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: true, MaxPtySize: DefaultMaxPtySize} } func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { @@ -569,7 +587,7 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp CmdStr: runPacket.Command, Remote: remotePtr, RemoteState: *remoteState, - TermOpts: makeTermOpts(), + TermOpts: makeTermOpts(runPacket), Status: status, StartPk: startPk, DonePk: nil, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index f1a932a7e..8bb4fb91f 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -26,6 +26,7 @@ type UIContextType struct { ScreenId string `json:"screenid"` WindowId string `json:"windowid"` Remote *sstore.RemotePtrType `json:"remote,omitempty"` + TermOpts *packet.TermOpts `json:"termopts,omitempty"` } type FeInputPacketType struct { From c3961891ce33e3caa6b3df8b9fd04d43aa2a2e0f Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 3 Sep 2022 23:57:05 -0700 Subject: [PATCH 094/397] pass realoffset back in header for /api/ptyout --- cmd/main-server.go | 4 +++- pkg/sstore/fileops.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 65cbbe222..2cdea3693 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "runtime/debug" + "strconv" "strings" "sync" "time" @@ -216,7 +217,7 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) return } - _, data, err := sstore.ReadFullPtyOutFile(r.Context(), sessionId, cmdId) + realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), sessionId, cmdId) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusOK) @@ -226,6 +227,7 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("error reading ptyout file: %v", err))) return } + w.Header().Set("X-PtyDataOffset", strconv.FormatInt(realOffset, 10)) w.WriteHeader(http.StatusOK) w.Write(data) } diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index e55002600..f7de7a8b5 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -50,6 +50,7 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat return nil } +// returns (offset, data, err) func ReadFullPtyOutFile(ctx context.Context, sessionId string, cmdId string) (int64, []byte, error) { ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) if err != nil { From 74953c71ac1eb0b34526a98a236bf8ac83fdb7b1 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 4 Sep 2022 13:51:49 -0700 Subject: [PATCH 095/397] more remote notifications (to keep FE in sync) --- pkg/remote/remote.go | 110 +++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 09b4bac2f..64cbf6cfc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -246,55 +246,55 @@ func makeShortHost(host string) string { return host[0:dotIdx] } -func (proc *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { - proc.Lock.Lock() - defer proc.Lock.Unlock() +func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { + msh.Lock.Lock() + defer msh.Lock.Unlock() state := RemoteRuntimeState{ - RemoteType: proc.Remote.RemoteType, - RemoteId: proc.Remote.RemoteId, - RemoteAlias: proc.Remote.RemoteAlias, - RemoteCanonicalName: proc.Remote.RemoteCanonicalName, - PhysicalId: proc.Remote.PhysicalId, - Status: proc.Status, - ConnectMode: proc.Remote.ConnectMode, + RemoteType: msh.Remote.RemoteType, + RemoteId: msh.Remote.RemoteId, + RemoteAlias: msh.Remote.RemoteAlias, + RemoteCanonicalName: msh.Remote.RemoteCanonicalName, + PhysicalId: msh.Remote.PhysicalId, + Status: msh.Status, + ConnectMode: msh.Remote.ConnectMode, } - if proc.Err != nil { - state.ErrorStr = proc.Err.Error() + if msh.Err != nil { + state.ErrorStr = msh.Err.Error() } - local := (proc.Remote.SSHOpts == nil || proc.Remote.SSHOpts.Local) + local := (msh.Remote.SSHOpts == nil || msh.Remote.SSHOpts.Local) vars := make(map[string]string) - vars["user"] = proc.Remote.RemoteUser + vars["user"] = msh.Remote.RemoteUser vars["bestuser"] = vars["user"] - vars["host"] = proc.Remote.RemoteHost - vars["shorthost"] = makeShortHost(proc.Remote.RemoteHost) - vars["alias"] = proc.Remote.RemoteAlias - vars["cname"] = proc.Remote.RemoteCanonicalName - vars["physicalid"] = proc.Remote.PhysicalId - vars["remoteid"] = proc.Remote.RemoteId - vars["status"] = proc.Status - vars["type"] = proc.Remote.RemoteType - if proc.Remote.RemoteSudo { + vars["host"] = msh.Remote.RemoteHost + vars["shorthost"] = makeShortHost(msh.Remote.RemoteHost) + vars["alias"] = msh.Remote.RemoteAlias + vars["cname"] = msh.Remote.RemoteCanonicalName + vars["physicalid"] = msh.Remote.PhysicalId + vars["remoteid"] = msh.Remote.RemoteId + vars["status"] = msh.Status + vars["type"] = msh.Remote.RemoteType + if msh.Remote.RemoteSudo { vars["sudo"] = "1" } if local { vars["local"] = "1" } - if proc.ServerProc != nil && proc.ServerProc.InitPk != nil { + if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{ - Cwd: proc.ServerProc.InitPk.Cwd, - Env0: proc.ServerProc.InitPk.Env0, + Cwd: msh.ServerProc.InitPk.Cwd, + Env0: msh.ServerProc.InitPk.Env0, } - vars["home"] = proc.ServerProc.InitPk.HomeDir - vars["remoteuser"] = proc.ServerProc.InitPk.User + vars["home"] = msh.ServerProc.InitPk.HomeDir + vars["remoteuser"] = msh.ServerProc.InitPk.User vars["bestuser"] = vars["remoteuser"] - vars["remotehost"] = proc.ServerProc.InitPk.HostName - vars["remoteshorthost"] = makeShortHost(proc.ServerProc.InitPk.HostName) + vars["remotehost"] = msh.ServerProc.InitPk.HostName + vars["remoteshorthost"] = makeShortHost(msh.ServerProc.InitPk.HostName) vars["besthost"] = vars["remotehost"] vars["bestshorthost"] = vars["remoteshorthost"] } - if local && proc.Remote.RemoteSudo { + if local && msh.Remote.RemoteSudo { vars["bestuser"] = "sudo" - } else if proc.Remote.RemoteSudo { + } else if msh.Remote.RemoteSudo { vars["bestuser"] = "sudo@" + vars["bestuser"] } if local { @@ -382,6 +382,7 @@ func (msh *MShellProc) setErrorStatus(err error) { defer msh.Lock.Unlock() msh.Status = StatusError msh.Err = err + go msh.NotifyUpdate() } func (msh *MShellProc) getRemoteCopy() sstore.RemoteType { @@ -446,6 +447,7 @@ func (msh *MShellProc) Launch() { cproc, uname, err := shexec.MakeClientProc(ecmd) msh.WithLock(func() { msh.UName = uname + // no notify here, because we'll call notify in either case below }) if err != nil { msh.setErrorStatus(err) @@ -456,6 +458,7 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { msh.ServerProc = cproc msh.Status = StatusConnected + go msh.NotifyUpdate() }) go func() { exitErr := cproc.Cmd.Wait() @@ -463,10 +466,10 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { if msh.Status == StatusConnected { msh.Status = StatusDisconnected + go msh.NotifyUpdate() } }) logf(&remoteCopy, "remote disconnected exitcode=%d", exitCode) - fmt.Printf("[error] RUNNER PROC EXITED code[%d]\n", exitCode) }() go msh.ProcessPackets() return @@ -631,9 +634,9 @@ func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) ( return nil, fmt.Errorf("invalid response packet received: %s", packet.AsString(rtnPk)) } -func (runner *MShellProc) WithLock(fn func()) { - runner.Lock.Lock() - defer runner.Lock.Unlock() +func (msh *MShellProc) WithLock(fn func()) { + msh.Lock.Lock() + defer msh.Lock.Unlock() fn() } @@ -685,26 +688,26 @@ func (msh *MShellProc) notifyHangups_nolock() { msh.RunningCmds = nil } -func (runner *MShellProc) ProcessPackets() { - defer runner.WithLock(func() { - if runner.Status == StatusConnected { - runner.Status = StatusDisconnected +func (msh *MShellProc) ProcessPackets() { + defer msh.WithLock(func() { + if msh.Status == StatusConnected { + msh.Status = StatusDisconnected } - err := sstore.HangupRunningCmdsByRemoteId(context.Background(), runner.Remote.RemoteId) + err := sstore.HangupRunningCmdsByRemoteId(context.Background(), msh.Remote.RemoteId) if err != nil { - fmt.Printf("[error] calling HUP on remoteid=%d cmds\n", runner.Remote.RemoteId) + logf(msh.Remote, "calling HUP on cmds %v", err) } - runner.notifyHangups_nolock() - go runner.NotifyUpdate() + msh.notifyHangups_nolock() + go msh.NotifyUpdate() }) dataPosMap := make(map[base.CommandKey]int64) - for pk := range runner.ServerProc.Output.MainCh { + for pk := range msh.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) if err != nil { ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) - runner.ServerProc.Input.SendPacket(ack) + msh.ServerProc.Input.SendPacket(ack) continue } var ack *packet.DataAckPacketType @@ -719,22 +722,27 @@ func (runner *MShellProc) ProcessPackets() { dataPosMap[dataPk.CK] += int64(len(realData)) } if ack != nil { - runner.ServerProc.Input.SendPacket(ack) + msh.ServerProc.Input.SendPacket(ack) } // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) continue } + if pk.GetType() == packet.DataAckPacketStr { + // TODO process ack (need to keep track of buffer size for sending) + // this is low priority though since most input is coming from keyboard and won't overflow this buffer + continue + } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) continue } if pk.GetType() == packet.CmdDonePacketStr { - runner.handleCmdDonePacket(pk.(*packet.CmdDonePacketType)) + msh.handleCmdDonePacket(pk.(*packet.CmdDonePacketType)) continue } if pk.GetType() == packet.CmdErrorPacketStr { - runner.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType)) + msh.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType)) continue } if pk.GetType() == packet.MessagePacketStr { @@ -749,10 +757,10 @@ func (runner *MShellProc) ProcessPackets() { } if pk.GetType() == packet.CmdStartPacketStr { startPk := pk.(*packet.CmdStartPacketType) - fmt.Printf("start> reqid=%s (%p)\n", startPk.RespId, runner.ServerProc.Output) + fmt.Printf("start> reqid=%s (%p)\n", startPk.RespId, msh.ServerProc.Output) continue } - fmt.Printf("MSH> %s\n", packet.AsString(pk)) + fmt.Printf("MSH> unhandled packet %s\n", packet.AsString(pk)) } } From b980fd6b743ba0172f4914108624afd138a91fe0 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Sep 2022 12:42:09 -0700 Subject: [PATCH 096/397] on ws connect, send sessionlist and remotelist --- cmd/main-server.go | 120 --------------------------------------- pkg/scpacket/scpacket.go | 26 +++++---- pkg/scws/scws.go | 75 ++++++++++++++++++------ pkg/sstore/updatebus.go | 3 + pkg/wsshell/wsshell.go | 2 +- 5 files changed, 76 insertions(+), 150 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 2cdea3693..e7b91b3fd 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -90,7 +90,6 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { defer func() { removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime) }() - shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error fmt.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr) state.RunWSRead() } @@ -117,57 +116,6 @@ func writeToFifo(fifoName string, data []byte) error { return nil } -// params: sessionid -func HandleGetSession(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") - if sessionId == "" { - WriteJsonError(w, fmt.Errorf("must specify a sessionid")) - return - } - session, err := sstore.GetSessionById(r.Context(), sessionId) - if err != nil { - WriteJsonError(w, err) - return - } - WriteJsonSuccess(w, session) - return -} - -func HandleGetAllSessions(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - list, err := sstore.GetAllSessions(r.Context()) - if err != nil { - WriteJsonError(w, fmt.Errorf("cannot get all sessions: %w", err)) - return - } - WriteJsonSuccess(w, list) - return -} - -// params: [none] -func HandleGetRemotes(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - remotes := remote.GetAllRemoteRuntimeState() - ifarr := make([]interface{}, len(remotes)) - for idx, r := range remotes { - ifarr[idx] = r - } - update := sstore.ModelUpdate{Remotes: ifarr} - WriteJsonSuccess(w, update) - return -} - // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -295,72 +243,6 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { return } -// /api/start-session -// returns: -// * userid -// * sessionid -// -// /api/ptyout (pos=[position]) - returns contents of ptyout file -// params: -// * sessionid -// * cmdid -// * pos -// returns: -// * stream of ptyout file (text, utf-8) -// -// POST /api/run-command -// params -// * userid -// * sessionid -// returns -// * cmdid -// -// /api/refresh-session -// params -// * sessionid -// * start -- can be negative -// * numlines -// returns -// * permissions (readonly, comment, command) -// * lines -// * lineid -// * ts -// * userid -// * linetype -// * text -// * cmdid - -// /ws -// ->watch-session: -// * sessionid -// ->watch: -// * sessionid -// * cmdid -// ->focus: -// * sessionid -// * cmdid -// ->input: -// * sessionid -// * cmdid -// * data -// ->signal: -// * sessionid -// * cmdid -// * data -// <-data: -// * sessionid -// * cmdid -// * pos -// * data -// <-session-data: -// * sessionid -// * line - -// session-doc -// timestamp | user | cmd-type | data -// cmd-type = comment -// cmd-type = command, commandid=ABC - func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) @@ -438,9 +320,7 @@ func main() { go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) - gr.HandleFunc("/api/get-all-sessions", HandleGetAllSessions) gr.HandleFunc("/api/get-window", HandleGetWindow) - gr.HandleFunc("/api/get-remotes", HandleGetRemotes) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ Addr: MainServerAddr, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 8bb4fb91f..c496ed815 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -13,12 +13,13 @@ const WatchScreenPacketStr = "watchscreen" const FeInputPacketStr = "feinput" type FeCommandPacketType struct { - Type string `json:"type"` - MetaCmd string `json:"metacmd"` - MetaSubCmd string `json:"metasubcmd,omitempty"` - Args []string `json:"args,omitempty"` - Kwargs map[string]string `json:"kwargs,omitempty"` - UIContext *UIContextType `json:"uicontext,omitempty"` + Type string `json:"type"` + MetaCmd string `json:"metacmd"` + MetaSubCmd string `json:"metasubcmd,omitempty"` + Args []string `json:"args,omitempty"` + Kwargs map[string]string `json:"kwargs,omitempty"` + UIContext *UIContextType `json:"uicontext,omitempty"` + Interactive bool `json:"interactive"` } type UIContextType struct { @@ -39,6 +40,13 @@ type FeInputPacketType struct { WinSizeCols int `json:"winsizecols"` } +type WatchScreenPacketType struct { + Type string `json:"type"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + Connect bool `json:"connect"` +} + func init() { packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{})) packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{})) @@ -72,12 +80,6 @@ func (p *FeInputPacketType) ConvertToInputPacket() *packet.InputPacketType { return rtn } -type WatchScreenPacketType struct { - Type string `json:"type"` - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` -} - func (*WatchScreenPacketType) GetType() string { return WatchScreenPacketStr } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index d8a810bcb..917457b1c 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -1,6 +1,7 @@ package scws import ( + "context" "fmt" "sync" "time" @@ -75,7 +76,7 @@ func (ws *WSState) WatchScreen(sessionId string, screenId string) { ws.SessionId = sessionId ws.ScreenId = screenId ws.UpdateCh = sstore.MainBus.RegisterChannel(ws.ClientId, ws.SessionId) - go ws.RunUpdates() + go ws.RunUpdates(ws.UpdateCh) } func (ws *WSState) UnWatchScreen() { @@ -84,6 +85,7 @@ func (ws *WSState) UnWatchScreen() { sstore.MainBus.UnregisterChannel(ws.ClientId) ws.SessionId = "" ws.ScreenId = "" + fmt.Printf("[ws] unwatch screen clientid=%s\n", ws.ClientId) } func (ws *WSState) getUpdateCh() chan interface{} { @@ -92,10 +94,9 @@ func (ws *WSState) getUpdateCh() chan interface{} { return ws.UpdateCh } -func (ws *WSState) RunUpdates() { - updateCh := ws.getUpdateCh() +func (ws *WSState) RunUpdates(updateCh chan interface{}) { if updateCh == nil { - return + panic("invalid nil updateCh passed to RunUpdates") } for update := range updateCh { shell := ws.GetShell() @@ -117,11 +118,60 @@ func (ws *WSState) ReplaceShell(shell *wsshell.WSShell) { return } +func (ws *WSState) handleConnection() error { + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + update, err := sstore.GetAllSessions(ctx) + if err != nil { + return fmt.Errorf("getting sessions: %w", err) + } + remotes := remote.GetAllRemoteRuntimeState() + ifarr := make([]interface{}, len(remotes)) + for idx, r := range remotes { + ifarr[idx] = r + } + update.Remotes = ifarr + update.Connect = true + err = ws.Shell.WriteJson(update) + if err != nil { + return err + } + return nil +} + +func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error { + if wsPk.SessionId != "" { + if _, err := uuid.Parse(wsPk.SessionId); err != nil { + return fmt.Errorf("invalid watchscreen sessionid: %w", err) + } + } + if wsPk.ScreenId != "" { + if _, err := uuid.Parse(wsPk.ScreenId); err != nil { + return fmt.Errorf("invalid watchscreen screenid: %w", err) + } + } + if wsPk.SessionId == "" || wsPk.ScreenId == "" { + ws.UnWatchScreen() + } else { + ws.WatchScreen(wsPk.SessionId, wsPk.ScreenId) + fmt.Printf("[ws %s] watchscreen %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) + } + if wsPk.Connect { + fmt.Printf("[ws %s] watchscreen connect\n", ws.ClientId) + err := ws.handleConnection() + if err != nil { + return fmt.Errorf("connect: %w", err) + } + } + return nil +} + func (ws *WSState) RunWSRead() { shell := ws.GetShell() if shell == nil { return } + shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error for msgBytes := range shell.ReadChan { pk, err := packet.ParseJsonPacket(msgBytes) if err != nil { @@ -149,20 +199,11 @@ func (ws *WSState) RunWSRead() { } if pk.GetType() == "watchscreen" { wsPk := pk.(*scpacket.WatchScreenPacketType) - if _, err := uuid.Parse(wsPk.SessionId); err != nil { - fmt.Printf("[error] invalid watchscreen sessionid: %v\n", err) - continue + err := ws.handleWatchScreen(wsPk) + if err != nil { + // TODO send errors back to client, likely unrecoverable + fmt.Printf("[ws %s] error %v\n", err) } - if wsPk.ScreenId == "" { - ws.UnWatchScreen() - continue - } - if _, err := uuid.Parse(wsPk.ScreenId); err != nil { - fmt.Printf("[error] invalid watchscreen screenid: %v\n", err) - continue - } - ws.WatchScreen(wsPk.SessionId, wsPk.ScreenId) - fmt.Printf("[ws] watch screen clientid=%s %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) continue } fmt.Printf("got ws bad message: %v\n", pk.GetType()) diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 56569d32f..69dece4d1 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -36,6 +36,8 @@ type ModelUpdate struct { Info *InfoMsgType `json:"info,omitempty"` Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState History *HistoryInfoType `json:"history,omitempty"` + Interactive bool `json:"interactive"` + Connect bool `json:"connect,omitempty"` } func (ModelUpdate) UpdateType() string { @@ -123,6 +125,7 @@ func MakeUpdateBus() *UpdateBus { } } +// always returns a new channel func (bus *UpdateBus) RegisterChannel(clientId string, sessionId string) chan interface{} { bus.Lock.Lock() defer bus.Lock.Unlock() diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index 8417e81a6..3bffac1fe 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -20,7 +20,7 @@ const initialPingTime = 1 * time.Second var upgrader = websocket.Upgrader{ ReadBufferSize: 4 * 1024, - WriteBufferSize: 4 * 1024, + WriteBufferSize: 32 * 1024, HandshakeTimeout: 1 * time.Second, CheckOrigin: func(r *http.Request) bool { return true }, } From 54e0ecffe149d0fe7dce241c4896614a89724a34 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Sep 2022 14:49:23 -0700 Subject: [PATCH 097/397] create a remote update queue to ensure that we send the line update before we send cmd updates --- pkg/cmdrunner/cmdrunner.go | 5 +- pkg/remote/remote.go | 150 ++++++++++++++++++++----------------- pkg/remote/updatequeue.go | 66 ++++++++++++++++ pkg/sstore/fileops.go | 13 ++-- 4 files changed, 157 insertions(+), 77 deletions(-) create mode 100644 pkg/remote/updatequeue.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index fe1e7ae81..4e46e7626 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -180,7 +180,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } } runPacket.Command = strings.TrimSpace(cmdStr) - cmd, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) + cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) + if callback != nil { + defer callback() + } if err != nil { return nil, err } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 64cbf6cfc..f6eeedab3 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -49,9 +49,10 @@ const ( var GlobalStore *Store type Store struct { - Lock *sync.Mutex - Map map[string]*MShellProc // key=remoteid - Log *CircleLog + Lock *sync.Mutex + Map map[string]*MShellProc // key=remoteid + Log *CircleLog + CmdWaitMap map[base.CommandKey][]sstore.UpdatePacket } type MShellProc struct { @@ -123,9 +124,10 @@ func (state RemoteRuntimeState) GetDisplayName(rptr *sstore.RemotePtrType) strin func LoadRemotes(ctx context.Context) error { GlobalStore = &Store{ - Lock: &sync.Mutex{}, - Map: make(map[string]*MShellProc), - Log: MakeCircleLog(100), + Lock: &sync.Mutex{}, + Map: make(map[string]*MShellProc), + Log: MakeCircleLog(100), + CmdWaitMap: make(map[base.CommandKey][]sstore.UpdatePacket), } allRemotes, err := sstore.GetAllRemotes(ctx) if err != nil { @@ -308,7 +310,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { return state } -func (msh *MShellProc) NotifyUpdate() { +func (msh *MShellProc) NotifyRemoteUpdate() { rstate := msh.GetRemoteRuntimeState() update := &sstore.ModelUpdate{Remotes: []interface{}{rstate}} sstore.MainBus.SendUpdate("", update) @@ -382,7 +384,7 @@ func (msh *MShellProc) setErrorStatus(err error) { defer msh.Lock.Unlock() msh.Status = StatusError msh.Err = err - go msh.NotifyUpdate() + go msh.NotifyRemoteUpdate() } func (msh *MShellProc) getRemoteCopy() sstore.RemoteType { @@ -458,7 +460,7 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { msh.ServerProc = cproc msh.Status = StatusConnected - go msh.NotifyUpdate() + go msh.NotifyRemoteUpdate() }) go func() { exitErr := cproc.Cmd.Wait() @@ -466,7 +468,7 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { if msh.Status == StatusConnected { msh.Status = StatusDisconnected - go msh.NotifyUpdate() + go msh.NotifyRemoteUpdate() } }) logf(&remoteCopy, "remote disconnected exitcode=%d", exitCode) @@ -546,39 +548,40 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: true, MaxPtySize: DefaultMaxPtySize} } -func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, error) { +// returns (cmdtype, allow-updates-callback, err) +func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, func(), error) { if remotePtr.OwnerId != "" { - return nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) + return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } msh := GetRemoteById(remotePtr.RemoteId) if msh == nil { - return nil, fmt.Errorf("no remote id=%s found", remotePtr.RemoteId) + return nil, nil, fmt.Errorf("no remote id=%s found", remotePtr.RemoteId) } if !msh.IsConnected() { - return nil, fmt.Errorf("remote '%s' is not connected", remotePtr.RemoteId) + return nil, nil, fmt.Errorf("remote '%s' is not connected", remotePtr.RemoteId) } if remoteState == nil { - return nil, fmt.Errorf("no remote state passed to RunCommand") + return nil, nil, fmt.Errorf("no remote state passed to RunCommand") } msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { - return nil, fmt.Errorf("sending run packet to remote: %w", err) + return nil, nil, fmt.Errorf("sending run packet to remote: %w", err) } rtnPk := msh.ServerProc.Output.WaitForResponse(ctx, runPacket.ReqId) if rtnPk == nil { - return nil, ctx.Err() + return nil, nil, ctx.Err() } startPk, ok := rtnPk.(*packet.CmdStartPacketType) if !ok { respPk, ok := rtnPk.(*packet.ResponsePacketType) if !ok { - return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) + return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } if respPk.Error != "" { - return nil, errors.New(respPk.Error) + return nil, nil, errors.New(respPk.Error) } - return nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) + return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk)) } status := sstore.CmdStatusRunning if runPacket.Detached { @@ -598,10 +601,11 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp } err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { - return nil, err + // TODO the cmd is running, so this is a tricky error to handle + return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) } msh.AddRunningCmd(startPk.CK) - return cmd, nil + return cmd, func() { removeCmdWait(startPk.CK) }, nil } func (msh *MShellProc) AddRunningCmd(ck base.CommandKey) { @@ -651,31 +655,6 @@ func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *pa return ack } -func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { - update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) - if err != nil { - fmt.Printf("[error] updating cmddone: %v\n", err) - return - } - if update != nil { - // TODO fix timing issue (this update gets to the FE before run-command returns for short lived commands) - go func() { - time.Sleep(10 * time.Millisecond) - sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) - }() - } - return -} - -func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { - err := sstore.AppendCmdErrorPk(context.Background(), errPk) - if err != nil { - fmt.Printf("[error] adding cmderr: %v\n", err) - return - } - return -} - func (msh *MShellProc) notifyHangups_nolock() { for _, ck := range msh.RunningCmds { cmd, err := sstore.GetCmdById(context.Background(), ck.GetSessionId(), ck.GetCmdId()) @@ -688,6 +667,59 @@ func (msh *MShellProc) notifyHangups_nolock() { msh.RunningCmds = nil } +func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { + update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) + if err != nil { + fmt.Printf("[error] updating cmddone: %v\n", err) + return + } + if update != nil { + // TODO fix timing issue (this update gets to the FE before run-command returns for short lived commands) + go func() { + time.Sleep(10 * time.Millisecond) + sendCmdUpdate(donePk.CK, update) + }() + } + return +} + +// TODO notify FE about cmd errors +func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { + err := sstore.AppendCmdErrorPk(context.Background(), errPk) + if err != nil { + fmt.Printf("[error] adding cmderr: %v\n", err) + return + } + return +} + +func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) { + realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) + if err != nil { + ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) + msh.ServerProc.Input.SendPacket(ack) + return + } + var ack *packet.DataAckPacketType + if len(realData) > 0 { + dataPos := dataPosMap[dataPk.CK] + update, err := sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, dataPos) + if err != nil { + ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) + } else { + ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) + } + dataPosMap[dataPk.CK] += int64(len(realData)) + if update != nil { + sendCmdUpdate(dataPk.CK, update) + } + } + if ack != nil { + msh.ServerProc.Input.SendPacket(ack) + } + // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) +} + func (msh *MShellProc) ProcessPackets() { defer msh.WithLock(func() { if msh.Status == StatusConnected { @@ -698,33 +730,13 @@ func (msh *MShellProc) ProcessPackets() { logf(msh.Remote, "calling HUP on cmds %v", err) } msh.notifyHangups_nolock() - go msh.NotifyUpdate() + go msh.NotifyRemoteUpdate() }) dataPosMap := make(map[base.CommandKey]int64) for pk := range msh.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) - realData, err := base64.StdEncoding.DecodeString(dataPk.Data64) - if err != nil { - ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) - msh.ServerProc.Input.SendPacket(ack) - continue - } - var ack *packet.DataAckPacketType - if len(realData) > 0 { - dataPos := dataPosMap[dataPk.CK] - err = sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, dataPos) - if err != nil { - ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) - } else { - ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) - } - dataPosMap[dataPk.CK] += int64(len(realData)) - } - if ack != nil { - msh.ServerProc.Input.SendPacket(ack) - } - // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) + msh.handleDataPacket(dataPk, dataPosMap) continue } if pk.GetType() == packet.DataAckPacketStr { diff --git a/pkg/remote/updatequeue.go b/pkg/remote/updatequeue.go new file mode 100644 index 000000000..105bb61fb --- /dev/null +++ b/pkg/remote/updatequeue.go @@ -0,0 +1,66 @@ +package remote + +import ( + "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +func pushCmdWaitIfRequired(ck base.CommandKey, update sstore.UpdatePacket) bool { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + updates, ok := GlobalStore.CmdWaitMap[ck] + if !ok { + return false + } + updates = append(updates, update) + GlobalStore.CmdWaitMap[ck] = updates + return true +} + +func sendCmdUpdate(ck base.CommandKey, update sstore.UpdatePacket) { + pushed := pushCmdWaitIfRequired(ck, update) + if pushed { + return + } + sstore.MainBus.SendUpdate(ck.GetSessionId(), update) +} + +func runCmdWaitUpdates(ck base.CommandKey) { + for { + update := removeFirstCmdWaitUpdate(ck) + if update == nil { + break + } + sstore.MainBus.SendUpdate(ck.GetSessionId(), update) + } +} + +func removeFirstCmdWaitUpdate(ck base.CommandKey) sstore.UpdatePacket { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + + updates := GlobalStore.CmdWaitMap[ck] + if len(updates) == 0 { + delete(GlobalStore.CmdWaitMap, ck) + return nil + } + if len(updates) == 1 { + delete(GlobalStore.CmdWaitMap, ck) + return updates[0] + } + update := updates[0] + GlobalStore.CmdWaitMap[ck] = updates[1:] + return update +} + +func removeCmdWait(ck base.CommandKey) { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + + updates := GlobalStore.CmdWaitMap[ck] + if len(updates) == 0 { + delete(GlobalStore.CmdWaitMap, ck) + return + } + go runCmdWaitUpdates(ck) +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index f7de7a8b5..0ecf9aa02 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -21,22 +21,22 @@ func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSi return f.Close() } -func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) error { +func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { if pos < 0 { - return fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) + return nil, fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) } ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) if err != nil { - return err + return nil, err } f, err := cirfile.OpenCirFile(ptyOutFileName) if err != nil { - return err + return nil, err } defer f.Close() err = f.WriteAt(ctx, data, pos) if err != nil { - return err + return nil, err } data64 := base64.StdEncoding.EncodeToString(data) update := &PtyDataUpdate{ @@ -46,8 +46,7 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat PtyData64: data64, PtyDataLen: int64(len(data)), } - MainBus.SendUpdate(sessionId, update) - return nil + return update, nil } // returns (offset, data, err) From a20ee78e6d33a83615925861c16a3e1bc84476e4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Sep 2022 14:54:17 -0700 Subject: [PATCH 098/397] send line update via websocket synchronously before calling the cmdwait callback. ensures line arrives on fe client before any command updates or pty updates --- pkg/cmdrunner/cmdrunner.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 4e46e7626..f74c8a049 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -191,7 +191,9 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } - return sstore.ModelUpdate{Line: rtnLine, Cmd: cmd}, nil + update := sstore.ModelUpdate{Line: rtnLine, Cmd: cmd, Interactive: pk.Interactive} + sstore.MainBus.SendUpdate(ids.SessionId, update) + return nil, nil } func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, isMetaCmd bool, hadError bool) error { From 54d2f5d76183b91184c1c3474b4a9a6263a45463 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Sep 2022 16:31:22 -0700 Subject: [PATCH 099/397] queue entire functions when cmdline has not been committed to DB yet. new inputpacket handling (for winsize) --- cmd/main-server.go | 13 +++++++++++ pkg/remote/remote.go | 37 ++++++++++++++++++-------------- pkg/remote/updatequeue.go | 45 +++++++++++++++++++-------------------- pkg/scpacket/scpacket.go | 14 +----------- pkg/scws/scws.go | 34 +++++++++++++++++------------ 5 files changed, 77 insertions(+), 66 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index e7b91b3fd..5add214a1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -261,7 +261,20 @@ func runWebSocketServer() { } } +func test() error { + return nil +} + func main() { + if len(os.Args) >= 2 && os.Args[1] == "--test" { + fmt.Printf("running test fn\n") + err := test() + if err != nil { + fmt.Printf("[error] %v\n", err) + } + return + } + scLock, err := scbase.AcquireSCLock() if err != nil || scLock == nil { fmt.Printf("[error] cannot acquire sh2 lock: %v\n", err) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index f6eeedab3..ffebfa837 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -52,7 +52,7 @@ type Store struct { Lock *sync.Mutex Map map[string]*MShellProc // key=remoteid Log *CircleLog - CmdWaitMap map[base.CommandKey][]sstore.UpdatePacket + CmdWaitMap map[base.CommandKey][]func() } type MShellProc struct { @@ -127,7 +127,7 @@ func LoadRemotes(ctx context.Context) error { Lock: &sync.Mutex{}, Map: make(map[string]*MShellProc), Log: MakeCircleLog(100), - CmdWaitMap: make(map[base.CommandKey][]sstore.UpdatePacket), + CmdWaitMap: make(map[base.CommandKey][]func()), } allRemotes, err := sstore.GetAllRemotes(ctx) if err != nil { @@ -530,17 +530,13 @@ func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { return false } -func (msh *MShellProc) SendInput(pk *packet.InputPacketType) error { +func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error { if !msh.IsConnected() { return fmt.Errorf("remote is not connected, cannot send input") } - if !msh.IsCmdRunning(pk.CK) { + if !msh.IsCmdRunning(dataPk.CK) { return fmt.Errorf("cannot send input, cmd is not running") } - dataPk := packet.MakeDataPacket() - dataPk.CK = pk.CK - dataPk.FdNum = 0 // stdin - dataPk.Data64 = pk.InputData64 return msh.ServerProc.Input.SendPacket(dataPk) } @@ -674,11 +670,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { return } if update != nil { - // TODO fix timing issue (this update gets to the FE before run-command returns for short lived commands) - go func() { - time.Sleep(10 * time.Millisecond) - sendCmdUpdate(donePk.CK, update) - }() + sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) } return } @@ -711,7 +703,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa } dataPosMap[dataPk.CK] += int64(len(realData)) if update != nil { - sendCmdUpdate(dataPk.CK, update) + sstore.MainBus.SendUpdate(dataPk.CK.GetSessionId(), update) } } if ack != nil { @@ -720,6 +712,18 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) } +func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) func() { + return func() { + msh.handleDataPacket(dataPk, dataPosMap) + } +} + +func (msh *MShellProc) makeHandleCmdDonePacketClosure(donePk *packet.CmdDonePacketType) func() { + return func() { + msh.handleCmdDonePacket(donePk) + } +} + func (msh *MShellProc) ProcessPackets() { defer msh.WithLock(func() { if msh.Status == StatusConnected { @@ -736,7 +740,7 @@ func (msh *MShellProc) ProcessPackets() { for pk := range msh.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) - msh.handleDataPacket(dataPk, dataPosMap) + runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, dataPosMap)) continue } if pk.GetType() == packet.DataAckPacketStr { @@ -750,7 +754,8 @@ func (msh *MShellProc) ProcessPackets() { continue } if pk.GetType() == packet.CmdDonePacketStr { - msh.handleCmdDonePacket(pk.(*packet.CmdDonePacketType)) + donePk := pk.(*packet.CmdDonePacketType) + runCmdUpdateFn(donePk.CK, msh.makeHandleCmdDonePacketClosure(donePk)) continue } if pk.GetType() == packet.CmdErrorPacketStr { diff --git a/pkg/remote/updatequeue.go b/pkg/remote/updatequeue.go index 105bb61fb..bab359a1d 100644 --- a/pkg/remote/updatequeue.go +++ b/pkg/remote/updatequeue.go @@ -2,65 +2,64 @@ package remote import ( "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) -func pushCmdWaitIfRequired(ck base.CommandKey, update sstore.UpdatePacket) bool { +func pushCmdWaitIfRequired(ck base.CommandKey, fn func()) bool { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() - updates, ok := GlobalStore.CmdWaitMap[ck] + fns, ok := GlobalStore.CmdWaitMap[ck] if !ok { return false } - updates = append(updates, update) - GlobalStore.CmdWaitMap[ck] = updates + fns = append(fns, fn) + GlobalStore.CmdWaitMap[ck] = fns return true } -func sendCmdUpdate(ck base.CommandKey, update sstore.UpdatePacket) { - pushed := pushCmdWaitIfRequired(ck, update) +func runCmdUpdateFn(ck base.CommandKey, fn func()) { + pushed := pushCmdWaitIfRequired(ck, fn) if pushed { return } - sstore.MainBus.SendUpdate(ck.GetSessionId(), update) + fn() } -func runCmdWaitUpdates(ck base.CommandKey) { +func runCmdWaitFns(ck base.CommandKey) { for { - update := removeFirstCmdWaitUpdate(ck) - if update == nil { + fn := removeFirstCmdWaitFn(ck) + if fn == nil { break } - sstore.MainBus.SendUpdate(ck.GetSessionId(), update) + fn() } } -func removeFirstCmdWaitUpdate(ck base.CommandKey) sstore.UpdatePacket { +func removeFirstCmdWaitFn(ck base.CommandKey) func() { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() - updates := GlobalStore.CmdWaitMap[ck] - if len(updates) == 0 { + fns := GlobalStore.CmdWaitMap[ck] + if len(fns) == 0 { delete(GlobalStore.CmdWaitMap, ck) return nil } - if len(updates) == 1 { + if len(fns) == 1 { delete(GlobalStore.CmdWaitMap, ck) - return updates[0] + return fns[0] } - update := updates[0] - GlobalStore.CmdWaitMap[ck] = updates[1:] - return update + fn := fns[0] + GlobalStore.CmdWaitMap[ck] = fns[1:] + return fn } func removeCmdWait(ck base.CommandKey) { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() - updates := GlobalStore.CmdWaitMap[ck] - if len(updates) == 0 { + fns := GlobalStore.CmdWaitMap[ck] + if len(fns) == 0 { delete(GlobalStore.CmdWaitMap, ck) return } - go runCmdWaitUpdates(ck) + go runCmdWaitFns(ck) } diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index c496ed815..b6d2ff950 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -36,8 +36,7 @@ type FeInputPacketType struct { Remote sstore.RemotePtrType `json:"remote"` InputData64 string `json:"inputdata"` SigNum int `json:"signum,omitempty"` - WinSizeRows int `json:"winsizerows"` - WinSizeCols int `json:"winsizecols"` + WinSize *packet.WinSize `json:"winsize,omitempty"` } type WatchScreenPacketType struct { @@ -69,17 +68,6 @@ func MakeFeInputPacket() *FeInputPacketType { return &FeInputPacketType{Type: FeInputPacketStr} } -func (p *FeInputPacketType) ConvertToInputPacket() *packet.InputPacketType { - rtn := packet.MakeInputPacket() - rtn.CK = p.CK - rtn.RemoteId = p.Remote.RemoteId - rtn.InputData64 = p.InputData64 - rtn.SigNum = p.SigNum - rtn.WinSizeRows = p.WinSizeRows - rtn.WinSizeCols = p.WinSizeCols - return rtn -} - func (*WatchScreenPacketType) GetType() string { return WatchScreenPacketStr } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 917457b1c..fb6426960 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -188,9 +188,8 @@ func (ws *WSState) RunWSRead() { fmt.Printf("[error] invalid input packet, remoteid is not set\n") continue } - inputPk := feInputPk.ConvertToInputPacket() go func() { - err = sendCmdInput(inputPk) + err = sendCmdInput(feInputPk) if err != nil { fmt.Printf("[error] sending command input: %v\n", err) } @@ -210,24 +209,31 @@ func (ws *WSState) RunWSRead() { } } -func sendCmdInput(pk *packet.InputPacketType) error { +func sendCmdInput(pk *scpacket.FeInputPacketType) error { err := pk.CK.Validate("input packet") if err != nil { return err } - if pk.RemoteId == "" { + if pk.Remote.RemoteId == "" { return fmt.Errorf("input must set remoteid") } - if len(pk.InputData64) == 0 && pk.SigNum == 0 { - return fmt.Errorf("empty input packet") + if len(pk.InputData64) > 0 { + inputLen := packet.B64DecodedLen(pk.InputData64) + if inputLen > MaxInputDataSize { + return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) + } + msh := remote.GetRemoteById(pk.Remote.RemoteId) + if msh == nil { + return fmt.Errorf("remote %d not found", pk.Remote.RemoteId) + } + dataPk := packet.MakeDataPacket() + dataPk.CK = pk.CK + dataPk.FdNum = 0 // stdin + dataPk.Data64 = pk.InputData64 + return msh.SendInput(dataPk) } - inputLen := packet.B64DecodedLen(pk.InputData64) - if inputLen > MaxInputDataSize { - return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) + if pk.SigNum != 0 || pk.WinSize != nil { + return fmt.Errorf("signum / winsize not supported") } - msh := remote.GetRemoteById(pk.RemoteId) - if msh == nil { - return fmt.Errorf("cannot connect to remote") - } - return msh.SendInput(pk) + return nil } From 2b7045443d1459e5feaf9ba0813cf3f52483760c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Sep 2022 20:08:59 -0700 Subject: [PATCH 100/397] update termopts for running commands, send specialinput packet --- pkg/cmdrunner/cmdrunner.go | 56 ++++++++++++++++++++++++++++++++++++++ pkg/cmdrunner/resolver.go | 20 ++++++++++---- pkg/remote/remote.go | 10 +++++++ pkg/scpacket/scpacket.go | 2 +- pkg/scws/scws.go | 22 +++++++++++---- pkg/sstore/dbops.go | 25 +++++++++++++++++ 6 files changed, 122 insertions(+), 13 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f74c8a049..cd0e33a8d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -80,6 +80,8 @@ func init() { registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) + registerCmdFn("window:resize", WindowResizeCommand) + registerCmdFn("history", HistoryCommand) } @@ -1123,3 +1125,57 @@ func splitLinesForInfo(str string) []string { } return rtn } + +func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) error { + fmt.Printf("resize running cmd %s/%s %d => %d\n", cmd.SessionId, cmd.CmdId, cmd.TermOpts.Cols, newCols) + siPk := packet.MakeSpecialInputPacket() + siPk.CK = base.MakeCommandKey(cmd.SessionId, cmd.CmdId) + siPk.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols} + msh := remote.GetRemoteById(cmd.Remote.RemoteId) + if msh == nil { + return fmt.Errorf("cannot resize, cmd remote not found") + } + err := msh.SendSpecialInput(siPk) + if err != nil { + return err + } + newTermOpts := cmd.TermOpts + newTermOpts.Cols = int64(newCols) + err = sstore.UpdateCmdTermOpts(ctx, cmd.SessionId, cmd.CmdId, newTermOpts) + if err != nil { + return err + } + return nil +} + +func WindowResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + colsStr := pk.Kwargs["cols"] + if colsStr == "" { + return nil, fmt.Errorf("/window:resize requires a numeric 'cols' argument") + } + cols, err := strconv.Atoi(colsStr) + if err != nil { + return nil, fmt.Errorf("/window:resize requires a numeric 'cols' argument: %v", err) + } + if cols <= 0 { + return nil, fmt.Errorf("/window:resize invalid zero/negative 'cols' argument") + } + cols = base.BoundInt(cols, shexec.MinTermCols, shexec.MaxTermCols) + runningCmds, err := sstore.GetRunningWindowCmds(ctx, ids.SessionId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("/window:resize cannot get running commands: %v", err) + } + if len(runningCmds) == 0 { + return nil, nil + } + for _, cmd := range runningCmds { + if int(cmd.TermOpts.Cols) != cols { + resizeRunningCommand(ctx, cmd, cols) + } + } + return nil, nil +} diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index ca3e89fc8..046337d2e 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -137,6 +137,15 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i } } } + if pk.Kwargs["window"] != "" { + windowId, err := resolveWindowArg(rtn.SessionId, rtn.ScreenId, pk.Kwargs["window"]) + if err != nil { + return rtn, err + } + if windowId != "" { + rtn.WindowId = windowId + } + } if rtype&R_Session > 0 && rtn.SessionId == "" { return rtn, fmt.Errorf("no session") } @@ -225,15 +234,14 @@ func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { return sessionId, nil } -func resolveWindowId(pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { - windowId := pk.Kwargs["window"] - if windowId == "" { +func resolveWindowArg(sessionId string, screenId string, windowArg string) (string, error) { + if windowArg == "" { return "", nil } - if _, err := uuid.Parse(windowId); err != nil { - return "", fmt.Errorf("invalid windowid '%s'", windowId) + if _, err := uuid.Parse(windowArg); err != nil { + return "", fmt.Errorf("invalid window arg specified (must be windowid) '%s'", windowArg) } - return windowId, nil + return windowArg, nil } func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index ffebfa837..d28f028c8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -540,6 +540,16 @@ func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error { return msh.ServerProc.Input.SendPacket(dataPk) } +func (msh *MShellProc) SendSpecialInput(siPk *packet.SpecialInputPacketType) error { + if !msh.IsConnected() { + return fmt.Errorf("remote is not connected, cannot send input") + } + if !msh.IsCmdRunning(siPk.CK) { + return fmt.Errorf("cannot send input, cmd is not running") + } + return msh.ServerProc.Input.SendPacket(siPk) +} + func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: true, MaxPtySize: DefaultMaxPtySize} } diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index b6d2ff950..0a7d7b959 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -34,7 +34,7 @@ type FeInputPacketType struct { Type string `json:"type"` CK base.CommandKey `json:"ck"` Remote sstore.RemotePtrType `json:"remote"` - InputData64 string `json:"inputdata"` + InputData64 string `json:"inputdata64"` SigNum int `json:"signum,omitempty"` WinSize *packet.WinSize `json:"winsize,omitempty"` } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index fb6426960..a64d67728 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -217,23 +217,33 @@ func sendCmdInput(pk *scpacket.FeInputPacketType) error { if pk.Remote.RemoteId == "" { return fmt.Errorf("input must set remoteid") } + msh := remote.GetRemoteById(pk.Remote.RemoteId) + if msh == nil { + return fmt.Errorf("remote %d not found", pk.Remote.RemoteId) + } if len(pk.InputData64) > 0 { inputLen := packet.B64DecodedLen(pk.InputData64) if inputLen > MaxInputDataSize { return fmt.Errorf("input data size too large, len=%d (max=%d)", inputLen, MaxInputDataSize) } - msh := remote.GetRemoteById(pk.Remote.RemoteId) - if msh == nil { - return fmt.Errorf("remote %d not found", pk.Remote.RemoteId) - } dataPk := packet.MakeDataPacket() dataPk.CK = pk.CK dataPk.FdNum = 0 // stdin dataPk.Data64 = pk.InputData64 - return msh.SendInput(dataPk) + err = msh.SendInput(dataPk) + if err != nil { + return err + } } if pk.SigNum != 0 || pk.WinSize != nil { - return fmt.Errorf("signum / winsize not supported") + siPk := packet.MakeSpecialInputPacket() + siPk.CK = pk.CK + siPk.SigNum = pk.SigNum + siPk.WinSize = pk.WinSize + err = msh.SendSpecialInput(siPk) + if err != nil { + return err + } } return nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e747fe544..1c2fd590f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -885,3 +885,28 @@ func ClearWindow(ctx context.Context, sessionId string, windowId string) (*Model } return &ModelUpdate{Window: win}, nil } + +func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string) ([]*CmdType, error) { + var rtn []*CmdType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?) AND status = ?` + cmdMaps := tx.SelectMaps(query, sessionId, windowId, CmdStatusRunning) + for _, m := range cmdMaps { + rtn = append(rtn, CmdFromMap(m)) + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + +func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, termOpts TermOpts) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET termopts = ? WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, termOpts, sessionId, cmdId) + return nil + }) + return txErr +} From 283d276e99eed66c14c356563c0ed66353bccf46 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 6 Sep 2022 12:58:16 -0700 Subject: [PATCH 101/397] show remote/ck for messages --- pkg/remote/remote.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d28f028c8..8640d5c26 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -405,6 +405,12 @@ func (msh *MShellProc) Disconnect() { msh.ServerProc.Close() } +func (msh *MShellProc) GetRemoteName() string { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Remote.GetName() +} + func (msh *MShellProc) Launch() { remoteCopy := msh.getRemoteCopy() logf(&remoteCopy, "starting launch") @@ -774,7 +780,7 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.MessagePacketStr { msgPacket := pk.(*packet.MessagePacketType) - fmt.Printf("# %s\n", msgPacket.Message) + fmt.Printf("# [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPacket.CK, msgPacket.Message) continue } if pk.GetType() == packet.RawPacketStr { From 46f48a947ca2153a1c9640bd103f67a3581a949a Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 6 Sep 2022 13:00:30 -0700 Subject: [PATCH 102/397] better logging for which remote received the packet --- pkg/remote/remote.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 8640d5c26..85235762f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -695,7 +695,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { err := sstore.AppendCmdErrorPk(context.Background(), errPk) if err != nil { - fmt.Printf("[error] adding cmderr: %v\n", err) + fmt.Printf("cmderr> [remote %s] [error] adding cmderr: %v\n", msh.GetRemoteName(), err) return } return @@ -766,7 +766,7 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - fmt.Printf("cmd-data %s pty=%d run=%d\n", dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) + fmt.Printf("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) continue } if pk.GetType() == packet.CmdDonePacketStr { @@ -785,15 +785,15 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.RawPacketStr { rawPacket := pk.(*packet.RawPacketType) - fmt.Printf("stderr> %s\n", rawPacket.Data) + fmt.Printf("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPacket.Data) continue } if pk.GetType() == packet.CmdStartPacketStr { startPk := pk.(*packet.CmdStartPacketType) - fmt.Printf("start> reqid=%s (%p)\n", startPk.RespId, msh.ServerProc.Output) + fmt.Printf("start> [remote %s] reqid=%s (%p)\n", msh.GetRemoteName(), startPk.RespId, msh.ServerProc.Output) continue } - fmt.Printf("MSH> unhandled packet %s\n", packet.AsString(pk)) + fmt.Printf("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk)) } } From cde8bed381df0e1b3972fcd93445a4f87ee84541 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 6 Sep 2022 16:41:05 -0700 Subject: [PATCH 103/397] switch to signame for cross system compat --- pkg/scpacket/scpacket.go | 2 +- pkg/scws/scws.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 0a7d7b959..73c056971 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -35,7 +35,7 @@ type FeInputPacketType struct { CK base.CommandKey `json:"ck"` Remote sstore.RemotePtrType `json:"remote"` InputData64 string `json:"inputdata64"` - SigNum int `json:"signum,omitempty"` + SigName string `json:"signame,omitempty"` WinSize *packet.WinSize `json:"winsize,omitempty"` } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index a64d67728..7326aa4b4 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -235,10 +235,10 @@ func sendCmdInput(pk *scpacket.FeInputPacketType) error { return err } } - if pk.SigNum != 0 || pk.WinSize != nil { + if pk.SigName != "" || pk.WinSize != nil { siPk := packet.MakeSpecialInputPacket() siPk.CK = pk.CK - siPk.SigNum = pk.SigNum + siPk.SigName = pk.SigName siPk.WinSize = pk.WinSize err = msh.SendSpecialInput(siPk) if err != nil { From 6f7186666346af1af904755746d62d4bc5f5ec94 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 13 Sep 2022 12:06:12 -0700 Subject: [PATCH 104/397] remote archiving, bug fixes --- pkg/cmdrunner/cmdrunner.go | 111 ++++++++++++++++++++++++++++++++----- pkg/remote/remote.go | 4 ++ pkg/remote/updatequeue.go | 4 -- pkg/sstore/dbops.go | 17 ++++++ pkg/sstore/sstore.go | 3 +- 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index cd0e33a8d..ab56f0cee 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -43,6 +43,16 @@ var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}= var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") var wsRe = regexp.MustCompile("\\s+") +type contextType string + +var historyContextKey = contextType("history") + +type historyContextType struct { + LineId string + CmdId string + RemotePtr *sstore.RemotePtrType +} + type MetaCmdFnType = func(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) type MetaCmdEntryType struct { IsAlias bool @@ -66,6 +76,7 @@ func init() { registerCmdFn("session:open", SessionOpenCommand) registerCmdAlias("session:new", SessionOpenCommand) registerCmdFn("session:set", SessionSetCommand) + registerCmdFn("session:delete", SessionDeleteCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:close", ScreenCloseCommand) @@ -77,11 +88,16 @@ func init() { registerCmdFn("remote:show", RemoteShowCommand) registerCmdFn("remote:showall", RemoteShowAllCommand) registerCmdFn("remote:new", RemoteNewCommand) + registerCmdFn("remote:archive", RemoteArchiveCommand) + registerCmdFn("remote:set", RemoteSetCommand) registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("window:resize", WindowResizeCommand) + registerCmdFn("line", LineCommand) + registerCmdFn("line:show", LineShowCommand) + registerCmdFn("history", HistoryCommand) } @@ -195,16 +211,26 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } update := sstore.ModelUpdate{Line: rtnLine, Cmd: cmd, Interactive: pk.Interactive} sstore.MainBus.SendUpdate(ids.SessionId, update) + ctxVal := ctx.Value(historyContextKey) + if ctxVal != nil { + hctx := ctxVal.(*historyContextType) + if rtnLine != nil { + hctx.LineId = rtnLine.LineId + } + if cmd != nil { + hctx.CmdId = cmd.CmdId + hctx.RemotePtr = &cmd.Remote + } + } return nil, nil } -func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, isMetaCmd bool, hadError bool) error { +func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, historyContext historyContextType, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return err } - lineId, cmdId, rptr := sstore.ReadHistoryDataFromUpdate(update) hitem := &sstore.HistoryItemType{ HistoryId: uuid.New().String(), Ts: time.Now().UnixMilli(), @@ -212,14 +238,14 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update SessionId: ids.SessionId, ScreenId: ids.ScreenId, WindowId: ids.WindowId, - LineId: lineId, + LineId: historyContext.LineId, HadError: hadError, - CmdId: cmdId, + CmdId: historyContext.CmdId, CmdStr: cmdStr, IsMetaCmd: isMetaCmd, } - if !isMetaCmd && rptr != nil { - hitem.Remote = ids.Remote.RemotePtr + if !isMetaCmd && historyContext.RemotePtr != nil { + hitem.Remote = *historyContext.RemotePtr } err = sstore.InsertHistoryItem(ctx, hitem) if err != nil { @@ -232,13 +258,15 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") } + var historyContext historyContextType + ctxWithHistory := context.WithValue(ctx, historyContextKey, &historyContext) var update sstore.UpdatePacket - newPk, rtnErr := EvalMetaCommand(ctx, pk) + newPk, rtnErr := EvalMetaCommand(ctxWithHistory, pk) if rtnErr == nil { - update, rtnErr = HandleCommand(ctx, newPk) + update, rtnErr = HandleCommand(ctxWithHistory, newPk) } if !resolveBool(pk.Kwargs["nohist"], false) { - err := addToHistory(ctx, pk, update, (newPk.MetaCmd != "run"), (rtnErr != nil)) + err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil)) if err != nil { fmt.Printf("[error] adding to history: %v\n", err) // continue... @@ -343,7 +371,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if firstArg == "" { return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified") } - ritem, err := resolveSessionScreen(ctx, ids.SessionId, firstArg, pk.Kwargs["screen"]) + ritem, err := resolveSessionScreen(ctx, ids.SessionId, firstArg, ids.ScreenId) if err != nil { return nil, err } @@ -505,6 +533,15 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return update, nil } +func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + if err != nil { + return nil, err + } + fmt.Printf("ids: %v\n", ids) + return nil, nil +} + func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -524,7 +561,7 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote '%s' info", ids.Remote.DisplayName), + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), InfoLines: splitLinesForInfo(buf.String()), }, }, nil @@ -550,6 +587,15 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) }, nil } +func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName) + return update, nil +} + func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { return nil, fmt.Errorf("/remote requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } @@ -568,7 +614,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("environment for [%s] remote", ids.Remote.DisplayName), + InfoTitle: fmt.Sprintf("environment for remote [%s]", ids.Remote.DisplayName), InfoLines: infoLines, }, } @@ -615,7 +661,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, err } if rptr == nil { - return nil, fmt.Errorf("/cr error: remote '%s' not found", newRemote) + return nil, fmt.Errorf("/cr error: remote [%s] not found", newRemote) } err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, *rptr) if err != nil { @@ -991,6 +1037,26 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return update, nil } +func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session) + if err != nil { + return nil, err + } + err = sstore.DeleteSession(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("cannot delete session: %v", err) + } + sessionIds, _ := sstore.GetAllSessionIds(ctx) // ignore error, session is already deleted so that's the main return value + delSession := &sstore.SessionType{SessionId: ids.SessionId, Remove: true} + update := sstore.ModelUpdate{ + Sessions: []*sstore.SessionType{delSession}, + } + if len(sessionIds) > 0 { + update.ActiveSessionId = sessionIds[0] + } + return update, nil +} + func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { @@ -1027,6 +1093,10 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, 0) + if err != nil { + return nil, err + } firstArg := firstArg(pk) if firstArg == "" { return nil, fmt.Errorf("usage /session [name|id|pos], no param specified") @@ -1036,7 +1106,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, err } ritems := sessionsToResolveItems(bareSessions) - ritem, err := genericResolve(firstArg, pk.Kwargs["session"], ritems, "session") + ritem, err := genericResolve(firstArg, ids.SessionId, ritems, "session") if err != nil { return nil, err } @@ -1179,3 +1249,16 @@ func WindowResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } return nil, nil } + +func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) +} + +func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + fmt.Printf("/line:show ids %v\n", ids) + return nil, nil +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 85235762f..1a7f293d3 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -413,6 +413,10 @@ func (msh *MShellProc) GetRemoteName() string { func (msh *MShellProc) Launch() { remoteCopy := msh.getRemoteCopy() + if remoteCopy.ConnectMode == sstore.ConnectModeArchive { + logf(&remoteCopy, "cannot launch archived remote") + return + } logf(&remoteCopy, "starting launch") ecmd := convertSSHOpts(remoteCopy.SSHOpts).MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) diff --git a/pkg/remote/updatequeue.go b/pkg/remote/updatequeue.go index bab359a1d..4665511ff 100644 --- a/pkg/remote/updatequeue.go +++ b/pkg/remote/updatequeue.go @@ -43,10 +43,6 @@ func removeFirstCmdWaitFn(ck base.CommandKey) func() { delete(GlobalStore.CmdWaitMap, ck) return nil } - if len(fns) == 1 { - delete(GlobalStore.CmdWaitMap, ck) - return fns[0] - } fn := fns[0] GlobalStore.CmdWaitMap[ck] = fns[1:] return fn diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1c2fd590f..1d7285172 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -122,6 +122,19 @@ func InsertRemote(ctx context.Context, remote *RemoteType) error { return txErr } +func ArchiveRemote(ctx context.Context, remoteId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT remoteid FROM remote WHERE remoteid = ?` + if !tx.Exists(query, remoteId) { + return fmt.Errorf("cannot archive, remote does not exist") + } + query = `UPDATE remote SET connectmode = ?, physicalid = '', remotetype = '', remotealias = '', initpk = '', sshopts = '', remoteopts = '', lastconnectts = 0 WHERE remoteid = ?` + tx.ExecWrap(query, ConnectModeArchive, remoteId) + return nil + }) + return txErr +} + func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if hitem == nil { return fmt.Errorf("cannot insert nil history item") @@ -910,3 +923,7 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term }) return txErr } + +func DeleteSession(ctx context.Context, sessionId string) error { + return nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index f67db53d1..2c6ba812f 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -55,6 +55,7 @@ const ( ConnectModeStartup = "startup" ConnectModeAuto = "auto" ConnectModeManual = "manual" + ConnectModeArchive = "archive" ) const ( @@ -71,7 +72,7 @@ func GetSessionDBName() string { } func IsValidConnectMode(mode string) bool { - return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual + return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual || mode == ConnectModeArchive } func GetDB(ctx context.Context) (*sqlx.DB, error) { From a74ee69da5042695de3e66affd25a5acd789895a Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 13 Sep 2022 17:11:36 -0700 Subject: [PATCH 105/397] working on setting up remotes --- db/migrations/000001_init.up.sql | 3 +- db/schema.sql | 3 +- pkg/cmdrunner/cmdrunner.go | 13 +++-- pkg/remote/remote.go | 81 +++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 59 +++++++++++------------ pkg/sstore/sstore.go | 17 ++++--- 6 files changed, 123 insertions(+), 53 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 281b7bb09..7bd73b0dd 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -86,7 +86,8 @@ CREATE TABLE remote ( initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, - lastconnectts bigint NOT NULL + lastconnectts bigint NOT NULL, + archived boolean NOT NULL ); CREATE TABLE cmd ( diff --git a/db/schema.sql b/db/schema.sql index 503a72463..d3bf932b3 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -81,7 +81,8 @@ CREATE TABLE remote ( initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, - lastconnectts bigint NOT NULL + lastconnectts bigint NOT NULL, + archived boolean NOT NULL ); CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ab56f0cee..9c58bbaa6 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -461,7 +461,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return nil, fmt.Errorf("invalid alias format") } } - connectMode := sstore.ConnectModeStartup + connectMode := sstore.ConnectModeAuto if pk.Kwargs["connectmode"] != "" { connectMode = pk.Kwargs["connectmode"] } @@ -520,7 +520,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss SSHOpts: sshOpts, RemoteOpts: remoteOpts, } - err := sstore.InsertRemote(ctx, r) + err := remote.AddRemote(ctx, r) if err != nil { return nil, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err) } @@ -554,11 +554,14 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s buf.WriteString(fmt.Sprintf(" %-15s %s\n", "physicalid", state.PhysicalId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "alias", state.RemoteAlias)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) if ids.Remote.RemoteState != nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.Remote.RemoteState.Cwd)) } + if state.ErrorStr != "" { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "error", state.ErrorStr)) + } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), @@ -592,6 +595,10 @@ func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } + err = remote.ArchiveRemote(ctx, ids.Remote.RemotePtr.RemoteId) + if err != nil { + return nil, fmt.Errorf("archiving remote: %v", err) + } update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName) return update, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 1a7f293d3..ed5b5c116 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -80,6 +80,7 @@ type RemoteRuntimeState struct { ErrorStr string `json:"errorstr,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` ConnectMode string `json:"connectmode"` + Archived bool `json:"archived"` } func logf(rem *sstore.RemoteType, fmtStr string, args ...interface{}) { @@ -165,6 +166,59 @@ func LoadRemoteById(ctx context.Context, remoteId string) error { return nil } +func AddRemote(ctx context.Context, r *sstore.RemoteType) error { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + + existingRemote := getRemoteByCanonicalName_nolock(r.RemoteCanonicalName) + if existingRemote != nil { + erCopy := existingRemote.getRemoteCopy() + if !erCopy.Archived { + return fmt.Errorf("duplicate canonical name %q: cannot create new remote", r.RemoteCanonicalName) + } + r.RemoteId = erCopy.RemoteId + } + err := sstore.UpsertRemote(ctx, r) + if err != nil { + return fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err) + } + newMsh := MakeMShell(r) + GlobalStore.Map[r.RemoteId] = newMsh + go newMsh.NotifyRemoteUpdate() + if r.ConnectMode == sstore.ConnectModeStartup { + go newMsh.Launch() + } + return nil +} + +func ArchiveRemote(ctx context.Context, remoteId string) error { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + msh := GlobalStore.Map[remoteId] + if msh == nil { + return fmt.Errorf("remote not found, cannot archive") + } + if msh.Status == StatusConnected { + return fmt.Errorf("cannot archive connected remote") + } + rcopy := msh.getRemoteCopy() + archivedRemote := &sstore.RemoteType{ + RemoteId: rcopy.RemoteId, + RemoteType: rcopy.RemoteType, + RemoteCanonicalName: rcopy.RemoteCanonicalName, + ConnectMode: sstore.ConnectModeManual, + Archived: true, + } + err := sstore.UpsertRemote(ctx, archivedRemote) + if err != nil { + return err + } + newMsh := MakeMShell(archivedRemote) + GlobalStore.Map[remoteId] = newMsh + go newMsh.NotifyRemoteUpdate() + return nil +} + func GetRemoteByName(name string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -176,6 +230,16 @@ func GetRemoteByName(name string) *MShellProc { return nil } +func getRemoteByCanonicalName_nolock(name string) *MShellProc { + for _, msh := range GlobalStore.Map { + rcopy := msh.getRemoteCopy() + if rcopy.RemoteCanonicalName == name { + return msh + } + } + return nil +} + func GetRemoteById(remoteId string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -259,6 +323,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { PhysicalId: msh.Remote.PhysicalId, Status: msh.Status, ConnectMode: msh.Remote.ConnectMode, + Archived: msh.Remote.Archived, } if msh.Err != nil { state.ErrorStr = msh.Err.Error() @@ -402,7 +467,9 @@ func (msh *MShellProc) GetNumRunningCommands() int { func (msh *MShellProc) Disconnect() { msh.Lock.Lock() defer msh.Lock.Unlock() - msh.ServerProc.Close() + if msh.ServerProc != nil { + msh.ServerProc.Close() + } } func (msh *MShellProc) GetRemoteName() string { @@ -413,12 +480,15 @@ func (msh *MShellProc) GetRemoteName() string { func (msh *MShellProc) Launch() { remoteCopy := msh.getRemoteCopy() - if remoteCopy.ConnectMode == sstore.ConnectModeArchive { + remoteName := remoteCopy.GetName() + if remoteCopy.Archived { logf(&remoteCopy, "cannot launch archived remote") return } logf(&remoteCopy, "starting launch") - ecmd := convertSSHOpts(remoteCopy.SSHOpts).MakeSSHExecCmd(MShellServerCommand) + sshOpts := convertSSHOpts(remoteCopy.SSHOpts) + sshOpts.SSHErrorsToTty = true + ecmd := sshOpts.MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) @@ -431,7 +501,6 @@ func (msh *MShellProc) Launch() { ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() } }() - remoteName := remoteCopy.GetName() go func() { buf := make([]byte, 100) for { @@ -444,9 +513,7 @@ func (msh *MShellProc) Launch() { break } readStr := string(buf[0:n]) - readStr = strings.ReplaceAll(readStr, "\r", "") - readStr = strings.ReplaceAll(readStr, "\n", "\\n") - fmt.Printf("[c-pty %s] %d '%s'\n", remoteName, n, readStr) + fmt.Printf("[c-pty %s] %d %q\n", remoteName, n, readStr) } }() if remoteName == "test2" { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1d7285172..c7e8f1891 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -71,6 +71,20 @@ func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { return remote, nil } +func GetRemoteByCanonicalName(ctx context.Context, cname string) (*RemoteType, error) { + var remote *RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote WHERE remotecanonicalname = ?` + m := tx.GetMap(query, cname) + remote = RemoteFromMap(m) + return nil + }) + if err != nil { + return nil, err + } + return remote, nil +} + func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, error) { var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { @@ -85,51 +99,32 @@ func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, return remote, nil } -func InsertRemote(ctx context.Context, remote *RemoteType) error { - if remote == nil { +func UpsertRemote(ctx context.Context, r *RemoteType) error { + if r == nil { return fmt.Errorf("cannot insert nil remote") } - if remote.RemoteId == "" { + if r.RemoteId == "" { return fmt.Errorf("cannot insert remote without id") } - if remote.RemoteCanonicalName == "" { + if r.RemoteCanonicalName == "" { return fmt.Errorf("cannot insert remote with canonicalname") } - if remote.RemoteType == "" { + if r.RemoteType == "" { return fmt.Errorf("cannot insert remote without type") } txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT remoteid FROM remote WHERE remoteid = ?` - if tx.Exists(query, remote.RemoteId) { - return fmt.Errorf("duplicate remoteid, cannot create") + if tx.Exists(query, r.RemoteId) { + tx.ExecWrap(`DELETE FROM remote WHERE remoteid = ?`, r.RemoteId) } - if remote.RemoteAlias != "" { - query = `SELECT remoteid FROM remote WHERE alias = ?` - if tx.Exists(query, remote.RemoteAlias) { - return fmt.Errorf("remote has duplicate alias '%s', cannot create", remote.RemoteAlias) - } - } - query = `SELECT remoteid FROM remote WHERE remotecanonicaname = ?` - if tx.Exists(query, remote.RemoteCanonicalName) { - return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", remote.RemoteCanonicalName) + query = `SELECT remoteid FROM remote WHERE remotecanonicalname = ?` + if tx.Exists(query, r.RemoteCanonicalName) { + return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", r.RemoteCanonicalName) } query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts)` - tx.NamedExecWrap(query, remote.ToMap()) - return nil - }) - return txErr -} - -func ArchiveRemote(ctx context.Context, remoteId string) error { - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT remoteid FROM remote WHERE remoteid = ?` - if !tx.Exists(query, remoteId) { - return fmt.Errorf("cannot archive, remote does not exist") - } - query = `UPDATE remote SET connectmode = ?, physicalid = '', remotetype = '', remotealias = '', initpk = '', sshopts = '', remoteopts = '', lastconnectts = 0 WHERE remoteid = ?` - tx.ExecWrap(query, ConnectModeArchive, remoteId) + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts, archived) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived)` + tx.NamedExecWrap(query, r.ToMap()) return nil }) return txErr diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 2c6ba812f..326934a3a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -55,7 +55,6 @@ const ( ConnectModeStartup = "startup" ConnectModeAuto = "auto" ConnectModeManual = "manual" - ConnectModeArchive = "archive" ) const ( @@ -72,7 +71,7 @@ func GetSessionDBName() string { } func IsValidConnectMode(mode string) bool { - return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual || mode == ConnectModeArchive + return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual } func GetDB(ctx context.Context) (*sqlx.DB, error) { @@ -431,16 +430,14 @@ type RemoteType struct { SSHOpts *SSHOpts `json:"sshopts"` RemoteOpts *RemoteOptsType `json:"remoteopts"` LastConnectTs int64 `json:"lastconnectts"` + Archived bool `json:"archived"` } func (r *RemoteType) GetName() string { if r.RemoteAlias != "" { return r.RemoteAlias } - if r.RemoteUser == "" { - return r.RemoteHost - } - return fmt.Sprintf("%s@%s", r.RemoteUser, r.RemoteHost) + return r.RemoteCanonicalName } type CmdType struct { @@ -473,6 +470,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["sshopts"] = quickJson(r.SSHOpts) rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs + rtn["archived"] = r.Archived return rtn } @@ -494,6 +492,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetJson(&r.SSHOpts, m, "sshopts") quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") + quickSetBool(&r.Archived, m, "archived") return &r } @@ -611,7 +610,7 @@ func EnsureLocalRemote(ctx context.Context) error { ConnectMode: ConnectModeStartup, SSHOpts: &SSHOpts{Local: true}, } - err = InsertRemote(ctx, localRemote) + err = UpsertRemote(ctx, localRemote) if err != nil { return err } @@ -643,7 +642,7 @@ func AddTest01Remote(ctx context.Context) error { }, ConnectMode: ConnectModeStartup, } - err = InsertRemote(ctx, testRemote) + err = UpsertRemote(ctx, testRemote) if err != nil { return err } @@ -674,7 +673,7 @@ func AddTest02Remote(ctx context.Context) error { }, ConnectMode: ConnectModeStartup, } - err = InsertRemote(ctx, testRemote) + err = UpsertRemote(ctx, testRemote) if err != nil { return err } From 002876a07b6740ff44313f7e1157bdfa27f02a23 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 14 Sep 2022 12:06:55 -0700 Subject: [PATCH 106/397] add remoteidx --- db/migrations/000001_init.up.sql | 3 ++- db/schema.sql | 3 ++- pkg/remote/remote.go | 2 ++ pkg/sstore/dbops.go | 5 ++++- pkg/sstore/sstore.go | 3 +++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 7bd73b0dd..c58399aa3 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -87,7 +87,8 @@ CREATE TABLE remote ( sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, - archived boolean NOT NULL + archived boolean NOT NULL, + remoteidx int NOT NULL ); CREATE TABLE cmd ( diff --git a/db/schema.sql b/db/schema.sql index d3bf932b3..9e71be1c3 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -82,7 +82,8 @@ CREATE TABLE remote ( sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, - archived boolean NOT NULL + archived boolean NOT NULL, + remoteidx int NOT NULL ); CREATE TABLE cmd ( sessionid varchar(36) NOT NULL, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index ed5b5c116..d32a44c98 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -81,6 +81,7 @@ type RemoteRuntimeState struct { DefaultState *sstore.RemoteState `json:"defaultstate"` ConnectMode string `json:"connectmode"` Archived bool `json:"archived"` + RemoteIdx int64 `json:"remoteidx"` } func logf(rem *sstore.RemoteType, fmtStr string, args ...interface{}) { @@ -324,6 +325,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { Status: msh.Status, ConnectMode: msh.Remote.ConnectMode, Archived: msh.Remote.Archived, + RemoteIdx: msh.Remote.RemoteIdx, } if msh.Err != nil { state.ErrorStr = msh.Err.Error() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c7e8f1891..a999de565 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -30,7 +30,7 @@ func NumSessions(ctx context.Context) (int, error) { func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { var rtn []*RemoteType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote` + query := `SELECT * FROM remote ORDER BY remoteidx` marr := tx.SelectMaps(query) for _, m := range marr { rtn = append(rtn, RemoteFromMap(m)) @@ -121,6 +121,9 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { if tx.Exists(query, r.RemoteCanonicalName) { return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", r.RemoteCanonicalName) } + query = `SELECT max(remoteidx) FROM remote` + maxRemoteIdx := tx.GetInt(query) + r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts, archived) VALUES (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived)` diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 326934a3a..7241df816 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -431,6 +431,7 @@ type RemoteType struct { RemoteOpts *RemoteOptsType `json:"remoteopts"` LastConnectTs int64 `json:"lastconnectts"` Archived bool `json:"archived"` + RemoteIdx int64 `json:"remoteidx"` } func (r *RemoteType) GetName() string { @@ -471,6 +472,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs rtn["archived"] = r.Archived + rtn["remoteidx"] = r.RemoteIdx return rtn } @@ -493,6 +495,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") quickSetBool(&r.Archived, m, "archived") + quickSetInt64(&r.RemoteIdx, m, "remoteidx") return &r } From c8b8f78249d32dd465979b871138e95cae5faa5a Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 14 Sep 2022 12:56:05 -0700 Subject: [PATCH 107/397] allow remote kwarg to be passed to select a different remote for a command --- pkg/cmdrunner/resolver.go | 81 +++++++++++++++++++++++++++------------ pkg/remote/remote.go | 13 ++++--- pkg/sstore/sstore.go | 23 +++++++++++ 3 files changed, 87 insertions(+), 30 deletions(-) diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 046337d2e..cbd95abef 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -31,9 +31,9 @@ type resolvedIds struct { type ResolvedRemote struct { DisplayName string RemotePtr sstore.RemotePtrType - RemoteState *sstore.RemoteState - RState remote.RemoteRuntimeState MShell *remote.MShellProc + RState remote.RemoteRuntimeState + RemoteState *sstore.RemoteState } type ResolveItem struct { @@ -118,6 +118,22 @@ func resolveByPosition(items []ResolveItem, curId string, posStr string) *Resolv return &items[pos-1] } +func resolveRemoteArg(remoteArg string) (*sstore.RemotePtrType, error) { + rrUser, rrRemote, rrName, err := parseFullRemoteRef(remoteArg) + if err != nil { + return nil, err + } + if rrUser != "" { + return nil, fmt.Errorf("remoteusers not supported") + } + msh := remote.GetRemoteByName(rrRemote) + if msh == nil { + return nil, nil + } + rcopy := msh.GetRemoteCopy() + return &sstore.RemotePtrType{RemoteId: rcopy.RemoteId, Name: rrName}, nil +} + func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int) (resolvedIds, error) { rtn := resolvedIds{} uictx := pk.UIContext @@ -125,17 +141,6 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i rtn.SessionId = uictx.SessionId rtn.ScreenId = uictx.ScreenId rtn.WindowId = uictx.WindowId - if uictx.Remote != nil && rtn.SessionId != "" && rtn.WindowId != "" { - rr, err := resolveRemoteFromPtr(ctx, uictx.Remote, rtn.SessionId, rtn.WindowId) - if err != nil { - if rtype&R_Remote > 0 || rtype&R_RemoteConnected > 0 { - return rtn, err - } - // otherwise just don't set uictx.Remote - } else { - rtn.Remote = rr - } - } } if pk.Kwargs["window"] != "" { windowId, err := resolveWindowArg(rtn.SessionId, rtn.ScreenId, pk.Kwargs["window"]) @@ -146,6 +151,30 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i rtn.WindowId = windowId } } + var rptr *sstore.RemotePtrType + var err error + if pk.Kwargs["remote"] != "" { + rptr, err = resolveRemoteArg(pk.Kwargs["remote"]) + if err != nil { + return rtn, err + } + if rptr == nil { + return rtn, fmt.Errorf("invalid remote argument %q passed, remote not found", pk.Kwargs["remote"]) + } + } else if uictx.Remote != nil { + rptr = uictx.Remote + } + if rptr != nil { + err = rptr.Validate() + if err != nil { + return rtn, fmt.Errorf("invalid resolved remote: %v", err) + } + rr, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.WindowId) + if err != nil { + return rtn, err + } + rtn.Remote = rr + } if rtype&R_Session > 0 && rtn.SessionId == "" { return rtn, fmt.Errorf("no session") } @@ -285,7 +314,7 @@ func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, windowId string) (*ResolvedRemote, error) { if rptr == nil || rptr.RemoteId == "" { - return nil, fmt.Errorf("no remote to resolve") + return nil, nil } msh := remote.GetRemoteById(rptr.RemoteId) if msh == nil { @@ -293,20 +322,24 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi } rstate := msh.GetRemoteRuntimeState() displayName := rstate.GetDisplayName(rptr) - state, err := sstore.GetRemoteState(ctx, sessionId, windowId, *rptr) - if err != nil { - return nil, fmt.Errorf("cannot resolve remote state '%s': %w", displayName, err) - } - if state == nil { - state = rstate.DefaultState - } - return &ResolvedRemote{ + rtn := &ResolvedRemote{ DisplayName: displayName, RemotePtr: *rptr, - RemoteState: state, + RemoteState: nil, RState: rstate, MShell: msh, - }, nil + } + if sessionId != "" && windowId != "" { + state, err := sstore.GetRemoteState(ctx, sessionId, windowId, *rptr) + if err != nil { + return nil, fmt.Errorf("cannot resolve remote state '%s': %w", displayName, err) + } + if state == nil { + state = rstate.DefaultState + } + rtn.RemoteState = state + } + return rtn, nil } // returns (remoteDisplayName, remoteptr, state, rstate, err) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d32a44c98..9a683e3ce 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -173,7 +173,7 @@ func AddRemote(ctx context.Context, r *sstore.RemoteType) error { existingRemote := getRemoteByCanonicalName_nolock(r.RemoteCanonicalName) if existingRemote != nil { - erCopy := existingRemote.getRemoteCopy() + erCopy := existingRemote.GetRemoteCopy() if !erCopy.Archived { return fmt.Errorf("duplicate canonical name %q: cannot create new remote", r.RemoteCanonicalName) } @@ -202,7 +202,7 @@ func ArchiveRemote(ctx context.Context, remoteId string) error { if msh.Status == StatusConnected { return fmt.Errorf("cannot archive connected remote") } - rcopy := msh.getRemoteCopy() + rcopy := msh.GetRemoteCopy() archivedRemote := &sstore.RemoteType{ RemoteId: rcopy.RemoteId, RemoteType: rcopy.RemoteType, @@ -224,7 +224,8 @@ func GetRemoteByName(name string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() for _, msh := range GlobalStore.Map { - if msh.Remote.RemoteAlias == name || msh.Remote.GetName() == name { + rcopy := msh.GetRemoteCopy() + if rcopy.RemoteAlias == name || rcopy.RemoteCanonicalName == name { return msh } } @@ -233,7 +234,7 @@ func GetRemoteByName(name string) *MShellProc { func getRemoteByCanonicalName_nolock(name string) *MShellProc { for _, msh := range GlobalStore.Map { - rcopy := msh.getRemoteCopy() + rcopy := msh.GetRemoteCopy() if rcopy.RemoteCanonicalName == name { return msh } @@ -454,7 +455,7 @@ func (msh *MShellProc) setErrorStatus(err error) { go msh.NotifyRemoteUpdate() } -func (msh *MShellProc) getRemoteCopy() sstore.RemoteType { +func (msh *MShellProc) GetRemoteCopy() sstore.RemoteType { msh.Lock.Lock() defer msh.Lock.Unlock() return *msh.Remote @@ -481,7 +482,7 @@ func (msh *MShellProc) GetRemoteName() string { } func (msh *MShellProc) Launch() { - remoteCopy := msh.getRemoteCopy() + remoteCopy := msh.GetRemoteCopy() remoteName := remoteCopy.GetName() if remoteCopy.Archived { logf(&remoteCopy, "cannot launch archived remote") diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 7241df816..9efcbcae8 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -12,6 +12,7 @@ import ( "os" "os/user" "path" + "regexp" "strings" "sync" "time" @@ -138,6 +139,8 @@ func (opts WindowShareOptsType) Value() (driver.Value, error) { return quickValueJson(opts) } +var RemoteNameRe = regexp.MustCompile("^\\*?[a-zA-Z0-9_-]+$") + type RemotePtrType struct { OwnerId string `json:"ownerid"` RemoteId string `json:"remoteid"` @@ -148,6 +151,26 @@ func (r RemotePtrType) IsSessionScope() bool { return strings.HasPrefix(r.Name, "*") } +func (r RemotePtrType) Validate() error { + if r.OwnerId != "" { + if _, err := uuid.Parse(r.OwnerId); err != nil { + return fmt.Errorf("invalid ownerid format: %v", err) + } + } + if r.RemoteId != "" { + if _, err := uuid.Parse(r.RemoteId); err != nil { + return fmt.Errorf("invalid remoteid format: %v", err) + } + } + if r.Name != "" { + ok := RemoteNameRe.MatchString(r.Name) + if !ok { + return fmt.Errorf("invalid remote name") + } + } + return nil +} + func (r RemotePtrType) MakeFullRemoteRef() string { if r.RemoteId == "" { return "" From 83974e10ddb06c64a31438265d9a7f8a1207df8d Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 14 Sep 2022 13:01:52 -0700 Subject: [PATCH 108/397] resolve a remote by id or partial id --- pkg/cmdrunner/resolver.go | 2 +- pkg/remote/remote.go | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index cbd95abef..f57563511 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -126,7 +126,7 @@ func resolveRemoteArg(remoteArg string) (*sstore.RemotePtrType, error) { if rrUser != "" { return nil, fmt.Errorf("remoteusers not supported") } - msh := remote.GetRemoteByName(rrRemote) + msh := remote.GetRemoteByArg(rrRemote) if msh == nil { return nil, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 9a683e3ce..2527dffda 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path" + "regexp" "strconv" "strings" "sync" @@ -220,12 +221,22 @@ func ArchiveRemote(ctx context.Context, remoteId string) error { return nil } -func GetRemoteByName(name string) *MShellProc { +var partialUUIDRe = regexp.MustCompile("^[0-9a-f]{8}$") + +func isPartialUUID(s string) bool { + return partialUUIDRe.MatchString(s) +} + +func GetRemoteByArg(arg string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() + isPuid := isPartialUUID(arg) for _, msh := range GlobalStore.Map { rcopy := msh.GetRemoteCopy() - if rcopy.RemoteAlias == name || rcopy.RemoteCanonicalName == name { + if rcopy.RemoteAlias == arg || rcopy.RemoteCanonicalName == arg || rcopy.RemoteId == arg { + return msh + } + if isPuid && strings.HasPrefix(rcopy.RemoteId, arg) { return msh } } From 42683e6f4af77bc90373b47306e7b7f0418cbee9 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 14 Sep 2022 23:10:35 -0700 Subject: [PATCH 109/397] working on remote pty output --- cmd/main-server.go | 30 +++++++++++ go.mod | 1 + go.sum | 2 + pkg/remote/remote.go | 108 ++++++++++++++++++++++++++-------------- pkg/sstore/updatebus.go | 5 +- 5 files changed, 108 insertions(+), 38 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 5add214a1..fd89e92bf 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -142,6 +142,35 @@ func HandleGetWindow(w http.ResponseWriter, r *http.Request) { return } +func HandleRemotePty(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + qvals := r.URL.Query() + remoteId := qvals.Get("remoteid") + if remoteId == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("must specify remoteid"))) + return + } + if _, err := uuid.Parse(remoteId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid remoteid: %v", err))) + return + } + realOffset, data, err := remote.ReadRemotePty(r.Context(), remoteId) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error reading ptyout file: %v", err))) + return + } + w.Header().Set("X-PtyDataOffset", strconv.FormatInt(realOffset, 10)) + w.WriteHeader(http.StatusOK) + w.Write(data) + return +} + func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") @@ -333,6 +362,7 @@ func main() { go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) + gr.HandleFunc("/api/remote-pty", HandleRemotePty) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") server := &http.Server{ diff --git a/go.mod b/go.mod index 3fe97fe9b..d4a4489f1 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/alessio/shellescape v1.4.1 // indirect + github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect github.com/creack/pty v1.1.18 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 1e458fc35..7465dfaa1 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 2527dffda..bff2b4df2 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -17,6 +17,7 @@ import ( "syscall" "time" + "github.com/armon/circbuf" "github.com/creack/pty" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" @@ -28,6 +29,7 @@ import ( const RemoteTypeMShell = "mshell" const DefaultTerm = "xterm-256color" const DefaultMaxPtySize = 1024 * 1024 +const CircBufSize = 64 * 1024 const MShellServerCommand = ` PATH=$PATH:~/.mshell; @@ -52,7 +54,6 @@ var GlobalStore *Store type Store struct { Lock *sync.Mutex Map map[string]*MShellProc // key=remoteid - Log *CircleLog CmdWaitMap map[base.CommandKey][]func() } @@ -66,6 +67,7 @@ type MShellProc struct { UName string Err error ControllingPty *os.File + PtyBuffer *circbuf.Buffer RunningCmds []base.CommandKey } @@ -85,21 +87,6 @@ type RemoteRuntimeState struct { RemoteIdx int64 `json:"remoteidx"` } -func logf(rem *sstore.RemoteType, fmtStr string, args ...interface{}) { - rname := rem.GetName() - str := fmt.Sprintf(fmtStr, args...) - fullStr := fmt.Sprintf("[remote %s] %s", rname, str) - fmt.Printf("%s\n", fullStr) - GlobalStore.Log.Add(fullStr) -} - -func logError(rem *sstore.RemoteType, err error) { - rname := rem.GetName() - fullStr := fmt.Sprintf("[remote %s] error: %v", rname, err) - fmt.Printf("%s\n", fullStr) - GlobalStore.Log.Add(fullStr) -} - func (state RemoteRuntimeState) IsConnected() bool { return state.Status == StatusConnected } @@ -129,7 +116,6 @@ func LoadRemotes(ctx context.Context) error { GlobalStore = &Store{ Lock: &sync.Mutex{}, Map: make(map[string]*MShellProc), - Log: MakeCircleLog(100), CmdWaitMap: make(map[base.CommandKey][]func()), } allRemotes, err := sstore.GetAllRemotes(ctx) @@ -168,6 +154,20 @@ func LoadRemoteById(ctx context.Context, remoteId string) error { return nil } +func ReadRemotePty(ctx context.Context, remoteId string) (int64, []byte, error) { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + msh := GlobalStore.Map[remoteId] + if msh == nil { + return 0, nil, nil + } + msh.Lock.Lock() + defer msh.Lock.Unlock() + barr := msh.PtyBuffer.Bytes() + offset := msh.PtyBuffer.TotalWritten() - int64(len(barr)) + return offset, barr, nil +} + func AddRemote(ctx context.Context, r *sstore.RemoteType) error { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -423,7 +423,17 @@ func GetDefaultRemoteStateById(remoteId string) (*sstore.RemoteState, error) { } func MakeMShell(r *sstore.RemoteType) *MShellProc { - rtn := &MShellProc{Lock: &sync.Mutex{}, Remote: r, Status: StatusInit} + buf, err := circbuf.NewBuffer(CircBufSize) + if err != nil { + panic(err) // this should never happen (NewBuffer only returns an error if CirBufSize <= 0) + } + rtn := &MShellProc{ + Lock: &sync.Mutex{}, + Remote: r, + Status: StatusInit, + PtyBuffer: buf, + } + rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) return rtn } @@ -492,21 +502,46 @@ func (msh *MShellProc) GetRemoteName() string { return msh.Remote.GetName() } +func (msh *MShellProc) WriteToPtyBuffer(strFmt string, args ...interface{}) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.writeToPtyBuffer_nolock(strFmt, args...) +} + +func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{}) { + // inefficient string manipulation here and read of PtyBuffer, but these messages are rare, nbd + realStr := fmt.Sprintf(strFmt, args...) + if !strings.HasPrefix(realStr, "~") { + realStr = strings.ReplaceAll(realStr, "\n", "\r\n") + if !strings.HasSuffix(realStr, "\r\n") { + realStr = realStr + "\r\n" + } + realStr = "\033[0m\033[32m[scripthaus]\033[0m " + realStr + barr := msh.PtyBuffer.Bytes() + if len(barr) > 0 && barr[len(barr)-1] != '\n' { + realStr = "\r\n" + realStr + } + } else { + realStr = realStr[1:] + } + msh.PtyBuffer.Write([]byte(realStr)) +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() remoteName := remoteCopy.GetName() if remoteCopy.Archived { - logf(&remoteCopy, "cannot launch archived remote") + msh.WriteToPtyBuffer("cannot launch archived remote\n") return } - logf(&remoteCopy, "starting launch") + msh.WriteToPtyBuffer("connecting to %s\n", remoteCopy.GetName()) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true ecmd := sshOpts.MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) - logError(&remoteCopy, statusErr) + msh.WriteToPtyBuffer("error, %s\n", statusErr.Error()) msh.setErrorStatus(statusErr) return } @@ -523,18 +558,19 @@ func (msh *MShellProc) Launch() { break } if readErr != nil { - logf(&remoteCopy, "error: reading from controlling-pty: %v", readErr) + msh.WriteToPtyBuffer("error reading from controlling-pty: %v\n", readErr) break } - readStr := string(buf[0:n]) - fmt.Printf("[c-pty %s] %d %q\n", remoteName, n, readStr) + msh.WithLock(func() { + msh.PtyBuffer.Write(buf[0:n]) + }) } }() if remoteName == "test2" { go func() { time.Sleep(2 * time.Second) cmdPty.Write([]byte(Test2Pw)) - fmt.Printf("[c-pty %s] wrote password!\n", remoteName) + msh.WriteToPtyBuffer("~[password sent]\r\n") }() } cproc, uname, err := shexec.MakeClientProc(ecmd) @@ -544,10 +580,10 @@ func (msh *MShellProc) Launch() { }) if err != nil { msh.setErrorStatus(err) - logf(&remoteCopy, "error connecting remote (%s): %v", msh.UName, err) + msh.WriteToPtyBuffer("error connecting to remote (uname=%q): %v\n", msh.UName, err) return } - fmt.Printf("connected remote %s\n", remoteCopy.GetName()) + msh.WriteToPtyBuffer("connected\n") msh.WithLock(func() { msh.ServerProc = cproc msh.Status = StatusConnected @@ -562,7 +598,7 @@ func (msh *MShellProc) Launch() { go msh.NotifyRemoteUpdate() } }) - logf(&remoteCopy, "remote disconnected exitcode=%d", exitCode) + msh.WriteToPtyBuffer("disconnected exitcode=%d\n", exitCode) }() go msh.ProcessPackets() return @@ -767,7 +803,7 @@ func (msh *MShellProc) notifyHangups_nolock() { func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) if err != nil { - fmt.Printf("[error] updating cmddone: %v\n", err) + msh.WriteToPtyBuffer("[error] updating cmddone: %v\n", err) return } if update != nil { @@ -780,7 +816,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { err := sstore.AppendCmdErrorPk(context.Background(), errPk) if err != nil { - fmt.Printf("cmderr> [remote %s] [error] adding cmderr: %v\n", msh.GetRemoteName(), err) + msh.WriteToPtyBuffer("cmderr> [remote %s] [error] adding cmderr: %v\n", msh.GetRemoteName(), err) return } return @@ -832,7 +868,7 @@ func (msh *MShellProc) ProcessPackets() { } err := sstore.HangupRunningCmdsByRemoteId(context.Background(), msh.Remote.RemoteId) if err != nil { - logf(msh.Remote, "calling HUP on cmds %v", err) + msh.writeToPtyBuffer_nolock("error calling HUP on cmds %v\n", err) } msh.notifyHangups_nolock() go msh.NotifyRemoteUpdate() @@ -851,7 +887,7 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - fmt.Printf("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) + msh.WriteToPtyBuffer("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) continue } if pk.GetType() == packet.CmdDonePacketStr { @@ -865,20 +901,20 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.MessagePacketStr { msgPacket := pk.(*packet.MessagePacketType) - fmt.Printf("# [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPacket.CK, msgPacket.Message) + msh.WriteToPtyBuffer("msg> [remote %s] [%s] %s\n", msh.GetRemoteName(), msgPacket.CK, msgPacket.Message) continue } if pk.GetType() == packet.RawPacketStr { rawPacket := pk.(*packet.RawPacketType) - fmt.Printf("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPacket.Data) + msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPacket.Data) continue } if pk.GetType() == packet.CmdStartPacketStr { startPk := pk.(*packet.CmdStartPacketType) - fmt.Printf("start> [remote %s] reqid=%s (%p)\n", msh.GetRemoteName(), startPk.RespId, msh.ServerProc.Output) + msh.WriteToPtyBuffer("start> [remote %s] reqid=%s (%p)\n", msh.GetRemoteName(), startPk.RespId, msh.ServerProc.Output) continue } - fmt.Printf("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk)) + msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk)) } } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 69dece4d1..524b5af3f 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -15,8 +15,9 @@ type UpdatePacket interface { } type PtyDataUpdate struct { - SessionId string `json:"sessionid"` - CmdId string `json:"cmdid"` + SessionId string `json:"sessionid,omitempty"` + CmdId string `json:"cmdid,omitempty"` + RemoteId string `json:"remoteid,omitempty"` PtyPos int64 `json:"ptypos"` PtyData64 string `json:"ptydata64"` PtyDataLen int64 `json:"ptydatalen"` From c600027d7274ec5d55b7a0bb68fabd81fda7b1e3 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 15 Sep 2022 00:17:23 -0700 Subject: [PATCH 110/397] remote pty work --- pkg/cmdrunner/cmdrunner.go | 5 +++-- pkg/remote/remote.go | 26 +++++++++++++++++++++++--- pkg/sstore/updatebus.go | 13 ++++++++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 9c58bbaa6..187bfa0d5 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -564,8 +564,9 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), - InfoLines: splitLinesForInfo(buf.String()), + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), + InfoLines: splitLinesForInfo(buf.String()), + PtyRemoteId: state.RemoteId, }, }, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index bff2b4df2..dca06d90a 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -30,6 +30,9 @@ const RemoteTypeMShell = "mshell" const DefaultTerm = "xterm-256color" const DefaultMaxPtySize = 1024 * 1024 const CircBufSize = 64 * 1024 +const RemoteTermRows = 10 +const RemoteTermCols = 80 +const PtyReadBufSize = 100 const MShellServerCommand = ` PATH=$PATH:~/.mshell; @@ -457,6 +460,7 @@ func (msh *MShellProc) addControllingTty(ecmd *exec.Cmd) (*os.File, error) { if err != nil { return nil, err } + pty.Setsize(cmdPty, &pty.Winsize{Rows: RemoteTermRows, Cols: RemoteTermCols}) msh.ControllingPty = cmdPty ecmd.ExtraFiles = append(ecmd.ExtraFiles, cmdTty) if ecmd.SysProcAttr == nil { @@ -524,7 +528,21 @@ func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{ } else { realStr = realStr[1:] } - msh.PtyBuffer.Write([]byte(realStr)) + curOffset := msh.PtyBuffer.TotalWritten() + data := []byte(realStr) + msh.PtyBuffer.Write(data) + sendRemotePtyUpdate(msh.Remote.RemoteId, curOffset, data) +} + +func sendRemotePtyUpdate(remoteId string, dataOffset int64, data []byte) { + data64 := base64.StdEncoding.EncodeToString(data) + update := &sstore.PtyDataUpdate{ + RemoteId: remoteId, + PtyPos: dataOffset, + PtyData64: data64, + PtyDataLen: int64(len(data)), + } + sstore.MainBus.SendUpdate("", update) } func (msh *MShellProc) Launch() { @@ -534,7 +552,7 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("cannot launch archived remote\n") return } - msh.WriteToPtyBuffer("connecting to %s\n", remoteCopy.GetName()) + msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.GetName()) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true ecmd := sshOpts.MakeSSHExecCmd(MShellServerCommand) @@ -551,7 +569,7 @@ func (msh *MShellProc) Launch() { } }() go func() { - buf := make([]byte, 100) + buf := make([]byte, PtyReadBufSize) for { n, readErr := cmdPty.Read(buf) if readErr == io.EOF { @@ -562,7 +580,9 @@ func (msh *MShellProc) Launch() { break } msh.WithLock(func() { + curOffset := msh.PtyBuffer.TotalWritten() msh.PtyBuffer.Write(buf[0:n]) + sendRemotePtyUpdate(remoteCopy.RemoteId, curOffset, buf[0:n]) }) } }() diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 524b5af3f..7942739aa 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -9,6 +9,7 @@ var MainBus *UpdateBus = MakeUpdateBus() const PtyDataUpdateStr = "pty" const ModelUpdateStr = "model" +const UpdateChSize = 100 type UpdatePacket interface { UpdateType() string @@ -86,6 +87,7 @@ type InfoMsgType struct { InfoCompsMore bool `json:"infocompssmore,omitempty"` InfoLines []string `json:"infolines,omitempty"` TimeoutMs int64 `json:"timeoutms,omitempty"` + PtyRemoteId string `json:"ptyremoteid,omitempty"` } type HistoryInfoType struct { @@ -134,12 +136,12 @@ func (bus *UpdateBus) RegisterChannel(clientId string, sessionId string) chan in if found { close(uch.Ch) uch.SessionId = sessionId - uch.Ch = make(chan interface{}) + uch.Ch = make(chan interface{}, UpdateChSize) } else { uch = UpdateChannel{ ClientId: clientId, SessionId: sessionId, - Ch: make(chan interface{}), + Ch: make(chan interface{}, UpdateChSize), } } bus.Channels[clientId] = uch @@ -161,7 +163,12 @@ func (bus *UpdateBus) SendUpdate(sessionId string, update interface{}) { defer bus.Lock.Unlock() for _, uch := range bus.Channels { if uch.Match(sessionId) { - uch.Ch <- update + select { + case uch.Ch <- update: + + default: + fmt.Printf("[error] dropped update on updatebus uch clientid=%s\n", uch.ClientId) + } } } } From fcc1737fc469f22136b5646534b381b2dd386770 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 15 Sep 2022 00:37:17 -0700 Subject: [PATCH 111/397] format errors --- pkg/remote/remote.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index dca06d90a..38b8b3772 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -520,7 +520,11 @@ func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{ if !strings.HasSuffix(realStr, "\r\n") { realStr = realStr + "\r\n" } - realStr = "\033[0m\033[32m[scripthaus]\033[0m " + realStr + if strings.HasPrefix(realStr, "*") { + realStr = "\033[0m\033[31m[scripthaus]\033[0m " + realStr[1:] + } else { + realStr = "\033[0m\033[32m[scripthaus]\033[0m " + realStr + } barr := msh.PtyBuffer.Bytes() if len(barr) > 0 && barr[len(barr)-1] != '\n' { realStr = "\r\n" + realStr @@ -552,14 +556,14 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("cannot launch archived remote\n") return } - msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.GetName()) + msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true ecmd := sshOpts.MakeSSHExecCmd(MShellServerCommand) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) - msh.WriteToPtyBuffer("error, %s\n", statusErr.Error()) + msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error()) msh.setErrorStatus(statusErr) return } @@ -576,7 +580,7 @@ func (msh *MShellProc) Launch() { break } if readErr != nil { - msh.WriteToPtyBuffer("error reading from controlling-pty: %v\n", readErr) + msh.WriteToPtyBuffer("*error reading from controlling-pty: %v\n", readErr) break } msh.WithLock(func() { @@ -600,7 +604,7 @@ func (msh *MShellProc) Launch() { }) if err != nil { msh.setErrorStatus(err) - msh.WriteToPtyBuffer("error connecting to remote (uname=%q): %v\n", msh.UName, err) + msh.WriteToPtyBuffer("*error connecting to remote (uname=%q): %v\n", msh.UName, err) return } msh.WriteToPtyBuffer("connected\n") @@ -618,7 +622,7 @@ func (msh *MShellProc) Launch() { go msh.NotifyRemoteUpdate() } }) - msh.WriteToPtyBuffer("disconnected exitcode=%d\n", exitCode) + msh.WriteToPtyBuffer("*disconnected exitcode=%d\n", exitCode) }() go msh.ProcessPackets() return From f2e0f9457df476fb48e30419b27fd951516be4c9 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 15 Sep 2022 17:09:04 -0700 Subject: [PATCH 112/397] handle remote input (can enter password manually now) --- pkg/cmdrunner/cmdrunner.go | 1 - pkg/remote/remote.go | 32 +++++++++++++++++++++++++++++--- pkg/scpacket/scpacket.go | 16 ++++++++++++++++ pkg/scws/scws.go | 19 +++++++++++++++++-- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 187bfa0d5..0caf8d58e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -565,7 +565,6 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), - InfoLines: splitLinesForInfo(buf.String()), PtyRemoteId: state.RemoteId, }, }, nil diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 38b8b3772..bc2aad142 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -23,6 +23,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -30,7 +31,7 @@ const RemoteTypeMShell = "mshell" const DefaultTerm = "xterm-256color" const DefaultMaxPtySize = 1024 * 1024 const CircBufSize = 64 * 1024 -const RemoteTermRows = 10 +const RemoteTermRows = 8 const RemoteTermCols = 80 const PtyReadBufSize = 100 @@ -440,6 +441,29 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { return rtn } +func SendRemoteInput(pk *scpacket.RemoteInputPacketType) error { + data, err := base64.StdEncoding.DecodeString(pk.InputData64) + if err != nil { + return fmt.Errorf("cannot decode base64: %v\n", err) + } + msh := GetRemoteById(pk.RemoteId) + if msh == nil { + return fmt.Errorf("remote not found") + } + var cmdPty *os.File + msh.WithLock(func() { + cmdPty = msh.ControllingPty + }) + if cmdPty == nil { + return fmt.Errorf("remote has no attached pty") + } + _, err = cmdPty.Write(data) + if err != nil { + return fmt.Errorf("writing to pty: %v", err) + } + return nil +} + func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { if opts == nil || opts.Local { opts = &sstore.SSHOpts{} @@ -521,9 +545,9 @@ func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{ realStr = realStr + "\r\n" } if strings.HasPrefix(realStr, "*") { - realStr = "\033[0m\033[31m[scripthaus]\033[0m " + realStr[1:] + realStr = "\033[0m\033[31mscripthaus>\033[0m " + realStr[1:] } else { - realStr = "\033[0m\033[32m[scripthaus]\033[0m " + realStr + realStr = "\033[0m\033[32mscripthaus>\033[0m " + realStr } barr := msh.PtyBuffer.Bytes() if len(barr) > 0 && barr[len(barr)-1] != '\n' { @@ -592,6 +616,8 @@ func (msh *MShellProc) Launch() { }() if remoteName == "test2" { go func() { + return + time.Sleep(2 * time.Second) cmdPty.Write([]byte(Test2Pw)) msh.WriteToPtyBuffer("~[password sent]\r\n") diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 73c056971..aab8a9e31 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -11,6 +11,7 @@ import ( const FeCommandPacketStr = "fecmd" const WatchScreenPacketStr = "watchscreen" const FeInputPacketStr = "feinput" +const RemoteInputPacketStr = "remoteinput" type FeCommandPacketType struct { Type string `json:"type"` @@ -39,6 +40,12 @@ type FeInputPacketType struct { WinSize *packet.WinSize `json:"winsize,omitempty"` } +type RemoteInputPacketType struct { + Type string `json:"type"` + RemoteId string `json:"remoteid"` + InputData64 string `json:"inputdata64"` +} + type WatchScreenPacketType struct { Type string `json:"type"` SessionId string `json:"sessionid"` @@ -50,6 +57,7 @@ func init() { packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{})) packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{})) packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{})) + packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{})) } func (*FeCommandPacketType) GetType() string { @@ -75,3 +83,11 @@ func (*WatchScreenPacketType) GetType() string { func MakeWatchScreenPacket() *WatchScreenPacketType { return &WatchScreenPacketType{Type: WatchScreenPacketStr} } + +func MakeRemoteInputPacket() *RemoteInputPacketType { + return &RemoteInputPacketType{Type: RemoteInputPacketStr} +} + +func (*RemoteInputPacketType) GetType() string { + return RemoteInputPacketStr +} diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 7326aa4b4..71e91c92a 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -178,7 +178,7 @@ func (ws *WSState) RunWSRead() { fmt.Printf("error unmarshalling ws message: %v\n", err) continue } - if pk.GetType() == "feinput" { + if pk.GetType() == scpacket.FeInputPacketStr { feInputPk := pk.(*scpacket.FeInputPacketType) if feInputPk.Remote.OwnerId != "" { fmt.Printf("[error] cannot send input to remote with ownerid\n") @@ -189,6 +189,7 @@ func (ws *WSState) RunWSRead() { continue } go func() { + // TODO enforce a strong ordering (channel with list) err = sendCmdInput(feInputPk) if err != nil { fmt.Printf("[error] sending command input: %v\n", err) @@ -196,7 +197,7 @@ func (ws *WSState) RunWSRead() { }() continue } - if pk.GetType() == "watchscreen" { + if pk.GetType() == scpacket.WatchScreenPacketStr { wsPk := pk.(*scpacket.WatchScreenPacketType) err := ws.handleWatchScreen(wsPk) if err != nil { @@ -205,6 +206,20 @@ func (ws *WSState) RunWSRead() { } continue } + if pk.GetType() == scpacket.RemoteInputPacketStr { + inputPk := pk.(*scpacket.RemoteInputPacketType) + if inputPk.RemoteId == "" { + fmt.Printf("[error] invalid remoteinput packet, remoteid is not set\n") + continue + } + go func() { + err = remote.SendRemoteInput(inputPk) + if err != nil { + fmt.Printf("[error] processing remote input: %v\n", err) + } + }() + continue + } fmt.Printf("got ws bad message: %v\n", pk.GetType()) } } From 06e3a86f5372224d355d75da9bf88a4a0dc9710d Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 15 Sep 2022 17:44:39 -0700 Subject: [PATCH 113/397] add status connecting --- pkg/remote/remote.go | 56 ++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index bc2aad142..3cee7ad3d 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -15,7 +15,6 @@ import ( "strings" "sync" "syscall" - "time" "github.com/armon/circbuf" "github.com/creack/pty" @@ -49,6 +48,7 @@ fi const ( StatusInit = "init" StatusConnected = "connected" + StatusConnecting = "connecting" StatusDisconnected = "disconnected" StatusError = "error" ) @@ -573,9 +573,27 @@ func sendRemotePtyUpdate(remoteId string, dataOffset int64, data []byte) { sstore.MainBus.SendUpdate("", update) } +func (msh *MShellProc) RunPtyReadLoop(cmdPty *os.File) { + buf := make([]byte, PtyReadBufSize) + for { + n, readErr := cmdPty.Read(buf) + if readErr == io.EOF { + break + } + if readErr != nil { + msh.WriteToPtyBuffer("*error reading from controlling-pty: %v\n", readErr) + break + } + msh.WithLock(func() { + curOffset := msh.PtyBuffer.TotalWritten() + msh.PtyBuffer.Write(buf[0:n]) + sendRemotePtyUpdate(msh.Remote.RemoteId, curOffset, buf[0:n]) + }) + } +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() - remoteName := remoteCopy.GetName() if remoteCopy.Archived { msh.WriteToPtyBuffer("cannot launch archived remote\n") return @@ -596,33 +614,11 @@ func (msh *MShellProc) Launch() { ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() } }() - go func() { - buf := make([]byte, PtyReadBufSize) - for { - n, readErr := cmdPty.Read(buf) - if readErr == io.EOF { - break - } - if readErr != nil { - msh.WriteToPtyBuffer("*error reading from controlling-pty: %v\n", readErr) - break - } - msh.WithLock(func() { - curOffset := msh.PtyBuffer.TotalWritten() - msh.PtyBuffer.Write(buf[0:n]) - sendRemotePtyUpdate(remoteCopy.RemoteId, curOffset, buf[0:n]) - }) - } - }() - if remoteName == "test2" { - go func() { - return - - time.Sleep(2 * time.Second) - cmdPty.Write([]byte(Test2Pw)) - msh.WriteToPtyBuffer("~[password sent]\r\n") - }() - } + go msh.RunPtyReadLoop(cmdPty) + msh.WithLock(func() { + msh.Status = StatusConnecting + go msh.NotifyRemoteUpdate() + }) cproc, uname, err := shexec.MakeClientProc(ecmd) msh.WithLock(func() { msh.UName = uname @@ -643,7 +639,7 @@ func (msh *MShellProc) Launch() { exitErr := cproc.Cmd.Wait() exitCode := shexec.GetExitCode(exitErr) msh.WithLock(func() { - if msh.Status == StatusConnected { + if msh.Status == StatusConnected || msh.Status == StatusConnecting { msh.Status = StatusDisconnected go msh.NotifyRemoteUpdate() } From fad718d57193bb52865cda4b0c5735f65a211dab Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 16 Sep 2022 12:28:09 -0700 Subject: [PATCH 114/397] fixup connect/disconnect to deal with connecting state. use context to cancel remote that is in connecting state --- pkg/cmdrunner/cmdrunner.go | 6 +++++- pkg/remote/remote.go | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0caf8d58e..2c2fd4bca 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -421,6 +421,9 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if ids.Remote.RState.IsConnected() { return sstore.InfoMsgUpdate("remote %q already connected (no action taken)", ids.Remote.DisplayName), nil } + if ids.Remote.RState.Status == remote.StatusConnecting { + return sstore.InfoMsgUpdate("remote %q is already trying to connect (no action taken)", ids.Remote.DisplayName), nil + } go ids.Remote.MShell.Launch() return sstore.InfoMsgUpdate("remote %q reconnecting", ids.Remote.DisplayName), nil } @@ -431,7 +434,8 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy return nil, err } force := resolveBool(pk.Kwargs["force"], false) - if !ids.Remote.RState.IsConnected() && !force { + status := ids.Remote.MShell.GetStatus() + if status != remote.StatusConnected && status != remote.StatusConnecting { return sstore.InfoMsgUpdate("remote %q already disconnected (no action taken)", ids.Remote.DisplayName), nil } numCommands := ids.Remote.MShell.GetNumRunningCommands() diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 3cee7ad3d..2bb9b0a87 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -66,12 +66,13 @@ type MShellProc struct { Remote *sstore.RemoteType // runtime - Status string - ServerProc *shexec.ClientProc - UName string - Err error - ControllingPty *os.File - PtyBuffer *circbuf.Buffer + Status string + ServerProc *shexec.ClientProc + UName string + Err error + ControllingPty *os.File + PtyBuffer *circbuf.Buffer + MakeClientCancelFn context.CancelFunc RunningCmds []base.CommandKey } @@ -95,6 +96,12 @@ func (state RemoteRuntimeState) IsConnected() bool { return state.Status == StatusConnected } +func (msh *MShellProc) GetStatus() string { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Status +} + func (state RemoteRuntimeState) GetBaseDisplayName() string { if state.RemoteAlias != "" { return state.RemoteAlias @@ -522,6 +529,10 @@ func (msh *MShellProc) Disconnect() { if msh.ServerProc != nil { msh.ServerProc.Close() } + if msh.MakeClientCancelFn != nil { + msh.MakeClientCancelFn() + msh.MakeClientCancelFn = nil + } } func (msh *MShellProc) GetRemoteName() string { @@ -598,6 +609,11 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("cannot launch archived remote\n") return } + curStatus := msh.GetStatus() + if curStatus == StatusConnecting { + msh.WriteToPtyBuffer("remote is already connecting, disconnect before trying to connect again\n") + return + } msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true @@ -615,15 +631,22 @@ func (msh *MShellProc) Launch() { } }() go msh.RunPtyReadLoop(cmdPty) + makeClientCtx, makeClientCancelFn := context.WithCancel(context.Background()) + defer makeClientCancelFn() msh.WithLock(func() { msh.Status = StatusConnecting + msh.MakeClientCancelFn = makeClientCancelFn go msh.NotifyRemoteUpdate() }) - cproc, uname, err := shexec.MakeClientProc(ecmd) + cproc, uname, err := shexec.MakeClientProc(makeClientCtx, ecmd) msh.WithLock(func() { msh.UName = uname + msh.MakeClientCancelFn = nil // no notify here, because we'll call notify in either case below }) + if err == context.Canceled { + err = fmt.Errorf("forced disconnection") + } if err != nil { msh.setErrorStatus(err) msh.WriteToPtyBuffer("*error connecting to remote (uname=%q): %v\n", msh.UName, err) From f75b75790c6b56af1f3d93d64f3450c36320bffd Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Sep 2022 14:15:20 -0700 Subject: [PATCH 115/397] session stats/size --- pkg/sstore/dbops.go | 28 +++++++++++++++++++ pkg/sstore/fileops.go | 65 +++++++++++++++++++++++++++++++++++++++++++ pkg/sstore/sstore.go | 9 ++++++ 3 files changed, 102 insertions(+) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a999de565..c88dfd57c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -925,3 +925,31 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term func DeleteSession(ctx context.Context, sessionId string) error { return nil } + +func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, error) { + rtn := &SessionStatsType{SessionId: sessionId} + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM session WHERE sessionid = ?` + if !tx.Exists(query, sessionId) { + return fmt.Errorf("not found") + } + query = `SELECT count(*) FROM screen WHERE sessionid = ?` + rtn.NumScreens = tx.GetInt(query, sessionId) + query = `SELECT count(*) FROM window WHERE sessionid = ?` + rtn.NumWindows = tx.GetInt(query, sessionId) + query = `SELECT count(*) FROM line WHERE sessionid = ?` + rtn.NumLines = tx.GetInt(query, sessionId) + query = `SELECT count(*) FROM cmd WHERE sessionid = ?` + rtn.NumCmds = tx.GetInt(query, sessionId) + return nil + }) + if txErr != nil { + return nil, txErr + } + diskSize, err := SessionDiskSize(sessionId) + if err != nil { + return nil, err + } + rtn.DiskStats = diskSize + return rtn, nil +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 0ecf9aa02..1fdad2cd8 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -4,7 +4,11 @@ import ( "context" "encoding/base64" "fmt" + "os" + "path" + "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/cirfile" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) @@ -62,3 +66,64 @@ func ReadFullPtyOutFile(ctx context.Context, sessionId string, cmdId string) (in defer f.Close() return f.ReadAll(ctx) } + +type SessionDiskSizeType struct { + NumFiles int + TotalSize int64 + ErrorCount int +} + +func directorySize(dirName string) (SessionDiskSizeType, error) { + var rtn SessionDiskSizeType + entries, err := os.ReadDir(dirName) + if err != nil { + return rtn, err + } + for _, entry := range entries { + if entry.IsDir() { + rtn.ErrorCount++ + continue + } + finfo, err := entry.Info() + if err != nil { + rtn.ErrorCount++ + continue + } + rtn.NumFiles++ + rtn.TotalSize += finfo.Size() + } + return rtn, nil +} + +func SessionDiskSize(sessionId string) (SessionDiskSizeType, error) { + sessionDir, err := base.EnsureSessionDir(sessionId) + if err != nil { + return SessionDiskSizeType{}, err + } + return directorySize(sessionDir) +} + +func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) { + sdir := base.GetSessionsDir() + entries, err := os.ReadDir(sdir) + if err != nil { + return nil, err + } + rtn := make(map[string]SessionDiskSizeType) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + name := entry.Name() + _, err = uuid.Parse(name) + if err != nil { + continue + } + diskSize, err := directorySize(path.Join(sdir, name)) + if err != nil { + continue + } + rtn[name] = diskSize + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 9efcbcae8..d1b073015 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -117,6 +117,15 @@ type SessionType struct { Full bool `json:"full,omitempty"` } +type SessionStatsType struct { + SessionId string `json:"sessionid"` + NumScreens int `json:"numscreens"` + NumWindows int `json:"numwindows"` + NumLines int `json:"numlines"` + NumCmds int `json:"numcmds"` + DiskStats SessionDiskSizeType `json:"diskstats"` +} + type WindowOptsType struct { } From eab785409a2fb6c692c1a674e877797fb7e5140b Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Sep 2022 14:23:53 -0700 Subject: [PATCH 116/397] fix remoteidx insert code --- pkg/sstore/dbops.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c88dfd57c..9c73d7ee4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -121,12 +121,12 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { if tx.Exists(query, r.RemoteCanonicalName) { return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", r.RemoteCanonicalName) } - query = `SELECT max(remoteidx) FROM remote` + query = `SELECT COALESCE(max(remoteidx), 0) FROM remote` maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts, archived) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived)` + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx)` tx.NamedExecWrap(query, r.ToMap()) return nil }) From db142d97ecaf18b452342743e60155bf2fa5fe53 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Sep 2022 17:01:25 -0700 Subject: [PATCH 117/397] add linenum to line, nextlinenum to window --- db/migrations/000001_init.up.sql | 6 +++- db/schema.sql | 5 +++- pkg/sstore/dbops.go | 31 +++++++++++++------- pkg/sstore/sstore.go | 50 ++++++++++++++++++-------------- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index c58399aa3..5e8ef70a1 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -22,6 +22,7 @@ CREATE TABLE window ( curremoteownerid varchar(36) NOT NULL, curremoteid varchar(36) NOT NULL, curremotename varchar(50) NOT NULL, + nextlinenum int NOT NULL, winopts json NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, @@ -63,10 +64,13 @@ CREATE TABLE remote_instance ( CREATE TABLE line ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - lineid varchar(36) NOT NULL, userid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, ts bigint NOT NULL, + linenum int NOT NULL, + linenumtemp boolean NOT NULL, linetype varchar(10) NOT NULL, + linelocal boolean NOT NULL, text text NOT NULL, cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, diff --git a/db/schema.sql b/db/schema.sql index 9e71be1c3..a7054dd2b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -59,10 +59,13 @@ CREATE TABLE remote_instance ( CREATE TABLE line ( sessionid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, - lineid varchar(36) NOT NULL, userid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, ts bigint NOT NULL, + linenum int NOT NULL, + linenumtemp boolean NOT NULL, linetype varchar(10) NOT NULL, + linelocal boolean NOT NULL, text text NOT NULL, cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9c73d7ee4..d19f7734c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -512,16 +512,17 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) string { w := &WindowType{ - SessionId: sessionId, - WindowId: uuid.New().String(), - CurRemote: curRemote, - WinOpts: WindowOptsType{}, - ShareMode: ShareModeLocal, - ShareOpts: WindowShareOptsType{}, + SessionId: sessionId, + WindowId: uuid.New().String(), + CurRemote: curRemote, + NextLineNum: 1, + WinOpts: WindowOptsType{}, + ShareMode: ShareModeLocal, + ShareOpts: WindowShareOptsType{}, } wmap := w.ToMap() - query := `INSERT INTO window ( sessionid, windowid, curremoteownerid, curremoteid, curremotename, winopts, ownerid, sharemode, shareopts) - VALUES (:sessionid,:windowid,:curremoteownerid,:curremoteid,:curremotename,:winopts,:ownerid,:sharemode,:shareopts)` + query := `INSERT INTO window ( sessionid, windowid, curremoteownerid, curremoteid, curremotename, nextlinenum, winopts, ownerid, sharemode, shareopts) + VALUES (:sessionid,:windowid,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:winopts,:ownerid,:sharemode,:shareopts)` tx.NamedExecWrap(query, wmap) return w.WindowId } @@ -533,6 +534,9 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line.LineId == "" { return fmt.Errorf("line must have lineid set") } + if line.LineNum != 0 { + return fmt.Errorf("line should not hage linenum set") + } return WithTx(ctx, func(tx *TxWrap) error { var windowId string query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` @@ -540,9 +544,14 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if !hasWindow { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } - query = `INSERT INTO line ( sessionid, windowid, lineid, ts, userid, linetype, text, cmdid, ephemeral) - VALUES (:sessionid,:windowid,:lineid,:ts,:userid,:linetype,:text,:cmdid,:ephemeral)` + query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` + nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) + line.LineNum = int64(nextLineNum) + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral)` tx.NamedExecWrap(query, line) + query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) if cmd != nil { cmdMap := cmd.ToMap() query = ` @@ -876,6 +885,8 @@ func ClearWindow(ctx context.Context, sessionId string, windowId string) (*Model lineIds = tx.SelectStrings(query, sessionId, windowId) query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) + query = `UPDATE window SET nextlinenum = 1 WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) return nil }) if txErr != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d1b073015..95ace511e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -197,15 +197,16 @@ func (r RemotePtrType) MakeFullRemoteRef() string { } type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - CurRemote RemotePtrType `json:"curremote"` - WinOpts WindowOptsType `json:"winopts"` - OwnerId string `json:"ownerid"` - ShareMode string `json:"sharemode"` - ShareOpts WindowShareOptsType `json:"shareopts"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + CurRemote RemotePtrType `json:"curremote"` + WinOpts WindowOptsType `json:"winopts"` + OwnerId string `json:"ownerid"` + NextLineNum int64 `json:"nextlinenum"` + ShareMode string `json:"sharemode"` + ShareOpts WindowShareOptsType `json:"shareopts"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` // only for updates Remove bool `json:"remove,omitempty"` @@ -218,6 +219,7 @@ func (w *WindowType) ToMap() map[string]interface{} { rtn["curremoteownerid"] = w.CurRemote.OwnerId rtn["curremoteid"] = w.CurRemote.RemoteId rtn["curremotename"] = w.CurRemote.Name + rtn["nextlinenum"] = w.NextLineNum rtn["winopts"] = quickJson(w.WinOpts) rtn["ownerid"] = w.OwnerId rtn["sharemode"] = w.ShareMode @@ -235,6 +237,7 @@ func WindowFromMap(m map[string]interface{}) *WindowType { quickSetStr(&w.CurRemote.OwnerId, m, "curremoteownerid") quickSetStr(&w.CurRemote.RemoteId, m, "curremoteid") quickSetStr(&w.CurRemote.Name, m, "curremotename") + quickSetInt64(&w.NextLineNum, m, "nextlinenum") quickSetJson(&w.WinOpts, m, "winopts") quickSetStr(&w.OwnerId, m, "ownerid") quickSetStr(&w.ShareMode, m, "sharemode") @@ -416,16 +419,19 @@ type RemoteInstance struct { } type LineType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - LineId string `json:"lineid"` - Ts int64 `json:"ts"` - UserId string `json:"userid"` - LineType string `json:"linetype"` - Text string `json:"text,omitempty"` - CmdId string `json:"cmdid,omitempty"` - Ephemeral bool `json:"ephemeral,omitempty"` - Remove bool `json:"remove,omitempty"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + UserId string `json:"userid"` + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineNumTemp bool `json:"linenumtemp,omitempty"` + LineLocal bool `json:"linelocal"` + LineType string `json:"linetype"` + Text string `json:"text,omitempty"` + CmdId string `json:"cmdid,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + Remove bool `json:"remove,omitempty"` } type SSHOpts struct { @@ -574,9 +580,10 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId + rtn.UserId = userId rtn.LineId = uuid.New().String() rtn.Ts = time.Now().UnixMilli() - rtn.UserId = userId + rtn.LineLocal = true rtn.LineType = LineTypeCmd rtn.CmdId = cmdId return rtn @@ -586,9 +593,10 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId + rtn.UserId = userId rtn.LineId = uuid.New().String() rtn.Ts = time.Now().UnixMilli() - rtn.UserId = userId + rtn.LineLocal = true rtn.LineType = LineTypeText rtn.Text = text return rtn From a6637b51c96c9422679a11974b9393d35b863068 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Sep 2022 17:37:49 -0700 Subject: [PATCH 118/397] getlinecmd and getlineidbyarg --- pkg/cmdrunner/cmdrunner.go | 7 ++-- pkg/scbase/scbase.go | 13 ++++++++ pkg/sstore/dbops.go | 67 ++++++++++++++++++++++++++++++++++---- pkg/sstore/sstore.go | 12 +++---- 4 files changed, 83 insertions(+), 16 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2c2fd4bca..4a7056b63 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -19,6 +19,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) @@ -178,7 +179,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, fmt.Errorf("/run error: %w", err) } - cmdId := uuid.New().String() + cmdId := scbase.GenSCUUID() cmdStr := firstArg(pk) runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() @@ -232,7 +233,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history return err } hitem := &sstore.HistoryItemType{ - HistoryId: uuid.New().String(), + HistoryId: scbase.GenSCUUID(), Ts: time.Now().UnixMilli(), UserId: DefaultUserId, SessionId: ids.SessionId, @@ -512,7 +513,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss remoteOpts.Color = color } r := &sstore.RemoteType{ - RemoteId: uuid.New().String(), + RemoteId: scbase.GenSCUUID(), PhysicalId: "", RemoteType: sstore.RemoteTypeSsh, RemoteAlias: alias, diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 094d7b389..1756bda2c 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -6,8 +6,10 @@ import ( "io/fs" "os" "path" + "strconv" "sync" + "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "golang.org/x/sys/unix" ) @@ -140,3 +142,14 @@ func (g ScFileNameGenerator) PtyOutFile(ck base.CommandKey) string { func (g ScFileNameGenerator) RunOutFile(ck base.CommandKey) string { return path.Join(g.ScHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".runout") } + +func GenSCUUID() string { + for { + rtn := uuid.New().String() + _, err := strconv.Atoi(rtn[0:8]) + if err == nil { // do not allow UUIDs where the initial 8 bytes parse to an integer + continue + } + return rtn + } +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d19f7734c..487d7073d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,10 +4,12 @@ import ( "context" "database/sql" "fmt" + "strconv" "strings" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd" @@ -371,7 +373,7 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { // also creates default window, returns sessionId // if sessionName == "", it will be generated func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (UpdatePacket, error) { - newSessionId := uuid.New().String() + newSessionId := scbase.GenSCUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) @@ -465,7 +467,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) - newScreenId = uuid.New().String() + newScreenId = scbase.GenSCUUID() query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} @@ -513,7 +515,7 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) string { w := &WindowType{ SessionId: sessionId, - WindowId: uuid.New().String(), + WindowId: scbase.GenSCUUID(), CurRemote: curRemote, NextLineNum: 1, WinOpts: WindowOptsType{}, @@ -527,6 +529,59 @@ func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) strin return w.WindowId } +func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lineArg string) (string, error) { + var lineId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + lineNum, err := strconv.Atoi(lineArg) + if err == nil { + // valid linenum + query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND linenum = ?` + lineId = tx.GetString(query, sessionId, windowId, lineNum) + } else if len(lineArg) == 8 { + // prefix id string match + query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND substr(lineid, 1, 8) = ?` + lineId = tx.GetString(query, sessionId, windowId, lineArg) + } else { + // id match + query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` + lineId = tx.GetString(query, sessionId, windowId, lineArg) + } + return nil + }) + if txErr != nil { + return "", txErr + } + return lineId, nil +} + +func GetLineCmd(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, *CmdType, error) { + var lineRtn *LineType + var cmdRtn *CmdType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("window not found") + } + var lineVal LineType + query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` + found := tx.GetWrap(&lineVal, query, sessionId, windowId, lineId) + if !found { + return nil + } + lineRtn = &lineVal + if lineVal.CmdId != "" { + query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` + m := tx.GetMap(query, sessionId, lineVal.CmdId) + cmdRtn = CmdFromMap(m) + } + return nil + }) + if txErr != nil { + return nil, nil, txErr + } + return lineRtn, cmdRtn, nil +} + func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line == nil { return fmt.Errorf("line cannot be nil") @@ -538,10 +593,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { return fmt.Errorf("line should not hage linenum set") } return WithTx(ctx, func(tx *TxWrap) error { - var windowId string query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - hasWindow := tx.GetWrap(&windowId, query, line.SessionId, line.WindowId) - if !hasWindow { + if !tx.Exists(query, line.SessionId, line.WindowId) { return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` @@ -744,7 +797,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if !found { ri = RemoteInstance{ - RIId: uuid.New().String(), + RIId: scbase.GenSCUUID(), Name: remotePtr.Name, SessionId: sessionId, WindowId: windowId, diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 95ace511e..5833c970d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -581,7 +581,7 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn.SessionId = sessionId rtn.WindowId = windowId rtn.UserId = userId - rtn.LineId = uuid.New().String() + rtn.LineId = scbase.GenSCUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeCmd @@ -594,7 +594,7 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri rtn.SessionId = sessionId rtn.WindowId = windowId rtn.UserId = userId - rtn.LineId = uuid.New().String() + rtn.LineId = scbase.GenSCUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeText @@ -642,7 +642,7 @@ func EnsureLocalRemote(ctx context.Context) error { } // create the local remote localRemote := &RemoteType{ - RemoteId: uuid.New().String(), + RemoteId: scbase.GenSCUUID(), PhysicalId: physicalId, RemoteType: RemoteTypeSsh, RemoteAlias: LocalRemoteAlias, @@ -670,7 +670,7 @@ func AddTest01Remote(ctx context.Context) error { return nil } testRemote := &RemoteType{ - RemoteId: uuid.New().String(), + RemoteId: scbase.GenSCUUID(), RemoteType: RemoteTypeSsh, RemoteAlias: "test01", RemoteCanonicalName: "ubuntu@test01.ec2", @@ -702,7 +702,7 @@ func AddTest02Remote(ctx context.Context) error { return nil } testRemote := &RemoteType{ - RemoteId: uuid.New().String(), + RemoteId: scbase.GenSCUUID(), RemoteType: RemoteTypeSsh, RemoteAlias: "test2", RemoteCanonicalName: "test2@test01.ec2", @@ -740,7 +740,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { } func createClientData(tx *TxWrap) error { - userId := uuid.New().String() + userId := scbase.GenSCUUID() curve := elliptic.P384() pkey, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { From 5a6bebc51f9a7168498edbc6ff0306729fc03774 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Sep 2022 21:50:36 -0700 Subject: [PATCH 119/397] show some line info --- pkg/cmdrunner/cmdrunner.go | 51 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 4a7056b63..0ce4afcec 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3,6 +3,7 @@ package cmdrunner import ( "bytes" "context" + "encoding/json" "fmt" "os" "path" @@ -1271,6 +1272,52 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, err } - fmt.Printf("/line:show ids %v\n", ids) - return nil, nil + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/line:show requires an argument (line number or id)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + line, cmd, err := sstore.GetLineCmd(ctx, ids.SessionId, ids.WindowId, lineId) + if err != nil { + return nil, fmt.Errorf("error getting line: %v", err) + } + if line == nil { + return nil, fmt.Errorf("line %q not found", lineArg) + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "lineid", line.LineId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", line.LineType)) + lineNumStr := strconv.FormatInt(line.LineNum, 10) + if line.LineNumTemp { + lineNumStr = "~" + lineNumStr + } + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "linenum", lineNumStr)) + ts := time.UnixMilli(line.Ts) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ts", ts.Format("2006-01-02 15:04:05"))) + if line.Ephemeral { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ephemeral", true)) + } + if cmd != nil { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef())) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", cmd.Status)) + if cmd.RemoteState.Cwd != "" { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.RemoteState.Cwd)) + } + termOptsOut, _ := json.Marshal(cmd.TermOpts) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", string(termOptsOut))) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("line %d info", line.LineNum), + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil } From 8f608d9d9e1a978bb148aff6d2cd3f9d85d826b1 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 21 Sep 2022 12:39:55 -0700 Subject: [PATCH 120/397] number formatting and better termopts formatting --- pkg/cmdrunner/cmdrunner.go | 18 +++++++++-- pkg/scbase/scbase.go | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0ce4afcec..2476813d9 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3,7 +3,6 @@ package cmdrunner import ( "bytes" "context" - "encoding/json" "fmt" "os" "path" @@ -1310,8 +1309,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if cmd.RemoteState.Cwd != "" { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.RemoteState.Cwd)) } - termOptsOut, _ := json.Marshal(cmd.TermOpts) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", string(termOptsOut))) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", formatTermOpts(cmd.TermOpts))) } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ @@ -1321,3 +1319,17 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst } return update, nil } + +func formatTermOpts(termOpts sstore.TermOpts) string { + if termOpts.Cols == 0 { + return "???" + } + rtnStr := fmt.Sprintf("%dx%d", termOpts.Rows, termOpts.Cols) + if termOpts.FlexRows { + rtnStr += " flexrows" + } + if termOpts.MaxPtySize > 0 { + rtnStr += " maxbuf=" + scbase.NumFormatB2(termOpts.MaxPtySize) + } + return rtnStr +} diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 1756bda2c..c1b8cd0b2 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -153,3 +153,66 @@ func GenSCUUID() string { return rtn } } + +func NumFormatDec(num int64) string { + var signStr string + absNum := num + if absNum < 0 { + absNum = -absNum + signStr = "-" + } + if absNum < 1000 { + // raw num + return signStr + strconv.FormatInt(absNum, 10) + } + if absNum < 1000000 { + // k num + kVal := float64(absNum) / 1000 + return signStr + strconv.FormatFloat(kVal, 'f', 2, 64) + "k" + } + if absNum < 1000000000 { + // M num + mVal := float64(absNum) / 1000000 + return signStr + strconv.FormatFloat(mVal, 'f', 2, 64) + "m" + } else { + // G num + gVal := float64(absNum) / 1000000000 + return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "g" + } +} + +func NumFormatB2(num int64) string { + var signStr string + absNum := num + if absNum < 0 { + absNum = -absNum + signStr = "-" + } + if absNum < 1024 { + // raw num + return signStr + strconv.FormatInt(absNum, 10) + } + if absNum < 1000000 { + // k num + if absNum%1024 == 0 { + return signStr + strconv.FormatInt(absNum/1024, 10) + "K" + } + kVal := float64(absNum) / 1024 + return signStr + strconv.FormatFloat(kVal, 'f', 2, 64) + "K" + } + if absNum < 1000000000 { + // M num + if absNum%(1024*1024) == 0 { + return signStr + strconv.FormatInt(absNum/(1024*1024), 10) + "M" + } + mVal := float64(absNum) / (1024 * 1024) + return signStr + strconv.FormatFloat(mVal, 'f', 2, 64) + "M" + } else { + // G num + if absNum%(1024*1024*1024) == 0 { + return signStr + strconv.FormatInt(absNum/(1024*1024*1024), 10) + "G" + } + gVal := float64(absNum) / (1024 * 1024 * 1024) + return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "G" + } +} From 21617298e9e4ab5ec9033aa45562bc98b47a91b8 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 21 Sep 2022 22:02:38 -0700 Subject: [PATCH 121/397] origtermopts, clientdata to/from map --- db/migrations/000001_init.up.sql | 2 + db/schema.sql | 3 ++ pkg/cmdrunner/cmdrunner.go | 3 ++ pkg/sstore/dbops.go | 5 ++- pkg/sstore/quick.go | 11 +++++ pkg/sstore/sstore.go | 72 ++++++++++++++++++++++++-------- 6 files changed, 76 insertions(+), 20 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 5e8ef70a1..20fd6ca9b 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -1,4 +1,5 @@ CREATE TABLE client ( + clientid varchar(36) NOT NULL, userid varchar(36) NOT NULL, activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, @@ -104,6 +105,7 @@ CREATE TABLE cmd ( cmdstr text NOT NULL, remotestate json NOT NULL, termopts json NOT NULL, + origtermopts json NOT NULL, status varchar(10) NOT NULL, startpk json NOT NULL, donepk json NOT NULL, diff --git a/db/schema.sql b/db/schema.sql index a7054dd2b..c049c2391 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,6 +1,7 @@ CREATE TABLE schema_migrations (version uint64,dirty bool); CREATE UNIQUE INDEX version_unique ON schema_migrations (version); CREATE TABLE client ( + clientid varchar(36) NOT NULL, userid varchar(36) NOT NULL, activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, @@ -22,6 +23,7 @@ CREATE TABLE window ( curremoteownerid varchar(36) NOT NULL, curremoteid varchar(36) NOT NULL, curremotename varchar(50) NOT NULL, + nextlinenum int NOT NULL, winopts json NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, @@ -97,6 +99,7 @@ CREATE TABLE cmd ( cmdstr text NOT NULL, remotestate json NOT NULL, termopts json NOT NULL, + origtermopts json NOT NULL, status varchar(10) NOT NULL, startpk json NOT NULL, donepk json NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2476813d9..74cd4309a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1310,6 +1310,9 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.RemoteState.Cwd)) } buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", formatTermOpts(cmd.TermOpts))) + if cmd.TermOpts != cmd.OrigTermOpts { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "orig-termopts", formatTermOpts(cmd.OrigTermOpts))) + } } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 487d7073d..20555ce2e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -592,6 +592,7 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line.LineNum != 0 { return fmt.Errorf("line should not hage linenum set") } + cmd.OrigTermOpts = cmd.TermOpts return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, line.SessionId, line.WindowId) { @@ -608,8 +609,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil { cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, status, startpk, donepk, runout, usedrows) - VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:status,:startpk,:donepk,:runout,:usedrows) +INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, origtermopts, status, startpk, donepk, runout, usedrows) + VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:origtermopts,:status,:startpk,:donepk,:runout,:usedrows) ` tx.NamedExecWrap(query, cmdMap) } diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index df7a26a71..df1882b76 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -54,6 +54,17 @@ func quickSetBool(bval *bool, m map[string]interface{}, name string) { } } +func quickSetBytes(bval *[]byte, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlBytes, ok := v.([]byte) + if ok { + *bval = sqlBytes + } +} + func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { v, ok := m[name] if !ok { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 5833c970d..f3e96fabb 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -92,6 +92,7 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { } type ClientData struct { + ClientId string `json:"clientid"` UserId string `json:"userid"` UserPrivateKeyBytes []byte `json:"-"` UserPublicKeyBytes []byte `json:"-"` @@ -100,6 +101,29 @@ type ClientData struct { ActiveSessionId string `json:"activesessionid"` } +func (c *ClientData) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["clientid"] = c.ClientId + rtn["userid"] = c.UserId + rtn["userprivatekeybytes"] = c.UserPrivateKeyBytes + rtn["userpublickeybytes"] = c.UserPublicKeyBytes + rtn["activesessionid"] = c.ActiveSessionId + return rtn +} + +func ClientDataFromMap(m map[string]interface{}) *ClientData { + if len(m) == 0 { + return nil + } + var c ClientData + quickSetStr(&c.ClientId, m, "clientid") + quickSetStr(&c.UserId, m, "userid") + quickSetBytes(&c.UserPrivateKeyBytes, m, "userprivatekeybytes") + quickSetBytes(&c.UserPublicKeyBytes, m, "userpublickeybytes") + quickSetStr(&c.ActiveSessionId, m, "activesessionid") + return &c +} + type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` @@ -480,18 +504,19 @@ func (r *RemoteType) GetName() string { } type CmdType struct { - SessionId string `json:"sessionid"` - CmdId string `json:"cmdid"` - Remote RemotePtrType `json:"remote"` - CmdStr string `json:"cmdstr"` - RemoteState RemoteState `json:"remotestate"` - TermOpts TermOpts `json:"termopts"` - Status string `json:"status"` - StartPk *packet.CmdStartPacketType `json:"startpk"` - DonePk *packet.CmdDonePacketType `json:"donepk"` - UsedRows int64 `json:"usedrows"` - RunOut []packet.PacketType `json:"runout"` - Remove bool `json:"remove"` + SessionId string `json:"sessionid"` + CmdId string `json:"cmdid"` + Remote RemotePtrType `json:"remote"` + CmdStr string `json:"cmdstr"` + RemoteState RemoteState `json:"remotestate"` + TermOpts TermOpts `json:"termopts"` + OrigTermOpts TermOpts `json:"origtermopts"` + Status string `json:"status"` + StartPk *packet.CmdStartPacketType `json:"startpk"` + DonePk *packet.CmdDonePacketType `json:"donepk"` + UsedRows int64 `json:"usedrows"` + RunOut []packet.PacketType `json:"runout"` + Remove bool `json:"remove"` } func (r *RemoteType) ToMap() map[string]interface{} { @@ -547,6 +572,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["cmdstr"] = cmd.CmdStr rtn["remotestate"] = quickJson(cmd.RemoteState) rtn["termopts"] = quickJson(cmd.TermOpts) + rtn["origtermopts"] = quickJson(cmd.OrigTermOpts) rtn["status"] = cmd.Status rtn["startpk"] = quickJson(cmd.StartPk) rtn["donepk"] = quickJson(cmd.DonePk) @@ -568,6 +594,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetStr(&cmd.CmdStr, m, "cmdstr") quickSetJson(&cmd.RemoteState, m, "remotestate") quickSetJson(&cmd.TermOpts, m, "termopts") + quickSetJson(&cmd.OrigTermOpts, m, "origtermopts") quickSetStr(&cmd.Status, m, "status") quickSetJson(&cmd.StartPk, m, "startpk") quickSetJson(&cmd.DonePk, m, "donepk") @@ -740,7 +767,6 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { } func createClientData(tx *TxWrap) error { - userId := scbase.GenSCUUID() curve := elliptic.P384() pkey, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { @@ -754,9 +780,17 @@ func createClientData(tx *TxWrap) error { if err != nil { return fmt.Errorf("marshaling (pkix) public key bytes: %w", err) } - query := `INSERT INTO client (userid, activesessionid, userpublickeybytes, userprivatekeybytes) VALUES (?, '', ?, ?)` - tx.ExecWrap(query, userId, pubBytes, pkBytes) - fmt.Printf("create new userid[%s] with public/private keypair\n", userId) + c := ClientData{ + ClientId: uuid.New().String(), + UserId: uuid.New().String(), + UserPrivateKeyBytes: pkBytes, + UserPublicKeyBytes: pubBytes, + ActiveSessionId: "", + } + query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes) + VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes)` + tx.NamedExecWrap(query, c.ToMap()) + fmt.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } @@ -774,10 +808,12 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { return createErr } } - found := tx.GetWrap(&rtn, "SELECT * FROM client") - if !found { + m := tx.GetMap("SELECT * FROM client") + cdata := ClientDataFromMap(m) + if cdata == nil { return fmt.Errorf("invalid client data") } + rtn = *cdata return nil }) if err != nil { From eb00fde596090a3ad28426a7d4d1697252e86412 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 21 Sep 2022 23:26:24 -0700 Subject: [PATCH 122/397] send uname/mshellversion with remotestate --- pkg/cmdrunner/cmdrunner.go | 14 -------------- pkg/remote/remote.go | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 74cd4309a..405cf070a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -553,20 +553,6 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return nil, err } state := ids.Remote.RState - var buf bytes.Buffer - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", state.RemoteType)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remoteid", state.RemoteId)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "physicalid", state.PhysicalId)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "alias", state.RemoteAlias)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "canonicalname", state.RemoteCanonicalName)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "connectmode", state.ConnectMode)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", state.Status)) - if ids.Remote.RemoteState != nil { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.Remote.RemoteState.Cwd)) - } - if state.ErrorStr != "" { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "error", state.ErrorStr)) - } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 2bb9b0a87..809524a14 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -90,6 +90,8 @@ type RemoteRuntimeState struct { ConnectMode string `json:"connectmode"` Archived bool `json:"archived"` RemoteIdx int64 `json:"remoteidx"` + UName string `json:"uname"` + MShellVersion string `json:"mshellversion"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -349,6 +351,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { ConnectMode: msh.Remote.ConnectMode, Archived: msh.Remote.Archived, RemoteIdx: msh.Remote.RemoteIdx, + UName: msh.UName, } if msh.Err != nil { state.ErrorStr = msh.Err.Error() @@ -376,6 +379,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { Cwd: msh.ServerProc.InitPk.Cwd, Env0: msh.ServerProc.InitPk.Env0, } + state.MShellVersion = msh.ServerProc.InitPk.Version vars["home"] = msh.ServerProc.InitPk.HomeDir vars["remoteuser"] = msh.ServerProc.InitPk.User vars["bestuser"] = vars["remoteuser"] @@ -517,6 +521,12 @@ func (msh *MShellProc) GetRemoteCopy() sstore.RemoteType { return *msh.Remote } +func (msh *MShellProc) GetUName() string { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.UName +} + func (msh *MShellProc) GetNumRunningCommands() int { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -642,6 +652,9 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { msh.UName = uname msh.MakeClientCancelFn = nil + if cproc != nil && cproc.InitPk != nil { + msh.Remote.InitPk = cproc.InitPk + } // no notify here, because we'll call notify in either case below }) if err == context.Canceled { From 982d600d9b51c7b832e48e81dda8bc2c159d18ce Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 24 Sep 2022 19:54:06 -0700 Subject: [PATCH 123/397] checkpoint, working on autoinstall, semantic versioning, remoteshowall --- db/migrations/000001_init.up.sql | 1 + go.mod | 1 + go.sum | 2 ++ pkg/cmdrunner/cmdrunner.go | 47 ++++++++++++++++++++++++++++++-- pkg/remote/remote.go | 21 ++++++++++++++ pkg/sstore/sstore.go | 3 ++ pkg/sstore/updatebus.go | 1 + 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 20fd6ca9b..7630007d2 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -88,6 +88,7 @@ CREATE TABLE remote ( remoteuser varchar(50) NOT NULL, remotehost varchar(200) NOT NULL, connectmode varchar(20) NOT NULL, + autoinstall boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, diff --git a/go.mod b/go.mod index d4a4489f1..c928ff767 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect go.uber.org/atomic v1.7.0 // indirect + golang.org/x/mod v0.5.1 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect mvdan.cc/sh/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index 7465dfaa1..97dd6f71f 100644 --- a/go.sum +++ b/go.sum @@ -1192,6 +1192,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 405cf070a..0f9f9ab6e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -575,8 +575,8 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show all remote info"), - InfoLines: splitLinesForInfo(buf.String()), + InfoTitle: fmt.Sprintf("show all remote info"), + RemoteShowAll: true, }, }, nil } @@ -1322,3 +1322,46 @@ func formatTermOpts(termOpts sstore.TermOpts) string { } return rtnStr } + +type ColMeta struct { + Title string + MinCols int + MaxCols int +} + +func toInterfaceArr(sarr []string) []interface{} { + rtn := make([]interface{}, len(sarr)) + for idx, s := range sarr { + rtn[idx] = s + } + return rtn +} + +func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string { + numCols := len(colMeta) + maxColLen := make([]int, len(colMeta)) + for i, cm := range colMeta { + maxColLen[i] = cm.MinCols + } + for _, row := range data { + for i := 0; i < numCols && i < len(row); i++ { + dlen := len(row[i]) + if dlen > maxColLen[i] { + maxColLen[i] = dlen + } + } + } + fmtStr := "" + for idx, clen := range maxColLen { + if idx != 0 { + fmtStr += " " + } + fmtStr += fmt.Sprintf("%%%ds", clen) + } + var rtn []string + for _, row := range data { + sval := fmt.Sprintf(fmtStr, toInterfaceArr(row)...) + rtn = append(rtn, sval) + } + return rtn +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 809524a14..bb328d84a 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -24,6 +24,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "golang.org/x/mod/semver" ) const RemoteTypeMShell = "mshell" @@ -34,6 +35,9 @@ const RemoteTermRows = 8 const RemoteTermCols = 80 const PtyReadBufSize = 100 +const MShellVersion = "v0.1.0" +const MShellVersionConstraint = "^0.1" + const MShellServerCommand = ` PATH=$PATH:~/.mshell; which mshell > /dev/null; @@ -53,6 +57,12 @@ const ( StatusError = "error" ) +func init() { + if MShellVersion != base.MShellVersion { + panic(fmt.Sprintf("sh2-server mshell version must match '%s' vs '%s'", MShellVersion, base.MShellVersion)) + } +} + var GlobalStore *Store type Store struct { @@ -73,6 +83,7 @@ type MShellProc struct { ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc + NeedsMShellUpgrade bool RunningCmds []base.CommandKey } @@ -88,6 +99,7 @@ type RemoteRuntimeState struct { ErrorStr string `json:"errorstr,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` Archived bool `json:"archived"` RemoteIdx int64 `json:"remoteidx"` UName string `json:"uname"` @@ -349,6 +361,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { PhysicalId: msh.Remote.PhysicalId, Status: msh.Status, ConnectMode: msh.Remote.ConnectMode, + AutoInstall: msh.Remote.AutoInstall, Archived: msh.Remote.Archived, RemoteIdx: msh.Remote.RemoteIdx, UName: msh.UName, @@ -649,17 +662,25 @@ func (msh *MShellProc) Launch() { go msh.NotifyRemoteUpdate() }) cproc, uname, err := shexec.MakeClientProc(makeClientCtx, ecmd) + var mshellVersion string msh.WithLock(func() { msh.UName = uname msh.MakeClientCancelFn = nil if cproc != nil && cproc.InitPk != nil { msh.Remote.InitPk = cproc.InitPk + mshellVersion = cproc.InitPk.Version + } + if semver.Compare(mshellVersion, MShellVersion) < 0 { + msh.NeedsMShellUpgrade = true } // no notify here, because we'll call notify in either case below }) if err == context.Canceled { err = fmt.Errorf("forced disconnection") } + if semver.MajorMinor(mshellVersion) != semver.MajorMinor(MShellVersion) { + err = fmt.Errorf("mshell version is not compatible current=%s remote=%s", MShellVersion, mshellVersion) + } if err != nil { msh.setErrorStatus(err) msh.WriteToPtyBuffer("*error connecting to remote (uname=%q): %v\n", msh.UName, err) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index f3e96fabb..143286c83 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -488,6 +488,7 @@ type RemoteType struct { RemoteUser string `json:"remoteuser"` RemoteHost string `json:"remotehost"` ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` InitPk *packet.InitPacketType `json:"inipk"` SSHOpts *SSHOpts `json:"sshopts"` RemoteOpts *RemoteOptsType `json:"remoteopts"` @@ -530,6 +531,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["remoteuser"] = r.RemoteUser rtn["remotehost"] = r.RemoteHost rtn["connectmode"] = r.ConnectMode + rtn["autoinstall"] = r.AutoInstall rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) rtn["remoteopts"] = quickJson(r.RemoteOpts) @@ -553,6 +555,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetStr(&r.RemoteUser, m, "remoteuser") quickSetStr(&r.RemoteHost, m, "remotehost") quickSetStr(&r.ConnectMode, m, "connectmode") + quickSetBool(&r.AutoInstall, m, "autoinstall") quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") quickSetJson(&r.RemoteOpts, m, "remoteopts") diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 7942739aa..01a6bd7f4 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -88,6 +88,7 @@ type InfoMsgType struct { InfoLines []string `json:"infolines,omitempty"` TimeoutMs int64 `json:"timeoutms,omitempty"` PtyRemoteId string `json:"ptyremoteid,omitempty"` + RemoteShowAll bool `json:"remoteshowall,omitempty"` } type HistoryInfoType struct { From bf4fa2031bec6427bc96fa8b10cfd111b2dd06b5 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 24 Sep 2022 22:42:52 -0700 Subject: [PATCH 124/397] autoinstall, fix version check --- pkg/cmdrunner/cmdrunner.go | 2 ++ pkg/remote/remote.go | 2 +- pkg/sstore/dbops.go | 4 ++-- pkg/sstore/sstore.go | 3 +++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0f9f9ab6e..b4405b686 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -473,6 +473,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if !sstore.IsValidConnectMode(connectMode) { return nil, fmt.Errorf("/remote:new invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) } + autoInstall := resolveBool(pk.Kwargs["autoinstall"], true) var isSudo bool if sudoStr != "" { isSudo = true @@ -522,6 +523,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss RemoteUser: remoteUser, RemoteHost: remoteHost, ConnectMode: connectMode, + AutoInstall: autoInstall, SSHOpts: sshOpts, RemoteOpts: remoteOpts, } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index bb328d84a..dbc3b5135 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -678,7 +678,7 @@ func (msh *MShellProc) Launch() { if err == context.Canceled { err = fmt.Errorf("forced disconnection") } - if semver.MajorMinor(mshellVersion) != semver.MajorMinor(MShellVersion) { + if err == nil && semver.MajorMinor(mshellVersion) != semver.MajorMinor(MShellVersion) { err = fmt.Errorf("mshell version is not compatible current=%s remote=%s", MShellVersion, mshellVersion) } if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 20555ce2e..55762a3ff 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -127,8 +127,8 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx)` + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx)` tx.NamedExecWrap(query, r.ToMap()) return nil }) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 143286c83..e6d39f68e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -681,6 +681,7 @@ func EnsureLocalRemote(ctx context.Context) error { RemoteUser: user.Username, RemoteHost: hostName, ConnectMode: ConnectModeStartup, + AutoInstall: true, SSHOpts: &SSHOpts{Local: true}, } err = UpsertRemote(ctx, localRemote) @@ -714,6 +715,7 @@ func AddTest01Remote(ctx context.Context) error { SSHIdentity: "/Users/mike/aws/mfmt.pem", }, ConnectMode: ConnectModeStartup, + AutoInstall: true, } err = UpsertRemote(ctx, testRemote) if err != nil { @@ -745,6 +747,7 @@ func AddTest02Remote(ctx context.Context) error { SSHUser: "test2", }, ConnectMode: ConnectModeStartup, + AutoInstall: true, } err = UpsertRemote(ctx, testRemote) if err != nil { From d251cbdd883c3c2e33155a67d552eb2e671a6c83 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 25 Sep 2022 00:26:33 -0700 Subject: [PATCH 125/397] save/restore winsize w/ clientdata --- cmd/main-server.go | 48 ++++++++++++++++++++++++++++++-- db/migrations/000001_init.up.sql | 3 +- pkg/sstore/dbops.go | 9 ++++++ pkg/sstore/sstore.go | 34 ++++++++++++++-------- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index fd89e92bf..14e7d4c6c 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -116,6 +116,46 @@ func writeToFifo(fifoName string, data []byte) error { return nil } +func HandleGetClientData(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + cdata, err := sstore.EnsureClientData(r.Context()) + if err != nil { + WriteJsonError(w, err) + return + } + WriteJsonSuccess(w, cdata) + return +} + +func HandleSetWinSize(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Vary", "Origin") + w.Header().Set("Cache-Control", "no-cache") + if r.Method == "GET" || r.Method == "OPTIONS" { + w.WriteHeader(200) + return + } + decoder := json.NewDecoder(r.Body) + var winSize sstore.ClientWinSizeType + err := decoder.Decode(&winSize) + if err != nil { + WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) + return + } + err = sstore.SetWinSize(r.Context(), winSize) + if err != nil { + WriteJsonError(w, fmt.Errorf("error setting winsize: %w", err)) + return + } + WriteJsonSuccess(w, true) + return +} + // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) @@ -322,12 +362,12 @@ func main() { fmt.Printf("[error] migrate up: %v\n", err) return } - userData, err := sstore.EnsureClientData(context.Background()) + clientData, err := sstore.EnsureClientData(context.Background()) if err != nil { - fmt.Printf("[error] ensuring user data: %v\n", err) + fmt.Printf("[error] ensuring client data: %v\n", err) return } - fmt.Printf("userid = %s\n", userData.UserId) + fmt.Printf("userid = %s\n", clientData.UserId) err = sstore.EnsureLocalRemote(context.Background()) if err != nil { fmt.Printf("[error] ensuring local remote: %v\n", err) @@ -365,6 +405,8 @@ func main() { gr.HandleFunc("/api/remote-pty", HandleRemotePty) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") + gr.HandleFunc("/api/get-client-data", HandleGetClientData) + gr.HandleFunc("/api/set-winsize", HandleSetWinSize) server := &http.Server{ Addr: MainServerAddr, ReadTimeout: HttpReadTimeout, diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 7630007d2..40c75e55f 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -3,7 +3,8 @@ CREATE TABLE client ( userid varchar(36) NOT NULL, activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, - userprivatekeybytes blob NOT NULL + userprivatekeybytes blob NOT NULL, + winsize json NOT NULL ); CREATE TABLE session ( diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 55762a3ff..10761cab8 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -419,6 +419,15 @@ func SetActiveSessionId(ctx context.Context, sessionId string) error { return txErr } +func SetWinSize(ctx context.Context, winSize ClientWinSizeType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET winsize = ?` + tx.ExecWrap(query, quickJson(winSize)) + return nil + }) + return txErr +} + func containsStr(strs []string, testStr string) bool { for _, s := range strs { if s == testStr { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index e6d39f68e..40dcfeccc 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -91,14 +91,23 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { return globalDB, globalDBErr } +type ClientWinSizeType struct { + Width int `json:"width"` + Height int `json:"height"` + Top int `json:"top"` + Left int `json:"left"` + FullScreen bool `json:"fullscreen,omitempty"` +} + type ClientData struct { - ClientId string `json:"clientid"` - UserId string `json:"userid"` - UserPrivateKeyBytes []byte `json:"-"` - UserPublicKeyBytes []byte `json:"-"` - UserPrivateKey *ecdsa.PrivateKey - UserPublicKey *ecdsa.PublicKey - ActiveSessionId string `json:"activesessionid"` + ClientId string `json:"clientid"` + UserId string `json:"userid"` + UserPrivateKeyBytes []byte `json:"-"` + UserPublicKeyBytes []byte `json:"-"` + UserPrivateKey *ecdsa.PrivateKey `json:"-"` + UserPublicKey *ecdsa.PublicKey `json:"-"` + ActiveSessionId string `json:"activesessionid"` + WinSize ClientWinSizeType `json:"winsize"` } func (c *ClientData) ToMap() map[string]interface{} { @@ -108,6 +117,7 @@ func (c *ClientData) ToMap() map[string]interface{} { rtn["userprivatekeybytes"] = c.UserPrivateKeyBytes rtn["userpublickeybytes"] = c.UserPublicKeyBytes rtn["activesessionid"] = c.ActiveSessionId + rtn["winsize"] = quickJson(c.WinSize) return rtn } @@ -121,6 +131,7 @@ func ClientDataFromMap(m map[string]interface{}) *ClientData { quickSetBytes(&c.UserPrivateKeyBytes, m, "userprivatekeybytes") quickSetBytes(&c.UserPublicKeyBytes, m, "userpublickeybytes") quickSetStr(&c.ActiveSessionId, m, "activesessionid") + quickSetJson(&c.WinSize, m, "winsize") return &c } @@ -792,9 +803,10 @@ func createClientData(tx *TxWrap) error { UserPrivateKeyBytes: pkBytes, UserPublicKeyBytes: pubBytes, ActiveSessionId: "", + WinSize: ClientWinSizeType{}, } - query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes) - VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes)` + query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) + VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` tx.NamedExecWrap(query, c.ToMap()) fmt.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil @@ -814,10 +826,10 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { return createErr } } - m := tx.GetMap("SELECT * FROM client") + m := tx.GetMap(`SELECT * FROM client`) cdata := ClientDataFromMap(m) if cdata == nil { - return fmt.Errorf("invalid client data") + return fmt.Errorf("no client data found") } rtn = *cdata return nil From d143ca282017f344ae3a44f7bf7830d10de95edd Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Sep 2022 21:09:43 -0700 Subject: [PATCH 126/397] working on /remote:install --- pkg/cmdrunner/cmdrunner.go | 5 +++ pkg/remote/remote.go | 82 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b4405b686..e31065859 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -93,6 +93,7 @@ func init() { registerCmdFn("remote:set", RemoteSetCommand) registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) + registerCmdFn("remote:install", RemoteInstallCommand) registerCmdFn("window:resize", WindowResizeCommand) @@ -414,6 +415,10 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index dbc3b5135..d9e46236f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -38,17 +38,21 @@ const PtyReadBufSize = 100 const MShellVersion = "v0.1.0" const MShellVersionConstraint = "^0.1" -const MShellServerCommand = ` +const MShellServerCommandFmt = ` PATH=$PATH:~/.mshell; -which mshell > /dev/null; +which mshell-[%VERSION%] > /dev/null; if [[ "$?" -ne 0 ]] then printf "\n##N{\"type\": \"init\", \"notfound\": true, \"uname\": \"%s | %s\"}\n" "$(uname -s)" "$(uname -m)" else - mshell --server + mshell-[%VERSION%] --server fi ` +func MakeServerCommandStr() string { + return strings.ReplaceAll(MShellServerCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion)) +} + const ( StatusInit = "init" StatusConnected = "connected" @@ -83,7 +87,12 @@ type MShellProc struct { ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc + + // install + InstallStatus string NeedsMShellUpgrade bool + InstallCancelFn context.CancelFunc + InstallErr error RunningCmds []base.CommandKey } @@ -116,6 +125,12 @@ func (msh *MShellProc) GetStatus() string { return msh.Status } +func (msh *MShellProc) GetInstallStatus() string { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.InstallStatus +} + func (state RemoteRuntimeState) GetBaseDisplayName() string { if state.RemoteAlias != "" { return state.RemoteAlias @@ -528,6 +543,14 @@ func (msh *MShellProc) setErrorStatus(err error) { go msh.NotifyRemoteUpdate() } +func (msh *MShellProc) setInstallErrorStatus(err error) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.InstallStatus = StatusError + msh.InstallErr = err + go msh.NotifyRemoteUpdate() +} + func (msh *MShellProc) GetRemoteCopy() sstore.RemoteType { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -626,6 +649,56 @@ func (msh *MShellProc) RunPtyReadLoop(cmdPty *os.File) { } } +func (msh *MShellProc) RunInstall() { + remoteCopy := msh.GetRemoteCopy() + if remoteCopy.Archived { + msh.WriteToPtyBuffer("cannot install on archived remote\n") + return + } + curStatus := msh.GetInstallStatus() + if curStatus == StatusConnecting { + msh.WriteToPtyBuffer("cannot install on remote that is already trying to install, cancel current install to try again") + return + } + msh.WriteToPtyBuffer("installing mshell %s to %s...\n", MShellVersion, remoteCopy.RemoteCanonicalName) + sshOpts := convertSSHOpts(remoteCopy.SSHOpts) + sshOpts.SSHErrorsToTty = true + cmdStr := shexec.MakeInstallCommandStr() + ecmd := sshOpts.MakeSSHExecCmd(cmdStr) + cmdPty, err := msh.addControllingTty(ecmd) + if err != nil { + statusErr := fmt.Errorf("cannot attach controlling tty to mshell install command: %w", err) + msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error()) + msh.setInstallErrorStatus(statusErr) + return + } + defer func() { + if len(ecmd.ExtraFiles) > 0 { + ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() + } + }() + go msh.RunPtyReadLoop(cmdPty) + clientCtx, clientCancelFn := context.WithCancel(context.Background()) + defer clientCancelFn() + msh.WithLock(func() { + msh.InstallStatus = StatusConnecting + msh.InstallCancelFn = clientCancelFn + go msh.NotifyRemoteUpdate() + }) + msgFn := func(msg string) { + msh.WriteToPtyBuffer("%s", msg) + } + err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, "", msgFn) + if err != nil { + statusErr := fmt.Errorf("install failed: %w", err) + msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error()) + msh.setInstallErrorStatus(statusErr) + return + } + msh.WriteToPtyBuffer("successfully installed mshell %s\n", MShellVersion) + return +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { @@ -640,7 +713,8 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true - ecmd := sshOpts.MakeSSHExecCmd(MShellServerCommand) + cmdStr := MakeServerCommandStr() + ecmd := sshOpts.MakeSSHExecCmd(cmdStr) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { statusErr := fmt.Errorf("cannot attach controlling tty to mshell command: %w", err) From a60680f8552ca53d9e1baa59c63b38f3521381b7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Sep 2022 23:23:04 -0700 Subject: [PATCH 127/397] remote install --- pkg/cmdrunner/cmdrunner.go | 61 +++++++++++++++------- pkg/remote/remote.go | 102 +++++++++++++++++++++++++++++++------ 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e31065859..457b1ad2c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -94,6 +94,7 @@ func init() { registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) + registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) registerCmdFn("window:resize", WindowResizeCommand) @@ -109,7 +110,7 @@ func getValidCommands() []string { if val.IsAlias { continue } - rtn = append(rtn, key) + rtn = append(rtn, "/"+key) } return rtn } @@ -416,7 +417,33 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + mshell := ids.Remote.MShell + go mshell.RunInstall() + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), + PtyRemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, nil +} + +func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + mshell := ids.Remote.MShell + go mshell.CancelInstall() + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), + PtyRemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, nil } func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -424,14 +451,13 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } - if ids.Remote.RState.IsConnected() { - return sstore.InfoMsgUpdate("remote %q already connected (no action taken)", ids.Remote.DisplayName), nil - } - if ids.Remote.RState.Status == remote.StatusConnecting { - return sstore.InfoMsgUpdate("remote %q is already trying to connect (no action taken)", ids.Remote.DisplayName), nil - } go ids.Remote.MShell.Launch() - return sstore.InfoMsgUpdate("remote %q reconnecting", ids.Remote.DisplayName), nil + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), + PtyRemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, nil } func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -440,16 +466,13 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy return nil, err } force := resolveBool(pk.Kwargs["force"], false) - status := ids.Remote.MShell.GetStatus() - if status != remote.StatusConnected && status != remote.StatusConnecting { - return sstore.InfoMsgUpdate("remote %q already disconnected (no action taken)", ids.Remote.DisplayName), nil - } - numCommands := ids.Remote.MShell.GetNumRunningCommands() - if numCommands > 0 && !force { - return nil, fmt.Errorf("remote not disconnected, %q has %d running commands. use 'force=1' to force disconnection", ids.Remote.DisplayName) - } - ids.Remote.MShell.Disconnect() - return sstore.InfoMsgUpdate("remote %q disconnected", ids.Remote.DisplayName), nil + go ids.Remote.MShell.Disconnect(force) + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), + PtyRemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, nil } func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d9e46236f..ea2ea4550 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -54,7 +54,6 @@ func MakeServerCommandStr() string { } const ( - StatusInit = "init" StatusConnected = "connected" StatusConnecting = "connecting" StatusDisconnected = "disconnected" @@ -94,7 +93,7 @@ type MShellProc struct { InstallCancelFn context.CancelFunc InstallErr error - RunningCmds []base.CommandKey + RunningCmds map[base.CommandKey]bool } type RemoteRuntimeState struct { @@ -106,6 +105,9 @@ type RemoteRuntimeState struct { RemoteVars map[string]string `json:"remotevars"` Status string `json:"status"` ErrorStr string `json:"errorstr,omitempty"` + InstallStatus string `json:"installstatus"` + InstallErrorStr string `json:"installerrorstr,omitempty"` + NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` DefaultState *sstore.RemoteState `json:"defaultstate"` ConnectMode string `json:"connectmode"` AutoInstall bool `json:"autoinstall"` @@ -380,10 +382,15 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { Archived: msh.Remote.Archived, RemoteIdx: msh.Remote.RemoteIdx, UName: msh.UName, + InstallStatus: msh.InstallStatus, + NeedsMShellUpgrade: msh.NeedsMShellUpgrade, } if msh.Err != nil { state.ErrorStr = msh.Err.Error() } + if msh.InstallErr != nil { + state.InstallErrorStr = msh.InstallErr.Error() + } local := (msh.Remote.SSHOpts == nil || msh.Remote.SSHOpts.Local) vars := make(map[string]string) vars["user"] = msh.Remote.RemoteUser @@ -471,10 +478,12 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { panic(err) // this should never happen (NewBuffer only returns an error if CirBufSize <= 0) } rtn := &MShellProc{ - Lock: &sync.Mutex{}, - Remote: r, - Status: StatusInit, - PtyBuffer: buf, + Lock: &sync.Mutex{}, + Remote: r, + Status: StatusDisconnected, + PtyBuffer: buf, + InstallStatus: StatusDisconnected, + RunningCmds: make(map[base.CommandKey]bool), } rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) return rtn @@ -544,6 +553,7 @@ func (msh *MShellProc) setErrorStatus(err error) { } func (msh *MShellProc) setInstallErrorStatus(err error) { + msh.WriteToPtyBuffer("*error, %s\n", err.Error()) msh.Lock.Lock() defer msh.Lock.Unlock() msh.InstallStatus = StatusError @@ -569,7 +579,17 @@ func (msh *MShellProc) GetNumRunningCommands() int { return len(msh.RunningCmds) } -func (msh *MShellProc) Disconnect() { +func (msh *MShellProc) Disconnect(force bool) { + status := msh.GetStatus() + if status != StatusConnected && status != StatusConnecting { + msh.WriteToPtyBuffer("remote already disconnected (no action taken)\n") + return + } + numCommands := msh.GetNumRunningCommands() + if numCommands > 0 && !force { + msh.WriteToPtyBuffer("remote not disconnected, has %d running commands. use force=1 to force disconnection\n", numCommands) + return + } msh.Lock.Lock() defer msh.Lock.Unlock() if msh.ServerProc != nil { @@ -581,6 +601,15 @@ func (msh *MShellProc) Disconnect() { } } +func (msh *MShellProc) CancelInstall() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + if msh.InstallCancelFn != nil { + msh.InstallCancelFn() + msh.InstallCancelFn = nil + } +} + func (msh *MShellProc) GetRemoteName() string { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -652,12 +681,17 @@ func (msh *MShellProc) RunPtyReadLoop(cmdPty *os.File) { func (msh *MShellProc) RunInstall() { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { - msh.WriteToPtyBuffer("cannot install on archived remote\n") + msh.WriteToPtyBuffer("*error: cannot install on archived remote\n") + return + } + baseStatus := msh.GetStatus() + if baseStatus == StatusConnecting || baseStatus == StatusConnected { + msh.WriteToPtyBuffer("*error: cannot install on remote that is connected/connecting, disconnect to install\n") return } curStatus := msh.GetInstallStatus() if curStatus == StatusConnecting { - msh.WriteToPtyBuffer("cannot install on remote that is already trying to install, cancel current install to try again") + msh.WriteToPtyBuffer("*error: cannot install on remote that is already trying to install, cancel current install to try again\n") return } msh.WriteToPtyBuffer("installing mshell %s to %s...\n", MShellVersion, remoteCopy.RemoteCanonicalName) @@ -668,7 +702,6 @@ func (msh *MShellProc) RunInstall() { cmdPty, err := msh.addControllingTty(ecmd) if err != nil { statusErr := fmt.Errorf("cannot attach controlling tty to mshell install command: %w", err) - msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error()) msh.setInstallErrorStatus(statusErr) return } @@ -676,11 +709,13 @@ func (msh *MShellProc) RunInstall() { if len(ecmd.ExtraFiles) > 0 { ecmd.ExtraFiles[len(ecmd.ExtraFiles)-1].Close() } + cmdPty.Close() }() go msh.RunPtyReadLoop(cmdPty) clientCtx, clientCancelFn := context.WithCancel(context.Background()) defer clientCancelFn() msh.WithLock(func() { + msh.InstallErr = nil msh.InstallStatus = StatusConnecting msh.InstallCancelFn = clientCancelFn go msh.NotifyRemoteUpdate() @@ -689,13 +724,26 @@ func (msh *MShellProc) RunInstall() { msh.WriteToPtyBuffer("%s", msg) } err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, "", msgFn) + if err == context.Canceled { + msh.WriteToPtyBuffer("*install canceled\n") + msh.WithLock(func() { + msh.InstallStatus = StatusDisconnected + go msh.NotifyRemoteUpdate() + }) + return + } if err != nil { statusErr := fmt.Errorf("install failed: %w", err) - msh.WriteToPtyBuffer("*error, %s\n", statusErr.Error()) msh.setInstallErrorStatus(statusErr) return } + msh.WithLock(func() { + msh.InstallStatus = StatusDisconnected + msh.InstallCancelFn = nil + msh.NeedsMShellUpgrade = false + }) msh.WriteToPtyBuffer("successfully installed mshell %s\n", MShellVersion) + go msh.NotifyRemoteUpdate() return } @@ -706,10 +754,19 @@ func (msh *MShellProc) Launch() { return } curStatus := msh.GetStatus() + if curStatus == StatusConnected { + msh.WriteToPtyBuffer("remote is already connected (no action taken)\n") + return + } if curStatus == StatusConnecting { msh.WriteToPtyBuffer("remote is already connecting, disconnect before trying to connect again\n") return } + istatus := msh.GetInstallStatus() + if istatus == StatusConnecting { + msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n") + return + } msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true @@ -731,6 +788,7 @@ func (msh *MShellProc) Launch() { makeClientCtx, makeClientCancelFn := context.WithCancel(context.Background()) defer makeClientCancelFn() msh.WithLock(func() { + msh.Err = nil msh.Status = StatusConnecting msh.MakeClientCancelFn = makeClientCancelFn go msh.NotifyRemoteUpdate() @@ -750,7 +808,12 @@ func (msh *MShellProc) Launch() { // no notify here, because we'll call notify in either case below }) if err == context.Canceled { - err = fmt.Errorf("forced disconnection") + msh.WriteToPtyBuffer("*forced disconnection\n") + msh.WithLock(func() { + msh.Status = StatusDisconnected + go msh.NotifyRemoteUpdate() + }) + return } if err == nil && semver.MajorMinor(mshellVersion) != semver.MajorMinor(MShellVersion) { err = fmt.Errorf("mshell version is not compatible current=%s remote=%s", MShellVersion, mshellVersion) @@ -826,7 +889,7 @@ func (state RemoteRuntimeState) ExpandHomeDir(pathStr string) (string, error) { func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool { msh.Lock.Lock() defer msh.Lock.Unlock() - for _, runningCk := range msh.RunningCmds { + for runningCk, _ := range msh.RunningCmds { if runningCk == ck { return true } @@ -921,7 +984,13 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp func (msh *MShellProc) AddRunningCmd(ck base.CommandKey) { msh.Lock.Lock() defer msh.Lock.Unlock() - msh.RunningCmds = append(msh.RunningCmds, ck) + msh.RunningCmds[ck] = true +} + +func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + delete(msh.RunningCmds, ck) } func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { @@ -966,7 +1035,7 @@ func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *pa } func (msh *MShellProc) notifyHangups_nolock() { - for _, ck := range msh.RunningCmds { + for ck, _ := range msh.RunningCmds { cmd, err := sstore.GetCmdById(context.Background(), ck.GetSessionId(), ck.GetCmdId()) if err != nil { continue @@ -974,10 +1043,11 @@ func (msh *MShellProc) notifyHangups_nolock() { update := sstore.ModelUpdate{Cmd: cmd} sstore.MainBus.SendUpdate(ck.GetSessionId(), update) } - msh.RunningCmds = nil + msh.RunningCmds = make(map[base.CommandKey]bool) } func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { + msh.RemoveRunningCmd(donePk.CK) update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) if err != nil { msh.WriteToPtyBuffer("[error] updating cmddone: %v\n", err) From 534ef04cca461931473d27118f7a07aa89b58ac3 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 30 Sep 2022 14:46:51 -0700 Subject: [PATCH 128/397] checkpoint on remote:add dialog --- pkg/cmdrunner/cmdrunner.go | 65 +++++++++++++++++++++++++++++++------- pkg/sstore/updatebus.go | 25 +++++++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 457b1ad2c..f885ff1ea 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -95,6 +95,7 @@ func init() { registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) + registerCmdFn("remote:edit", RemoteEditCommand) registerCmdFn("window:resize", WindowResizeCommand) @@ -475,23 +476,61 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy }, nil } +func RemoteEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, err + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + RemoteEdit: &sstore.RemoteEditType{ + RemoteEdit: true, + RemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, + } + return update, nil +} + +func makeRemoteEditErrorReturn(visual bool, err error) (sstore.UpdatePacket, error) { + if visual { + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + RemoteEdit: &sstore.RemoteEditType{ + RemoteEdit: true, + ErrorStr: err.Error(), + }, + }, + }, nil + } + return nil, err +} + func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - if len(pk.Args) == 0 || pk.Args[0] == "" { - return nil, fmt.Errorf("/remote:new requires one positional argument of 'user@host'") + visualEdit := resolveBool(pk.Kwargs["visual"], false) + isSubmitted := resolveBool(pk.Kwargs["submit"], false) + if (len(pk.Args) == 0 || pk.Args[0] == "") && !isSubmitted { + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + RemoteEdit: &sstore.RemoteEditType{ + RemoteEdit: true, + }, + }, + }, nil } userHost := pk.Args[0] m := userHostRe.FindStringSubmatch(userHost) if m == nil { - return nil, fmt.Errorf("/remote:new invalid format of user@host argument") + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid format of user@host argument")) } sudoStr, remoteUser, remoteHost := m[1], m[2], m[3] alias := pk.Kwargs["alias"] if alias != "" { if len(alias) > MaxRemoteAliasLen { - return nil, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen) + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen)) } if !remoteAliasRe.MatchString(alias) { - return nil, fmt.Errorf("invalid alias format") + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("invalid alias format")) } } connectMode := sstore.ConnectModeAuto @@ -499,7 +538,8 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss connectMode = pk.Kwargs["connectmode"] } if !sstore.IsValidConnectMode(connectMode) { - return nil, fmt.Errorf("/remote:new invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) + err := fmt.Errorf("/remote:new invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) + return makeRemoteEditErrorReturn(visualEdit, err) } autoInstall := resolveBool(pk.Kwargs["autoinstall"], true) var isSudo bool @@ -509,7 +549,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if pk.Kwargs["sudo"] != "" { sudoArg := resolveBool(pk.Kwargs["sudo"], false) if isSudo && !sudoArg { - return nil, fmt.Errorf("/remote:new invalid 'sudo@' argument, with sudo kw arg set to false") + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid 'sudo@' argument, with sudo kw arg set to false")) } if !isSudo && sudoArg { isSudo = true @@ -528,7 +568,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss fd.Close() } if err != nil { - return nil, fmt.Errorf("/remote:new invalid key %q (cannot read): %v", keyFile, err) + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid key %q (cannot read): %v", keyFile, err)) } sshOpts.SSHIdentity = keyFile } @@ -537,7 +577,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss color := pk.Kwargs["color"] err := validateRemoteColor(color, "remote color") if err != nil { - return nil, err + return makeRemoteEditErrorReturn(visualEdit, err) } remoteOpts.Color = color } @@ -557,9 +597,10 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } err := remote.AddRemote(ctx, r) if err != nil { - return nil, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err) + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } - update := &sstore.ModelUpdate{ + // SUCCESS + update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("remote %q created", r.RemoteCanonicalName), TimeoutMs: 2000, @@ -1205,7 +1246,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, err } show := !resolveBool(pk.Kwargs["noshow"], false) - update := &sstore.ModelUpdate{} + update := sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ HistoryType: htype, SessionId: ids.SessionId, diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 01a6bd7f4..49dcc9eed 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -79,16 +79,23 @@ func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { } } +type RemoteEditType struct { + RemoteEdit bool `json:"remoteedit"` + RemoteId string `json:"remoteid,omitempty"` + ErrorStr string `json:"errorstr,omitempty"` +} + type InfoMsgType struct { - InfoTitle string `json:"infotitle"` - InfoError string `json:"infoerror,omitempty"` - InfoMsg string `json:"infomsg,omitempty"` - InfoComps []string `json:"infocomps,omitempty"` - InfoCompsMore bool `json:"infocompssmore,omitempty"` - InfoLines []string `json:"infolines,omitempty"` - TimeoutMs int64 `json:"timeoutms,omitempty"` - PtyRemoteId string `json:"ptyremoteid,omitempty"` - RemoteShowAll bool `json:"remoteshowall,omitempty"` + InfoTitle string `json:"infotitle"` + InfoError string `json:"infoerror,omitempty"` + InfoMsg string `json:"infomsg,omitempty"` + InfoComps []string `json:"infocomps,omitempty"` + InfoCompsMore bool `json:"infocompssmore,omitempty"` + InfoLines []string `json:"infolines,omitempty"` + TimeoutMs int64 `json:"timeoutms,omitempty"` + PtyRemoteId string `json:"ptyremoteid,omitempty"` + RemoteShowAll bool `json:"remoteshowall,omitempty"` + RemoteEdit *RemoteEditType `json:"remoteedit,omitempty"` } type HistoryInfoType struct { From 62c3390d31a5e9f8aa40168eaae2d354617421ec Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 30 Sep 2022 16:05:48 -0700 Subject: [PATCH 129/397] use batchmode for non-manual connections. don't allow duplicate aliases for remotes --- pkg/remote/remote.go | 5 ++++- pkg/sstore/dbops.go | 4 ++++ pkg/sstore/updatebus.go | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index ea2ea4550..31abc29ca 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -770,6 +770,9 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true + if remoteCopy.ConnectMode != sstore.ConnectModeManual { + sshOpts.BatchMode = true + } cmdStr := MakeServerCommandStr() ecmd := sshOpts.MakeSSHExecCmd(cmdStr) cmdPty, err := msh.addControllingTty(ecmd) @@ -820,7 +823,7 @@ func (msh *MShellProc) Launch() { } if err != nil { msh.setErrorStatus(err) - msh.WriteToPtyBuffer("*error connecting to remote (uname=%q): %v\n", msh.UName, err) + msh.WriteToPtyBuffer("*error connecting to remote: %v\n", err) return } msh.WriteToPtyBuffer("connected\n") diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 10761cab8..65c5c2f76 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -123,6 +123,10 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { if tx.Exists(query, r.RemoteCanonicalName) { return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", r.RemoteCanonicalName) } + query = `SELECT remoteid FROM remote WHERE alias = ?` + if r.RemoteAlias != "" && tx.Exists(query, r.RemoteAlias) { + return fmt.Errorf("remote has duplicate alias '%s', cannot create", r.RemoteAlias) + } query = `SELECT COALESCE(max(remoteidx), 0) FROM remote` maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 49dcc9eed..59b91bfcc 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -83,6 +83,7 @@ type RemoteEditType struct { RemoteEdit bool `json:"remoteedit"` RemoteId string `json:"remoteid,omitempty"` ErrorStr string `json:"errorstr,omitempty"` + InfoStr string `json:"infostr,omitempty"` } type InfoMsgType struct { From 23759b72837e030da11ba8c6ca41f004331eb7e4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 30 Sep 2022 16:23:40 -0700 Subject: [PATCH 130/397] working on remote:new --- pkg/cmdrunner/cmdrunner.go | 12 ++++++++++++ pkg/sstore/dbops.go | 2 +- pkg/sstore/sstore.go | 7 ++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f885ff1ea..2871ed29c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -563,6 +563,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } if pk.Kwargs["key"] != "" { keyFile := pk.Kwargs["key"] + keyFile = base.ExpandHomeDir(keyFile) fd, err := os.Open(keyFile) if fd != nil { fd.Close() @@ -572,6 +573,17 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } sshOpts.SSHIdentity = keyFile } + if pk.Kwargs["port"] != "" { + portStr := pk.Kwargs["port"] + portVal, err := strconv.Atoi(portStr) + if err != nil { + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid port %q: %v", portStr, err)) + } + if portVal <= 0 { + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid port %d (must be positive)", portVal)) + } + sshOpts.SSHPort = portVal + } remoteOpts := &sstore.RemoteOptsType{} if pk.Kwargs["color"] != "" { color := pk.Kwargs["color"] diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 65c5c2f76..f3c7cff67 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -123,7 +123,7 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { if tx.Exists(query, r.RemoteCanonicalName) { return fmt.Errorf("remote has duplicate canonicalname '%s', cannot create", r.RemoteCanonicalName) } - query = `SELECT remoteid FROM remote WHERE alias = ?` + query = `SELECT remoteid FROM remote WHERE remotealias = ?` if r.RemoteAlias != "" && tx.Exists(query, r.RemoteAlias) { return fmt.Errorf("remote has duplicate alias '%s', cannot create", r.RemoteAlias) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 40dcfeccc..23ee71f5f 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -470,11 +470,12 @@ type LineType struct { } type SSHOpts struct { - Local bool `json:"local"` + Local bool `json:"local,omitempty"` SSHHost string `json:"sshhost"` - SSHOptsStr string `json:"sshopts"` - SSHIdentity string `json:"sshidentity"` SSHUser string `json:"sshuser"` + SSHOptsStr string `json:"sshopts,omitempty"` + SSHIdentity string `json:"sshidentity,omitempty"` + SSHPort int `json:"sshport,omitempty"` } type RemoteOptsType struct { From 3beb00998be0dd84c217841dc6c3a10a0c15fbca Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 30 Sep 2022 17:22:28 -0700 Subject: [PATCH 131/397] support sending password to remote --- cmd/main-server.go | 10 +++--- pkg/cmdrunner/cmdrunner.go | 12 +++++-- pkg/remote/remote.go | 73 ++++++++++++++++++++++++++++++++++++-- pkg/sstore/sstore.go | 1 + 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 14e7d4c6c..e3a6e4c38 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -378,11 +378,11 @@ func main() { fmt.Printf("[error] ensuring test01 remote: %v\n", err) return } - err = sstore.AddTest02Remote(context.Background()) - if err != nil { - fmt.Printf("[error] ensuring test02 remote: %v\n", err) - return - } + //err = sstore.AddTest02Remote(context.Background()) + //if err != nil { + // fmt.Printf("[error] ensuring test02 remote: %v\n", err) + // return + //} _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2871ed29c..0fdaa7518 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -593,6 +593,9 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } remoteOpts.Color = color } + if pk.Kwargs["password"] != "" { + sshOpts.SSHPassword = pk.Kwargs["password"] + } r := &sstore.RemoteType{ RemoteId: scbase.GenSCUUID(), PhysicalId: "", @@ -737,12 +740,15 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if newRemote == "" { return nil, nil } - remoteName, rptr, _, _, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + remoteName, rptr, _, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { return nil, err } if rptr == nil { - return nil, fmt.Errorf("/cr error: remote [%s] not found", newRemote) + return nil, fmt.Errorf("/cr error: remote %q not found", newRemote) + } + if rstate.Archived { + return nil, fmt.Errorf("/cr error: remote %q cannot switch to archived remote", newRemote) } err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, *rptr) if err != nil { @@ -755,7 +761,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up CurRemote: *rptr, }, Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("current remote = %s", remoteName), + InfoMsg: fmt.Sprintf("current remote = %q", remoteName), TimeoutMs: 2000, }, } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 31abc29ca..f5e56fce6 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -15,6 +15,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/armon/circbuf" "github.com/creack/pty" @@ -111,10 +112,11 @@ type RemoteRuntimeState struct { DefaultState *sstore.RemoteState `json:"defaultstate"` ConnectMode string `json:"connectmode"` AutoInstall bool `json:"autoinstall"` - Archived bool `json:"archived"` + Archived bool `json:"archived,omitempty"` RemoteIdx int64 `json:"remoteidx"` UName string `json:"uname"` MShellVersion string `json:"mshellversion"` + WaitingForPassword bool `json:"waitingforpassword,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -391,6 +393,9 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { if msh.InstallErr != nil { state.InstallErrorStr = msh.InstallErr.Error() } + if msh.Status == StatusConnecting { + state.WaitingForPassword = msh.isWaitingForPassword_nolock() + } local := (msh.Remote.SSHOpts == nil || msh.Remote.SSHOpts.Local) vars := make(map[string]string) vars["user"] = msh.Remote.RemoteUser @@ -659,8 +664,25 @@ func sendRemotePtyUpdate(remoteId string, dataOffset int64, data []byte) { sstore.MainBus.SendUpdate("", update) } +func (msh *MShellProc) isWaitingForPassword_nolock() bool { + barr := msh.PtyBuffer.Bytes() + if len(barr) == 0 { + return false + } + nlIdx := bytes.LastIndex(barr, []byte{'\n'}) + var lastLine string + if nlIdx == -1 { + lastLine = string(barr) + } else { + lastLine = string(barr[nlIdx+1:]) + } + pwIdx := strings.Index(lastLine, "assword") + return pwIdx != -1 +} + func (msh *MShellProc) RunPtyReadLoop(cmdPty *os.File) { buf := make([]byte, PtyReadBufSize) + var isWaiting bool for { n, readErr := cmdPty.Read(buf) if readErr == io.EOF { @@ -670,11 +692,55 @@ func (msh *MShellProc) RunPtyReadLoop(cmdPty *os.File) { msh.WriteToPtyBuffer("*error reading from controlling-pty: %v\n", readErr) break } + var newIsWaiting bool msh.WithLock(func() { curOffset := msh.PtyBuffer.TotalWritten() msh.PtyBuffer.Write(buf[0:n]) sendRemotePtyUpdate(msh.Remote.RemoteId, curOffset, buf[0:n]) + newIsWaiting = msh.isWaitingForPassword_nolock() }) + if newIsWaiting != isWaiting { + isWaiting = newIsWaiting + go msh.NotifyRemoteUpdate() + } + } +} + +func (msh *MShellProc) WaitAndSendPassword(pw string) { + var numWaits int + for { + var isWaiting bool + var isConnecting bool + msh.WithLock(func() { + isWaiting = msh.isWaitingForPassword_nolock() + isConnecting = msh.Status == StatusConnecting + }) + if !isConnecting { + break + } + if !isWaiting { + numWaits = 0 + time.Sleep(100 * time.Millisecond) + continue + } + numWaits++ + if numWaits < 10 { + time.Sleep(100 * time.Millisecond) + } else { + // send password + msh.WithLock(func() { + if msh.ControllingPty == nil { + return + } + pwBytes := []byte(pw + "\r") + msh.writeToPtyBuffer_nolock("~[sent password]\r\n") + _, err := msh.ControllingPty.Write(pwBytes) + if err != nil { + msh.writeToPtyBuffer_nolock("*cannot write password to controlling pty: %v\n", err) + } + }) + break + } } } @@ -770,7 +836,7 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true - if remoteCopy.ConnectMode != sstore.ConnectModeManual { + if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" { sshOpts.BatchMode = true } cmdStr := MakeServerCommandStr() @@ -788,6 +854,9 @@ func (msh *MShellProc) Launch() { } }() go msh.RunPtyReadLoop(cmdPty) + if remoteCopy.SSHOpts.SSHPassword != "" { + go msh.WaitAndSendPassword(remoteCopy.SSHOpts.SSHPassword) + } makeClientCtx, makeClientCancelFn := context.WithCancel(context.Background()) defer makeClientCancelFn() msh.WithLock(func() { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 23ee71f5f..d6b6f94fb 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -476,6 +476,7 @@ type SSHOpts struct { SSHOptsStr string `json:"sshopts,omitempty"` SSHIdentity string `json:"sshidentity,omitempty"` SSHPort int `json:"sshport,omitempty"` + SSHPassword string `json:"sshpassword,omitempty"` } type RemoteOptsType struct { From f342cae630d92e240ea0de3ddc40ef28b5852d99 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 1 Oct 2022 13:23:36 -0700 Subject: [PATCH 132/397] refactor the remote edit argument parsing --- pkg/cmdrunner/cmdrunner.go | 229 +++++++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 89 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 0fdaa7518..ab44bb0bd 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -166,7 +166,25 @@ func resolveBool(arg string, def bool) bool { return true } -func resolveInt(arg string, def int) (int, error) { +func resolveFile(arg string) (string, error) { + if arg == "" { + return "", nil + } + fileName := base.ExpandHomeDir(arg) + if !strings.HasPrefix(fileName, "/") { + return "", fmt.Errorf("must be absolute, cannot be a relative path") + } + fd, err := os.Open(fileName) + if fd != nil { + fd.Close() + } + if err != nil { + return "", fmt.Errorf("cannot open file: %v", err) + } + return fileName, nil +} + +func resolvePosInt(arg string, def int) (int, error) { if arg == "" { return def, nil } @@ -174,6 +192,9 @@ func resolveInt(arg string, def int) (int, error) { if err != nil { return 0, err } + if ival <= 0 { + return 0, fmt.Errorf("must be greater than 0") + } return ival, nil } @@ -506,6 +527,108 @@ func makeRemoteEditErrorReturn(visual bool, err error) (sstore.UpdatePacket, err return nil, err } +type RemoteEditArgs struct { + SSHOpts *sstore.SSHOpts + Sudo bool + ConnectMode string + Alias string + AutoInstall bool + UserHost string + SSHPassword string + SSHKeyFile string + Color string +} + +func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteEditArgs, error) { + var userHost string + var sshOpts *sstore.SSHOpts + var isSudo bool + + if isNew { + userHost = pk.Args[0] + m := userHostRe.FindStringSubmatch(userHost) + if m == nil { + return nil, fmt.Errorf("invalid format of user@host argument") + } + sudoStr, remoteUser, remoteHost := m[1], m[2], m[3] + if sudoStr != "" { + isSudo = true + } + if pk.Kwargs["sudo"] != "" { + sudoArg := resolveBool(pk.Kwargs["sudo"], false) + if isSudo && !sudoArg { + return nil, fmt.Errorf("invalid 'sudo@' argument, with sudo kw arg set to false") + } + if !isSudo && sudoArg { + isSudo = true + userHost = "sudo@" + userHost + } + } + sshOpts = &sstore.SSHOpts{ + Local: false, + SSHHost: remoteHost, + SSHUser: remoteUser, + } + portVal, err := resolvePosInt(pk.Kwargs["port"], 0) + if err != nil { + return nil, fmt.Errorf("invalid port %q: %v", pk.Kwargs["port"], err) + } + sshOpts.SSHPort = portVal + } else { + if pk.Kwargs["sudo"] != "" { + return nil, fmt.Errorf("cannot update 'sudo' value") + } + if pk.Kwargs["port"] != "" { + return nil, fmt.Errorf("cannot update 'port' value") + } + } + alias := pk.Kwargs["alias"] + if alias != "" { + if len(alias) > MaxRemoteAliasLen { + return nil, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen) + } + if !remoteAliasRe.MatchString(alias) { + return nil, fmt.Errorf("invalid alias format") + } + } + connectMode := sstore.ConnectModeAuto + if pk.Kwargs["connectmode"] != "" { + connectMode = pk.Kwargs["connectmode"] + } + if !sstore.IsValidConnectMode(connectMode) { + err := fmt.Errorf("invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) + return nil, err + } + autoInstall := resolveBool(pk.Kwargs["autoinstall"], true) + keyFile, err := resolveFile(pk.Kwargs["key"]) + if err != nil { + return nil, fmt.Errorf("invalid ssh keyfile %q: %v", pk.Kwargs["key"], err) + } + color := pk.Kwargs["color"] + if color != "" { + err := validateRemoteColor(color, "remote color") + if err != nil { + return nil, err + } + } + sshPassword := pk.Kwargs["password"] + if sshOpts != nil { + sshOpts.SSHIdentity = keyFile + sshOpts.SSHPassword = sshPassword + } + return &RemoteEditArgs{ + SSHOpts: sshOpts, + Sudo: isSudo, + ConnectMode: connectMode, + Alias: alias, + AutoInstall: autoInstall, + UserHost: userHost, + SSHKeyFile: keyFile, + SSHPassword: sshPassword, + Color: color, + }, nil +} + func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { visualEdit := resolveBool(pk.Kwargs["visual"], false) isSubmitted := resolveBool(pk.Kwargs["submit"], false) @@ -518,99 +641,27 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss }, }, nil } - userHost := pk.Args[0] - m := userHostRe.FindStringSubmatch(userHost) - if m == nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid format of user@host argument")) - } - sudoStr, remoteUser, remoteHost := m[1], m[2], m[3] - alias := pk.Kwargs["alias"] - if alias != "" { - if len(alias) > MaxRemoteAliasLen { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen)) - } - if !remoteAliasRe.MatchString(alias) { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("invalid alias format")) - } - } - connectMode := sstore.ConnectModeAuto - if pk.Kwargs["connectmode"] != "" { - connectMode = pk.Kwargs["connectmode"] - } - if !sstore.IsValidConnectMode(connectMode) { - err := fmt.Errorf("/remote:new invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) - return makeRemoteEditErrorReturn(visualEdit, err) - } - autoInstall := resolveBool(pk.Kwargs["autoinstall"], true) - var isSudo bool - if sudoStr != "" { - isSudo = true - } - if pk.Kwargs["sudo"] != "" { - sudoArg := resolveBool(pk.Kwargs["sudo"], false) - if isSudo && !sudoArg { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid 'sudo@' argument, with sudo kw arg set to false")) - } - if !isSudo && sudoArg { - isSudo = true - userHost = "sudo@" + userHost - } - } - sshOpts := &sstore.SSHOpts{ - Local: false, - SSHHost: remoteHost, - SSHUser: remoteUser, - } - if pk.Kwargs["key"] != "" { - keyFile := pk.Kwargs["key"] - keyFile = base.ExpandHomeDir(keyFile) - fd, err := os.Open(keyFile) - if fd != nil { - fd.Close() - } - if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid key %q (cannot read): %v", keyFile, err)) - } - sshOpts.SSHIdentity = keyFile - } - if pk.Kwargs["port"] != "" { - portStr := pk.Kwargs["port"] - portVal, err := strconv.Atoi(portStr) - if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid port %q: %v", portStr, err)) - } - if portVal <= 0 { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new invalid port %d (must be positive)", portVal)) - } - sshOpts.SSHPort = portVal - } - remoteOpts := &sstore.RemoteOptsType{} - if pk.Kwargs["color"] != "" { - color := pk.Kwargs["color"] - err := validateRemoteColor(color, "remote color") - if err != nil { - return makeRemoteEditErrorReturn(visualEdit, err) - } - remoteOpts.Color = color - } - if pk.Kwargs["password"] != "" { - sshOpts.SSHPassword = pk.Kwargs["password"] + editArgs, err := parseRemoteEditArgs(true, pk) + if err != nil { + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new %v", err)) } r := &sstore.RemoteType{ RemoteId: scbase.GenSCUUID(), PhysicalId: "", RemoteType: sstore.RemoteTypeSsh, - RemoteAlias: alias, - RemoteCanonicalName: userHost, - RemoteSudo: isSudo, - RemoteUser: remoteUser, - RemoteHost: remoteHost, - ConnectMode: connectMode, - AutoInstall: autoInstall, - SSHOpts: sshOpts, - RemoteOpts: remoteOpts, + RemoteAlias: editArgs.Alias, + RemoteCanonicalName: editArgs.UserHost, + RemoteSudo: editArgs.Sudo, + RemoteUser: editArgs.SSHOpts.SSHUser, + RemoteHost: editArgs.SSHOpts.SSHHost, + ConnectMode: editArgs.ConnectMode, + AutoInstall: editArgs.AutoInstall, + SSHOpts: editArgs.SSHOpts, } - err := remote.AddRemote(ctx, r) + if editArgs.Color != "" { + r.RemoteOpts = &sstore.RemoteOptsType{Color: editArgs.Color} + } + err = remote.AddRemote(ctx, r) if err != nil { return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } @@ -1234,7 +1285,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - maxItems, err := resolveInt(pk.Kwargs["maxitems"], DefaultMaxHistoryItems) + maxItems, err := resolvePosInt(pk.Kwargs["maxitems"], DefaultMaxHistoryItems) if err != nil { return nil, fmt.Errorf("invalid maxitems value '%s' (must be a number): %v", pk.Kwargs["maxitems"], err) } From 4d075e32bf256f10052d97b0d2fd73f14b51730a Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 2 Oct 2022 18:52:55 -0700 Subject: [PATCH 133/397] updateremote (non-visual) --- pkg/cmdrunner/cmdrunner.go | 63 +++++++++++++++++++++++++++++++++++--- pkg/remote/remote.go | 15 +++++++++ pkg/sstore/dbops.go | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ab44bb0bd..37fa0eeaa 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -91,6 +91,7 @@ func init() { registerCmdFn("remote:new", RemoteNewCommand) registerCmdFn("remote:archive", RemoteArchiveCommand) registerCmdFn("remote:set", RemoteSetCommand) + registerCmdAlias("remote:edit", RemoteSetCommand) registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) @@ -537,6 +538,7 @@ type RemoteEditArgs struct { SSHPassword string SSHKeyFile string Color string + EditMap map[string]interface{} } func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteEditArgs, error) { @@ -591,11 +593,14 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE return nil, fmt.Errorf("invalid alias format") } } - connectMode := sstore.ConnectModeAuto + var connectMode string + if isNew { + connectMode = sstore.ConnectModeAuto + } if pk.Kwargs["connectmode"] != "" { connectMode = pk.Kwargs["connectmode"] } - if !sstore.IsValidConnectMode(connectMode) { + if connectMode != "" && !sstore.IsValidConnectMode(connectMode) { err := fmt.Errorf("invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false)) return nil, err } @@ -616,6 +621,28 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE sshOpts.SSHIdentity = keyFile sshOpts.SSHPassword = sshPassword } + + // set up editmap + editMap := make(map[string]interface{}) + if _, found := pk.Kwargs[sstore.RemoteField_Alias]; found { + editMap[sstore.RemoteField_Alias] = alias + } + if connectMode != "" { + editMap[sstore.RemoteField_ConnectMode] = connectMode + } + if _, found := pk.Kwargs[sstore.RemoteField_AutoInstall]; found { + editMap[sstore.RemoteField_AutoInstall] = autoInstall + } + if _, found := pk.Kwargs["key"]; found { + editMap[sstore.RemoteField_SSHKey] = keyFile + } + if _, found := pk.Kwargs[sstore.RemoteField_Color]; found { + editMap[sstore.RemoteField_Color] = color + } + if _, found := pk.Kwargs["password"]; found { + editMap[sstore.RemoteField_SSHPassword] = sshPassword + } + return &RemoteEditArgs{ SSHOpts: sshOpts, Sudo: isSudo, @@ -626,6 +653,7 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE SSHKeyFile: keyFile, SSHPassword: sshPassword, Color: color, + EditMap: editMap, }, nil } @@ -676,12 +704,37 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { return nil, err } - fmt.Printf("ids: %v\n", ids) - return nil, nil + visualEdit := resolveBool(pk.Kwargs["visual"], false) + isSubmitted := resolveBool(pk.Kwargs["submit"], false) + editArgs, err := parseRemoteEditArgs(false, pk) + if err != nil { + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new %v", err)) + } + if visualEdit && !isSubmitted && len(editArgs.EditMap) == 0 { + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + RemoteEdit: &sstore.RemoteEditType{ + RemoteEdit: true, + RemoteId: ids.Remote.RemotePtr.RemoteId, + }, + }, + }, nil + } + err = ids.Remote.MShell.UpdateRemote(ctx, editArgs.EditMap) + if err != nil { + return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err)) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("remote %q updated", ids.Remote.DisplayName), + TimeoutMs: 2000, + }, + } + return update, nil } func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index f5e56fce6..250053792 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -584,6 +584,21 @@ func (msh *MShellProc) GetNumRunningCommands() int { return len(msh.RunningCmds) } +func (msh *MShellProc) UpdateRemote(ctx context.Context, editMap map[string]interface{}) error { + msh.Lock.Lock() + defer msh.Lock.Unlock() + updatedRemote, err := sstore.UpdateRemote(ctx, msh.Remote.RemoteId, editMap) + if err != nil { + return err + } + if updatedRemote == nil { + return fmt.Errorf("no remote returned from UpdateRemote") + } + msh.Remote = updatedRemote + go msh.NotifyRemoteUpdate() + return nil +} + func (msh *MShellProc) Disconnect(force bool) { status := msh.GetStatus() if status != StatusConnected && status != StatusConnecting { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f3c7cff67..27d59bdbd 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1031,3 +1031,61 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, rtn.DiskStats = diskSize return rtn, nil } + +const ( + RemoteField_Alias = "alias" // string + RemoteField_ConnectMode = "connectmode" // string + RemoteField_AutoInstall = "autoinstall" // bool + RemoteField_SSHKey = "sshkey" // string + RemoteField_SSHPassword = "sshpassword" // string + RemoteField_Color = "color" // string +) + +// editMap: alias, connectmode, autoinstall, sshkey, color, sshpassword (from constants) +func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]interface{}) (*RemoteType, error) { + var rtn *RemoteType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT remoteid FROM remote WHERE remoteid = ?` + if !tx.Exists(query, remoteId) { + return fmt.Errorf("remote not found") + } + if alias, found := editMap[RemoteField_Alias]; found { + query = `SELECT remoteid FROM remote WHERE remotealias = ? AND remoteid <> ?` + if tx.Exists(query, alias, remoteId) { + return fmt.Errorf("remote has duplicate alias, cannot update") + } + query = `UPDATE remote SET remotealias = ? WHERE remoteid = ?` + tx.ExecWrap(query, alias, remoteId) + } + if mode, found := editMap[RemoteField_ConnectMode]; found { + query = `UPDATE remote SET connectmode = ? WHERE remoteid = ?` + tx.ExecWrap(query, mode, remoteId) + } + if autoInstall, found := editMap[RemoteField_AutoInstall]; found { + query = `UPDATE remote SET autoinstall = ? WHERE remoteid = ?` + tx.ExecWrap(query, autoInstall, remoteId) + } + if sshKey, found := editMap[RemoteField_SSHKey]; found { + query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshidentity', ?) WHERE remoteid = ?` + tx.ExecWrap(query, sshKey, remoteId) + } + if sshPassword, found := editMap[RemoteField_SSHPassword]; found { + query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshpassword', ?) WHERE remoteid = ?` + tx.ExecWrap(query, sshPassword, remoteId) + } + if color, found := editMap[RemoteField_Color]; found { + query = `UPDATE remote SET remoteopts = json_set(remoteopts, '$.color', ?) WHERE remoteid = ?` + tx.ExecWrap(query, color, remoteId) + } + var err error + rtn, err = GetRemoteById(tx.Context(), remoteId) + if err != nil { + return err + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} From 1c46b79de351535ee045c57c6da6ddf81e6b70c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Oct 2022 12:25:43 -0700 Subject: [PATCH 134/397] checkpoint, visual editing of remotes --- pkg/cmdrunner/cmdrunner.go | 77 ++++++++++++++++++++++---------------- pkg/remote/remote.go | 4 ++ 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 37fa0eeaa..f2ae7fc96 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -38,7 +38,7 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") -var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)$") +var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") @@ -448,7 +448,6 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) go mshell.RunInstall() return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -463,7 +462,6 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke go mshell.CancelInstall() return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -477,7 +475,6 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) go ids.Remote.MShell.Launch() return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -492,7 +489,6 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy go ids.Remote.MShell.Disconnect(force) return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -529,41 +525,48 @@ func makeRemoteEditErrorReturn(visual bool, err error) (sstore.UpdatePacket, err } type RemoteEditArgs struct { - SSHOpts *sstore.SSHOpts - Sudo bool - ConnectMode string - Alias string - AutoInstall bool - UserHost string - SSHPassword string - SSHKeyFile string - Color string - EditMap map[string]interface{} + CanonicalName string + SSHOpts *sstore.SSHOpts + Sudo bool + ConnectMode string + Alias string + AutoInstall bool + SSHPassword string + SSHKeyFile string + Color string + EditMap map[string]interface{} } func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteEditArgs, error) { - var userHost string + var canonicalName string var sshOpts *sstore.SSHOpts var isSudo bool if isNew { - userHost = pk.Args[0] + userHost := pk.Args[0] m := userHostRe.FindStringSubmatch(userHost) if m == nil { return nil, fmt.Errorf("invalid format of user@host argument") } - sudoStr, remoteUser, remoteHost := m[1], m[2], m[3] + sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4] + var uhPort int + if remotePortStr != "" { + var err error + uhPort, err = strconv.Atoi(remotePortStr) + if err != nil { + return nil, fmt.Errorf("invalid port specified on user@host argument") + } + } if sudoStr != "" { isSudo = true } if pk.Kwargs["sudo"] != "" { sudoArg := resolveBool(pk.Kwargs["sudo"], false) if isSudo && !sudoArg { - return nil, fmt.Errorf("invalid 'sudo@' argument, with sudo kw arg set to false") + return nil, fmt.Errorf("invalid 'sudo' argument, with sudo kw arg set to false") } if !isSudo && sudoArg { isSudo = true - userHost = "sudo@" + userHost } } sshOpts = &sstore.SSHOpts{ @@ -575,7 +578,17 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE if err != nil { return nil, fmt.Errorf("invalid port %q: %v", pk.Kwargs["port"], err) } + if portVal != 0 && uhPort != 0 && portVal != uhPort { + return nil, fmt.Errorf("invalid port argument, does not match port specified in 'user@host:port' argument") + } + if portVal == 0 && uhPort != 0 { + portVal = uhPort + } sshOpts.SSHPort = portVal + canonicalName = remoteUser + "@" + remoteHost + if isSudo { + canonicalName = "sudo@" + canonicalName + } } else { if pk.Kwargs["sudo"] != "" { return nil, fmt.Errorf("cannot update 'sudo' value") @@ -644,16 +657,16 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE } return &RemoteEditArgs{ - SSHOpts: sshOpts, - Sudo: isSudo, - ConnectMode: connectMode, - Alias: alias, - AutoInstall: autoInstall, - UserHost: userHost, - SSHKeyFile: keyFile, - SSHPassword: sshPassword, - Color: color, - EditMap: editMap, + SSHOpts: sshOpts, + Sudo: isSudo, + ConnectMode: connectMode, + Alias: alias, + AutoInstall: autoInstall, + CanonicalName: canonicalName, + SSHKeyFile: keyFile, + SSHPassword: sshPassword, + Color: color, + EditMap: editMap, }, nil } @@ -678,7 +691,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss PhysicalId: "", RemoteType: sstore.RemoteTypeSsh, RemoteAlias: editArgs.Alias, - RemoteCanonicalName: editArgs.UserHost, + RemoteCanonicalName: editArgs.CanonicalName, RemoteSudo: editArgs.Sudo, RemoteUser: editArgs.SSHOpts.SSHUser, RemoteHost: editArgs.SSHOpts.SSHHost, @@ -745,7 +758,6 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s state := ids.Remote.RState return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show remote [%s] info", ids.Remote.DisplayName), PtyRemoteId: state.RemoteId, }, }, nil @@ -765,7 +777,6 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("show all remote info"), RemoteShowAll: true, }, }, nil diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 250053792..a1ca8486c 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -414,6 +414,10 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { if local { vars["local"] = "1" } + vars["port"] = "22" + if msh.Remote.SSHOpts != nil && msh.Remote.SSHOpts.SSHPort != 0 { + vars["port"] = strconv.Itoa(msh.Remote.SSHOpts.SSHPort) + } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{ Cwd: msh.ServerProc.InitPk.Cwd, From 998a65b873d1bd465339376b82e26fc961d9f509 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Oct 2022 19:04:48 -0700 Subject: [PATCH 135/397] remote editing checkpoint --- pkg/cmdrunner/cmdrunner.go | 91 ++++++++++++++++++++++---------------- pkg/cmdrunner/resolver.go | 3 ++ pkg/remote/remote.go | 9 +++- pkg/sstore/dbops.go | 2 +- pkg/sstore/updatebus.go | 10 +++-- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f2ae7fc96..c18bfc82c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -33,9 +33,11 @@ const ( const DefaultUserId = "sawka" const MaxNameLen = 50 const MaxRemoteAliasLen = 50 +const PasswordUnchangedSentinel = "--unchanged--" var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} +var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") @@ -96,7 +98,6 @@ func init() { registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) - registerCmdFn("remote:edit", RemoteEditCommand) registerCmdFn("window:resize", WindowResizeCommand) @@ -494,32 +495,51 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy }, nil } -func RemoteEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) +func makeRemoteEditUpdate_new(err error) sstore.UpdatePacket { + redit := &sstore.RemoteEditType{ + RemoteEdit: true, + } if err != nil { - return nil, err + redit.ErrorStr = err.Error() } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - RemoteEdit: &sstore.RemoteEditType{ - RemoteEdit: true, - RemoteId: ids.Remote.RemotePtr.RemoteId, - }, + RemoteEdit: redit, }, } - return update, nil + return update } -func makeRemoteEditErrorReturn(visual bool, err error) (sstore.UpdatePacket, error) { +func makeRemoteEditErrorReturn_new(visual bool, err error) (sstore.UpdatePacket, error) { if visual { - return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - RemoteEdit: &sstore.RemoteEditType{ - RemoteEdit: true, - ErrorStr: err.Error(), - }, - }, - }, nil + return makeRemoteEditUpdate_new(err), nil + } + return nil, err +} + +func makeRemoteEditUpdate_edit(ids resolvedIds, err error) sstore.UpdatePacket { + redit := &sstore.RemoteEditType{ + RemoteEdit: true, + } + redit.RemoteId = ids.Remote.RemotePtr.RemoteId + if ids.Remote.RemoteCopy.SSHOpts != nil { + redit.KeyStr = ids.Remote.RemoteCopy.SSHOpts.SSHIdentity + redit.HasPassword = (ids.Remote.RemoteCopy.SSHOpts.SSHPassword != "") + } + if err != nil { + redit.ErrorStr = err.Error() + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + RemoteEdit: redit, + }, + } + return update +} + +func makeRemoteEditErrorReturn_edit(ids resolvedIds, visual bool, err error) (sstore.UpdatePacket, error) { + if visual { + return makeRemoteEditUpdate_edit(ids, err), nil } return nil, err } @@ -543,6 +563,9 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE var isSudo bool if isNew { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/remote:new must specify user@host argument (set visual=1 to edit in UI)") + } userHost := pk.Args[0] m := userHostRe.FindStringSubmatch(userHost) if m == nil { @@ -652,7 +675,7 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE if _, found := pk.Kwargs[sstore.RemoteField_Color]; found { editMap[sstore.RemoteField_Color] = color } - if _, found := pk.Kwargs["password"]; found { + if _, found := pk.Kwargs["password"]; found && pk.Kwargs["password"] != PasswordUnchangedSentinel { editMap[sstore.RemoteField_SSHPassword] = sshPassword } @@ -673,18 +696,12 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { visualEdit := resolveBool(pk.Kwargs["visual"], false) isSubmitted := resolveBool(pk.Kwargs["submit"], false) - if (len(pk.Args) == 0 || pk.Args[0] == "") && !isSubmitted { - return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - RemoteEdit: &sstore.RemoteEditType{ - RemoteEdit: true, - }, - }, - }, nil + if visualEdit && !isSubmitted && len(pk.Args) == 0 { + return makeRemoteEditUpdate_new(nil), nil } editArgs, err := parseRemoteEditArgs(true, pk) if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new %v", err)) + return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("/remote:new %v", err)) } r := &sstore.RemoteType{ RemoteId: scbase.GenSCUUID(), @@ -704,7 +721,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } err = remote.AddRemote(ctx, r) if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) + return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } // SUCCESS update := sstore.ModelUpdate{ @@ -725,21 +742,17 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss isSubmitted := resolveBool(pk.Kwargs["submit"], false) editArgs, err := parseRemoteEditArgs(false, pk) if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new %v", err)) + return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new %v", err)) } if visualEdit && !isSubmitted && len(editArgs.EditMap) == 0 { - return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - RemoteEdit: &sstore.RemoteEditType{ - RemoteEdit: true, - RemoteId: ids.Remote.RemotePtr.RemoteId, - }, - }, - }, nil + return makeRemoteEditUpdate_edit(ids, nil), nil + } + if !visualEdit && len(editArgs.EditMap) == 0 { + return nil, fmt.Errorf("/remote:set no updates, can set %s. (set visual=1 to edit in UI)", formatStrs(RemoteSetArgs, "or", false)) } err = ids.Remote.MShell.UpdateRemote(ctx, editArgs.EditMap) if err != nil { - return makeRemoteEditErrorReturn(visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err)) + return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err)) } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index f57563511..09a701a9c 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -34,6 +34,7 @@ type ResolvedRemote struct { MShell *remote.MShellProc RState remote.RemoteRuntimeState RemoteState *sstore.RemoteState + RemoteCopy *sstore.RemoteType } type ResolveItem struct { @@ -321,6 +322,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi return nil, fmt.Errorf("invalid remote '%s', not found", rptr.RemoteId) } rstate := msh.GetRemoteRuntimeState() + rcopy := msh.GetRemoteCopy() displayName := rstate.GetDisplayName(rptr) rtn := &ResolvedRemote{ DisplayName: displayName, @@ -328,6 +330,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi RemoteState: nil, RState: rstate, MShell: msh, + RemoteCopy: &rcopy, } if sessionId != "" && windowId != "" { state, err := sstore.GetRemoteState(ctx, sessionId, windowId, *rptr) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a1ca8486c..225f536e6 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -415,8 +415,13 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["local"] = "1" } vars["port"] = "22" - if msh.Remote.SSHOpts != nil && msh.Remote.SSHOpts.SSHPort != 0 { - vars["port"] = strconv.Itoa(msh.Remote.SSHOpts.SSHPort) + if msh.Remote.SSHOpts != nil { + if msh.Remote.SSHOpts.SSHPort != 0 { + vars["port"] = strconv.Itoa(msh.Remote.SSHOpts.SSHPort) + } + } + if msh.Remote.RemoteOpts != nil && msh.Remote.RemoteOpts.Color != "" { + vars["color"] = msh.Remote.RemoteOpts.Color } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { state.DefaultState = &sstore.RemoteState{ diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 27d59bdbd..494eed77f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1051,7 +1051,7 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter } if alias, found := editMap[RemoteField_Alias]; found { query = `SELECT remoteid FROM remote WHERE remotealias = ? AND remoteid <> ?` - if tx.Exists(query, alias, remoteId) { + if alias != "" && tx.Exists(query, alias, remoteId) { return fmt.Errorf("remote has duplicate alias, cannot update") } query = `UPDATE remote SET remotealias = ? WHERE remoteid = ?` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 59b91bfcc..9378722ba 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -80,10 +80,12 @@ func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { } type RemoteEditType struct { - RemoteEdit bool `json:"remoteedit"` - RemoteId string `json:"remoteid,omitempty"` - ErrorStr string `json:"errorstr,omitempty"` - InfoStr string `json:"infostr,omitempty"` + RemoteEdit bool `json:"remoteedit"` + RemoteId string `json:"remoteid,omitempty"` + ErrorStr string `json:"errorstr,omitempty"` + InfoStr string `json:"infostr,omitempty"` + KeyStr string `json:"keystr,omitempty"` + HasPassword bool `json:"haspassword,omitempty"` } type InfoMsgType struct { From 539e71ad47ecbbd2cbb484320a25eb8433ddbc5c Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Oct 2022 11:15:59 -0700 Subject: [PATCH 136/397] only show remote upgrade if initpk was received --- pkg/remote/remote.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 225f536e6..10b2fa62d 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -897,9 +897,10 @@ func (msh *MShellProc) Launch() { if cproc != nil && cproc.InitPk != nil { msh.Remote.InitPk = cproc.InitPk mshellVersion = cproc.InitPk.Version - } - if semver.Compare(mshellVersion, MShellVersion) < 0 { - msh.NeedsMShellUpgrade = true + if semver.Compare(mshellVersion, MShellVersion) < 0 { + // only set NeedsMShellUpgrade if we got an InitPk + msh.NeedsMShellUpgrade = true + } } // no notify here, because we'll call notify in either case below }) From f5b9ea07a19c2871eff77d6deb0751b58f997ded Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Oct 2022 11:45:24 -0700 Subject: [PATCH 137/397] local flag on remote, ensure 1 local remote. on archive, change to local remote --- cmd/main-server.go | 10 ----- db/migrations/000001_init.up.sql | 1 + db/schema.sql | 5 ++- pkg/cmdrunner/cmdrunner.go | 8 ++++ pkg/remote/remote.go | 49 +++++++++++++++++++-- pkg/sstore/dbops.go | 18 +++++++- pkg/sstore/sstore.go | 75 +++----------------------------- 7 files changed, 81 insertions(+), 85 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index e3a6e4c38..508fea726 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -373,16 +373,6 @@ func main() { fmt.Printf("[error] ensuring local remote: %v\n", err) return } - err = sstore.AddTest01Remote(context.Background()) - if err != nil { - fmt.Printf("[error] ensuring test01 remote: %v\n", err) - return - } - //err = sstore.AddTest02Remote(context.Background()) - //if err != nil { - // fmt.Printf("[error] ensuring test02 remote: %v\n", err) - // return - //} _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { fmt.Printf("[error] ensuring default session: %v\n", err) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 40c75e55f..08029a1dc 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -94,6 +94,7 @@ CREATE TABLE remote ( sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, + local boolean NOT NULL, archived boolean NOT NULL, remoteidx int NOT NULL ); diff --git a/db/schema.sql b/db/schema.sql index c049c2391..0b998b390 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -5,7 +5,8 @@ CREATE TABLE client ( userid varchar(36) NOT NULL, activesessionid varchar(36) NOT NULL, userpublickeybytes blob NOT NULL, - userprivatekeybytes blob NOT NULL + userprivatekeybytes blob NOT NULL, + winsize json NOT NULL ); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, @@ -83,10 +84,12 @@ CREATE TABLE remote ( remoteuser varchar(50) NOT NULL, remotehost varchar(200) NOT NULL, connectmode varchar(20) NOT NULL, + autoinstall boolean NOT NULL, initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, + local boolean NOT NULL, archived boolean NOT NULL, remoteidx int NOT NULL ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c18bfc82c..60c04964d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -805,6 +805,14 @@ func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("archiving remote: %v", err) } update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName) + localRemote := remote.GetLocalRemote() + if localRemote != nil { + update.Window = &sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + CurRemote: sstore.RemotePtrType{RemoteId: localRemote.GetRemoteId()}, + } + } return update, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 10b2fa62d..ba7721e93 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -117,6 +117,7 @@ type RemoteRuntimeState struct { UName string `json:"uname"` MShellVersion string `json:"mshellversion"` WaitingForPassword bool `json:"waitingforpassword,omitempty"` + Local bool `json:"local,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -129,6 +130,12 @@ func (msh *MShellProc) GetStatus() string { return msh.Status } +func (msh *MShellProc) GetRemoteId() string { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Remote.RemoteId +} + func (msh *MShellProc) GetInstallStatus() string { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -166,12 +173,22 @@ func LoadRemotes(ctx context.Context) error { if err != nil { return err } + var numLocal int for _, remote := range allRemotes { msh := MakeMShell(remote) GlobalStore.Map[remote.RemoteId] = msh if remote.ConnectMode == sstore.ConnectModeStartup { go msh.Launch() } + if remote.Local { + numLocal++ + } + } + if numLocal == 0 { + return fmt.Errorf("no local remote found") + } + if numLocal > 1 { + return fmt.Errorf("multiple local remotes found") } return nil } @@ -224,6 +241,10 @@ func AddRemote(ctx context.Context, r *sstore.RemoteType) error { } r.RemoteId = erCopy.RemoteId } + if r.Local { + return fmt.Errorf("cannot create another local remote (there can be only one)") + } + err := sstore.UpsertRemote(ctx, r) if err != nil { return fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err) @@ -247,6 +268,9 @@ func ArchiveRemote(ctx context.Context, remoteId string) error { if msh.Status == StatusConnected { return fmt.Errorf("cannot archive connected remote") } + if msh.Remote.Local { + return fmt.Errorf("cannot archive local remote") + } rcopy := msh.GetRemoteCopy() archivedRemote := &sstore.RemoteType{ RemoteId: rcopy.RemoteId, @@ -303,6 +327,17 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } +func GetLocalRemote() *MShellProc { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + for _, msh := range GlobalStore.Map { + if msh.IsLocal() { + return msh + } + } + return nil +} + func ResolveRemoteRef(remoteRef string) *RemoteRuntimeState { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -369,6 +404,12 @@ func makeShortHost(host string) string { return host[0:dotIdx] } +func (msh *MShellProc) IsLocal() bool { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Remote.Local +} + func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -386,6 +427,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { UName: msh.UName, InstallStatus: msh.InstallStatus, NeedsMShellUpgrade: msh.NeedsMShellUpgrade, + Local: msh.Remote.Local, } if msh.Err != nil { state.ErrorStr = msh.Err.Error() @@ -396,7 +438,6 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { if msh.Status == StatusConnecting { state.WaitingForPassword = msh.isWaitingForPassword_nolock() } - local := (msh.Remote.SSHOpts == nil || msh.Remote.SSHOpts.Local) vars := make(map[string]string) vars["user"] = msh.Remote.RemoteUser vars["bestuser"] = vars["user"] @@ -411,7 +452,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { if msh.Remote.RemoteSudo { vars["sudo"] = "1" } - if local { + if msh.Remote.Local { vars["local"] = "1" } vars["port"] = "22" @@ -437,12 +478,12 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["besthost"] = vars["remotehost"] vars["bestshorthost"] = vars["remoteshorthost"] } - if local && msh.Remote.RemoteSudo { + if msh.Remote.Local && msh.Remote.RemoteSudo { vars["bestuser"] = "sudo" } else if msh.Remote.RemoteSudo { vars["bestuser"] = "sudo@" + vars["bestuser"] } - if local { + if msh.Remote.Local { vars["bestname"] = vars["bestuser"] + "@local" vars["bestshortname"] = vars["bestuser"] + "@local" } else { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 494eed77f..b1f8bf767 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -73,6 +73,20 @@ func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { return remote, nil } +func GetLocalRemote(ctx context.Context) (*RemoteType, error) { + var remote *RemoteType + err := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote WHERE local` + m := tx.GetMap(query) + remote = RemoteFromMap(m) + return nil + }) + if err != nil { + return nil, err + } + return remote, nil +} + func GetRemoteByCanonicalName(ctx context.Context, cname string) (*RemoteType, error) { var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { @@ -131,8 +145,8 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx)` + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx, local) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local)` tx.NamedExecWrap(query, r.ToMap()) return nil }) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d6b6f94fb..2225ccdc7 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -508,6 +508,7 @@ type RemoteType struct { LastConnectTs int64 `json:"lastconnectts"` Archived bool `json:"archived"` RemoteIdx int64 `json:"remoteidx"` + Local bool `json:"local"` } func (r *RemoteType) GetName() string { @@ -551,6 +552,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["lastconnectts"] = r.LastConnectTs rtn["archived"] = r.Archived rtn["remoteidx"] = r.RemoteIdx + rtn["local"] = r.Local return rtn } @@ -575,6 +577,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetInt64(&r.LastConnectTs, m, "lastconnectts") quickSetBool(&r.Archived, m, "archived") quickSetInt64(&r.RemoteIdx, m, "remoteidx") + quickSetBool(&r.Local, m, "local") return &r } @@ -668,9 +671,9 @@ func EnsureLocalRemote(ctx context.Context) error { if err != nil { return fmt.Errorf("getting local physical remoteid: %w", err) } - remote, err := GetRemoteByPhysicalId(ctx, physicalId) + remote, err := GetLocalRemote(ctx) if err != nil { - return fmt.Errorf("getting remote[%s] from db: %w", physicalId, err) + return fmt.Errorf("getting local remote from db: %w", err) } if remote != nil { return nil @@ -696,77 +699,13 @@ func EnsureLocalRemote(ctx context.Context) error { ConnectMode: ConnectModeStartup, AutoInstall: true, SSHOpts: &SSHOpts{Local: true}, + Local: true, } err = UpsertRemote(ctx, localRemote) if err != nil { return err } - log.Printf("[db] added remote '%s', id=%s\n", localRemote.GetName(), localRemote.RemoteId) - return nil -} - -func AddTest01Remote(ctx context.Context) error { - remote, err := GetRemoteByAlias(ctx, "test01") - if err != nil { - return fmt.Errorf("getting remote[test01] from db: %w", err) - } - if remote != nil { - return nil - } - testRemote := &RemoteType{ - RemoteId: scbase.GenSCUUID(), - RemoteType: RemoteTypeSsh, - RemoteAlias: "test01", - RemoteCanonicalName: "ubuntu@test01.ec2", - RemoteSudo: false, - RemoteUser: "ubuntu", - RemoteHost: "test01.ec2", - SSHOpts: &SSHOpts{ - Local: false, - SSHHost: "test01.ec2", - SSHUser: "ubuntu", - SSHIdentity: "/Users/mike/aws/mfmt.pem", - }, - ConnectMode: ConnectModeStartup, - AutoInstall: true, - } - err = UpsertRemote(ctx, testRemote) - if err != nil { - return err - } - log.Printf("[db] added remote '%s', id=%s\n", testRemote.GetName(), testRemote.RemoteId) - return nil -} - -func AddTest02Remote(ctx context.Context) error { - remote, err := GetRemoteByAlias(ctx, "test2") - if err != nil { - return fmt.Errorf("getting remote[test01] from db: %w", err) - } - if remote != nil { - return nil - } - testRemote := &RemoteType{ - RemoteId: scbase.GenSCUUID(), - RemoteType: RemoteTypeSsh, - RemoteAlias: "test2", - RemoteCanonicalName: "test2@test01.ec2", - RemoteSudo: false, - RemoteUser: "test2", - RemoteHost: "test01.ec2", - SSHOpts: &SSHOpts{ - Local: false, - SSHHost: "test01.ec2", - SSHUser: "test2", - }, - ConnectMode: ConnectModeStartup, - AutoInstall: true, - } - err = UpsertRemote(ctx, testRemote) - if err != nil { - return err - } - log.Printf("[db] added remote '%s', id=%s\n", testRemote.GetName(), testRemote.RemoteId) + log.Printf("[db] added local remote '%s', id=%s\n", localRemote.RemoteCanonicalName, localRemote.RemoteId) return nil } From fbe652b9321ebca2faf816bd3e02c88741e9e884 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 6 Oct 2022 18:33:54 -0700 Subject: [PATCH 138/397] add scrolltop and selectedline to screen_window --- db/migrations/000001_init.up.sql | 2 ++ pkg/cmdrunner/cmdrunner.go | 43 ++++++++++++++++++++++++++++++++ pkg/cmdrunner/resolver.go | 38 ++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 35 +++++++++++++++++++++++--- pkg/sstore/sstore.go | 12 +++++---- pkg/sstore/updatebus.go | 23 +++++++++-------- 6 files changed, 134 insertions(+), 19 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 08029a1dc..7d7885029 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -50,6 +50,8 @@ CREATE TABLE screen_window ( windowid varchar(36) NOT NULL, name varchar(50) NOT NULL, layout json NOT NULL, + selectedline int NOT NULL, + scrolltop int NOT NULL, PRIMARY KEY (sessionid, screenid, windowid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 60c04964d..43a589868 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -99,6 +99,8 @@ func init() { registerCmdFn("remote:install", RemoteInstallCommand) registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) + registerCmdFn("sw:set", SwSetCommand) + registerCmdFn("window:resize", WindowResizeCommand) registerCmdFn("line", LineCommand) @@ -200,6 +202,20 @@ func resolvePosInt(arg string, def int) (int, error) { return ival, nil } +func resolveNonNegInt(arg string, def int) (int, error) { + if arg == "" { + return def, nil + } + ival, err := strconv.Atoi(arg) + if err != nil { + return 0, err + } + if ival < 0 { + return 0, fmt.Errorf("cannot be negative") + } + return ival, nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { @@ -409,6 +425,33 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } +func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, fmt.Errorf("/sw:set cannot resolve current screen-window: %w", err) + } + var setNonST bool // scrolltop does not receive an update + updateMap := make(map[string]interface{}) + if pk.Kwargs["scrolltop"] != "" { + stVal, err := resolveNonNegInt(pk.Kwargs["scrolltop"], 0) + if err != nil { + return nil, fmt.Errorf("/sw:set invalid scrolltop argument: %v", err) + } + updateMap[sstore.SWField_ScrollTop] = stVal + } + if len(updateMap) == 0 { + return nil, fmt.Errorf("/sw:set no updates, can set %s", formatStrs([]string{"line", "scrolltop"}, "or", false)) + } + sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + if err != nil { + return nil, fmt.Errorf("/sw:set failed to update: %v", err) + } + if !setNonST { + return nil, nil + } + return sstore.ModelUpdate{ScreenWindow: sw}, nil +} + func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 09a701a9c..4fb1cf2c8 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -143,6 +143,24 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i rtn.ScreenId = uictx.ScreenId rtn.WindowId = uictx.WindowId } + if pk.Kwargs["session"] != "" { + sessionId, err := resolveSessionArg(pk.Kwargs["session"]) + if err != nil { + return rtn, err + } + if sessionId != "" { + rtn.SessionId = sessionId + } + } + if pk.Kwargs["screen"] != "" { + screenId, err := resolveScreenArg(rtn.SessionId, pk.Kwargs["screen"]) + if err != nil { + return rtn, err + } + if screenId != "" { + rtn.ScreenId = screenId + } + } if pk.Kwargs["window"] != "" { windowId, err := resolveWindowArg(rtn.SessionId, rtn.ScreenId, pk.Kwargs["window"]) if err != nil { @@ -274,6 +292,26 @@ func resolveWindowArg(sessionId string, screenId string, windowArg string) (stri return windowArg, nil } +func resolveSessionArg(sessionArg string) (string, error) { + if sessionArg == "" { + return "", nil + } + if _, err := uuid.Parse(sessionArg); err != nil { + return "", fmt.Errorf("invalid session arg specified (must be sessionid) '%s'", sessionArg) + } + return sessionArg, nil +} + +func resolveScreenArg(sessionId string, screenArg string) (string, error) { + if screenArg == "" { + return "", nil + } + if _, err := uuid.Parse(screenArg); err != nil { + return "", fmt.Errorf("invalid screen arg specified (must be sessionid) '%s'", screenArg) + } + return screenArg, nil +} + func resolveScreenId(ctx context.Context, pk *scpacket.FeCommandPacketType, sessionId string) (string, error) { screenArg := pk.Kwargs["screen"] if screenArg == "" { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index b1f8bf767..350eb2ea2 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -498,8 +498,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} - query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout) VALUES (?, ?, ?, ?, ?)` - tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout) + query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, scrolltop) VALUES (?, ?, ?, ?, ?, ?, ?)` + tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, 0) if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` tx.ExecWrap(query, newScreenId, sessionId) @@ -619,7 +619,6 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line.LineNum != 0 { return fmt.Errorf("line should not hage linenum set") } - cmd.OrigTermOpts = cmd.TermOpts return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, line.SessionId, line.WindowId) { @@ -634,6 +633,7 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) if cmd != nil { + cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, origtermopts, status, startpk, donepk, runout, usedrows) @@ -1103,3 +1103,32 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter } return rtn, nil } + +const ( + SWField_ScrollTop = "scrolltop" // int +) + +func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, windowId string, editMap map[string]interface{}) (*ScreenWindowType, error) { + var rtn *ScreenWindowType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, screenId, windowId) { + return fmt.Errorf("screen-window not found") + } + if stVal, found := editMap[SWField_ScrollTop]; found { + query = `UPDATE screen_window SET scrolltop = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` + tx.ExecWrap(query, stVal, sessionId, screenId, windowId) + } + var sw ScreenWindowType + query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` + found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) + if found { + rtn = &sw + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 2225ccdc7..f18658bd0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -376,11 +376,13 @@ func (l LayoutType) Value() (driver.Value, error) { } type ScreenWindowType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - Layout LayoutType `json:"layout"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + Layout LayoutType `json:"layout"` + SelectedLine int `json:"selectedline"` + ScrollTop int `json:"scrolltop"` // only for updates Remove bool `json:"remove,omitempty"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 9378722ba..e73df3435 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -29,17 +29,18 @@ func (PtyDataUpdate) UpdateType() string { } type ModelUpdate struct { - Sessions []*SessionType `json:"sessions,omitempty"` - ActiveSessionId string `json:"activesessionid,omitempty"` - Window *WindowType `json:"window,omitempty"` - Line *LineType `json:"line,omitempty"` - Cmd *CmdType `json:"cmd,omitempty"` - CmdLine *CmdLineType `json:"cmdline,omitempty"` - Info *InfoMsgType `json:"info,omitempty"` - Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState - History *HistoryInfoType `json:"history,omitempty"` - Interactive bool `json:"interactive"` - Connect bool `json:"connect,omitempty"` + Sessions []*SessionType `json:"sessions,omitempty"` + ActiveSessionId string `json:"activesessionid,omitempty"` + Window *WindowType `json:"window,omitempty"` + ScreenWindow *ScreenWindowType `json:"screenwindow,omitempty"` + Line *LineType `json:"line,omitempty"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` + Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState + History *HistoryInfoType `json:"history,omitempty"` + Interactive bool `json:"interactive"` + Connect bool `json:"connect,omitempty"` } func (ModelUpdate) UpdateType() string { From 2d089b98fbda7e09ce30a29b8e5a860e3b17083a Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 6 Oct 2022 23:58:38 -0700 Subject: [PATCH 139/397] tweaks to genericresolver (and bug fix) --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/cmdrunner/resolver.go | 131 ++++++++++++++++++++++++------------- pkg/sstore/dbops.go | 15 ++++- pkg/sstore/sstore.go | 6 ++ 4 files changed, 108 insertions(+), 46 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 43a589868..1e7ce512d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1372,7 +1372,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, err } ritems := sessionsToResolveItems(bareSessions) - ritem, err := genericResolve(firstArg, ids.SessionId, ritems, "session") + ritem, err := genericResolve(firstArg, ids.SessionId, ritems, false, "session") if err != nil { return nil, err } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 4fb1cf2c8..984a9e4ea 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -37,10 +37,7 @@ type ResolvedRemote struct { RemoteCopy *sstore.RemoteType } -type ResolveItem struct { - Name string - Id string -} +type ResolveItem = sstore.ResolveItem func itemNames(items []ResolveItem) []string { if len(items) == 0 { @@ -75,48 +72,74 @@ func screensToResolveItems(screens []*sstore.ScreenType) []ResolveItem { return rtn } -func resolveByPosition(items []ResolveItem, curId string, posStr string) *ResolveItem { - if len(items) == 0 { - return nil +// 1-indexed +func boundInt(ival int, maxVal int, wrap bool) int { + if maxVal == 0 { + return 0 } + if ival < 1 { + if wrap { + return maxVal + } else { + return 1 + } + } + if ival > maxVal { + if wrap { + return 1 + } else { + return maxVal + } + } + return ival +} + +type posArgType struct { + Pos int + IsWrap bool + IsRelative bool +} + +func parsePosArg(posStr string) *posArgType { if !positionRe.MatchString(posStr) { return nil } - curIdx := 1 // if no match, curIdx will be first item - for idx, item := range items { - if item.Id == curId { - curIdx = idx + 1 - break - } - } - isRelative := strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") - isWrap := posStr == "+" || posStr == "-" - var pos int - if isWrap && posStr == "+" { - pos = 1 - } else if isWrap && posStr == "-" { - pos = -1 + rtn := &posArgType{} + rtn.IsRelative = strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") + rtn.IsWrap = posStr == "+" || posStr == "-" + if rtn.IsWrap && posStr == "+" { + rtn.Pos = 1 + } else if rtn.IsWrap && posStr == "-" { + rtn.Pos = -1 } else { - pos, _ = strconv.Atoi(posStr) + rtn.Pos, _ = strconv.Atoi(posStr) // don't need to check error because of positionRe.Match } - if isRelative { - pos = curIdx + pos + return rtn +} + +func resolveByPosition(isNumeric bool, items []ResolveItem, curId string, posStr string) *ResolveItem { + if len(items) == 0 { + return nil } - if pos < 1 { - if isWrap { - pos = len(items) - } else { - pos = 1 + posArg := parsePosArg(posStr) + if posArg == nil { + return nil + } + var finalPos int + if posArg.IsRelative { + curIdx := 1 // if no match, curIdx will be first item + for idx, item := range items { + if item.Id == curId { + curIdx = idx + 1 + break + } } + finalPos = curIdx + posArg.Pos + } else { + finalPos = posArg.Pos } - if pos > len(items) { - if isWrap { - pos = 1 - } else { - pos = len(items) - } - } - return &items[pos-1] + finalPos = boundInt(finalPos, len(items), posArg.IsWrap) + return &items[finalPos-1] } func resolveRemoteArg(remoteArg string) (*sstore.RemotePtrType, error) { @@ -223,7 +246,7 @@ func resolveSessionScreen(ctx context.Context, sessionId string, screenArg strin return nil, fmt.Errorf("could not retreive screens for session=%s", sessionId) } ritems := screensToResolveItems(screens) - return genericResolve(screenArg, curScreenArg, ritems, "screen") + return genericResolve(screenArg, curScreenArg, ritems, false, "screen") } func getSessionIds(sarr []*sstore.SessionType) []string { @@ -240,26 +263,46 @@ func isPartialUUID(s string) bool { return partialUUIDRe.MatchString(s) } -func genericResolve(arg string, curArg string, items []ResolveItem, typeStr string) (*ResolveItem, error) { +func getResolveItemById(id string, items []ResolveItem) *ResolveItem { + if id == "" { + return nil + } + for _, item := range items { + if item.Id == id { + return &item + } + } + return nil +} + +func genericResolve(arg string, curArg string, items []ResolveItem, isNumeric bool, typeStr string) (*ResolveItem, error) { + if len(items) == 0 || arg == "" { + return nil, nil + } var curId string if curArg != "" { - curItem, _ := genericResolve(curArg, "", items, typeStr) + curItem, _ := genericResolve(curArg, "", items, isNumeric, typeStr) if curItem != nil { curId = curItem.Id } } - rtnItem := resolveByPosition(items, curId, arg) + rtnItem := resolveByPosition(isNumeric, items, curId, arg) if rtnItem != nil { return rtnItem, nil } tryPuid := isPartialUUID(arg) var prefixMatches []ResolveItem for _, item := range items { - if item.Id == arg || item.Name == arg || (tryPuid && strings.HasPrefix(item.Id, arg)) { + if item.Id == arg || (tryPuid && strings.HasPrefix(item.Id, arg)) { return &item, nil } - if strings.HasPrefix(item.Name, arg) { - prefixMatches = append(prefixMatches, item) + if item.Name != "" { + if item.Name == arg { + return &item, nil + } + if strings.HasPrefix(item.Name, arg) { + prefixMatches = append(prefixMatches, item) + } } } if len(prefixMatches) == 1 { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 350eb2ea2..709c08fd9 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -335,7 +335,7 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win return nil } rtnWindow = WindowFromMap(m) - query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ?` + query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` tx.SelectWrap(&rtnWindow.Lines, query, sessionId, windowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` cmdMaps := tx.SelectMaps(query, sessionId, windowId) @@ -1132,3 +1132,16 @@ func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, } return rtn, nil } + +func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) ([]ResolveItem, error) { + var rtn []ResolveItem + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT lineid as id, linenum as num FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` + tx.SelectWrap(&rtn, query, sessionId, windowId) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index f18658bd0..fbf80c0b1 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -471,6 +471,12 @@ type LineType struct { Remove bool `json:"remove,omitempty"` } +type ResolveItem struct { + Name string + Num int + Id string +} + type SSHOpts struct { Local bool `json:"local,omitempty"` SSHHost string `json:"sshhost"` From 43cf55b25ea9a5d38a38abf4dfbfa4df9fe0a5bf Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 7 Oct 2022 01:08:03 -0700 Subject: [PATCH 140/397] commands for line selection, updated resolver to allow 'S' and 'E' --- pkg/cmdrunner/cmdrunner.go | 21 +++++++++- pkg/cmdrunner/resolver.go | 84 +++++++++++++++++++++++++++++--------- pkg/sstore/dbops.go | 24 ++++++++++- 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1e7ce512d..2aaf5184f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -43,7 +43,7 @@ var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") -var positionRe = regexp.MustCompile("^((\\+|-)?[0-9]+|(\\+|-))$") +var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$") var wsRe = regexp.MustCompile("\\s+") type contextType string @@ -439,6 +439,25 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } updateMap[sstore.SWField_ScrollTop] = stVal } + if pk.Kwargs["line"] != "" { + sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("/sw:set cannot get screen-window: %v", err) + } + var selectedLineStr string + if sw.SelectedLine > 0 { + selectedLineStr = strconv.Itoa(sw.SelectedLine) + } + ritem, err := resolveLine(ctx, ids.SessionId, ids.WindowId, pk.Kwargs["line"], selectedLineStr) + if err != nil { + return nil, fmt.Errorf("/sw:set error resolving line: %v", err) + } + if ritem == nil { + return nil, fmt.Errorf("/sw:set could not resolve line %q", pk.Kwargs["line"]) + } + setNonST = true + updateMap[sstore.SWField_SelectedLine] = ritem.Num + } if len(updateMap) == 0 { return nil, fmt.Errorf("/sw:set no updates, can set %s", formatStrs([]string{"line", "scrolltop"}, "or", false)) } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 984a9e4ea..0db0dd414 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -95,26 +95,40 @@ func boundInt(ival int, maxVal int, wrap bool) int { } type posArgType struct { - Pos int - IsWrap bool - IsRelative bool + Pos int + IsWrap bool + IsRelative bool + StartAnchor bool + EndAnchor bool } func parsePosArg(posStr string) *posArgType { if !positionRe.MatchString(posStr) { return nil } - rtn := &posArgType{} - rtn.IsRelative = strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") - rtn.IsWrap = posStr == "+" || posStr == "-" - if rtn.IsWrap && posStr == "+" { - rtn.Pos = 1 - } else if rtn.IsWrap && posStr == "-" { - rtn.Pos = -1 - } else { - rtn.Pos, _ = strconv.Atoi(posStr) // don't need to check error because of positionRe.Match + if posStr == "+" { + return &posArgType{Pos: 1, IsWrap: true, IsRelative: true} + } else if posStr == "-" { + return &posArgType{Pos: -1, IsWrap: true, IsRelative: true} + } else if posStr == "S" { + return &posArgType{Pos: 0, IsRelative: true, StartAnchor: true} + } else if posStr == "E" { + return &posArgType{Pos: 0, IsRelative: true, EndAnchor: true} } - return rtn + if strings.HasPrefix(posStr, "S+") { + pos, _ := strconv.Atoi(posStr[2:]) + return &posArgType{Pos: pos, IsRelative: true, StartAnchor: true} + } + if strings.HasPrefix(posStr, "E-") { + pos, _ := strconv.Atoi(posStr[1:]) + return &posArgType{Pos: pos, IsRelative: true, EndAnchor: true} + } + if strings.HasPrefix(posStr, "+") || strings.HasPrefix(posStr, "-") { + pos, _ := strconv.Atoi(posStr) + return &posArgType{Pos: pos, IsRelative: true} + } + pos, _ := strconv.Atoi(posStr) + return &posArgType{Pos: pos} } func resolveByPosition(isNumeric bool, items []ResolveItem, curId string, posStr string) *ResolveItem { @@ -127,15 +141,31 @@ func resolveByPosition(isNumeric bool, items []ResolveItem, curId string, posStr } var finalPos int if posArg.IsRelative { - curIdx := 1 // if no match, curIdx will be first item - for idx, item := range items { - if item.Id == curId { - curIdx = idx + 1 - break + var curIdx int + if posArg.StartAnchor { + curIdx = 1 + } else if posArg.EndAnchor { + curIdx = len(items) + } else { + curIdx = 1 // if no match, curIdx will be first item + for idx, item := range items { + if item.Id == curId { + curIdx = idx + 1 + break + } } } finalPos = curIdx + posArg.Pos + } else if isNumeric { + // these resolve items have a "Num" set that should be used to look up non-relative positions + for _, item := range items { + if item.Num == posArg.Pos { + return &item + } + } + return nil } else { + // non-numeric means position is just the index finalPos = posArg.Pos } finalPos = boundInt(finalPos, len(items), posArg.IsWrap) @@ -243,12 +273,20 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string, curScreenArg string) (*ResolveItem, error) { screens, err := sstore.GetSessionScreens(ctx, sessionId) if err != nil { - return nil, fmt.Errorf("could not retreive screens for session=%s", sessionId) + return nil, fmt.Errorf("could not retreive screens for session=%s: %v", sessionId, err) } ritems := screensToResolveItems(screens) return genericResolve(screenArg, curScreenArg, ritems, false, "screen") } +func resolveLine(ctx context.Context, sessionId string, windowId string, lineArg string, curLineArg string) (*ResolveItem, error) { + lines, err := sstore.GetLineResolveItems(ctx, sessionId, windowId) + if err != nil { + return nil, fmt.Errorf("could not get lines: %v", err) + } + return genericResolve(lineArg, curLineArg, lines, true, "line") +} + func getSessionIds(sarr []*sstore.SessionType) []string { rtn := make([]string, len(sarr)) for idx, s := range sarr { @@ -263,6 +301,11 @@ func isPartialUUID(s string) bool { return partialUUIDRe.MatchString(s) } +func isUUID(s string) bool { + _, err := uuid.Parse(s) + return err == nil +} + func getResolveItemById(id string, items []ResolveItem) *ResolveItem { if id == "" { return nil @@ -290,10 +333,11 @@ func genericResolve(arg string, curArg string, items []ResolveItem, isNumeric bo if rtnItem != nil { return rtnItem, nil } + isUuid := isUUID(arg) tryPuid := isPartialUUID(arg) var prefixMatches []ResolveItem for _, item := range items { - if item.Id == arg || (tryPuid && strings.HasPrefix(item.Id, arg)) { + if (isUuid && item.Id == arg) || (tryPuid && strings.HasPrefix(item.Id, arg)) { return &item, nil } if item.Name != "" { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 709c08fd9..feb5ced6d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1105,7 +1105,8 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter } const ( - SWField_ScrollTop = "scrolltop" // int + SWField_ScrollTop = "scrolltop" // int + SWField_SelectedLine = "selectedline" // int ) func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, windowId string, editMap map[string]interface{}) (*ScreenWindowType, error) { @@ -1119,6 +1120,10 @@ func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, query = `UPDATE screen_window SET scrolltop = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` tx.ExecWrap(query, stVal, sessionId, screenId, windowId) } + if sline, found := editMap[SWField_SelectedLine]; found { + query = `UPDATE screen_window SET selectedline = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` + tx.ExecWrap(query, sline, sessionId, screenId, windowId) + } var sw ScreenWindowType query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) @@ -1133,6 +1138,23 @@ func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, return rtn, nil } +func GetScreenWindowByIds(ctx context.Context, sessionId string, screenId string, windowId string) (*ScreenWindowType, error) { + var rtn *ScreenWindowType + txErr := WithTx(ctx, func(tx *TxWrap) error { + var sw ScreenWindowType + query := `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` + found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) + if found { + rtn = &sw + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) ([]ResolveItem, error) { var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { From d19f29c46753da7fdeae26076fd25c19afc9cfb1 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 10 Oct 2022 17:30:48 -0700 Subject: [PATCH 141/397] single-thread DB access. send selectedline updates for run/comment commands --- pkg/cmdrunner/cmdrunner.go | 33 +++++++++++++++++-- pkg/sstore/dbops.go | 66 ++++++++++++++++++-------------------- pkg/sstore/txwrap.go | 7 ++++ 3 files changed, 68 insertions(+), 38 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2aaf5184f..3aefe3c00 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -217,7 +217,7 @@ func resolveNonNegInt(arg string, def int) (int, error) { } func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) if err != nil { return nil, fmt.Errorf("/run error: %w", err) } @@ -252,7 +252,26 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } - update := sstore.ModelUpdate{Line: rtnLine, Cmd: cmd, Interactive: pk.Interactive} + sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) + if err != nil { + // ignore error here, because the command has already run (nothing to do) + fmt.Printf("/run error getting screen-window: %v\n", err) + } + if sw != nil { + updateMap := make(map[string]interface{}) + updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + if err != nil { + // ignore error again (nothing to do) + fmt.Printf("/run error updating screen-window selected line: %v\n", err) + } + } + update := sstore.ModelUpdate{ + Line: rtnLine, + Cmd: cmd, + ScreenWindow: sw, + Interactive: pk.Interactive, + } sstore.MainBus.SendUpdate(ids.SessionId, update) ctxVal := ctx.Value(historyContextKey) if ctxVal != nil { @@ -1237,7 +1256,15 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - return sstore.ModelUpdate{Line: rtnLine}, nil + updateMap := make(map[string]interface{}) + updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + if err != nil { + // ignore error again (nothing to do) + fmt.Printf("/comment error updating screen-window selected line: %v\n", err) + } + update := sstore.ModelUpdate{Line: rtnLine, ScreenWindow: sw} + return update, nil } func maybeQuote(s string, quote bool) string { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index feb5ced6d..d34f01728 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2,7 +2,6 @@ package sstore import ( "context" - "database/sql" "fmt" "strconv" "strings" @@ -16,17 +15,13 @@ const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, linei const DefaultMaxHistoryItems = 1000 func NumSessions(ctx context.Context) (int, error) { - db, err := GetDB(ctx) - if err != nil { - return 0, err - } - query := "SELECT count(*) FROM session" - var count int - err = db.GetContext(ctx, &count, query) - if err != nil { - return 0, err - } - return count, nil + var numSessions int + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := "SELECT count(*) FROM session" + numSessions = tx.GetInt(query) + return nil + }) + return numSessions, txErr } func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { @@ -157,17 +152,14 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if hitem == nil { return fmt.Errorf("cannot insert nil history item") } - db, err := GetDB(ctx) - if err != nil { - return err - } - query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd)` - _, err = db.NamedExec(query, hitem.ToMap()) - if err != nil { - return err - } - return nil + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `INSERT INTO history + ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd)` + tx.NamedExecWrap(query, hitem.ToMap()) + return nil + }) + return txErr } func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { @@ -372,20 +364,24 @@ func GetSessionById(ctx context.Context, id string) (*SessionType, error) { } func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { - db, err := GetDB(ctx) - if err != nil { - return nil, err - } - var sessionId string - query := `SELECT sessionid FROM session WHERE name = ?` - err = db.GetContext(ctx, &sessionId, query, name) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil + var session *SessionType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid FROM session WHERE name = ?` + sessionId := tx.GetString(query, name) + if sessionId == "" { + return nil } - return nil, err + var err error + session, err = GetSessionById(tx.Context(), sessionId) + if err != nil { + return err + } + return nil + }) + if txErr != nil { + return nil, txErr } - return GetSessionById(ctx, sessionId) + return session, nil } // also creates default window, returns sessionId diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 8033c5f9e..02e5698a9 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -3,6 +3,7 @@ package sstore import ( "context" "database/sql" + "sync" "github.com/jmoiron/sqlx" ) @@ -15,6 +16,9 @@ type TxWrap struct { type txWrapKey struct{} +// single-threaded access to DB +var globalNestingLock = &sync.Mutex{} + func IsTxWrapContext(ctx context.Context) bool { ctxVal := ctx.Value(txWrapKey{}) return ctxVal != nil @@ -30,6 +34,9 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { } } if txWrap == nil { + globalNestingLock.Lock() + defer globalNestingLock.Unlock() + db, err := GetDB(ctx) if err != nil { return err From 62fc2594ef75cb75cc20e968c62d2b70f0e63590 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 11 Oct 2022 01:11:04 -0700 Subject: [PATCH 142/397] checkpoint cmd-fg --- db/migrations/000001_init.up.sql | 3 ++- db/schema.sql | 3 +++ pkg/cmdrunner/cmdrunner.go | 41 ++++++++++++++++++++++---------- pkg/sstore/dbops.go | 28 +++++++++++++++++----- pkg/sstore/sstore.go | 34 ++++++++++++++++++++------ pkg/sstore/updatebus.go | 24 +++++++++---------- 6 files changed, 95 insertions(+), 38 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 7d7885029..55b4c25a4 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -51,7 +51,8 @@ CREATE TABLE screen_window ( name varchar(50) NOT NULL, layout json NOT NULL, selectedline int NOT NULL, - scrolltop int NOT NULL, + anchor json NOT NULL, + focustype varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid, windowid) ); diff --git a/db/schema.sql b/db/schema.sql index 0b998b390..c0d09ed5f 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -48,6 +48,9 @@ CREATE TABLE screen_window ( windowid varchar(36) NOT NULL, name varchar(50) NOT NULL, layout json NOT NULL, + selectedline int NOT NULL, + anchor json NOT NULL, + focustype varchar(12) NOT NULL, PRIMARY KEY (sessionid, screenid, windowid) ); CREATE TABLE remote_instance ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3aefe3c00..b8361db5c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -260,6 +260,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if sw != nil { updateMap := make(map[string]interface{}) updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + updateMap[sstore.SWField_Focus] = sstore.SWFocusCmd sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { // ignore error again (nothing to do) @@ -267,10 +268,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } } update := sstore.ModelUpdate{ - Line: rtnLine, - Cmd: cmd, - ScreenWindow: sw, - Interactive: pk.Interactive, + Line: rtnLine, + Cmd: cmd, + ScreenWindows: []*sstore.ScreenWindowType{sw}, + Interactive: pk.Interactive, } sstore.MainBus.SendUpdate(ids.SessionId, update) ctxVal := ctx.Value(historyContextKey) @@ -444,6 +445,8 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } +var swAnchorRe = regexp.MustCompile("^(\\d+)(?::(\\d+))?$") + func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { @@ -451,12 +454,25 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } var setNonST bool // scrolltop does not receive an update updateMap := make(map[string]interface{}) - if pk.Kwargs["scrolltop"] != "" { - stVal, err := resolveNonNegInt(pk.Kwargs["scrolltop"], 0) - if err != nil { - return nil, fmt.Errorf("/sw:set invalid scrolltop argument: %v", err) + if pk.Kwargs["anchor"] != "" { + m := swAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"]) + if m == nil { + return nil, fmt.Errorf("/sw:set invalid anchor argument (must be [line] or [line]:[offset])") } - updateMap[sstore.SWField_ScrollTop] = stVal + anchorLine, _ := strconv.Atoi(m[1]) + updateMap[sstore.SWField_AnchorLine] = anchorLine + if m[2] != "" { + anchorOffset, _ := strconv.Atoi(m[2]) + updateMap[sstore.SWField_AnchorOffset] = anchorOffset + } + } + if pk.Kwargs["focus"] != "" { + focusVal := pk.Kwargs["focus"] + if focusVal != sstore.SWFocusInput && focusVal != sstore.SWFocusCmd && focusVal != sstore.SWFocusCmdFg { + return nil, fmt.Errorf("/sw:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.SWFocusInput, sstore.SWFocusCmd, sstore.SWFocusCmdFg}, "or", false)) + } + updateMap[sstore.SWField_Focus] = focusVal + setNonST = true } if pk.Kwargs["line"] != "" { sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) @@ -478,7 +494,7 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore updateMap[sstore.SWField_SelectedLine] = ritem.Num } if len(updateMap) == 0 { - return nil, fmt.Errorf("/sw:set no updates, can set %s", formatStrs([]string{"line", "scrolltop"}, "or", false)) + return nil, fmt.Errorf("/sw:set no updates, can set %s", formatStrs([]string{"line", "scrolltop", "focus"}, "or", false)) } sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { @@ -487,7 +503,7 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if !setNonST { return nil, nil } - return sstore.ModelUpdate{ScreenWindow: sw}, nil + return sstore.ModelUpdate{ScreenWindows: []*sstore.ScreenWindowType{sw}}, nil } func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -1258,12 +1274,13 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } updateMap := make(map[string]interface{}) updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + updateMap[sstore.SWField_Focus] = sstore.SWFocusInput sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { // ignore error again (nothing to do) fmt.Printf("/comment error updating screen-window selected line: %v\n", err) } - update := sstore.ModelUpdate{Line: rtnLine, ScreenWindow: sw} + update := sstore.ModelUpdate{Line: rtnLine, ScreenWindows: []*sstore.ScreenWindowType{sw}} return update, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d34f01728..745d86653 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -494,8 +494,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} - query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, scrolltop) VALUES (?, ?, ?, ?, ?, ?, ?)` - tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, 0) + query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, "", "input") if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` tx.ExecWrap(query, newScreenId, sessionId) @@ -690,6 +690,12 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err }) } +type SWKey struct { + SessionId string + ScreenId string + WindowId string +} + func HangupAllRunningCmds(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE status = ?` @@ -1101,8 +1107,10 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter } const ( - SWField_ScrollTop = "scrolltop" // int + SWField_AnchorLine = "anchorline" // int + SWField_AnchorOffset = "anchoroffset" // int SWField_SelectedLine = "selectedline" // int + SWField_Focus = "focustype" // string ) func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, windowId string, editMap map[string]interface{}) (*ScreenWindowType, error) { @@ -1112,14 +1120,22 @@ func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, if !tx.Exists(query, sessionId, screenId, windowId) { return fmt.Errorf("screen-window not found") } - if stVal, found := editMap[SWField_ScrollTop]; found { - query = `UPDATE screen_window SET scrolltop = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.ExecWrap(query, stVal, sessionId, screenId, windowId) + if anchorLine, found := editMap[SWField_AnchorLine]; found { + query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchorline', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` + tx.ExecWrap(query, anchorLine, sessionId, screenId, windowId) + } + if anchorOffset, found := editMap[SWField_AnchorOffset]; found { + query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchoroffset', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` + tx.ExecWrap(query, anchorOffset, sessionId, screenId, windowId) } if sline, found := editMap[SWField_SelectedLine]; found { query = `UPDATE screen_window SET selectedline = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` tx.ExecWrap(query, sline, sessionId, screenId, windowId) } + if focusType, found := editMap[SWField_Focus]; found { + query = `UPDATE screen_window SET focustype = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` + tx.ExecWrap(query, focusType, sessionId, screenId, windowId) + } var sw ScreenWindowType query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index fbf80c0b1..6d3075443 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -62,6 +62,12 @@ const ( RemoteTypeSsh = "ssh" ) +const ( + SWFocusInput = "input" + SWFocusCmd = "cmd" + SWFocusCmdFg = "cmd-fg" +) + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error @@ -375,14 +381,28 @@ func (l LayoutType) Value() (driver.Value, error) { return quickValueJson(l) } +type SWAnchorType struct { + AnchorLine int `json:"anchorline,omitempty"` + AnchorOffset int `json:"anchoroffset,omitempty"` +} + +func (a *SWAnchorType) Scan(val interface{}) error { + return quickScanJson(a, val) +} + +func (a SWAnchorType) Value() (driver.Value, error) { + return quickValueJson(a) +} + type ScreenWindowType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - Layout LayoutType `json:"layout"` - SelectedLine int `json:"selectedline"` - ScrollTop int `json:"scrolltop"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + Layout LayoutType `json:"layout"` + SelectedLine int `json:"selectedline"` + Anchor SWAnchorType `json:"anchor"` + FocusType string `json:"focustype"` // only for updates Remove bool `json:"remove,omitempty"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index e73df3435..5d3baf6b2 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -29,18 +29,18 @@ func (PtyDataUpdate) UpdateType() string { } type ModelUpdate struct { - Sessions []*SessionType `json:"sessions,omitempty"` - ActiveSessionId string `json:"activesessionid,omitempty"` - Window *WindowType `json:"window,omitempty"` - ScreenWindow *ScreenWindowType `json:"screenwindow,omitempty"` - Line *LineType `json:"line,omitempty"` - Cmd *CmdType `json:"cmd,omitempty"` - CmdLine *CmdLineType `json:"cmdline,omitempty"` - Info *InfoMsgType `json:"info,omitempty"` - Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState - History *HistoryInfoType `json:"history,omitempty"` - Interactive bool `json:"interactive"` - Connect bool `json:"connect,omitempty"` + Sessions []*SessionType `json:"sessions,omitempty"` + ActiveSessionId string `json:"activesessionid,omitempty"` + Window *WindowType `json:"window,omitempty"` + ScreenWindows []*ScreenWindowType `json:"screenwindows,omitempty"` + Line *LineType `json:"line,omitempty"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` + Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState + History *HistoryInfoType `json:"history,omitempty"` + Interactive bool `json:"interactive"` + Connect bool `json:"connect,omitempty"` } func (ModelUpdate) UpdateType() string { From 61dac018fbe5c7a316505cd6d98f86832894e1d9 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 11 Oct 2022 13:23:38 -0700 Subject: [PATCH 143/397] allow negative numbers for anchor offset --- pkg/cmdrunner/cmdrunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b8361db5c..f7999b178 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -445,7 +445,7 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } -var swAnchorRe = regexp.MustCompile("^(\\d+)(?::(\\d+))?$") +var swAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) From c940c7b85be45d0b55568a9198c484608388ea95 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 11 Oct 2022 23:11:43 -0700 Subject: [PATCH 144/397] cmd-fg, update when cmd done back to input --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/remote/remote.go | 6 +++++ pkg/sstore/dbops.go | 46 +++++++++++++++++++++++++++++++------- pkg/sstore/sstore.go | 6 +++++ 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f7999b178..733eb6f3a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -260,7 +260,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if sw != nil { updateMap := make(map[string]interface{}) updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum - updateMap[sstore.SWField_Focus] = sstore.SWFocusCmd + updateMap[sstore.SWField_Focus] = sstore.SWFocusCmdFg sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { // ignore error again (nothing to do) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index ba7721e93..b1ea69835 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1191,6 +1191,12 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("[error] updating cmddone: %v\n", err) return } + sws, err := sstore.UpdateSWsWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + if err != nil { + fmt.Printf("[error] trying to update cmd-fg screen windows: %v\n", err) + // fall-through (nothing to do) + } + update.ScreenWindows = sws if update != nil { sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 745d86653..bf3fbb553 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -655,7 +655,7 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, return cmd, nil } -func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (UpdatePacket, error) { +func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (*ModelUpdate, error) { if donePk == nil || donePk.CK.IsEmpty() { return nil, fmt.Errorf("invalid cmddone packet (no ck)") } @@ -676,7 +676,7 @@ func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (Upd if rtnCmd == nil { return nil, fmt.Errorf("cmd data not found for ck[%s]", donePk.CK) } - return ModelUpdate{Cmd: rtnCmd}, nil + return &ModelUpdate{Cmd: rtnCmd}, nil } func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { @@ -690,12 +690,6 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err }) } -type SWKey struct { - SessionId string - ScreenId string - WindowId string -} - func HangupAllRunningCmds(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE status = ?` @@ -1179,3 +1173,39 @@ func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) } return rtn, nil } + +func UpdateSWsWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([]*ScreenWindowType, error) { + var rtn []*ScreenWindowType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT sessionid, screenid, windowid + FROM screen_window sw + WHERE + sessionid = ? + AND focustype = 'cmd-fg' + AND selectedline IN (SELECT linenum + FROM line l + WHERE l.sessionid = sw.sessionid + AND l.windowid = sw.windowid + AND l.cmdid = ? + )` + var swKeys []SWKey + tx.SelectWrap(&swKeys, query, sessionId, cmdId) + if len(swKeys) == 0 { + return nil + } + for _, key := range swKeys { + editMap := make(map[string]interface{}) + editMap[SWField_Focus] = SWFocusInput + sw, err := UpdateScreenWindow(tx.Context(), key.SessionId, key.ScreenId, key.WindowId, editMap) + if err != nil { + return err + } + rtn = append(rtn, sw) + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 6d3075443..2eb38a3f2 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -394,6 +394,12 @@ func (a SWAnchorType) Value() (driver.Value, error) { return quickValueJson(a) } +type SWKey struct { + SessionId string + ScreenId string + WindowId string +} + type ScreenWindowType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` From fdc5a289a9cf33f88488986f9cc65b1080944bd4 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 16 Oct 2022 23:51:04 -0700 Subject: [PATCH 145/397] use packet.ShellState, send aliases and functions as part of state --- pkg/cmdrunner/cmdrunner.go | 5 ++-- pkg/cmdrunner/resolver.go | 5 ++-- pkg/remote/remote.go | 61 ++++++++++++++++++-------------------- pkg/sstore/dbops.go | 35 +++++++++++----------- pkg/sstore/sstore.go | 56 +++++++++++++++++++++------------- 5 files changed, 87 insertions(+), 75 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 733eb6f3a..7e4d2f684 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -226,9 +226,8 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) - runPacket.Cwd = ids.Remote.RemoteState.Cwd - runPacket.Env0 = ids.Remote.RemoteState.Env0 - runPacket.EnvComplete = true + runPacket.State = ids.Remote.RemoteState + runPacket.StateComplete = true runPacket.UsePty = true runPacket.TermOpts = &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} if pk.UIContext != nil && pk.UIContext.TermOpts != nil { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 0db0dd414..2ab269151 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -33,7 +34,7 @@ type ResolvedRemote struct { RemotePtr sstore.RemotePtrType MShell *remote.MShellProc RState remote.RemoteRuntimeState - RemoteState *sstore.RemoteState + RemoteState *packet.ShellState RemoteCopy *sstore.RemoteType } @@ -471,7 +472,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi } // returns (remoteDisplayName, remoteptr, state, rstate, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *sstore.RemoteState, *remote.RemoteRuntimeState, error) { +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *packet.ShellState, *remote.RemoteRuntimeState, error) { if fullRemoteRef == "" { return "", nil, nil, nil, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index b1ea69835..d2b889a89 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -36,8 +36,8 @@ const RemoteTermRows = 8 const RemoteTermCols = 80 const PtyReadBufSize = 100 -const MShellVersion = "v0.1.0" -const MShellVersionConstraint = "^0.1" +const MShellVersion = "v0.2.0" +const MShellVersionConstraint = "^0.2" const MShellServerCommandFmt = ` PATH=$PATH:~/.mshell; @@ -98,26 +98,26 @@ type MShellProc struct { } type RemoteRuntimeState struct { - RemoteType string `json:"remotetype"` - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalremoteid"` - RemoteAlias string `json:"remotealias,omitempty"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteVars map[string]string `json:"remotevars"` - Status string `json:"status"` - ErrorStr string `json:"errorstr,omitempty"` - InstallStatus string `json:"installstatus"` - InstallErrorStr string `json:"installerrorstr,omitempty"` - NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` - DefaultState *sstore.RemoteState `json:"defaultstate"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - Archived bool `json:"archived,omitempty"` - RemoteIdx int64 `json:"remoteidx"` - UName string `json:"uname"` - MShellVersion string `json:"mshellversion"` - WaitingForPassword bool `json:"waitingforpassword,omitempty"` - Local bool `json:"local,omitempty"` + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalremoteid"` + RemoteAlias string `json:"remotealias,omitempty"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteVars map[string]string `json:"remotevars"` + Status string `json:"status"` + ErrorStr string `json:"errorstr,omitempty"` + InstallStatus string `json:"installstatus"` + InstallErrorStr string `json:"installerrorstr,omitempty"` + NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` + DefaultState *packet.ShellState `json:"defaultstate"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + Archived bool `json:"archived,omitempty"` + RemoteIdx int64 `json:"remoteidx"` + UName string `json:"uname"` + MShellVersion string `json:"mshellversion"` + WaitingForPassword bool `json:"waitingforpassword,omitempty"` + Local bool `json:"local,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -465,10 +465,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["color"] = msh.Remote.RemoteOpts.Color } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { - state.DefaultState = &sstore.RemoteState{ - Cwd: msh.ServerProc.InitPk.Cwd, - Env0: msh.ServerProc.InitPk.Env0, - } + state.DefaultState = msh.ServerProc.InitPk.State state.MShellVersion = msh.ServerProc.InitPk.Version vars["home"] = msh.ServerProc.InitPk.HomeDir vars["remoteuser"] = msh.ServerProc.InitPk.User @@ -512,7 +509,7 @@ func GetAllRemoteRuntimeState() []RemoteRuntimeState { return rtn } -func GetDefaultRemoteStateById(remoteId string) (*sstore.RemoteState, error) { +func GetDefaultRemoteStateById(remoteId string) (*packet.ShellState, error) { remote := GetRemoteById(remoteId) if remote == nil { return nil, fmt.Errorf("remote not found") @@ -988,13 +985,13 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } -func (msh *MShellProc) GetDefaultState() *sstore.RemoteState { +func (msh *MShellProc) GetDefaultState() *packet.ShellState { msh.Lock.Lock() defer msh.Lock.Unlock() if msh.ServerProc == nil || msh.ServerProc.InitPk == nil { return nil } - return &sstore.RemoteState{Cwd: msh.ServerProc.InitPk.HomeDir, Env0: msh.ServerProc.InitPk.Env0} + return msh.ServerProc.InitPk.State } func replaceHomePath(pathStr string, homeDir string) string { @@ -1060,7 +1057,7 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { } // returns (cmdtype, allow-updates-callback, err) -func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *sstore.RemoteState, runPacket *packet.RunPacketType) (*sstore.CmdType, func(), error) { +func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *packet.ShellState, runPacket *packet.RunPacketType) (*sstore.CmdType, func(), error) { if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } @@ -1332,7 +1329,7 @@ func isDigit(r rune) bool { return r >= '0' && r <= '9' // just check ascii digits (not unicode) } -func EvalPrompt(promptFmt string, vars map[string]string, state *sstore.RemoteState) string { +func EvalPrompt(promptFmt string, vars map[string]string, state *packet.ShellState) string { var buf bytes.Buffer promptRunes := []rune(promptFmt) for i := 0; i < len(promptRunes); i++ { @@ -1373,7 +1370,7 @@ func EvalPrompt(promptFmt string, vars map[string]string, state *sstore.RemoteSt return buf.String() } -func evalPromptEsc(escCode string, vars map[string]string, state *sstore.RemoteState) string { +func evalPromptEsc(escCode string, vars map[string]string, state *packet.ShellState) string { if strings.HasPrefix(escCode, "x{") && strings.HasSuffix(escCode, "}") { varName := escCode[2 : len(escCode)-1] return vars[varName] diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index bf3fbb553..986c022cd 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -300,9 +300,9 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { screen.Windows = append(screen.Windows, sw) } query = `SELECT * FROM remote_instance` - var ris []*RemoteInstance - tx.SelectWrap(&ris, query) - for _, ri := range ris { + riMaps := tx.SelectMaps(query) + for _, m := range riMaps { + ri := RIFromMap(m) s := sessionMap[ri.SessionId] if s != nil { s.Remotes = append(s.Remotes, ri) @@ -776,13 +776,13 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat return update, nil } -func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*RemoteState, error) { - var remoteState *RemoteState +func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, error) { + var remoteState *packet.ShellState txErr := WithTx(ctx, func(tx *TxWrap) error { - var ri RemoteInstance query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) - if found { + m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + ri := RIFromMap(m) + if ri != nil { remoteState = &ri.State return nil } @@ -807,20 +807,21 @@ func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error } } -func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, state RemoteState) (*RemoteInstance, error) { +func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, state packet.ShellState) (*RemoteInstance, error) { if remotePtr.IsSessionScope() { windowId = "" } - var ri RemoteInstance + var ri *RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { err := validateSessionWindow(tx, sessionId, windowId) if err != nil { return fmt.Errorf("cannot update remote instance cwd: %w", err) } query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - found := tx.GetWrap(&ri, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) - if !found { - ri = RemoteInstance{ + m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + ri = RIFromMap(m) + if ri == nil { + ri = &RemoteInstance{ RIId: scbase.GenSCUUID(), Name: remotePtr.Name, SessionId: sessionId, @@ -831,15 +832,15 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r } query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, state) VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:state)` - tx.NamedExecWrap(query, ri) + tx.NamedExecWrap(query, ri.ToMap()) return nil } - query = `UPDATE remote_instance SET state = ? WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + query = `UPDATE remote_instance SET state = ? WHERE riid = ?` ri.State = state - tx.ExecWrap(query, ri.State, ri.SessionId, ri.WindowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + tx.ExecWrap(query, quickJson(ri.State), ri.RIId) return nil }) - return &ri, txErr + return ri, txErr } func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) error { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 2eb38a3f2..0350296c0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -440,19 +440,6 @@ type HistoryQueryOpts struct { FromTs int64 } -type RemoteState struct { - Cwd string `json:"cwd"` - Env0 []byte `json:"env0"` // "env -0" format -} - -func (s *RemoteState) Scan(val interface{}) error { - return quickScanJson(s, val) -} - -func (s RemoteState) Value() (driver.Value, error) { - return quickValueJson(s) -} - type TermOpts struct { Rows int64 `json:"rows"` Cols int64 `json:"cols"` @@ -469,18 +456,45 @@ func (opts TermOpts) Value() (driver.Value, error) { } type RemoteInstance struct { - RIId string `json:"riid"` - Name string `json:"name"` - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - RemoteOwnerId string `json:"remoteownerid"` - RemoteId string `json:"remoteid"` - State RemoteState `json:"state"` + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteOwnerId string `json:"remoteownerid"` + RemoteId string `json:"remoteid"` + State packet.ShellState `json:"state"` // only for updates Remove bool `json:"remove,omitempty"` } +func (ri *RemoteInstance) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["riid"] = ri.RIId + rtn["name"] = ri.Name + rtn["sessionid"] = ri.SessionId + rtn["windowid"] = ri.WindowId + rtn["remoteownerid"] = ri.RemoteOwnerId + rtn["remoteid"] = ri.RemoteId + rtn["state"] = quickJson(ri.State) + return rtn +} + +func RIFromMap(m map[string]interface{}) *RemoteInstance { + if len(m) == 0 { + return nil + } + var ri RemoteInstance + quickSetStr(&ri.RIId, m, "riid") + quickSetStr(&ri.Name, m, "name") + quickSetStr(&ri.SessionId, m, "sessionid") + quickSetStr(&ri.WindowId, m, "windowid") + quickSetStr(&ri.RemoteOwnerId, m, "remoteownerid") + quickSetStr(&ri.RemoteId, m, "remoteid") + quickSetJson(&ri.State, m, "state") + return &ri +} + type LineType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` @@ -557,7 +571,7 @@ type CmdType struct { CmdId string `json:"cmdid"` Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` - RemoteState RemoteState `json:"remotestate"` + RemoteState packet.ShellState `json:"remotestate"` TermOpts TermOpts `json:"termopts"` OrigTermOpts TermOpts `json:"origtermopts"` Status string `json:"status"` From e55f7fb4fe892663cb73e8cd9fee641b6cd0fab6 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 18 Oct 2022 18:03:02 -0700 Subject: [PATCH 146/397] working with new static commands --- pkg/cmdrunner/cmdrunner.go | 154 ++++++++++++++++++++++++++----------- pkg/cmdrunner/shparse.go | 1 + pkg/remote/remote.go | 10 +-- pkg/scpacket/scpacket.go | 26 +++++++ 4 files changed, 140 insertions(+), 51 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 7e4d2f684..9a23aaa7c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -247,43 +247,12 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } - rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd) + update, err := addLineForCmd(ctx, "/run", true, ids, cmd) if err != nil { return nil, err } - sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) - if err != nil { - // ignore error here, because the command has already run (nothing to do) - fmt.Printf("/run error getting screen-window: %v\n", err) - } - if sw != nil { - updateMap := make(map[string]interface{}) - updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum - updateMap[sstore.SWField_Focus] = sstore.SWFocusCmdFg - sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) - if err != nil { - // ignore error again (nothing to do) - fmt.Printf("/run error updating screen-window selected line: %v\n", err) - } - } - update := sstore.ModelUpdate{ - Line: rtnLine, - Cmd: cmd, - ScreenWindows: []*sstore.ScreenWindowType{sw}, - Interactive: pk.Interactive, - } + update.Interactive = pk.Interactive sstore.MainBus.SendUpdate(ids.SessionId, update) - ctxVal := ctx.Value(historyContextKey) - if ctxVal != nil { - hctx := ctxVal.(*historyContextType) - if rtnLine != nil { - hctx.LineId = rtnLine.LineId - } - if cmd != nil { - hctx.CmdId = cmd.CmdId - hctx.RemotePtr = &cmd.Remote - } - } return nil, nil } @@ -520,19 +489,27 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore delete(envMap, argStr) unsetVars[argStr] = true } + if len(unsetVars) == 0 { + return nil, fmt.Errorf("no variables provided to unset") + } state := *ids.Remote.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err } - update := sstore.ModelUpdate{ - Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] unset vars: %s", ids.Remote.DisplayName, formatStrs(mapToStrs(unsetVars), "and", false)), - TimeoutMs: 2000, - }, + var cmdOutput bytes.Buffer + for varName, _ := range unsetVars { + cmdOutput.WriteString(fmt.Sprintf("unset %s\r\n", shellescape.Quote(varName))) } + cmd, err := makeStaticCmd(ctx, "unset", ids, pk.GetRawStr(), cmdOutput.Bytes()) + update, err := addLineForCmd(ctx, "/unset", false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Interactive = pk.Interactive + update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) return update, nil } @@ -1043,16 +1020,101 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if err != nil { return nil, err } - update := sstore.ModelUpdate{ - Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst), - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, newDir), - TimeoutMs: 2000, - }, + cmdOutput := fmt.Sprintf("cwd = %s", shellescape.Quote(newDir)) + cmd, err := makeStaticCmd(ctx, "cd", ids, pk.GetRawStr(), []byte(cmdOutput)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err } + update, err := addLineForCmd(ctx, "/cd", false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Interactive = pk.Interactive + update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) + //update.Info = &sstore.InfoMsgType{ + // InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, newDir), + // TimeoutMs: 2000, + //} return update, nil } +func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { + cmd := &sstore.CmdType{ + SessionId: ids.SessionId, + CmdId: scbase.GenSCUUID(), + CmdStr: cmdStr, + Remote: ids.Remote.RemotePtr, + TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}, + Status: sstore.CmdStatusDone, + StartPk: nil, + DonePk: nil, + RunOut: nil, + } + if ids.Remote.RemoteState != nil { + cmd.RemoteState = *ids.Remote.RemoteState + } + err := sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) + } + // can ignore ptyupdate + _, err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, cmdOutput, 0) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, fmt.Errorf("cannot append to local ptyout file for %s command: %v", metaCmd, err) + } + return cmd, nil +} + +func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType) (*sstore.ModelUpdate, error) { + rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd) + if err != nil { + return nil, err + } + sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) + if err != nil { + // ignore error here, because the command has already run (nothing to do) + fmt.Printf("%s error getting screen-window: %v\n", metaCmd, err) + } + if sw != nil { + updateMap := make(map[string]interface{}) + updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + if shouldFocus { + updateMap[sstore.SWField_Focus] = sstore.SWFocusCmdFg + } + sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + if err != nil { + // ignore error again (nothing to do) + fmt.Printf("%s error updating screen-window selected line: %v\n", metaCmd, err) + } + } + update := &sstore.ModelUpdate{ + Line: rtnLine, + Cmd: cmd, + ScreenWindows: []*sstore.ScreenWindowType{sw}, + } + updateHistoryContext(ctx, rtnLine, cmd) + return update, nil +} + +func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstore.CmdType) { + ctxVal := ctx.Value(historyContextKey) + if ctxVal == nil { + return + } + hctx := ctxVal.(*historyContextType) + if line != nil { + hctx.LineId = line.LineId + } + if cmd != nil { + hctx.CmdId = cmd.CmdId + hctx.RemotePtr = &cmd.Remote + } +} + func getStrArr(v interface{}, field string) []string { if v == nil { return nil diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index aa8592f08..a788ce1f7 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -164,6 +164,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) rtnPk.MetaSubCmd = metaSubCmd rtnPk.Kwargs = make(map[string]string) rtnPk.UIContext = origPk.UIContext + rtnPk.RawStr = origPk.RawStr for key, val := range origPk.Kwargs { rtnPk.Kwargs[key] = val } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d2b889a89..6d077fcc3 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -927,14 +927,14 @@ func (msh *MShellProc) Launch() { msh.MakeClientCancelFn = makeClientCancelFn go msh.NotifyRemoteUpdate() }) - cproc, uname, err := shexec.MakeClientProc(makeClientCtx, ecmd) + cproc, initPk, err := shexec.MakeClientProc(makeClientCtx, ecmd) var mshellVersion string msh.WithLock(func() { - msh.UName = uname msh.MakeClientCancelFn = nil - if cproc != nil && cproc.InitPk != nil { - msh.Remote.InitPk = cproc.InitPk - mshellVersion = cproc.InitPk.Version + if initPk != nil { + msh.Remote.InitPk = initPk + msh.UName = initPk.UName + mshellVersion = initPk.Version if semver.Compare(mshellVersion, MShellVersion) < 0 { // only set NeedsMShellUpgrade if we got an InitPk msh.NeedsMShellUpgrade = true diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index aab8a9e31..c27d367c5 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -1,8 +1,11 @@ package scpacket import ( + "fmt" "reflect" + "strings" + "github.com/alessio/shellescape" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -19,10 +22,33 @@ type FeCommandPacketType struct { MetaSubCmd string `json:"metasubcmd,omitempty"` Args []string `json:"args,omitempty"` Kwargs map[string]string `json:"kwargs,omitempty"` + RawStr string `json:"rawstr,omitempty"` UIContext *UIContextType `json:"uicontext,omitempty"` Interactive bool `json:"interactive"` } +func (pk *FeCommandPacketType) GetRawStr() string { + if pk.RawStr != "" { + return pk.RawStr + } + cmd := "/" + pk.MetaCmd + if pk.MetaSubCmd != "" { + cmd = cmd + ":" + pk.MetaSubCmd + } + var args []string + for k, v := range pk.Kwargs { + argStr := fmt.Sprintf("%s=%s", shellescape.Quote(k), shellescape.Quote(v)) + args = append(args, argStr) + } + for _, arg := range pk.Args { + args = append(args, shellescape.Quote(arg)) + } + if len(args) == 0 { + return cmd + } + return cmd + " " + strings.Join(args, " ") +} + type UIContextType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` From caf8b1d151bcac9ab8dbf72a307d528aa24eb665 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 20 Oct 2022 16:14:14 -0700 Subject: [PATCH 147/397] use statediff for output for setenv, unset, and cd --- pkg/cmdrunner/cmdrunner.go | 84 +++++++++++++++++++++++++++------ pkg/cmdrunner/shparse.go | 96 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 14 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 9a23aaa7c..c865707bd 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -499,9 +499,7 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return nil, err } var cmdOutput bytes.Buffer - for varName, _ := range unsetVars { - cmdOutput.WriteString(fmt.Sprintf("unset %s\r\n", shellescape.Quote(varName))) - } + displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) cmd, err := makeStaticCmd(ctx, "unset", ids, pk.GetRawStr(), cmdOutput.Bytes()) update, err := addLineForCmd(ctx, "/unset", false, ids, cmd) if err != nil { @@ -926,17 +924,20 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } state := *ids.Remote.RemoteState state.Env0 = shexec.MakeEnv0(envMap) - remote, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err } - update := sstore.ModelUpdate{ - Sessions: sstore.MakeSessionsUpdateForRemote(ids.SessionId, remote), - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] set vars: %s", ids.Remote.DisplayName, formatStrs(mapToStrs(setVars), "and", false)), - TimeoutMs: 2000, - }, + var cmdOutput bytes.Buffer + displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) + cmd, err := makeStaticCmd(ctx, "setenv", ids, pk.GetRawStr(), cmdOutput.Bytes()) + update, err := addLineForCmd(ctx, "/setenv", false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err } + update.Interactive = pk.Interactive + update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) return update, nil } @@ -1020,8 +1021,9 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if err != nil { return nil, err } - cmdOutput := fmt.Sprintf("cwd = %s", shellescape.Quote(newDir)) - cmd, err := makeStaticCmd(ctx, "cd", ids, pk.GetRawStr(), []byte(cmdOutput)) + var cmdOutput bytes.Buffer + displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) + cmd, err := makeStaticCmd(ctx, "cd", ids, pk.GetRawStr(), cmdOutput.Bytes()) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1542,7 +1544,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, fmt.Errorf("invalid maxitems value '%s' (must be a number): %v", pk.Kwargs["maxitems"], err) } if maxItems < 0 { - return nil, fmt.Errorf("invalid maxitems value '%s' (cannot be negative)", maxItems) + return nil, fmt.Errorf("invalid maxitems value '%d' (cannot be negative)", maxItems) } if maxItems == 0 { maxItems = DefaultMaxHistoryItems @@ -1678,7 +1680,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst ts := time.UnixMilli(line.Ts) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ts", ts.Format("2006-01-02 15:04:05"))) if line.Ephemeral { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ephemeral", true)) + buf.WriteString(fmt.Sprintf(" %-15s %v\n", "ephemeral", true)) } if cmd != nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) @@ -1757,3 +1759,57 @@ func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string } return rtn } + +func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState packet.ShellState) { + if newState.Cwd != oldState.Cwd { + buf.WriteString(fmt.Sprintf("cwd %s\r\n", newState.Cwd)) + } + if !bytes.Equal(newState.Env0, oldState.Env0) { + newEnvMap := shexec.ParseEnv0(newState.Env0) + oldEnvMap := shexec.ParseEnv0(oldState.Env0) + for key, newVal := range newEnvMap { + oldVal, found := oldEnvMap[key] + if !found || oldVal != newVal { + buf.WriteString(fmt.Sprintf("%s=%s\r\n", shellescape.Quote(key), shellescape.Quote(newVal))) + } + } + for key, _ := range oldEnvMap { + _, found := newEnvMap[key] + if !found { + buf.WriteString(fmt.Sprintf("unset %s\r\n", shellescape.Quote(key))) + } + } + } + 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", shellescape.Quote(aliasName))) + } + } + for aliasName, _ := range oldAliasMap { + _, found := newAliasMap[aliasName] + if !found { + buf.WriteString(fmt.Sprintf("unalias %s\r\n", shellescape.Quote(aliasName))) + } + } + } + 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", shellescape.Quote(funcName))) + } + } + for funcName, _ := range oldFuncMap { + _, found := newFuncMap[funcName] + if !found { + buf.WriteString(fmt.Sprintf("unset -f %s\r\n", shellescape.Quote(funcName))) + } + } + } +} diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index a788ce1f7..156c53f59 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -217,3 +217,99 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) } return rtnPk, nil } + +func parseAliasStmt(stmt *syntax.Stmt) (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] + val, err := quotedLitToStr(secondWord) + if err != nil { + return "", "", err + } + eqIdx := strings.Index(val, "=") + if eqIdx == -1 { + return "", "", fmt.Errorf("no '=' in alias definition") + } + return val[0:eqIdx], val[eqIdx+1:], nil +} + +func quotedLitToStr(word *syntax.Word) (string, error) { + cfg := &expand.Config{ + Env: &parseEnviron{Env: make(map[string]string)}, + GlobStar: false, + NullGlob: false, + NoUnset: false, + CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst("", w, word) }, + ProcSubst: doProcSubst, + ReadDir: nil, + } + return expand.Literal(cfg, word) +} + +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) + 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 { + fmt.Printf("stmt-err: %v\n", err) + continue + } + if funcName != "" { + rtn[funcName] = funcVal + } + } + return rtn, nil +} From 3b7dd7c01e944a0e239ec1ae94fc14ca0fba70d3 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 22 Oct 2022 14:46:39 -0700 Subject: [PATCH 148/397] working on source command --- pkg/cmdrunner/cmdrunner.go | 79 ++++++++++++++++++++++++++++++-------- pkg/cmdrunner/shparse.go | 2 + pkg/remote/remote.go | 3 ++ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c865707bd..ecb6a747d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -39,6 +39,10 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} +var WindowCmds = []string{"run", "comment", "cd", "cr", "setenv", "unset", "clear", "sw", "alias", "unalias", "function", "source"} +var NoHistCmds = []string{"compgen", "line", "history"} +var GlobalCmds = []string{"session", "screen", "remote"} + var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") @@ -100,13 +104,17 @@ func init() { registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) registerCmdFn("sw:set", SwSetCommand) + registerCmdFn("sw:resize", SwResizeCommand) - registerCmdFn("window:resize", WindowResizeCommand) + // sw:resize + registerCmdFn("window:resize", SwResizeCommand) registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) registerCmdFn("history", HistoryCommand) + + registerCmdFn("source", SourceCommand) } func getValidCommands() []string { @@ -216,6 +224,54 @@ func resolveNonNegInt(arg string, def int) (int, error) { return ival, nil } +func getUITermOpts(uiContext *scpacket.UIContextType) *packet.TermOpts { + termOpts := &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} + if uiContext != nil && uiContext.TermOpts != nil { + pkOpts := uiContext.TermOpts + if pkOpts.Cols > 0 { + termOpts.Cols = base.BoundInt(pkOpts.Cols, shexec.MinTermCols, shexec.MaxTermCols) + } + if pkOpts.MaxPtySize > 0 { + termOpts.MaxPtySize = base.BoundInt64(pkOpts.MaxPtySize, shexec.MinMaxPtySize, shexec.MaxMaxPtySize) + } + } + return termOpts +} + +func SourceCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) + if err != nil { + return nil, fmt.Errorf("/source error: %w", err) + } + if len(pk.Args) != 1 { + return nil, fmt.Errorf("/source takes one argument (the file to source)") + } + cmdId := scbase.GenSCUUID() + runPacket := packet.MakeRunPacket() + runPacket.ReqId = uuid.New().String() + runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) + runPacket.State = ids.Remote.RemoteState + runPacket.StateComplete = true + runPacket.UsePty = true + runPacket.TermOpts = getUITermOpts(pk.UIContext) + runPacket.Command = strings.TrimSpace(fmt.Sprintf("source %s", shellescape.Quote(pk.Args[0]))) + runPacket.ReturnState = true + cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) + if callback != nil { + defer callback() + } + if err != nil { + return nil, err + } + update, err := addLineForCmd(ctx, "/source", true, ids, cmd) + if err != nil { + return nil, err + } + update.Interactive = pk.Interactive + sstore.MainBus.SendUpdate(ids.SessionId, update) + return nil, nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) if err != nil { @@ -229,16 +285,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.State = ids.Remote.RemoteState runPacket.StateComplete = true runPacket.UsePty = true - runPacket.TermOpts = &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} - if pk.UIContext != nil && pk.UIContext.TermOpts != nil { - pkOpts := pk.UIContext.TermOpts - if pkOpts.Cols > 0 { - runPacket.TermOpts.Cols = base.BoundInt(pkOpts.Cols, shexec.MinTermCols, shexec.MaxTermCols) - } - if pkOpts.MaxPtySize > 0 { - runPacket.TermOpts.MaxPtySize = base.BoundInt64(pkOpts.MaxPtySize, shexec.MinMaxPtySize, shexec.MaxMaxPtySize) - } - } + runPacket.TermOpts = getUITermOpts(pk.UIContext) runPacket.Command = strings.TrimSpace(cmdStr) cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) if callback != nil { @@ -1610,26 +1657,26 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) return nil } -func WindowResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func SwResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, err } colsStr := pk.Kwargs["cols"] if colsStr == "" { - return nil, fmt.Errorf("/window:resize requires a numeric 'cols' argument") + return nil, fmt.Errorf("/sw:resize requires a numeric 'cols' argument") } cols, err := strconv.Atoi(colsStr) if err != nil { - return nil, fmt.Errorf("/window:resize requires a numeric 'cols' argument: %v", err) + return nil, fmt.Errorf("/sw:resize requires a numeric 'cols' argument: %v", err) } if cols <= 0 { - return nil, fmt.Errorf("/window:resize invalid zero/negative 'cols' argument") + return nil, fmt.Errorf("/sw:resize invalid zero/negative 'cols' argument") } cols = base.BoundInt(cols, shexec.MinTermCols, shexec.MaxTermCols) runningCmds, err := sstore.GetRunningWindowCmds(ctx, ids.SessionId, ids.WindowId) if err != nil { - return nil, fmt.Errorf("/window:resize cannot get running commands: %v", err) + return nil, fmt.Errorf("/sw:resize cannot get running commands: %v", err) } if len(runningCmds) == 0 { return nil, nil diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 156c53f59..e0fab17ca 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -97,6 +97,8 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"export", "setenv"}, BareMetaCmdDecl{"unset", "unset"}, BareMetaCmdDecl{"clear", "clear"}, + BareMetaCmdDecl{".", "source"}, + BareMetaCmdDecl{"source", "source"}, } func SubMetaCmd(cmd string) string { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 6d077fcc3..e0b363552 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1196,6 +1196,9 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { update.ScreenWindows = sws if update != nil { sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) + } + if donePk.FinalState != nil { + } return } From c9195b3cfd608ecbb9e17ed30687239c96cb9e86 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 24 Oct 2022 21:29:11 -0700 Subject: [PATCH 149/397] move some sh parsing to shexec --- pkg/cmdrunner/cmdrunner.go | 9 ++++-- pkg/cmdrunner/shparse.go | 61 +++----------------------------------- 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ecb6a747d..00b1a4f80 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -39,7 +39,7 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var WindowCmds = []string{"run", "comment", "cd", "cr", "setenv", "unset", "clear", "sw", "alias", "unalias", "function", "source"} +var WindowCmds = []string{"run", "comment", "cd", "cr", "setenv", "unset", "clear", "sw", "alias", "unalias", "function", "source", "reset"} var NoHistCmds = []string{"compgen", "line", "history"} var GlobalCmds = []string{"session", "screen", "remote"} @@ -78,6 +78,7 @@ func init() { registerCmdFn("setenv", SetEnvCommand) registerCmdFn("unset", UnSetCommand) registerCmdFn("clear", ClearCommand) + registerCmdFn("reset", ResetCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -947,7 +948,7 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if len(pk.Args) == 0 { var infoLines []string for varName, varVal := range envMap { - line := fmt.Sprintf("%s=%s", varName, shellescape.Quote(varVal)) + line := fmt.Sprintf("%s=%s", shellescape.Quote(varName), shellescape.Quote(varVal)) infoLines = append(infoLines, line) } update := sstore.ModelUpdate{ @@ -1563,6 +1564,10 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return update, nil } +func ResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index e0fab17ca..25c671058 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -3,40 +3,15 @@ package cmdrunner import ( "context" "fmt" - "io" "regexp" "strings" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) -type parseEnviron struct { - Env map[string]string -} - -func (e *parseEnviron) Get(name string) expand.Variable { - val, ok := e.Env[name] - if !ok { - return expand.Variable{} - } - return expand.Variable{ - Exported: true, - Kind: expand.String, - Str: val, - } -} - -func (e *parseEnviron) Each(fn func(name string, vr expand.Variable) bool) { - for key, _ := range e.Env { - rtn := fn(key, e.Get(key)) - if !rtn { - break - } - } -} - func DumpPacket(pk *scpacket.FeCommandPacketType) { if pk == nil || pk.MetaCmd == "" { fmt.Printf("[no metacmd]\n") @@ -55,14 +30,6 @@ func DumpPacket(pk *scpacket.FeCommandPacketType) { } } -func doCmdSubst(commandStr string, w io.Writer, word *syntax.CmdSubst) error { - return nil -} - -func doProcSubst(w *syntax.ProcSubst) (string, error) { - return "", nil -} - func isQuoted(source string, w *syntax.Word) bool { if w == nil { return false @@ -99,6 +66,7 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"clear", "clear"}, BareMetaCmdDecl{".", "source"}, BareMetaCmdDecl{"source", "source"}, + BareMetaCmdDecl{"reset", "reset"}, } func SubMetaCmd(cmd string) string { @@ -186,15 +154,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("parsing metacmd, position %v", err) } envMap := make(map[string]string) // later we can add vars like session, window, screen, remote, and user - cfg := &expand.Config{ - Env: &parseEnviron{Env: envMap}, - GlobStar: false, - NullGlob: false, - NoUnset: false, - CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst(commandArgs, w, word) }, - ProcSubst: doProcSubst, - ReadDir: nil, - } + cfg := shexec.GetParserConfig(envMap) // process arguments for idx, w := range words { literalVal, err := expand.Literal(cfg, w) @@ -234,7 +194,7 @@ func parseAliasStmt(stmt *syntax.Stmt) (string, string, error) { return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')") } secondWord := callExpr.Args[1] - val, err := quotedLitToStr(secondWord) + val, err := shexec.QuotedLitToStr(secondWord) if err != nil { return "", "", err } @@ -245,19 +205,6 @@ func parseAliasStmt(stmt *syntax.Stmt) (string, string, error) { return val[0:eqIdx], val[eqIdx+1:], nil } -func quotedLitToStr(word *syntax.Word) (string, error) { - cfg := &expand.Config{ - Env: &parseEnviron{Env: make(map[string]string)}, - GlobStar: false, - NullGlob: false, - NoUnset: false, - CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst("", w, word) }, - ProcSubst: doProcSubst, - ReadDir: nil, - } - return expand.Literal(cfg, word) -} - func ParseAliases(aliases string) (map[string]string, error) { r := strings.NewReader(aliases) parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) From cabf8a50fffa91f428101a747f50eed92c3ecfe5 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 25 Oct 2022 12:31:29 -0700 Subject: [PATCH 150/397] updates for new shellvars --- pkg/cmdrunner/cmdrunner.go | 28 ++++++++++++++-------------- pkg/remote/remote.go | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 00b1a4f80..c9fd39758 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -527,21 +527,21 @@ func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if err != nil { return nil, fmt.Errorf("cannot unset: %v", err) } - envMap := shexec.ParseEnv0(ids.Remote.RemoteState.Env0) + declMap := shexec.DeclMapFromState(ids.Remote.RemoteState) unsetVars := make(map[string]bool) for _, argStr := range pk.Args { eqIdx := strings.Index(argStr, "=") if eqIdx != -1 { return nil, fmt.Errorf("invalid argument to setenv, '%s' (cannot contain equal sign)", argStr) } - delete(envMap, argStr) + delete(declMap, argStr) unsetVars[argStr] = true } if len(unsetVars) == 0 { return nil, fmt.Errorf("no variables provided to unset") } state := *ids.Remote.RemoteState - state.Env0 = shexec.MakeEnv0(envMap) + state.ShellVars = shexec.SerializeDeclMap(declMap) remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err @@ -944,11 +944,11 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, fmt.Errorf("cannot setenv: %v", err) } - envMap := shexec.ParseEnv0(ids.Remote.RemoteState.Env0) + declMap := shexec.DeclMapFromState(ids.Remote.RemoteState) if len(pk.Args) == 0 { var infoLines []string - for varName, varVal := range envMap { - line := fmt.Sprintf("%s=%s", shellescape.Quote(varName), shellescape.Quote(varVal)) + for _, decl := range declMap { + line := fmt.Sprintf("%s=%s", decl.Name, shellescape.Quote(decl.Value)) infoLines = append(infoLines, line) } update := sstore.ModelUpdate{ @@ -967,11 +967,11 @@ func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } envName := argStr[:eqIdx] envVal := argStr[eqIdx+1:] - envMap[envName] = envVal + declMap[envName] = &shexec.DeclareDeclType{Args: "x", Name: envName, Value: envVal} setVars[envName] = true } state := *ids.Remote.RemoteState - state.Env0 = shexec.MakeEnv0(envMap) + state.ShellVars = shexec.SerializeDeclMap(declMap) remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) if err != nil { return nil, err @@ -1816,19 +1816,19 @@ func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState if newState.Cwd != oldState.Cwd { buf.WriteString(fmt.Sprintf("cwd %s\r\n", newState.Cwd)) } - if !bytes.Equal(newState.Env0, oldState.Env0) { - newEnvMap := shexec.ParseEnv0(newState.Env0) - oldEnvMap := shexec.ParseEnv0(oldState.Env0) + 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 || oldVal != newVal { - buf.WriteString(fmt.Sprintf("%s=%s\r\n", shellescape.Quote(key), shellescape.Quote(newVal))) + if !found || oldVal.Value != newVal.Value { + buf.WriteString(fmt.Sprintf("%s=%s\r\n", key, shellescape.Quote(newVal.Value))) } } for key, _ := range oldEnvMap { _, found := newEnvMap[key] if !found { - buf.WriteString(fmt.Sprintf("unset %s\r\n", shellescape.Quote(key))) + buf.WriteString(fmt.Sprintf("unset %s\r\n", key)) } } } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e0b363552..cc325e8a4 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1383,7 +1383,7 @@ func evalPromptEsc(escCode string, vars map[string]string, state *packet.ShellSt return "" } varName := escCode[2 : len(escCode)-1] - varMap := shexec.ParseEnv0(state.Env0) + varMap := shexec.ShellVarMapFromState(state) return varMap[varName] } if escCode == "h" { From d50ed6ca6c56c3fc08e8ea22d51a95d4ad390751 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 26 Oct 2022 14:51:12 -0700 Subject: [PATCH 151/397] work on evalbracketargs --- pkg/cmdrunner/shparse.go | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 25c671058..3a841e4cd 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -6,6 +6,7 @@ import ( "regexp" "strings" + "github.com/alessio/shellescape" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "mvdan.cc/sh/v3/expand" @@ -121,6 +122,87 @@ func onlyRawArgs(metaCmd string, metaSubCmd string) bool { return metaCmd == "run" || metaCmd == "comment" } +// minimum maxlen=6 +func ForceQuote(val string, maxLen int) string { + if maxLen < 6 { + maxLen = 6 + } + rtn := shellescape.Quote(val) + if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") { + if len(rtn) > maxLen { + return rtn[0:maxLen-4] + "..." + rtn[0:1] + } + return rtn + } + if len(rtn) > maxLen-2 { + return "\"" + rtn[0:maxLen-5] + "...\"" + } + return "\"" + rtn + "\"" +} + +func setBracketArgs(argMap map[string]string, bracketStr string) error { + bracketStr = strings.TrimSpace(bracketStr) + if bracketStr == "" { + return nil + } + strReader := strings.NewReader(bracketStr) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + var wordErr error + err := parser.Words(strReader, func(w *syntax.Word) bool { + litStr, err := shexec.QuotedLitToStr(w) + if err != nil { + wordErr = fmt.Errorf("invalid expr in bracket args: %v", err) + return false + } + eqIdx := strings.Index(litStr, "=") + var varName, varVal string + if eqIdx == -1 { + varName = litStr + } else { + varName = litStr[0:eqIdx] + varVal = litStr[eqIdx+1:] + } + if !shexec.IsValidBashIdentifier(varName) { + wordErr = fmt.Errorf("invalid identifier %s in bracket args", ForceQuote(varName, 20)) + return false + } + if varVal == "" { + varVal = "1" + } + argMap[varName] = varVal + return true + }) + if err != nil { + return err + } + if wordErr != nil { + return wordErr + } + return nil +} + +func EvalBracketArgs(origCmdStr string) (map[string]string, string, error) { + rtn := make(map[string]string) + if strings.HasPrefix(origCmdStr, " ") { + rtn["nohist"] = "1" + } + cmdStr := strings.TrimSpace(origCmdStr) + if !strings.HasPrefix(cmdStr, "[") { + return rtn, origCmdStr, nil + } + rbIdx := strings.Index(cmdStr, "]") + if rbIdx == -1 { + return nil, "", fmt.Errorf("unmatched '[' found in command") + } + bracketStr := cmdStr[1:rbIdx] + restStr := strings.TrimSpace(cmdStr[rbIdx+1:]) + err := setBracketArgs(rtn, bracketStr) + if err != nil { + return nil, "", err + } + return rtn, restStr, nil +} + func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) (*scpacket.FeCommandPacketType, error) { if len(origPk.Args) == 0 { return nil, fmt.Errorf("empty command (no fields)") From 0060c8ffc298a0efa3ea567bd92eadc954f968b4 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 27 Oct 2022 00:33:50 -0700 Subject: [PATCH 152/397] big update to handle cmd returnstate (still need to process new state with donepacket) --- cmd/main-server.go | 31 ++++++ db/migrations/000001_init.up.sql | 3 +- pkg/cmdrunner/cmdrunner.go | 166 ++++++------------------------- pkg/cmdrunner/shparse.go | 66 +++++++++--- pkg/remote/remote.go | 1 + pkg/sstore/dbops.go | 8 +- pkg/sstore/sstore.go | 44 ++++---- 7 files changed, 148 insertions(+), 171 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 508fea726..c1ed442be 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -182,6 +182,36 @@ func HandleGetWindow(w http.ResponseWriter, r *http.Request) { return } +func HandleRtnState(w http.ResponseWriter, r *http.Request) { + qvals := r.URL.Query() + sessionId := qvals.Get("sessionid") + cmdId := qvals.Get("cmdid") + if sessionId == "" || cmdId == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid"))) + return + } + if _, err := uuid.Parse(sessionId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid sessionid: %v", err))) + return + } + if _, err := uuid.Parse(cmdId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) + return + } + data, err := cmdrunner.GetRtnStateDiff(r.Context(), sessionId, cmdId) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("cannot get rtnstate diff: %v", err))) + return + } + w.WriteHeader(http.StatusOK) + w.Write(data) + return +} + func HandleRemotePty(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) w.Header().Set("Access-Control-Allow-Credentials", "true") @@ -393,6 +423,7 @@ func main() { gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) gr.HandleFunc("/api/remote-pty", HandleRemotePty) + gr.HandleFunc("/api/rtnstate", HandleRtnState) gr.HandleFunc("/api/get-window", HandleGetWindow) gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") gr.HandleFunc("/api/get-client-data", HandleGetClientData) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 55b4c25a4..bc54d74d0 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -79,6 +79,7 @@ CREATE TABLE line ( text text NOT NULL, cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, + contentheight int NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); @@ -116,7 +117,7 @@ CREATE TABLE cmd ( startpk json NOT NULL, donepk json NOT NULL, runout json NOT NULL, - usedrows int NOT NULL, + rtnstate bool NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c9fd39758..3bed888e3 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -39,7 +39,7 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var WindowCmds = []string{"run", "comment", "cd", "cr", "setenv", "unset", "clear", "sw", "alias", "unalias", "function", "source", "reset"} +var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "alias", "unalias", "function", "reset"} var NoHistCmds = []string{"compgen", "line", "history"} var GlobalCmds = []string{"session", "screen", "remote"} @@ -72,11 +72,9 @@ func init() { registerCmdFn("run", RunCommand) registerCmdFn("eval", EvalCommand) registerCmdFn("comment", CommentCommand) - registerCmdFn("cd", CdCommand) + // registerCmdFn("cd", CdCommand) registerCmdFn("cr", CrCommand) registerCmdFn("compgen", CompGenCommand) - registerCmdFn("setenv", SetEnvCommand) - registerCmdFn("unset", UnSetCommand) registerCmdFn("clear", ClearCommand) registerCmdFn("reset", ResetCommand) @@ -114,8 +112,6 @@ func init() { registerCmdFn("line:show", LineShowCommand) registerCmdFn("history", HistoryCommand) - - registerCmdFn("source", SourceCommand) } func getValidCommands() []string { @@ -239,40 +235,6 @@ func getUITermOpts(uiContext *scpacket.UIContextType) *packet.TermOpts { return termOpts } -func SourceCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) - if err != nil { - return nil, fmt.Errorf("/source error: %w", err) - } - if len(pk.Args) != 1 { - return nil, fmt.Errorf("/source takes one argument (the file to source)") - } - cmdId := scbase.GenSCUUID() - runPacket := packet.MakeRunPacket() - runPacket.ReqId = uuid.New().String() - runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) - runPacket.State = ids.Remote.RemoteState - runPacket.StateComplete = true - runPacket.UsePty = true - runPacket.TermOpts = getUITermOpts(pk.UIContext) - runPacket.Command = strings.TrimSpace(fmt.Sprintf("source %s", shellescape.Quote(pk.Args[0]))) - runPacket.ReturnState = true - cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) - if callback != nil { - defer callback() - } - if err != nil { - return nil, err - } - update, err := addLineForCmd(ctx, "/source", true, ids, cmd) - if err != nil { - return nil, err - } - update.Interactive = pk.Interactive - sstore.MainBus.SendUpdate(ids.SessionId, update) - return nil, nil -} - func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) if err != nil { @@ -280,6 +242,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } cmdId := scbase.GenSCUUID() cmdStr := firstArg(pk) + isRtnStateCmd := IsReturnStateCommand(cmdStr) runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) @@ -288,6 +251,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket.UsePty = true runPacket.TermOpts = getUITermOpts(pk.UIContext) runPacket.Command = strings.TrimSpace(cmdStr) + runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd) cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) if callback != nil { defer callback() @@ -522,43 +486,6 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return sstore.ModelUpdate{ScreenWindows: []*sstore.ScreenWindowType{sw}}, nil } -func UnSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) - if err != nil { - return nil, fmt.Errorf("cannot unset: %v", err) - } - declMap := shexec.DeclMapFromState(ids.Remote.RemoteState) - unsetVars := make(map[string]bool) - for _, argStr := range pk.Args { - eqIdx := strings.Index(argStr, "=") - if eqIdx != -1 { - return nil, fmt.Errorf("invalid argument to setenv, '%s' (cannot contain equal sign)", argStr) - } - delete(declMap, argStr) - unsetVars[argStr] = true - } - if len(unsetVars) == 0 { - return nil, fmt.Errorf("no variables provided to unset") - } - state := *ids.Remote.RemoteState - state.ShellVars = shexec.SerializeDeclMap(declMap) - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) - if err != nil { - return nil, err - } - var cmdOutput bytes.Buffer - displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) - cmd, err := makeStaticCmd(ctx, "unset", ids, pk.GetRawStr(), cmdOutput.Bytes()) - update, err := addLineForCmd(ctx, "/unset", false, ids, cmd) - if err != nil { - // TODO tricky error since the command was a success, but we can't show the output - return nil, err - } - update.Interactive = pk.Interactive - update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) - return update, nil -} - func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -939,56 +866,6 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/remote requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } -func SetEnvCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) - if err != nil { - return nil, fmt.Errorf("cannot setenv: %v", err) - } - declMap := shexec.DeclMapFromState(ids.Remote.RemoteState) - if len(pk.Args) == 0 { - var infoLines []string - for _, decl := range declMap { - line := fmt.Sprintf("%s=%s", decl.Name, shellescape.Quote(decl.Value)) - infoLines = append(infoLines, line) - } - update := sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - InfoTitle: fmt.Sprintf("environment for remote [%s]", ids.Remote.DisplayName), - InfoLines: infoLines, - }, - } - return update, nil - } - setVars := make(map[string]bool) - for _, argStr := range pk.Args { - eqIdx := strings.Index(argStr, "=") - if eqIdx == -1 { - return nil, fmt.Errorf("invalid argument to setenv, '%s' (no equal sign)", argStr) - } - envName := argStr[:eqIdx] - envVal := argStr[eqIdx+1:] - declMap[envName] = &shexec.DeclareDeclType{Args: "x", Name: envName, Value: envVal} - setVars[envName] = true - } - state := *ids.Remote.RemoteState - state.ShellVars = shexec.SerializeDeclMap(declMap) - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) - if err != nil { - return nil, err - } - var cmdOutput bytes.Buffer - displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) - cmd, err := makeStaticCmd(ctx, "setenv", ids, pk.GetRawStr(), cmdOutput.Bytes()) - update, err := addLineForCmd(ctx, "/setenv", false, ids, cmd) - if err != nil { - // TODO tricky error since the command was a success, but we can't show the output - return nil, err - } - update.Interactive = pk.Interactive - update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) - return update, nil -} - func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) if err != nil { @@ -1814,21 +1691,25 @@ func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState packet.ShellState) { if newState.Cwd != oldState.Cwd { - buf.WriteString(fmt.Sprintf("cwd %s\r\n", newState.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 || oldVal.Value != newVal.Value { - buf.WriteString(fmt.Sprintf("%s=%s\r\n", key, shellescape.Quote(newVal.Value))) + if !found || ((oldVal.Value != newVal.Value) || (oldVal.IsExport() != newVal.IsExport())) { + var exportStr string + if newVal.IsExport() { + exportStr = "export " + } + buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, ShellQuote(newVal.Value, false, 50))) } } for key, _ := range oldEnvMap { _, found := newEnvMap[key] if !found { - buf.WriteString(fmt.Sprintf("unset %s\r\n", key)) + buf.WriteString(fmt.Sprintf("unset %s\n", key)) } } } @@ -1844,7 +1725,7 @@ func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState for aliasName, _ := range oldAliasMap { _, found := newAliasMap[aliasName] if !found { - buf.WriteString(fmt.Sprintf("unalias %s\r\n", shellescape.Quote(aliasName))) + buf.WriteString(fmt.Sprintf("unalias %s\n", shellescape.Quote(aliasName))) } } } @@ -1860,8 +1741,27 @@ func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState for funcName, _ := range oldFuncMap { _, found := newFuncMap[funcName] if !found { - buf.WriteString(fmt.Sprintf("unset -f %s\r\n", shellescape.Quote(funcName))) + buf.WriteString(fmt.Sprintf("unset -f %s\n", shellescape.Quote(funcName))) } } } } + +func GetRtnStateDiff(ctx context.Context, sessionId string, cmdId string) ([]byte, error) { + cmd, err := sstore.GetCmdById(ctx, sessionId, cmdId) + if err != nil { + return nil, err + } + if cmd == nil { + return nil, nil + } + if !cmd.RtnState { + return nil, nil + } + if cmd.DonePk == nil || cmd.DonePk.FinalState == nil { + return nil, nil + } + var outputBytes bytes.Buffer + displayStateUpdate(&outputBytes, cmd.RemoteState, *cmd.DonePk.FinalState) + return outputBytes.Bytes(), nil +} diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 3a841e4cd..898db34f8 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -59,14 +59,8 @@ type BareMetaCmdDecl struct { } var BareMetaCmds = []BareMetaCmdDecl{ - BareMetaCmdDecl{"cd", "cd"}, BareMetaCmdDecl{"cr", "cr"}, - BareMetaCmdDecl{"setenv", "setenv"}, - BareMetaCmdDecl{"export", "setenv"}, - BareMetaCmdDecl{"unset", "unset"}, BareMetaCmdDecl{"clear", "clear"}, - BareMetaCmdDecl{".", "source"}, - BareMetaCmdDecl{"source", "source"}, BareMetaCmdDecl{"reset", "reset"}, } @@ -123,7 +117,7 @@ func onlyRawArgs(metaCmd string, metaSubCmd string) bool { } // minimum maxlen=6 -func ForceQuote(val string, maxLen int) string { +func ShellQuote(val string, forceQuote bool, maxLen int) string { if maxLen < 6 { maxLen = 6 } @@ -134,10 +128,17 @@ func ForceQuote(val string, maxLen int) string { } return rtn } - if len(rtn) > maxLen-2 { - return "\"" + rtn[0:maxLen-5] + "...\"" + if forceQuote { + if len(rtn) > maxLen-2 { + return "\"" + rtn[0:maxLen-5] + "...\"" + } + return "\"" + rtn + "\"" + } else { + if len(rtn) > maxLen { + return rtn[0:maxLen-3] + "..." + } + return rtn } - return "\"" + rtn + "\"" } func setBracketArgs(argMap map[string]string, bracketStr string) error { @@ -163,7 +164,7 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { varVal = litStr[eqIdx+1:] } if !shexec.IsValidBashIdentifier(varName) { - wordErr = fmt.Errorf("invalid identifier %s in bracket args", ForceQuote(varName, 20)) + wordErr = fmt.Errorf("invalid identifier %s in bracket args", ShellQuote(varName, true, 20)) return false } if varVal == "" { @@ -181,6 +182,35 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { return nil } +// detects: export, declare, ., source, X=1, unset +func IsReturnStateCommand(cmdStr string) bool { + cmdReader := strings.NewReader(cmdStr) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + file, err := parser.Parse(cmdReader, "cmd") + if err != nil { + return false + } + for _, stmt := range file.Stmts { + if callExpr, ok := stmt.Cmd.(*syntax.CallExpr); ok { + if len(callExpr.Assigns) > 0 && len(callExpr.Args) == 0 { + return true + } + if len(callExpr.Args) > 0 && len(callExpr.Args[0].Parts) > 0 { + lit, ok := callExpr.Args[0].Parts[0].(*syntax.Lit) + if ok { + if lit.Value == "." || lit.Value == "source" || lit.Value == "unset" || lit.Value == "cd" { + return true + } + } + + } + } else if _, ok := stmt.Cmd.(*syntax.DeclClause); ok { + return true + } + } + return false +} + func EvalBracketArgs(origCmdStr string) (map[string]string, string, error) { rtn := make(map[string]string) if strings.HasPrefix(origCmdStr, " ") { @@ -210,7 +240,11 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) if strings.TrimSpace(origPk.Args[0]) == "" { return nil, fmt.Errorf("empty command") } - metaCmd, metaSubCmd, commandArgs := parseMetaCmd(origPk.Args[0]) + bracketArgs, cmdStr, err := EvalBracketArgs(origPk.Args[0]) + if err != nil { + return nil, err + } + metaCmd, metaSubCmd, commandArgs := parseMetaCmd(cmdStr) rtnPk := scpacket.MakeFeCommandPacket() rtnPk.MetaCmd = metaCmd rtnPk.MetaSubCmd = metaSubCmd @@ -220,6 +254,9 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) for key, val := range origPk.Kwargs { rtnPk.Kwargs[key] = val } + for key, val := range bracketArgs { + rtnPk.Kwargs[key] = val + } if onlyRawArgs(metaCmd, metaSubCmd) { // don't evaluate arguments for /run or /comment rtnPk.Args = []string{commandArgs} @@ -228,7 +265,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) commandReader := strings.NewReader(commandArgs) parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) var words []*syntax.Word - err := parser.Words(commandReader, func(w *syntax.Word) bool { + err = parser.Words(commandReader, func(w *syntax.Word) bool { words = append(words, w) return true }) @@ -338,6 +375,9 @@ func ParseFuncs(funcs string) (map[string]string, error) { fmt.Printf("stmt-err: %v\n", err) continue } + if strings.HasPrefix(funcName, "_scripthaus_") { + continue + } if funcName != "" { rtn[funcName] = funcVal } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index cc325e8a4..7b76ebb9f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1106,6 +1106,7 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp StartPk: startPk, DonePk: nil, RunOut: nil, + RtnState: runPacket.ReturnState, } err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 986c022cd..fa07751ef 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -623,8 +623,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight)` tx.NamedExecWrap(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) @@ -632,8 +632,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, origtermopts, status, startpk, donepk, runout, usedrows) - VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:origtermopts,:status,:startpk,:donepk,:runout,:usedrows) +INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, origtermopts, status, startpk, donepk, rtnstate, runout) + VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:origtermopts,:status,:startpk,:donepk,:rtnstate,:runout) ` tx.NamedExecWrap(query, cmdMap) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 0350296c0..175173f9d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -28,6 +28,7 @@ import ( const LineTypeCmd = "cmd" const LineTypeText = "text" +const LineNoHeight = -1 const DBFileName = "sh2.db" const DefaultSessionName = "default" @@ -496,19 +497,20 @@ func RIFromMap(m map[string]interface{}) *RemoteInstance { } type LineType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - UserId string `json:"userid"` - LineId string `json:"lineid"` - Ts int64 `json:"ts"` - LineNum int64 `json:"linenum"` - LineNumTemp bool `json:"linenumtemp,omitempty"` - LineLocal bool `json:"linelocal"` - LineType string `json:"linetype"` - Text string `json:"text,omitempty"` - CmdId string `json:"cmdid,omitempty"` - Ephemeral bool `json:"ephemeral,omitempty"` - Remove bool `json:"remove,omitempty"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + UserId string `json:"userid"` + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineNumTemp bool `json:"linenumtemp,omitempty"` + LineLocal bool `json:"linelocal"` + LineType string `json:"linetype"` + Text string `json:"text,omitempty"` + CmdId string `json:"cmdid,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + Remove bool `json:"remove,omitempty"` + ContentHeight int64 `json:"contentheight,omitempty"` } type ResolveItem struct { @@ -575,11 +577,11 @@ type CmdType struct { TermOpts TermOpts `json:"termopts"` OrigTermOpts TermOpts `json:"origtermopts"` Status string `json:"status"` - StartPk *packet.CmdStartPacketType `json:"startpk"` - DonePk *packet.CmdDonePacketType `json:"donepk"` - UsedRows int64 `json:"usedrows"` - RunOut []packet.PacketType `json:"runout"` - Remove bool `json:"remove"` + StartPk *packet.CmdStartPacketType `json:"startpk,omitempty"` + DonePk *packet.CmdDonePacketType `json:"donepk,omitempty"` + RunOut []packet.PacketType `json:"runout,omitempty"` + RtnState bool `json:"rtnstate,omitempty"` + Remove bool `json:"remove,omitempty"` } func (r *RemoteType) ToMap() map[string]interface{} { @@ -644,7 +646,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["startpk"] = quickJson(cmd.StartPk) rtn["donepk"] = quickJson(cmd.DonePk) rtn["runout"] = quickJson(cmd.RunOut) - rtn["usedrows"] = cmd.UsedRows + rtn["rtnstate"] = cmd.RtnState return rtn } @@ -666,7 +668,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetJson(&cmd.StartPk, m, "startpk") quickSetJson(&cmd.DonePk, m, "donepk") quickSetJson(&cmd.RunOut, m, "runout") - quickSetInt64(&cmd.UsedRows, m, "usedrows") + quickSetBool(&cmd.RtnState, m, "rtnstate") return &cmd } @@ -680,6 +682,7 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn.LineLocal = true rtn.LineType = LineTypeCmd rtn.CmdId = cmdId + rtn.ContentHeight = LineNoHeight return rtn } @@ -693,6 +696,7 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri rtn.LineLocal = true rtn.LineType = LineTypeText rtn.Text = text + rtn.ContentHeight = LineNoHeight return rtn } From 56259e3f05118f68d5f9131960583318672a7dc2 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 27 Oct 2022 17:10:36 -0700 Subject: [PATCH 153/397] fix cmd done lock ordering (actually start the cmdwait). implement reset command to re-initialize the terminal --- pkg/cmdrunner/cmdrunner.go | 33 +++++++++++++++++++++++++- pkg/cmdrunner/shparse.go | 26 ++++++++++----------- pkg/remote/remote.go | 47 +++++++++++++++++++++++++++++++++----- pkg/remote/updatequeue.go | 6 +++++ 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3bed888e3..c0a57c54f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1442,7 +1442,35 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func ResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + initPk, err := ids.Remote.MShell.ReInit(ctx) + if err != nil { + return nil, err + } + if initPk == nil || initPk.State == nil { + return nil, fmt.Errorf("invalid initpk received from remote (no remote state)") + } + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, *initPk.State) + if err != nil { + return nil, err + } + outputStr := "reset remote state" + cmd, err := makeStaticCmd(ctx, "reset", ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update, err := addLineForCmd(ctx, "/cd", false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Interactive = pk.Interactive + update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) + return update, nil } func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -1622,6 +1650,9 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if cmd.TermOpts != cmd.OrigTermOpts { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "orig-termopts", formatTermOpts(cmd.OrigTermOpts))) } + if cmd.RtnState { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "rtnstate", "true")) + } } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 898db34f8..5a5ea0212 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -13,6 +13,19 @@ import ( "mvdan.cc/sh/v3/syntax" ) +var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(?::([a-z][a-z0-9_-]*))?$") + +type BareMetaCmdDecl struct { + CmdStr string + MetaCmd string +} + +var BareMetaCmds = []BareMetaCmdDecl{ + BareMetaCmdDecl{"cr", "cr"}, + BareMetaCmdDecl{"clear", "clear"}, + BareMetaCmdDecl{"reset", "reset"}, +} + func DumpPacket(pk *scpacket.FeCommandPacketType) { if pk == nil || pk.MetaCmd == "" { fmt.Printf("[no metacmd]\n") @@ -51,19 +64,6 @@ func getSourceStr(source string, w *syntax.Word) string { return source[offset:end] } -var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(?::([a-z][a-z0-9_-]*))?$") - -type BareMetaCmdDecl struct { - CmdStr string - MetaCmd string -} - -var BareMetaCmds = []BareMetaCmdDecl{ - BareMetaCmdDecl{"cr", "cr"}, - BareMetaCmdDecl{"clear", "clear"}, - BareMetaCmdDecl{"reset", "reset"}, -} - func SubMetaCmd(cmd string) string { switch cmd { case "s": diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 7b76ebb9f..8e8e9f56c 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -875,6 +875,26 @@ func (msh *MShellProc) RunInstall() { return } +func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, error) { + reinitPk := packet.MakeReInitPacket() + reinitPk.ReqId = uuid.New().String() + resp, err := msh.PacketRpcRaw(ctx, reinitPk) + if err != nil { + return nil, err + } + if resp == nil { + return nil, fmt.Errorf("no response") + } + initPk, ok := resp.(*packet.InitPacketType) + if !ok { + return nil, fmt.Errorf("invalid reinit response (not an initpacket): %T", resp) + } + msh.WithLock(func() { + msh.Remote.InitPk = initPk + }) + return initPk, nil +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { @@ -1057,7 +1077,7 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { } // returns (cmdtype, allow-updates-callback, err) -func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *packet.ShellState, runPacket *packet.RunPacketType) (*sstore.CmdType, func(), error) { +func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *packet.ShellState, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } @@ -1071,6 +1091,15 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp if remoteState == nil { return nil, nil, fmt.Errorf("no remote state passed to RunCommand") } + callbackFn := func() { + removeCmdWait(runPacket.CK) + } + startCmdWait(runPacket.CK) + defer func() { + if rtnErr != nil { + callbackFn() + } + }() msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { @@ -1114,7 +1143,7 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) } msh.AddRunningCmd(startPk.CK) - return cmd, func() { removeCmdWait(startPk.CK) }, nil + return cmd, callbackFn, nil } func (msh *MShellProc) AddRunningCmd(ck base.CommandKey) { @@ -1129,7 +1158,7 @@ func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { delete(msh.RunningCmds, ck) } -func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { +func (msh *MShellProc) PacketRpcRaw(ctx context.Context, pk packet.RpcPacketType) (packet.RpcResponsePacketType, error) { if !msh.IsConnected() { return nil, fmt.Errorf("runner is not connected") } @@ -1147,6 +1176,14 @@ func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) ( if rtnPk == nil { return nil, ctx.Err() } + return rtnPk, nil +} + +func (msh *MShellProc) PacketRpc(ctx context.Context, pk packet.RpcPacketType) (*packet.ResponsePacketType, error) { + rtnPk, err := msh.PacketRpcRaw(ctx, pk) + if err != nil { + return nil, err + } if respPk, ok := rtnPk.(*packet.ResponsePacketType); ok { return respPk, nil } @@ -1195,9 +1232,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { // fall-through (nothing to do) } update.ScreenWindows = sws - if update != nil { - sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) - } + sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) if donePk.FinalState != nil { } diff --git a/pkg/remote/updatequeue.go b/pkg/remote/updatequeue.go index 4665511ff..7b978e38d 100644 --- a/pkg/remote/updatequeue.go +++ b/pkg/remote/updatequeue.go @@ -4,6 +4,12 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" ) +func startCmdWait(ck base.CommandKey) { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + GlobalStore.CmdWaitMap[ck] = nil +} + func pushCmdWaitIfRequired(ck base.CommandKey, fn func()) bool { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() From 2df33621fd011cbec1cdd48addab044c197f7bb2 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 27 Oct 2022 22:00:10 -0700 Subject: [PATCH 154/397] stateful commands block other commands while they are running, introduce waiting state --- pkg/cmdrunner/cmdrunner.go | 7 ++-- pkg/remote/remote.go | 82 +++++++++++++++++++++++++++++++++----- pkg/sstore/dbops.go | 28 ++++++++++++- pkg/sstore/sstore.go | 1 + 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c0a57c54f..acd7a8ffe 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -240,19 +240,18 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, fmt.Errorf("/run error: %w", err) } - cmdId := scbase.GenSCUUID() cmdStr := firstArg(pk) isRtnStateCmd := IsReturnStateCommand(cmdStr) runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() - runPacket.CK = base.MakeCommandKey(ids.SessionId, cmdId) + runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenSCUUID()) runPacket.State = ids.Remote.RemoteState runPacket.StateComplete = true runPacket.UsePty = true runPacket.TermOpts = getUITermOpts(pk.UIContext) runPacket.Command = strings.TrimSpace(cmdStr) runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd) - cmd, callback, err := remote.RunCommand(ctx, cmdId, ids.Remote.RemotePtr, ids.Remote.RemoteState, runPacket) + cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, runPacket) if callback != nil { defer callback() } @@ -1619,7 +1618,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - line, cmd, err := sstore.GetLineCmd(ctx, ids.SessionId, ids.WindowId, lineId) + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) if err != nil { return nil, fmt.Errorf("error getting line: %v", err) } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 8e8e9f56c..94413bd52 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -94,7 +94,14 @@ type MShellProc struct { InstallCancelFn context.CancelFunc InstallErr error - RunningCmds map[base.CommandKey]bool + RunningCmds map[base.CommandKey]bool + WaitingCmds []RunCmdType + PendingStateCmds map[string]base.CommandKey // key=[remoteinstance name] +} + +type RunCmdType struct { + RemotePtr sstore.RemotePtrType + RunPacket *packet.RunPacketType } type RemoteRuntimeState struct { @@ -530,12 +537,13 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { panic(err) // this should never happen (NewBuffer only returns an error if CirBufSize <= 0) } rtn := &MShellProc{ - Lock: &sync.Mutex{}, - Remote: r, - Status: StatusDisconnected, - PtyBuffer: buf, - InstallStatus: StatusDisconnected, - RunningCmds: make(map[base.CommandKey]bool), + Lock: &sync.Mutex{}, + Remote: r, + Status: StatusDisconnected, + PtyBuffer: buf, + InstallStatus: StatusDisconnected, + RunningCmds: make(map[base.CommandKey]bool), + PendingStateCmds: make(map[string]base.CommandKey), } rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) return rtn @@ -1076,11 +1084,40 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: true, MaxPtySize: DefaultMaxPtySize} } +// returns (ok, currentPSC) +func (msh *MShellProc) testAndSetPendingStateCmd(name string, newCK *base.CommandKey) (bool, *base.CommandKey) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + ck, found := msh.PendingStateCmds[name] + if found { + return false, &ck + } + if newCK != nil { + msh.PendingStateCmds[name] = *newCK + } + return true, nil +} + +func (msh *MShellProc) removePendingStateCmd(name string, ck base.CommandKey) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + existingCK, found := msh.PendingStateCmds[name] + if !found { + return + } + if existingCK == ck { + delete(msh.PendingStateCmds, name) + } +} + // returns (cmdtype, allow-updates-callback, err) -func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrType, remoteState *packet.ShellState, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { +func RunCommand(ctx context.Context, sessionId string, windowId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } + if sessionId != runPacket.CK.GetSessionId() { + return nil, nil, fmt.Errorf("run commands sessionids do not match") + } msh := GetRemoteById(remotePtr.RemoteId) if msh == nil { return nil, nil, fmt.Errorf("no remote id=%s found", remotePtr.RemoteId) @@ -1088,9 +1125,24 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp if !msh.IsConnected() { return nil, nil, fmt.Errorf("remote '%s' is not connected", remotePtr.RemoteId) } - if remoteState == nil { + if runPacket.State == nil { return nil, nil, fmt.Errorf("no remote state passed to RunCommand") } + var newPSC *base.CommandKey + if runPacket.ReturnState { + newPSC = &runPacket.CK + } + ok, existingPSC := msh.testAndSetPendingStateCmd(remotePtr.Name, newPSC) + if !ok { + line, _, err := sstore.GetLineCmdByCmdId(ctx, sessionId, windowId, existingPSC.GetCmdId()) + if err != nil { + return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) + } + if line == nil { + return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC, windowId) + } + return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) + } callbackFn := func() { removeCmdWait(runPacket.CK) } @@ -1098,6 +1150,9 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp defer func() { if rtnErr != nil { callbackFn() + if newPSC != nil { + msh.removePendingStateCmd(remotePtr.Name, *newPSC) + } } }() msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) @@ -1126,10 +1181,10 @@ func RunCommand(ctx context.Context, cmdId string, remotePtr sstore.RemotePtrTyp } cmd := &sstore.CmdType{ SessionId: runPacket.CK.GetSessionId(), - CmdId: startPk.CK.GetCmdId(), + CmdId: runPacket.CK.GetCmdId(), CmdStr: runPacket.Command, Remote: remotePtr, - RemoteState: *remoteState, + RemoteState: *runPacket.State, TermOpts: makeTermOpts(runPacket), Status: status, StartPk: startPk, @@ -1156,6 +1211,11 @@ func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { msh.Lock.Lock() defer msh.Lock.Unlock() delete(msh.RunningCmds, ck) + for name, pendingCk := range msh.PendingStateCmds { + if pendingCk == ck { + delete(msh.PendingStateCmds, name) + } + } } func (msh *MShellProc) PacketRpcRaw(ctx context.Context, pk packet.RpcPacketType) (packet.RpcResponsePacketType, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index fa07751ef..9bd1d3f09 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -577,7 +577,7 @@ func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lin return lineId, nil } -func GetLineCmd(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, *CmdType, error) { +func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, *CmdType, error) { var lineRtn *LineType var cmdRtn *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -605,6 +605,32 @@ func GetLineCmd(ctx context.Context, sessionId string, windowId string, lineId s return lineRtn, cmdRtn, nil } +func GetLineCmdByCmdId(ctx context.Context, sessionId string, windowId string, cmdId string) (*LineType, *CmdType, error) { + var lineRtn *LineType + var cmdRtn *CmdType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("window not found") + } + var lineVal LineType + query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND cmdid = ?` + found := tx.GetWrap(&lineVal, query, sessionId, windowId, cmdId) + if !found { + return nil + } + lineRtn = &lineVal + query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` + m := tx.GetMap(query, sessionId, cmdId) + cmdRtn = CmdFromMap(m) + return nil + }) + if txErr != nil { + return nil, nil, txErr + } + return lineRtn, cmdRtn, nil +} + func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line == nil { return fmt.Errorf("line cannot be nil") diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 175173f9d..10f343583 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -44,6 +44,7 @@ const ( CmdStatusError = "error" CmdStatusDone = "done" CmdStatusHangup = "hangup" + CmdStatusWaiting = "waiting" ) const ( From 31a38cbdae52acdc2a6c4f212a945d76b8c2168f Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 27 Oct 2022 22:22:17 -0700 Subject: [PATCH 155/397] state updates (yay) --- pkg/remote/remote.go | 73 +++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 94413bd52..cf89e9bdc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -80,6 +80,7 @@ type MShellProc struct { Remote *sstore.RemoteType // runtime + RemoteId string // can be read without a lock Status string ServerProc *shexec.ClientProc UName string @@ -94,12 +95,14 @@ type MShellProc struct { InstallCancelFn context.CancelFunc InstallErr error - RunningCmds map[base.CommandKey]bool + RunningCmds map[base.CommandKey]RunCmdType WaitingCmds []RunCmdType PendingStateCmds map[string]base.CommandKey // key=[remoteinstance name] } type RunCmdType struct { + SessionId string + WindowId string RemotePtr sstore.RemotePtrType RunPacket *packet.RunPacketType } @@ -539,10 +542,11 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { rtn := &MShellProc{ Lock: &sync.Mutex{}, Remote: r, + RemoteId: r.RemoteId, Status: StatusDisconnected, PtyBuffer: buf, InstallStatus: StatusDisconnected, - RunningCmds: make(map[base.CommandKey]bool), + RunningCmds: make(map[base.CommandKey]RunCmdType), PendingStateCmds: make(map[string]base.CommandKey), } rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) @@ -1085,33 +1089,39 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { } // returns (ok, currentPSC) -func (msh *MShellProc) testAndSetPendingStateCmd(name string, newCK *base.CommandKey) (bool, *base.CommandKey) { +func (msh *MShellProc) testAndSetPendingStateCmd(riName string, newCK *base.CommandKey) (bool, *base.CommandKey) { msh.Lock.Lock() defer msh.Lock.Unlock() - ck, found := msh.PendingStateCmds[name] + ck, found := msh.PendingStateCmds[riName] if found { return false, &ck } if newCK != nil { - msh.PendingStateCmds[name] = *newCK + msh.PendingStateCmds[riName] = *newCK } return true, nil } -func (msh *MShellProc) removePendingStateCmd(name string, ck base.CommandKey) { +func (msh *MShellProc) removePendingStateCmd(riName string, ck base.CommandKey) { msh.Lock.Lock() defer msh.Lock.Unlock() - existingCK, found := msh.PendingStateCmds[name] + existingCK, found := msh.PendingStateCmds[riName] if !found { return } if existingCK == ck { - delete(msh.PendingStateCmds, name) + delete(msh.PendingStateCmds, riName) } } // returns (cmdtype, allow-updates-callback, err) func RunCommand(ctx context.Context, sessionId string, windowId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { + rct := RunCmdType{ + SessionId: sessionId, + WindowId: windowId, + RemotePtr: remotePtr, + RunPacket: runPacket, + } if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } @@ -1143,13 +1153,10 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt } return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) } - callbackFn := func() { - removeCmdWait(runPacket.CK) - } startCmdWait(runPacket.CK) defer func() { if rtnErr != nil { - callbackFn() + removeCmdWait(runPacket.CK) if newPSC != nil { msh.removePendingStateCmd(remotePtr.Name, *newPSC) } @@ -1197,14 +1204,24 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt // TODO the cmd is running, so this is a tricky error to handle return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) } - msh.AddRunningCmd(startPk.CK) - return cmd, callbackFn, nil + msh.AddRunningCmd(rct) + return cmd, func() { removeCmdWait(runPacket.CK) }, nil } -func (msh *MShellProc) AddRunningCmd(ck base.CommandKey) { +func (msh *MShellProc) AddRunningCmd(rct RunCmdType) { msh.Lock.Lock() defer msh.Lock.Unlock() - msh.RunningCmds[ck] = true + msh.RunningCmds[rct.RunPacket.CK] = rct +} + +func (msh *MShellProc) GetRunningCmd(ck base.CommandKey) *RunCmdType { + msh.Lock.Lock() + defer msh.Lock.Unlock() + rct, found := msh.RunningCmds[ck] + if !found { + return nil + } + return &rct } func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { @@ -1276,26 +1293,38 @@ func (msh *MShellProc) notifyHangups_nolock() { update := sstore.ModelUpdate{Cmd: cmd} sstore.MainBus.SendUpdate(ck.GetSessionId(), update) } - msh.RunningCmds = make(map[base.CommandKey]bool) + msh.RunningCmds = make(map[base.CommandKey]RunCmdType) } func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { - msh.RemoveRunningCmd(donePk.CK) + // this will remove from RunningCmds and from PendingStateCmds + defer msh.RemoveRunningCmd(donePk.CK) + update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) if err != nil { - msh.WriteToPtyBuffer("[error] updating cmddone: %v\n", err) + msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return } sws, err := sstore.UpdateSWsWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) if err != nil { - fmt.Printf("[error] trying to update cmd-fg screen windows: %v\n", err) + msh.WriteToPtyBuffer("*error trying to update cmd-fg screen windows: %v\n", err) // fall-through (nothing to do) } update.ScreenWindows = sws - sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) if donePk.FinalState != nil { - + rct := msh.GetRunningCmd(donePk.CK) + if rct != nil { + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *donePk.FinalState) + if err != nil { + msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) + // fall-through (nothing to do) + } + if remoteInst != nil { + update.Sessions = sstore.MakeSessionsUpdateForRemote(rct.SessionId, remoteInst) + } + } } + sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) return } From 29dbf5dde9b94fb0725dc95823e8b4bd7bb408d2 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 27 Oct 2022 22:52:38 -0700 Subject: [PATCH 156/397] runcommand now sets the state at the time of execution --- pkg/cmdrunner/cmdrunner.go | 3 +-- pkg/remote/remote.go | 54 +++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index acd7a8ffe..de84d097b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -245,8 +245,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenSCUUID()) - runPacket.State = ids.Remote.RemoteState - runPacket.StateComplete = true + // runPacket.State is set in remote.RunCommand() runPacket.UsePty = true runPacket.TermOpts = getUITermOpts(pk.UIContext) runPacket.Command = strings.TrimSpace(cmdStr) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index cf89e9bdc..e197076ab 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -901,6 +901,9 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro if !ok { return nil, fmt.Errorf("invalid reinit response (not an initpacket): %T", resp) } + if initPk.State == nil { + return nil, fmt.Errorf("invalid reinit response initpk does not contain remote state") + } msh.WithLock(func() { msh.Remote.InitPk = initPk }) @@ -960,6 +963,7 @@ func (msh *MShellProc) Launch() { go msh.NotifyRemoteUpdate() }) cproc, initPk, err := shexec.MakeClientProc(makeClientCtx, ecmd) + // TODO check if initPk.State is not nil var mshellVersion string msh.WithLock(func() { msh.MakeClientCancelFn = nil @@ -1135,8 +1139,8 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt if !msh.IsConnected() { return nil, nil, fmt.Errorf("remote '%s' is not connected", remotePtr.RemoteId) } - if runPacket.State == nil { - return nil, nil, fmt.Errorf("no remote state passed to RunCommand") + if runPacket.State != nil { + return nil, nil, fmt.Errorf("runPacket.State should not be set, it is set in RunCommand") } var newPSC *base.CommandKey if runPacket.ReturnState { @@ -1162,8 +1166,21 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt } } }() + // get current remote-instance state + currentState, err := sstore.GetRemoteState(ctx, sessionId, windowId, remotePtr) + if err != nil { + return nil, nil, fmt.Errorf("cannot get current remote state: %w", err) + } + if currentState == nil { + currentState = msh.ServerProc.InitPk.State + } + if currentState == nil { + return nil, nil, fmt.Errorf("cannot run command, no valid remote state") + } + runPacket.State = currentState + runPacket.StateComplete = true msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) - err := shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) + err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) if err != nil { return nil, nil, fmt.Errorf("sending run packet to remote: %w", err) } @@ -1208,6 +1225,37 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt return cmd, func() { removeCmdWait(runPacket.CK) }, nil } +func (msh *MShellProc) AddWaitingCmd(rct RunCmdType) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + msh.WaitingCmds = append(msh.WaitingCmds, rct) +} + +func (msh *MShellProc) reExecSingle(rct RunCmdType) { + // TODO fixme + ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second) + defer cancelFn() + _, callback, _ := RunCommand(ctx, rct.SessionId, rct.WindowId, rct.RemotePtr, rct.RunPacket) + if callback != nil { + defer callback() + } +} + +func (msh *MShellProc) ReExecWaitingCmds() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + for len(msh.WaitingCmds) > 0 { + rct := msh.WaitingCmds[0] + go msh.reExecSingle(rct) + if rct.RunPacket.ReturnState { + break + } + } + if len(msh.WaitingCmds) == 0 { + msh.WaitingCmds = nil + } +} + func (msh *MShellProc) AddRunningCmd(rct RunCmdType) { msh.Lock.Lock() defer msh.Lock.Unlock() From 260fc2d313bec17de930d6eb1baa734cb19ae3cd Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Oct 2022 12:52:40 -0700 Subject: [PATCH 157/397] working on server control and packaging --- pkg/cmdrunner/cmdrunner.go | 14 +++++++++++++- pkg/sstore/updatebus.go | 1 + scripthaus.md | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index de84d097b..5623526f3 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -11,6 +11,7 @@ import ( "sort" "strconv" "strings" + "syscall" "time" "github.com/alessio/shellescape" @@ -41,7 +42,7 @@ var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoins var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "alias", "unalias", "function", "reset"} var NoHistCmds = []string{"compgen", "line", "history"} -var GlobalCmds = []string{"session", "screen", "remote"} +var GlobalCmds = []string{"session", "screen", "remote", "killserver"} var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") @@ -112,6 +113,8 @@ func init() { registerCmdFn("line:show", LineShowCommand) registerCmdFn("history", HistoryCommand) + + registerCmdFn("killserver", KillServerCommand) } func getValidCommands() []string { @@ -1661,6 +1664,15 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return update, nil } +func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + go func() { + fmt.Printf("received /killserver, shutting down\n") + time.Sleep(1 * time.Second) + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + }() + return nil, nil +} + func formatTermOpts(termOpts sstore.TermOpts) string { if termOpts.Cols == 0 { return "???" diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 5d3baf6b2..b5ad09613 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -37,6 +37,7 @@ type ModelUpdate struct { Cmd *CmdType `json:"cmd,omitempty"` CmdLine *CmdLineType `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"` diff --git a/scripthaus.md b/scripthaus.md index 6ae418626..a7f1964d3 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -12,5 +12,5 @@ sqlite3 /Users/mike/scripthaus/sh2.db ```bash # @scripthaus command build -go build -o server cmd/main-server.go +go build -o ~/scripthaus/local-server cmd/main-server.go ``` From f35adf1da0b112e21a8866ed66949a741ead75e8 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Oct 2022 13:05:53 -0700 Subject: [PATCH 158/397] watch stdin, kill server is stdin is closed --- cmd/main-server.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/main-server.go b/cmd/main-server.go index c1ed442be..554f811de 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "github.com/google/uuid" @@ -364,6 +365,19 @@ func test() error { return nil } +// watch stdin, kill server if stdin is closed +func stdinReadWatch() { + buf := make([]byte, 1024) + for { + _, err := os.Stdin.Read(buf) + if err != nil { + fmt.Printf("stdin closed/error, shutting down: %v\n", err) + time.Sleep(1 * time.Second) + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + } + } +} + func main() { if len(os.Args) >= 2 && os.Args[1] == "--test" { fmt.Printf("running test fn\n") @@ -419,6 +433,7 @@ func main() { fmt.Printf("[error] calling HUP on all running commands\n") } + go stdinReadWatch() go runWebSocketServer() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", HandleGetPtyOut) From d6ba16613cb6b4160284211de6652fd9e615e4b7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 31 Oct 2022 12:24:21 -0700 Subject: [PATCH 159/397] store migrations in go source (via embed) --- db/db.go | 9 +++++++++ pkg/sstore/migrate.go | 15 ++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 db/db.go diff --git a/db/db.go b/db/db.go new file mode 100644 index 000000000..de43f7c6d --- /dev/null +++ b/db/db.go @@ -0,0 +1,9 @@ +// provides the io/fs for DB migrations +package db + +import "embed" + +// since embeds must be relative to the package directory, this source file is required + +//go:embed migrations/*.sql +var MigrationFS embed.FS diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 7e8151280..e0f6e5f0f 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -2,27 +2,28 @@ package sstore import ( "fmt" - "os" - "path" "strconv" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/mattn/go-sqlite3" + sh2db "github.com/scripthaus-dev/sh2-server/db" "github.com/golang-migrate/migrate/v4" ) func MakeMigrate() (*migrate.Migrate, error) { - wd, err := os.Getwd() + fsVar, err := iofs.New(sh2db.MigrationFS, "migrations") if err != nil { - return nil, err + return nil, fmt.Errorf("opening iofs: %w", err) } - migrationPathUrl := fmt.Sprintf("file://%s", path.Join(wd, "db", "migrations")) + // migrationPathUrl := fmt.Sprintf("file://%s", path.Join(wd, "db", "migrations")) dbUrl := fmt.Sprintf("sqlite3://%s", GetSessionDBName()) - m, err := migrate.New(migrationPathUrl, dbUrl) + m, err := migrate.NewWithSourceInstance("iofs", fsVar, dbUrl) + // m, err := migrate.New(migrationPathUrl, dbUrl) if err != nil { - return nil, fmt.Errorf("making migration [%s] db[%s]: %w", migrationPathUrl, GetSessionDBName(), err) + return nil, fmt.Errorf("making migration db[%s]: %w", GetSessionDBName(), err) } return m, nil } From d9163d6af465ea5d6ea16eeda4ebbcf61a88e722 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 31 Oct 2022 12:40:45 -0700 Subject: [PATCH 160/397] use log.Printf, ensure sc home dir --- cmd/main-server.go | 42 +++++++++++++++++++++----------------- pkg/cmdrunner/cmdrunner.go | 12 +++++------ pkg/cmdrunner/shparse.go | 2 +- pkg/remote/remote.go | 2 +- pkg/scbase/scbase.go | 23 ++++++++------------- pkg/scws/scws.go | 23 +++++++++++---------- pkg/sstore/dbops.go | 2 -- pkg/sstore/migrate.go | 3 ++- pkg/sstore/sstore.go | 2 +- pkg/sstore/updatebus.go | 3 ++- 10 files changed, 56 insertions(+), 58 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 554f811de..77868f944 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/fs" + "log" "net/http" "os" "runtime/debug" @@ -69,7 +70,7 @@ func removeWSStateAfterTimeout(clientId string, connectTime time.Time, waitDurat func HandleWs(w http.ResponseWriter, r *http.Request) { shell, err := wsshell.StartWS(w, r) if err != nil { - fmt.Printf("WebSocket Upgrade Failed %T: %v\n", w, err) + log.Printf("WebSocket Upgrade Failed %T: %v\n", w, err) return } defer shell.Conn.Close() @@ -91,7 +92,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { defer func() { removeWSStateAfterTimeout(clientId, stateConnectTime, WSStateReconnectTime) }() - fmt.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr) + log.Printf("WebSocket opened %s %s\n", state.ClientId, shell.RemoteAddr) state.RunWSRead() } @@ -313,7 +314,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { if r == nil { return } - fmt.Printf("[error] in run-command: %v\n", r) + log.Printf("[error] in run-command: %v\n", r) debug.PrintStack() WriteJsonError(w, fmt.Errorf("panic: %v", r)) return @@ -354,10 +355,10 @@ func runWebSocketServer() { Handler: gr, } server.SetKeepAlivesEnabled(false) - fmt.Printf("Running websocket server on %s\n", WebSocketServerAddr) + log.Printf("Running websocket server on %s\n", WebSocketServerAddr) err := server.ListenAndServe() if err != nil { - fmt.Printf("[error] trying to run websocket server: %v\n", err) + log.Printf("[error] trying to run websocket server: %v\n", err) } } @@ -371,7 +372,7 @@ func stdinReadWatch() { for { _, err := os.Stdin.Read(buf) if err != nil { - fmt.Printf("stdin closed/error, shutting down: %v\n", err) + log.Printf("stdin closed/error, shutting down: %v\n", err) time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) } @@ -380,57 +381,60 @@ func stdinReadWatch() { func main() { if len(os.Args) >= 2 && os.Args[1] == "--test" { - fmt.Printf("running test fn\n") + log.Printf("running test fn\n") err := test() if err != nil { - fmt.Printf("[error] %v\n", err) + log.Printf("[error] %v\n", err) } return } + scHomeDir := scbase.GetScHomeDir() + log.Printf("[scripthaus] homedir = %q\n", scHomeDir) + scLock, err := scbase.AcquireSCLock() if err != nil || scLock == nil { - fmt.Printf("[error] cannot acquire sh2 lock: %v\n", err) + log.Printf("[error] cannot acquire sh2 lock: %v\n", err) return } if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") { err := sstore.MigrateCommandOpts(os.Args[1:]) if err != nil { - fmt.Printf("[error] migrate cmd: %v\n", err) + log.Printf("[error] migrate cmd: %v\n", err) } return } err = sstore.TryMigrateUp() if err != nil { - fmt.Printf("[error] migrate up: %v\n", err) + log.Printf("[error] migrate up: %v\n", err) return } clientData, err := sstore.EnsureClientData(context.Background()) if err != nil { - fmt.Printf("[error] ensuring client data: %v\n", err) + log.Printf("[error] ensuring client data: %v\n", err) return } - fmt.Printf("userid = %s\n", clientData.UserId) + log.Printf("userid = %s\n", clientData.UserId) err = sstore.EnsureLocalRemote(context.Background()) if err != nil { - fmt.Printf("[error] ensuring local remote: %v\n", err) + log.Printf("[error] ensuring local remote: %v\n", err) return } _, err = sstore.EnsureDefaultSession(context.Background()) if err != nil { - fmt.Printf("[error] ensuring default session: %v\n", err) + log.Printf("[error] ensuring default session: %v\n", err) return } err = remote.LoadRemotes(context.Background()) if err != nil { - fmt.Printf("[error] loading remotes: %v\n", err) + log.Printf("[error] loading remotes: %v\n", err) return } err = sstore.HangupAllRunningCmds(context.Background()) if err != nil { - fmt.Printf("[error] calling HUP on all running commands\n") + log.Printf("[error] calling HUP on all running commands\n") } go stdinReadWatch() @@ -451,9 +455,9 @@ func main() { Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"), } server.SetKeepAlivesEnabled(false) - fmt.Printf("Running main server on %s\n", MainServerAddr) + log.Printf("Running main server on %s\n", MainServerAddr) err = server.ListenAndServe() if err != nil { - fmt.Printf("ERROR: %v\n", err) + log.Printf("ERROR: %v\n", err) } } diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 5623526f3..565aca6b2 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log" "os" "path" "path/filepath" @@ -312,7 +313,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if !resolveBool(pk.Kwargs["nohist"], false) { err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil)) if err != nil { - fmt.Printf("[error] adding to history: %v\n", err) + log.Printf("[error] adding to history: %v\n", err) // continue... } } @@ -1005,7 +1006,7 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) if err != nil { // ignore error here, because the command has already run (nothing to do) - fmt.Printf("%s error getting screen-window: %v\n", metaCmd, err) + log.Printf("%s error getting screen-window: %v\n", metaCmd, err) } if sw != nil { updateMap := make(map[string]interface{}) @@ -1016,7 +1017,7 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { // ignore error again (nothing to do) - fmt.Printf("%s error updating screen-window selected line: %v\n", metaCmd, err) + log.Printf("%s error updating screen-window selected line: %v\n", metaCmd, err) } } update := &sstore.ModelUpdate{ @@ -1267,7 +1268,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) if err != nil { // ignore error again (nothing to do) - fmt.Printf("/comment error updating screen-window selected line: %v\n", err) + log.Printf("/comment error updating screen-window selected line: %v\n", err) } update := sstore.ModelUpdate{Line: rtnLine, ScreenWindows: []*sstore.ScreenWindowType{sw}} return update, nil @@ -1547,7 +1548,6 @@ func splitLinesForInfo(str string) []string { } func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) error { - fmt.Printf("resize running cmd %s/%s %d => %d\n", cmd.SessionId, cmd.CmdId, cmd.TermOpts.Cols, newCols) siPk := packet.MakeSpecialInputPacket() siPk.CK = base.MakeCommandKey(cmd.SessionId, cmd.CmdId) siPk.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols} @@ -1666,7 +1666,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { go func() { - fmt.Printf("received /killserver, shutting down\n") + log.Printf("received /killserver, shutting down\n") time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) }() diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 5a5ea0212..0450b2e36 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -372,7 +372,7 @@ func ParseFuncs(funcs string) (map[string]string, error) { for _, stmt := range file.Stmts { funcName, funcVal, err := parseFuncStmt(stmt, funcs) if err != nil { - fmt.Printf("stmt-err: %v\n", err) + // TODO where to put parse errors continue } if strings.HasPrefix(funcName, "_scripthaus_") { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e197076ab..aa96f9046 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1410,7 +1410,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa if ack != nil { msh.ServerProc.Input.SendPacket(ack) } - // fmt.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) + // log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error) } func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) func() { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index c1b8cd0b2..dac39888e 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io/fs" + "log" "os" "path" "strconv" @@ -17,8 +18,8 @@ import ( const HomeVarName = "HOME" const ScHomeVarName = "SCRIPTHAUS_HOME" const SessionsDirBaseName = "sessions" -const RemotesDirBaseName = "remotes" const SCLockFile = "sh2.lock" +const ScriptHausDirName = "scripthaus" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} @@ -30,13 +31,17 @@ func GetScHomeDir() string { if homeVar == "" { homeVar = "/" } - scHome = path.Join(homeVar, "scripthaus") + scHome = path.Join(homeVar, ScriptHausDirName) } return scHome } func AcquireSCLock() (*os.File, error) { homeDir := GetScHomeDir() + err := ensureDir(homeDir) + if err != nil { + return nil, fmt.Errorf("cannot find/create SCRIPTHAUS_HOME directory %q", homeDir) + } lockFileName := path.Join(homeDir, SCLockFile) fd, err := os.Create(lockFileName) if err != nil { @@ -79,6 +84,7 @@ func ensureDir(dirName string) error { if err != nil { return err } + log.Printf("[scripthaus] created directory %q\n", dirName) info, err = os.Stat(dirName) } if err != nil { @@ -118,19 +124,6 @@ func RunOutFile(sessionId string, cmdId string) (string, error) { return fmt.Sprintf("%s/%s.runout", sdir, cmdId), nil } -func RemotePtyOut(remoteId string) (string, error) { - if remoteId == "" { - return "", fmt.Errorf("cannot get remote ptyout file for blank remoteid") - } - scHome := GetScHomeDir() - rdir := path.Join(scHome, RemotesDirBaseName) - err := ensureDir(rdir) - if err != nil { - return "", err - } - return fmt.Sprintf("%s/%s.ptyout.cf", rdir, remoteId), nil -} - type ScFileNameGenerator struct { ScHome string } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 71e91c92a..045e39b2e 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -3,6 +3,7 @@ package scws import ( "context" "fmt" + "log" "sync" "time" @@ -85,7 +86,7 @@ func (ws *WSState) UnWatchScreen() { sstore.MainBus.UnregisterChannel(ws.ClientId) ws.SessionId = "" ws.ScreenId = "" - fmt.Printf("[ws] unwatch screen clientid=%s\n", ws.ClientId) + log.Printf("[ws] unwatch screen clientid=%s\n", ws.ClientId) } func (ws *WSState) getUpdateCh() chan interface{} { @@ -154,10 +155,10 @@ func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error ws.UnWatchScreen() } else { ws.WatchScreen(wsPk.SessionId, wsPk.ScreenId) - fmt.Printf("[ws %s] watchscreen %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) + log.Printf("[ws %s] watchscreen %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) } if wsPk.Connect { - fmt.Printf("[ws %s] watchscreen connect\n", ws.ClientId) + log.Printf("[ws %s] watchscreen connect\n", ws.ClientId) err := ws.handleConnection() if err != nil { return fmt.Errorf("connect: %w", err) @@ -175,24 +176,24 @@ func (ws *WSState) RunWSRead() { for msgBytes := range shell.ReadChan { pk, err := packet.ParseJsonPacket(msgBytes) if err != nil { - fmt.Printf("error unmarshalling ws message: %v\n", err) + log.Printf("error unmarshalling ws message: %v\n", err) continue } if pk.GetType() == scpacket.FeInputPacketStr { feInputPk := pk.(*scpacket.FeInputPacketType) if feInputPk.Remote.OwnerId != "" { - fmt.Printf("[error] cannot send input to remote with ownerid\n") + log.Printf("[error] cannot send input to remote with ownerid\n") continue } if feInputPk.Remote.RemoteId == "" { - fmt.Printf("[error] invalid input packet, remoteid is not set\n") + log.Printf("[error] invalid input packet, remoteid is not set\n") continue } go func() { // TODO enforce a strong ordering (channel with list) err = sendCmdInput(feInputPk) if err != nil { - fmt.Printf("[error] sending command input: %v\n", err) + log.Printf("[error] sending command input: %v\n", err) } }() continue @@ -202,25 +203,25 @@ func (ws *WSState) RunWSRead() { err := ws.handleWatchScreen(wsPk) if err != nil { // TODO send errors back to client, likely unrecoverable - fmt.Printf("[ws %s] error %v\n", err) + log.Printf("[ws %s] error %v\n", err) } continue } if pk.GetType() == scpacket.RemoteInputPacketStr { inputPk := pk.(*scpacket.RemoteInputPacketType) if inputPk.RemoteId == "" { - fmt.Printf("[error] invalid remoteinput packet, remoteid is not set\n") + log.Printf("[error] invalid remoteinput packet, remoteid is not set\n") continue } go func() { err = remote.SendRemoteInput(inputPk) if err != nil { - fmt.Printf("[error] processing remote input: %v\n", err) + log.Printf("[error] processing remote input: %v\n", err) } }() continue } - fmt.Printf("got ws bad message: %v\n", pk.GetType()) + log.Printf("got ws bad message: %v\n", pk.GetType()) } } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9bd1d3f09..787e5eb4b 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -733,7 +733,6 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { } func getNextId(ids []string, delId string) string { - fmt.Printf("getnextid %v | %v\n", ids, delId) if len(ids) == 0 { return "" } @@ -779,7 +778,6 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId) - fmt.Printf("delete-screen %s %s | %v\n", sessionId, screenId, isActive) if isActive { screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index e0f6e5f0f..db43816de 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -2,6 +2,7 @@ package sstore import ( "fmt" + "log" "strconv" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" @@ -91,7 +92,7 @@ func MigratePrintVersion() error { if dirty { return fmt.Errorf("error db is dirty, version=%d", version) } - fmt.Printf("[db] version=%d\n", version) + log.Printf("[db] version=%d\n", version) return nil } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 10f343583..772d7d59a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -802,7 +802,7 @@ func createClientData(tx *TxWrap) error { query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` tx.NamedExecWrap(query, c.ToMap()) - fmt.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) + log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index b5ad09613..a224ab40d 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -2,6 +2,7 @@ package sstore import ( "fmt" + "log" "sync" ) @@ -180,7 +181,7 @@ func (bus *UpdateBus) SendUpdate(sessionId string, update interface{}) { case uch.Ch <- update: default: - fmt.Printf("[error] dropped update on updatebus uch clientid=%s\n", uch.ClientId) + log.Printf("[error] dropped update on updatebus uch clientid=%s\n", uch.ClientId) } } } From d4e4b497fbe2383644369389c34f1875175c0c21 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 1 Nov 2022 21:42:56 -0700 Subject: [PATCH 161/397] scripthaus env vars. also mshellbinaryfrompackage for install --- pkg/cmdrunner/cmdrunner.go | 1 - pkg/remote/remote.go | 57 ++++++++++++++++++++++++++++---------- pkg/scbase/scbase.go | 27 ++++++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 565aca6b2..25e11f7fa 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -98,7 +98,6 @@ func init() { registerCmdFn("remote:new", RemoteNewCommand) registerCmdFn("remote:archive", RemoteArchiveCommand) registerCmdFn("remote:set", RemoteSetCommand) - registerCmdAlias("remote:edit", RemoteSetCommand) registerCmdFn("remote:disconnect", RemoteDisconnectCommand) registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index aa96f9046..6583710a7 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -23,6 +23,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "golang.org/x/mod/semver" @@ -88,6 +89,7 @@ type MShellProc struct { ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc + DefaultState *packet.ShellState // install InstallStatus string @@ -140,6 +142,12 @@ func (msh *MShellProc) GetStatus() string { return msh.Status } +func (msh *MShellProc) GetDefaultState() *packet.ShellState { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.DefaultState +} + func (msh *MShellProc) GetRemoteId() string { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -475,7 +483,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["color"] = msh.Remote.RemoteOpts.Color } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { - state.DefaultState = msh.ServerProc.InitPk.State + state.DefaultState = msh.DefaultState state.MShellVersion = msh.ServerProc.InitPk.Version vars["home"] = msh.ServerProc.InitPk.HomeDir vars["remoteuser"] = msh.ServerProc.InitPk.User @@ -863,7 +871,7 @@ func (msh *MShellProc) RunInstall() { msgFn := func(msg string) { msh.WriteToPtyBuffer("%s", msg) } - err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, "", msgFn) + err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, nil, scbase.MShellBinaryFromPackage, msgFn) if err == context.Canceled { msh.WriteToPtyBuffer("*install canceled\n") msh.WithLock(func() { @@ -906,10 +914,35 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro } msh.WithLock(func() { msh.Remote.InitPk = initPk + msh.DefaultState = initPk.State }) return initPk, nil } +func addScVarsToState(state *packet.ShellState) *packet.ShellState { + if state == nil { + return nil + } + rtn := *state + envMap := shexec.DeclMapFromState(&rtn) + envMap["SCRIPTHAUS"] = &shexec.DeclareDeclType{Name: "SCRIPTHAUS", Value: "1"} + envMap["SCRIPTHAUS_VERSION"] = &shexec.DeclareDeclType{Name: "SCRIPTHAUS_VERSION", Value: scbase.ScriptHausVersion} + rtn.ShellVars = shexec.SerializeDeclMap(envMap) + return &rtn +} + +func stripScVarsFromState(state *packet.ShellState) *packet.ShellState { + if state == nil { + return nil + } + rtn := *state + envMap := shexec.DeclMapFromState(&rtn) + delete(envMap, "SCRIPTHAUS") + delete(envMap, "SCRIPTHAUS_VERSION") + rtn.ShellVars = shexec.SerializeDeclMap(envMap) + return &rtn +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { @@ -968,6 +1001,7 @@ func (msh *MShellProc) Launch() { msh.WithLock(func() { msh.MakeClientCancelFn = nil if initPk != nil { + msh.DefaultState = initPk.State msh.Remote.InitPk = initPk msh.UName = initPk.UName mshellVersion = initPk.Version @@ -1021,15 +1055,6 @@ func (msh *MShellProc) IsConnected() bool { return msh.Status == StatusConnected } -func (msh *MShellProc) GetDefaultState() *packet.ShellState { - msh.Lock.Lock() - defer msh.Lock.Unlock() - if msh.ServerProc == nil || msh.ServerProc.InitPk == nil { - return nil - } - return msh.ServerProc.InitPk.State -} - func replaceHomePath(pathStr string, homeDir string) string { if homeDir == "" { return pathStr @@ -1172,12 +1197,12 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt return nil, nil, fmt.Errorf("cannot get current remote state: %w", err) } if currentState == nil { - currentState = msh.ServerProc.InitPk.State + currentState = msh.GetDefaultState() } if currentState == nil { return nil, nil, fmt.Errorf("cannot run command, no valid remote state") } - runPacket.State = currentState + runPacket.State = addScVarsToState(currentState) runPacket.StateComplete = true msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket) @@ -1203,12 +1228,13 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt if runPacket.Detached { status = sstore.CmdStatusDetached } + cmdState := stripScVarsFromState(runPacket.State) cmd := &sstore.CmdType{ SessionId: runPacket.CK.GetSessionId(), CmdId: runPacket.CK.GetCmdId(), CmdStr: runPacket.Command, Remote: remotePtr, - RemoteState: *runPacket.State, + RemoteState: *cmdState, TermOpts: makeTermOpts(runPacket), Status: status, StartPk: startPk, @@ -1347,6 +1373,9 @@ func (msh *MShellProc) notifyHangups_nolock() { func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { // this will remove from RunningCmds and from PendingStateCmds defer msh.RemoveRunningCmd(donePk.CK) + if donePk.FinalState != nil { + donePk.FinalState = stripScVarsFromState(donePk.FinalState) + } update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) if err != nil { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index dac39888e..13004c9b3 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -3,6 +3,7 @@ package scbase import ( "errors" "fmt" + "io" "io/fs" "log" "os" @@ -12,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" + "golang.org/x/mod/semver" "golang.org/x/sys/unix" ) @@ -20,10 +22,13 @@ const ScHomeVarName = "SCRIPTHAUS_HOME" const SessionsDirBaseName = "sessions" const SCLockFile = "sh2.lock" const ScriptHausDirName = "scripthaus" +const ScriptHausAppPathVarName = "SCRIPTHAUS_APP_PATH" +const ScriptHausVersion = "v0.1.0" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} +// must match js func GetScHomeDir() string { scHome := os.Getenv(ScHomeVarName) if scHome == "" { @@ -36,6 +41,28 @@ func GetScHomeDir() string { return scHome } +func MShellBinaryFromPackage(version string, goos string, goarch string) (io.ReadCloser, error) { + appPath := os.Getenv(ScriptHausAppPathVarName) + if appPath == "" { + return base.MShellBinaryFromOptDir(version, goos, goarch) + } + if !base.ValidGoArch(goos, goarch) { + return nil, fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch) + } + versionStr := semver.MajorMinor(version) + if versionStr == "" { + return nil, fmt.Errorf("invalid mshell version: %q", version) + } + fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch) + fullFileName := path.Join(appPath, "bin", "mshell", fileName) + log.Printf("mshell-binary %q\n", fullFileName) + fd, err := os.Open(fullFileName) + if err != nil { + return nil, fmt.Errorf("cannot open mshell binary %q: %v", fullFileName, err) + } + return fd, nil +} + func AcquireSCLock() (*os.File, error) { homeDir := GetScHomeDir() err := ensureDir(homeDir) From d7b67582eb1c842331889be88979ba406a6f85a6 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 2 Nov 2022 18:45:13 -0700 Subject: [PATCH 162/397] use deepequal for vars --- pkg/cmdrunner/cmdrunner.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 25e11f7fa..bccb274b4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -948,7 +948,7 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, err } var cmdOutput bytes.Buffer - displayStateUpdate(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) + displayStateUpdateDiff(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) cmd, err := makeStaticCmd(ctx, "cd", ids, pk.GetRawStr(), cmdOutput.Bytes()) if err != nil { // TODO tricky error since the command was a success, but we can't show the output @@ -1729,7 +1729,7 @@ func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string return rtn } -func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState packet.ShellState) { +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)) } @@ -1738,7 +1738,7 @@ func displayStateUpdate(buf *bytes.Buffer, oldState packet.ShellState, newState oldEnvMap := shexec.DeclMapFromState(&oldState) for key, newVal := range newEnvMap { oldVal, found := oldEnvMap[key] - if !found || ((oldVal.Value != newVal.Value) || (oldVal.IsExport() != newVal.IsExport())) { + if !found || !shexec.DeclsEqual(oldVal, newVal) { var exportStr string if newVal.IsExport() { exportStr = "export " @@ -1802,6 +1802,6 @@ func GetRtnStateDiff(ctx context.Context, sessionId string, cmdId string) ([]byt return nil, nil } var outputBytes bytes.Buffer - displayStateUpdate(&outputBytes, cmd.RemoteState, *cmd.DonePk.FinalState) + displayStateUpdateDiff(&outputBytes, cmd.RemoteState, *cmd.DonePk.FinalState) return outputBytes.Bytes(), nil } From d225c988ed93da049c21bb7821fe0420c0981e39 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 3 Nov 2022 19:16:14 -0700 Subject: [PATCH 163/397] checkpoint on new completion parsing function --- pkg/comp/comp.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 pkg/comp/comp.go diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go new file mode 100644 index 000000000..780dddfa2 --- /dev/null +++ b/pkg/comp/comp.go @@ -0,0 +1,210 @@ +// scripthaus completion +package comp + +import ( + "context" + "fmt" + "strings" + "unicode" + + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "mvdan.cc/sh/v3/syntax" +) + +const ( + SimpleCompGenTypeFile = "file" + SimpleCompGenTypeDir = "dir" + SimpleCompGenTypeCommand = "command" + SimpleCompGenTypeRemote = "remote" + SimpleCompGenTypeRemoteInstance = "remoteinstance" + SimpleCompGenTypeMetaCmd = "metacmd" + SimpleCompGenTypeGlobalCmd = "globalcmd" + SimpleCompGenTypeVariable = "variable" +) + +type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, rptr sstore.RemotePtrType, state *packet.ShellState, args []interface{}) + +type SimpleCompPoint struct { + Word string + Pos int +} + +type ParsedWord struct { + Offset int + Word string + Prefix string +} + +type CompPoint struct { + Words []ParsedWord + CompWord int + CompWordPos int + Prefix string + Suffix string +} + +func (p *CompPoint) dump() { + if p.Prefix != "" { + fmt.Printf("prefix: %s\n", p.Prefix) + } + fmt.Printf("cpos: %d %d\n", p.CompWord, p.CompWordPos) + for idx, w := range p.Words { + fmt.Printf("w[%d]: ", idx) + if w.Prefix != "" { + fmt.Printf("{%s}", w.Prefix) + } + if idx == p.CompWord { + fmt.Printf("%s\n", strWithCursor(w.Word, p.CompWordPos)) + } else { + fmt.Printf("%s\n", w.Word) + } + } + if p.Suffix != "" { + fmt.Printf("suffix: %s\n", p.Suffix) + } + fmt.Printf("\n") +} + +type CompEntry struct { + Word string +} + +type CompReturn struct { + Entries []CompEntry + HasMore bool +} + +var SimpleCompGenFns map[string]SimpleCompGenFnType + +func strWithCursor(str string, pos int) string { + if pos < 0 { + return "[*]_" + str + } + if pos >= len(str) { + if pos > len(str) { + return str + "_[*]" + } + return str + "[*]" + } else { + return str[:pos] + "[*]" + str[pos:] + } +} + +func isWhitespace(str string) bool { + return strings.TrimSpace(str) == "" +} + +func splitInitialWhitespace(str string) (string, string) { + for pos, ch := range str { // rune iteration :/ + if !unicode.IsSpace(ch) { + return str[:pos], str[pos:] + } + } + return str, "" +} + +func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { + // fmt.Printf("---\n") + // fmt.Printf("cmd: %s\n", strWithCursor(fullCmdStr, pos)) + + // first, find the stmt that the pos appears in + cmdReader := strings.NewReader(fullCmdStr) + parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) + var foundStmt *syntax.Stmt + var lastStmt *syntax.Stmt + var restStartPos int + parser.Stmts(cmdReader, func(stmt *syntax.Stmt) bool { // ignore parse errors (since stmtStr will be the unparsed part) + restStartPos = int(stmt.End().Offset()) + lastStmt = stmt + if uint(pos) >= stmt.Pos().Offset() && uint(pos) < stmt.End().Offset() { + foundStmt = stmt + return false + } + // fmt.Printf("stmt: [[%s]] %d:%d (%d)\n", fullCmdStr[stmt.Pos().Offset():stmt.End().Offset()], stmt.Pos().Offset(), stmt.End().Offset(), stmt.Semicolon.Offset()) + return true + }) + restStr := fullCmdStr[restStartPos:] + if foundStmt == nil && lastStmt != nil && isWhitespace(restStr) && lastStmt.Semicolon.Offset() == 0 { + foundStmt = lastStmt + } + var rtnPoint CompPoint + var stmtStr string + var stmtPos int + if foundStmt != nil { + stmtPos = pos - int(foundStmt.Pos().Offset()) + rtnPoint.Prefix = fullCmdStr[:foundStmt.Pos().Offset()] + if isWhitespace(fullCmdStr[foundStmt.End().Offset():]) { + stmtStr = fullCmdStr[foundStmt.Pos().Offset():] + rtnPoint.Suffix = "" + } else { + stmtStr = fullCmdStr[foundStmt.Pos().Offset():foundStmt.End().Offset()] + rtnPoint.Suffix = fullCmdStr[foundStmt.End().Offset():] + } + } else { + stmtStr = restStr + stmtPos = pos - restStartPos + rtnPoint.Prefix = fullCmdStr[:restStartPos] + rtnPoint.Suffix = fullCmdStr[restStartPos+len(stmtStr):] + } + if stmtPos > len(stmtStr) { + // this should not happen and will cause a jump in completed strings + stmtPos = len(stmtStr) + } + // fmt.Printf("found: ((%s))%s((%s))\n", rtnPoint.Prefix, strWithCursor(stmtStr, stmtPos), rtnPoint.Suffix) + + // now, find the word that the pos appears in within the stmt above + stmtReader := strings.NewReader(stmtStr) + lastWordPos := 0 + parser.Words(stmtReader, func(w *syntax.Word) bool { + var pword ParsedWord + pword.Offset = lastWordPos + if int(w.Pos().Offset()) > lastWordPos { + pword.Prefix = stmtStr[lastWordPos:w.Pos().Offset()] + } + pword.Word = stmtStr[w.Pos().Offset():w.End().Offset()] + rtnPoint.Words = append(rtnPoint.Words, pword) + lastWordPos = int(w.End().Offset()) + return true + }) + if lastWordPos < len(stmtStr) { + pword := ParsedWord{Offset: lastWordPos} + pword.Prefix, pword.Word = splitInitialWhitespace(stmtStr[lastWordPos:]) + rtnPoint.Words = append(rtnPoint.Words, pword) + } + if len(rtnPoint.Words) == 0 { + rtnPoint.Words = append(rtnPoint.Words, ParsedWord{}) + } + for idx, w := range rtnPoint.Words { + if stmtPos > w.Offset && stmtPos <= w.Offset+len(w.Prefix)+len(w.Word) { + rtnPoint.CompWord = idx + rtnPoint.CompWordPos = stmtPos - w.Offset - len(w.Prefix) + if rtnPoint.CompWordPos < 0 { + splitCompWord(&rtnPoint) + } + } + } + // rtnPoint.dump() + return &rtnPoint, nil +} + +func splitCompWord(p *CompPoint) { + w := p.Words[p.CompWord] + prefixPos := p.CompWordPos + len(w.Prefix) + + w1 := ParsedWord{Offset: w.Offset, Prefix: w.Prefix[:prefixPos], Word: ""} + w2 := ParsedWord{Offset: w.Offset + prefixPos, Prefix: w.Prefix[prefixPos:], Word: w.Word} + p.CompWord = p.CompWord // the same (w1) + p.CompWordPos = 0 // will be at 0 since w1 has a word length of 0 + var newWords []ParsedWord + if p.CompWord > 0 { + newWords = append(newWords, p.Words[0:p.CompWord]...) + } + newWords = append(newWords, w1, w2) + newWords = append(newWords, p.Words[p.CompWord+1:]...) + p.Words = newWords +} + +func DoCompGen(ctx context.Context, point CompPoint, rptr sstore.RemotePtrType, state *packet.ShellState) (*CompReturn, error) { + return nil, nil +} From 848f7164a3599206f28bc94982cd8167c645b6e1 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 9 Nov 2022 20:38:28 -0800 Subject: [PATCH 164/397] checkpoint on completion --- pkg/cmdrunner/shparse.go | 17 ++++------ pkg/comp/comp.go | 71 ++++++++++++++++++++++++++++++++-------- pkg/comp/simplecomp.go | 54 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 pkg/comp/simplecomp.go diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 0450b2e36..ccdb7f409 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -149,12 +149,9 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { strReader := strings.NewReader(bracketStr) parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) var wordErr error + var ectx shexec.SimpleExpandContext // do not set HomeDir (we don't expand ~ in bracket args) err := parser.Words(strReader, func(w *syntax.Word) bool { - litStr, err := shexec.QuotedLitToStr(w) - if err != nil { - wordErr = fmt.Errorf("invalid expr in bracket args: %v", err) - return false - } + litStr := shexec.SimpleExpandWord(ectx, w, bracketStr) eqIdx := strings.Index(litStr, "=") var varName, varVal string if eqIdx == -1 { @@ -299,7 +296,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) return rtnPk, nil } -func parseAliasStmt(stmt *syntax.Stmt) (string, string, error) { +func parseAliasStmt(stmt *syntax.Stmt, sourceStr string) (string, string, error) { cmd := stmt.Cmd callExpr, ok := cmd.(*syntax.CallExpr) if !ok { @@ -313,10 +310,8 @@ func parseAliasStmt(stmt *syntax.Stmt) (string, string, error) { return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')") } secondWord := callExpr.Args[1] - val, err := shexec.QuotedLitToStr(secondWord) - if err != nil { - return "", "", err - } + var ectx shexec.SimpleExpandContext // no homedir, do not want ~ expansion + val := shexec.SimpleExpandWord(ectx, secondWord, sourceStr) eqIdx := strings.Index(val, "=") if eqIdx == -1 { return "", "", fmt.Errorf("no '=' in alias definition") @@ -333,7 +328,7 @@ func ParseAliases(aliases string) (map[string]string, error) { } rtn := make(map[string]string) for _, stmt := range file.Stmts { - aliasName, aliasVal, err := parseAliasStmt(stmt) + aliasName, aliasVal, err := parseAliasStmt(stmt, aliases) if err != nil { // fmt.Printf("stmt-err: %v\n", err) continue diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 780dddfa2..5d555f64a 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -8,6 +8,7 @@ import ( "unicode" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "mvdan.cc/sh/v3/syntax" ) @@ -23,7 +24,13 @@ const ( SimpleCompGenTypeVariable = "variable" ) -type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, rptr sstore.RemotePtrType, state *packet.ShellState, args []interface{}) +type CompContext struct { + RemotePtr sstore.RemotePtrType + State *packet.ShellState + ForDisplay bool +} + +type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error) type SimpleCompPoint struct { Word string @@ -31,12 +38,14 @@ type SimpleCompPoint struct { } type ParsedWord struct { - Offset int - Word string - Prefix string + Offset int + Word *syntax.Word + PartialWord string + Prefix string } type CompPoint struct { + StmtStr string Words []ParsedWord CompWord int CompWordPos int @@ -44,6 +53,38 @@ type CompPoint struct { Suffix string } +func (p *CompPoint) wordAsStr(w ParsedWord) string { + if w.Word != nil { + return p.StmtStr[w.Word.Pos().Offset():w.Word.End().Offset()] + } + return w.PartialWord +} + +func (p *CompPoint) simpleExpandWord(w ParsedWord) string { + ectx := shexec.SimpleExpandContext{} + if w.Word != nil { + return SimpleExpandWord(ectx, w.Word, p.StmtStr) + } + return SimpleExpandPartialWord(ectx, p.PartialWord, false) +} + +func (p *CompPoint) compPrefix() string { + pword := p.Words[p.CompWord] + if p.CompWordPos == 0 { + return "" + } + wordStr := p.wordAsStr(pword) + if p.CompWordPos == len(wordStr) { + return p.simpleExpandWord(pword) + } + // TODO we can do better, if p.Word is not nil, we can look for which WordPart + // our pos is in. we can then do a normal word expand on the previous parts + // and a partial on just the current part. this is an uncommon case though + // and has very little upside (even bash does not expand multipart words correctly) + partialWordStr := wordStr[:p.CompWordPos] + return SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) +} + func (p *CompPoint) dump() { if p.Prefix != "" { fmt.Printf("prefix: %s\n", p.Prefix) @@ -55,9 +96,9 @@ func (p *CompPoint) dump() { fmt.Printf("{%s}", w.Prefix) } if idx == p.CompWord { - fmt.Printf("%s\n", strWithCursor(w.Word, p.CompWordPos)) + fmt.Printf("%s\n", strWithCursor(p.wordAsStr(w), p.CompWordPos)) } else { - fmt.Printf("%s\n", w.Word) + fmt.Printf("%s\n", p.wordAsStr(w)) } } if p.Suffix != "" { @@ -66,8 +107,10 @@ func (p *CompPoint) dump() { fmt.Printf("\n") } +// directories will have a trailing "/" type CompEntry struct { - Word string + Word string + IsMetaCmd bool } type CompReturn struct { @@ -154,6 +197,7 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { // fmt.Printf("found: ((%s))%s((%s))\n", rtnPoint.Prefix, strWithCursor(stmtStr, stmtPos), rtnPoint.Suffix) // now, find the word that the pos appears in within the stmt above + rtnPoint.StmtStr = stmtStr stmtReader := strings.NewReader(stmtStr) lastWordPos := 0 parser.Words(stmtReader, func(w *syntax.Word) bool { @@ -162,21 +206,22 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { if int(w.Pos().Offset()) > lastWordPos { pword.Prefix = stmtStr[lastWordPos:w.Pos().Offset()] } - pword.Word = stmtStr[w.Pos().Offset():w.End().Offset()] + pword.Word = w rtnPoint.Words = append(rtnPoint.Words, pword) lastWordPos = int(w.End().Offset()) return true }) if lastWordPos < len(stmtStr) { pword := ParsedWord{Offset: lastWordPos} - pword.Prefix, pword.Word = splitInitialWhitespace(stmtStr[lastWordPos:]) + pword.Prefix, pword.PartialWord = splitInitialWhitespace(stmtStr[lastWordPos:]) rtnPoint.Words = append(rtnPoint.Words, pword) } if len(rtnPoint.Words) == 0 { rtnPoint.Words = append(rtnPoint.Words, ParsedWord{}) } for idx, w := range rtnPoint.Words { - if stmtPos > w.Offset && stmtPos <= w.Offset+len(w.Prefix)+len(w.Word) { + wordLen := len(rtnPoint.wordAsStr(w)) + if stmtPos > w.Offset && stmtPos <= w.Offset+len(w.Prefix)+wordLen { rtnPoint.CompWord = idx rtnPoint.CompWordPos = stmtPos - w.Offset - len(w.Prefix) if rtnPoint.CompWordPos < 0 { @@ -184,7 +229,7 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { } } } - // rtnPoint.dump() + rtnPoint.dump() return &rtnPoint, nil } @@ -192,8 +237,8 @@ func splitCompWord(p *CompPoint) { w := p.Words[p.CompWord] prefixPos := p.CompWordPos + len(w.Prefix) - w1 := ParsedWord{Offset: w.Offset, Prefix: w.Prefix[:prefixPos], Word: ""} - w2 := ParsedWord{Offset: w.Offset + prefixPos, Prefix: w.Prefix[prefixPos:], Word: w.Word} + w1 := ParsedWord{Offset: w.Offset, Prefix: w.Prefix[:prefixPos]} + w2 := ParsedWord{Offset: w.Offset + prefixPos, Prefix: w.Prefix[prefixPos:], Word: w.Word, PartialWord: w.PartialWord} p.CompWord = p.CompWord // the same (w1) p.CompWordPos = 0 // will be at 0 since w1 has a word length of 0 var newWords []ParsedWord diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go new file mode 100644 index 000000000..b34a2e7b7 --- /dev/null +++ b/pkg/comp/simplecomp.go @@ -0,0 +1,54 @@ +package comp + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/remote" +) + +func compsToCompReturn(comps []string, hasMore bool) *CompReturn { + var rtn CompReturn + rtn.HasMore = hasMore + for _, comp := range comps { + rtn.Entries = append(rtn.Entries, CompEntry{Word: comp}) + } + return &rtn +} + +func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType string, forDisplay bool) (*CompReturn, error) { + if !packet.IsValidCompGenType(compType) { + return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) + } + msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId) + if msh == nil { + return nil, false, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr) + } + cgPacket := packet.MakeCompGenPacket() + cgPacket.ReqId = uuid.New().String() + cgPacket.CompType = compType + cgPacket.Prefix = prefix + cgPacket.Cwd = compCtx.State.Cwd + resp, err := msh.PacketRpc(ctx, cgPacket) + if err != nil { + return nil, false, err + } + if err = resp.Err(); err != nil { + return nil, false, err + } + comps := getStrArr(resp.Data, "comps") + hasMore := getBool(resp.Data, "hasmore") + return compsToCompReturn(conmps, hasMore), nil +} + +func SimpleCompFile(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error) { + pword := point.Words[p.CompWord] + prefix := "" + crtn, err := doCompGen(ctx, prefix, "file") + if err != nil { + return nil, err + } + return crtn, nil +} From 055dc7c8acd6b16c60314836f6e52126bdb20ddd Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 10 Nov 2022 13:52:51 -0800 Subject: [PATCH 165/397] checkpoint on compgen --- pkg/cmdrunner/cmdrunner.go | 74 ++++++---------- pkg/cmdrunner/shparse.go | 29 +------ pkg/comp/comp.go | 169 ++++++++++++++++++++++++++++++++----- pkg/comp/simplecomp.go | 79 ++++++++++++----- pkg/utilfn/utilfn.go | 76 +++++++++++++++++ 5 files changed, 315 insertions(+), 112 deletions(-) create mode 100644 pkg/utilfn/utilfn.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index bccb274b4..f788e918f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -20,10 +20,12 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/comp" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" ) const ( @@ -32,6 +34,11 @@ const ( HistoryTypeGlobal = "global" ) +func init() { + comp.RegisterSimpleCompFn("meta", simpleCompMeta) + comp.RegisterSimpleCompFn("command+meta", simpleCompCommandMeta) +} + const DefaultUserId = "sawka" const MaxNameLen = 50 const MaxRemoteAliasLen = 50 @@ -1043,50 +1050,6 @@ func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstor } } -func getStrArr(v interface{}, field string) []string { - if v == nil { - return nil - } - m, ok := v.(map[string]interface{}) - if !ok { - return nil - } - fieldVal := m[field] - if fieldVal == nil { - return nil - } - iarr, ok := fieldVal.([]interface{}) - if !ok { - return nil - } - var sarr []string - for _, iv := range iarr { - if sv, ok := iv.(string); ok { - sarr = append(sarr, sv) - } - } - return sarr -} - -func getBool(v interface{}, field string) bool { - if v == nil { - return false - } - m, ok := v.(map[string]interface{}) - if !ok { - return false - } - fieldVal := m[field] - if fieldVal == nil { - return false - } - bval, ok := fieldVal.(bool) - if !ok { - return false - } - return bval -} - func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { sort.Slice(comps, func(i int, j int) bool { c1 := comps[i] @@ -1156,6 +1119,23 @@ func longestPrefix(root string, comps []string) string { return lcp } +func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { + compsCmd, _ := comp.DoSimpleComp(ctx, "command", prefix, compCtx, nil) + compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil) + return comp.CombineCompReturn(compsCmd, compsMeta), nil +} + +func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { + rtn := comp.CompReturn{} + validCommands := getValidCommands() + for _, cmd := range validCommands { + if strings.HasPrefix(cmd, prefix) { + rtn.Entries = append(rtn.Entries, comp.CompEntry{Word: cmd, IsMetaCmd: true}) + } + } + return &rtn, nil +} + func doMetaCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, forDisplay bool) ([]string, bool, error) { ids, err := resolveUiIds(ctx, pk, 0) // best effort var comps []string @@ -1202,8 +1182,8 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str if err = resp.Err(); err != nil { return nil, false, err } - comps := getStrArr(resp.Data, "comps") - hasMore := getBool(resp.Data, "hasmore") + comps := utilfn.GetStrArr(resp.Data, "comps") + hasMore := utilfn.GetBool(resp.Data, "hasmore") return comps, hasMore, nil } @@ -1743,7 +1723,7 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt if newVal.IsExport() { exportStr = "export " } - buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, ShellQuote(newVal.Value, false, 50))) + buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, utilfn.ShellQuote(newVal.Value, false, 50))) } } for key, _ := range oldEnvMap { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index ccdb7f409..941c9e9da 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -6,9 +6,9 @@ import ( "regexp" "strings" - "github.com/alessio/shellescape" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) @@ -116,31 +116,6 @@ func onlyRawArgs(metaCmd string, metaSubCmd string) bool { return metaCmd == "run" || metaCmd == "comment" } -// minimum maxlen=6 -func ShellQuote(val string, forceQuote bool, maxLen int) string { - if maxLen < 6 { - maxLen = 6 - } - rtn := shellescape.Quote(val) - if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") { - if len(rtn) > maxLen { - return rtn[0:maxLen-4] + "..." + rtn[0:1] - } - return rtn - } - if forceQuote { - if len(rtn) > maxLen-2 { - return "\"" + rtn[0:maxLen-5] + "...\"" - } - return "\"" + rtn + "\"" - } else { - if len(rtn) > maxLen { - return rtn[0:maxLen-3] + "..." - } - return rtn - } -} - func setBracketArgs(argMap map[string]string, bracketStr string) error { bracketStr = strings.TrimSpace(bracketStr) if bracketStr == "" { @@ -161,7 +136,7 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { varVal = litStr[eqIdx+1:] } if !shexec.IsValidBashIdentifier(varName) { - wordErr = fmt.Errorf("invalid identifier %s in bracket args", ShellQuote(varName, true, 20)) + wordErr = fmt.Errorf("invalid identifier %s in bracket args", utilfn.ShellQuote(varName, true, 20)) return false } if varVal == "" { diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 5d555f64a..cc6fffeae 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -2,17 +2,23 @@ package comp import ( + "bytes" "context" "fmt" + "sort" + "strconv" "strings" "unicode" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" "mvdan.cc/sh/v3/syntax" ) +const MaxCompQuoteLen = 5000 + const ( SimpleCompGenTypeFile = "file" SimpleCompGenTypeDir = "dir" @@ -24,19 +30,31 @@ const ( SimpleCompGenTypeVariable = "variable" ) +const ( + QuoteTypeLiteral = "" + QuoteTypeDQ = "\"" + QuoteTypeANSI = "$'" + QuoteTypeSQ = "'" +) + type CompContext struct { RemotePtr sstore.RemotePtrType State *packet.ShellState ForDisplay bool } -type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error) - type SimpleCompPoint struct { Word string Pos int } +type fullCompPrefix struct { + RawStr string + RawPos int + CompPrefix string + QuoteTypePref string +} + type ParsedWord struct { Offset int Word *syntax.Word @@ -53,6 +71,75 @@ type CompPoint struct { Suffix string } +// directories will have a trailing "/" +type CompEntry struct { + Word string + IsMetaCmd bool +} + +type CompReturn struct { + Entries []CompEntry + HasMore bool +} + +func compQuoteDQString(s string, close bool) string { + var buf bytes.Buffer + buf.WriteByte('"') + for _, ch := range s { + if ch == '"' || ch == '\\' || ch == '$' || ch == '`' { + buf.WriteByte('\\') + buf.WriteRune(ch) + continue + } + buf.WriteRune(ch) + } + if close { + buf.WriteByte('"') + } + return buf.String() +} + +func compQuoteString(s string, quoteType string, close bool) string { + if quoteType != QuoteTypeANSI { + for _, ch := range s { + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) || ch == '!' { + quoteType = QuoteTypeANSI + break + } + if ch == '\'' { + if quoteType == QuoteTypeSQ || quoteType == QuoteTypeLiteral { + quoteType = QuoteTypeANSI + break + } + } + } + } + if quoteType == QuoteTypeANSI { + rtn := strconv.QuoteToASCII(s) + rtn = "$'" + strings.ReplaceAll(rtn[1:len(rtn)-1], "'", "\\'") + if close { + rtn = rtn + "'" + } + return rtn + } + if quoteType == QuoteTypeLiteral { + rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen) + if len(rtn) > 0 && rtn[0] == '\'' && !close { + rtn = rtn[0 : len(rtn)-1] + } + return rtn + } + if quoteType == QuoteTypeSQ { + rtn := utilfn.ShellQuote(s, true, MaxCompQuoteLen) + if !close { + rtn = rtn[0 : len(rtn)-1] + } + return rtn + } + // QuoteTypeDQ + return compQuoteDQString(s, close) +} + func (p *CompPoint) wordAsStr(w ParsedWord) string { if w.Word != nil { return p.StmtStr[w.Word.Pos().Offset():w.Word.End().Offset()] @@ -63,16 +150,29 @@ func (p *CompPoint) wordAsStr(w ParsedWord) string { func (p *CompPoint) simpleExpandWord(w ParsedWord) string { ectx := shexec.SimpleExpandContext{} if w.Word != nil { - return SimpleExpandWord(ectx, w.Word, p.StmtStr) + return shexec.SimpleExpandWord(ectx, w.Word, p.StmtStr) } - return SimpleExpandPartialWord(ectx, p.PartialWord, false) + return shexec.SimpleExpandPartialWord(ectx, w.PartialWord, false) } -func (p *CompPoint) compPrefix() string { - pword := p.Words[p.CompWord] +func getQuoteTypePref(str string) string { + if strings.HasPrefix(str, QuoteTypeANSI) { + return QuoteTypeANSI + } + if strings.HasPrefix(str, QuoteTypeDQ) { + return QuoteTypeDQ + } + if strings.HasPrefix(str, QuoteTypeSQ) { + return QuoteTypeSQ + } + return QuoteTypeLiteral +} + +func (p *CompPoint) getCompPrefix() string { if p.CompWordPos == 0 { return "" } + pword := p.Words[p.CompWord] wordStr := p.wordAsStr(pword) if p.CompWordPos == len(wordStr) { return p.simpleExpandWord(pword) @@ -82,7 +182,21 @@ func (p *CompPoint) compPrefix() string { // and a partial on just the current part. this is an uncommon case though // and has very little upside (even bash does not expand multipart words correctly) partialWordStr := wordStr[:p.CompWordPos] - return SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) + return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) +} + +func (p *CompPoint) extendWord(newWord string, newWordComplete bool) (string, int) { + pword := p.Words[p.CompWord] + wordStr := p.wordAsStr(pword) + quotePref := getQuoteTypePref(wordStr) + needsClose := newWordComplete && (len(wordStr) == p.CompWordPos) + wordSuffix := wordStr[p.CompWordPos:] + newQuotedStr := compQuoteString(newWord, quotePref, needsClose) + if needsClose && wordSuffix == "" { + newQuotedStr = newQuotedStr + " " + } + newPos := len(newQuotedStr) + return newQuotedStr + wordSuffix, newPos } func (p *CompPoint) dump() { @@ -107,17 +221,6 @@ func (p *CompPoint) dump() { fmt.Printf("\n") } -// directories will have a trailing "/" -type CompEntry struct { - Word string - IsMetaCmd bool -} - -type CompReturn struct { - Entries []CompEntry - HasMore bool -} - var SimpleCompGenFns map[string]SimpleCompGenFnType func strWithCursor(str string, pos int) string { @@ -229,7 +332,6 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { } } } - rtnPoint.dump() return &rtnPoint, nil } @@ -253,3 +355,32 @@ func splitCompWord(p *CompPoint) { func DoCompGen(ctx context.Context, point CompPoint, rptr sstore.RemotePtrType, state *packet.ShellState) (*CompReturn, error) { return nil, nil } + +func SortCompReturnEntries(c *CompReturn) { + sort.Slice(c.Entries, func(i int, j int) bool { + e1 := c.Entries[i] + e2 := c.Entries[j] + if e1.Word < e2.Word { + return true + } + if e1.Word == e2.Word && e1.IsMetaCmd && !e2.IsMetaCmd { + return true + } + return false + }) +} + +func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn { + if c1 == nil { + return c2 + } + if c2 == nil { + return c1 + } + var rtn CompReturn + rtn.HasMore = c1.HasMore || c2.HasMore + rtn.Entries = append([]CompEntry{}, c1.Entries...) + rtn.Entries = append(rtn.Entries, c2.Entries...) + SortCompReturnEntries(&rtn) + return &rtn +} diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index b34a2e7b7..ddae3f46e 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -3,12 +3,47 @@ package comp import ( "context" "fmt" + "sync" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" ) +var globalLock = &sync.Mutex{} +var simpleCompMap = map[string]SimpleCompGenFnType{ + "file": simpleCompFile, + "directory": simpleCompDir, + "variable": simpleCompVar, + "command": simpleCompCommand, +} + +type SimpleCompGenFnType = func(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) + +func RegisterSimpleCompFn(compType string, fn SimpleCompGenFnType) { + globalLock.Lock() + defer globalLock.Unlock() + if _, ok := simpleCompMap[compType]; ok { + panic(fmt.Sprintf("simpleCompFn %q already registered", compType)) + } + simpleCompMap[compType] = fn +} + +func getSimpleCompFn(compType string) SimpleCompGenFnType { + globalLock.Lock() + defer globalLock.Unlock() + return simpleCompMap[compType] +} + +func DoSimpleComp(ctx context.Context, compType string, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { + compFn := getSimpleCompFn(compType) + if compFn == nil { + return nil, fmt.Errorf("no simple comp fn for %q", compType) + } + return compFn(ctx, prefix, compCtx, args) +} + func compsToCompReturn(comps []string, hasMore bool) *CompReturn { var rtn CompReturn rtn.HasMore = hasMore @@ -18,13 +53,13 @@ func compsToCompReturn(comps []string, hasMore bool) *CompReturn { return &rtn } -func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType string, forDisplay bool) (*CompReturn, error) { +func doCompGen(ctx context.Context, prefix string, compType string, compCtx CompContext) (*CompReturn, error) { if !packet.IsValidCompGenType(compType) { - return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, fmt.Errorf("/compgen invalid type '%s'", compType) } msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId) if msh == nil { - return nil, false, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr) + return nil, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() @@ -32,23 +67,29 @@ func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType cgPacket.Prefix = prefix cgPacket.Cwd = compCtx.State.Cwd resp, err := msh.PacketRpc(ctx, cgPacket) - if err != nil { - return nil, false, err - } - if err = resp.Err(); err != nil { - return nil, false, err - } - comps := getStrArr(resp.Data, "comps") - hasMore := getBool(resp.Data, "hasmore") - return compsToCompReturn(conmps, hasMore), nil -} - -func SimpleCompFile(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error) { - pword := point.Words[p.CompWord] - prefix := "" - crtn, err := doCompGen(ctx, prefix, "file") if err != nil { return nil, err } - return crtn, nil + if err = resp.Err(); err != nil { + return nil, err + } + comps := utilfn.GetStrArr(resp.Data, "comps") + hasMore := utilfn.GetBool(resp.Data, "hasmore") + return compsToCompReturn(comps, hasMore), nil +} + +func simpleCompFile(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { + return doCompGen(ctx, prefix, "file", compCtx) +} + +func simpleCompDir(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { + return doCompGen(ctx, prefix, "directory", compCtx) +} + +func simpleCompVar(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { + return doCompGen(ctx, prefix, "variable", compCtx) +} + +func simpleCompCommand(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { + return doCompGen(ctx, prefix, "command", compCtx) } diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go new file mode 100644 index 000000000..a7fc04de2 --- /dev/null +++ b/pkg/utilfn/utilfn.go @@ -0,0 +1,76 @@ +package utilfn + +import ( + "strings" + + "github.com/alessio/shellescape" +) + +func GetStrArr(v interface{}, field string) []string { + if v == nil { + return nil + } + m, ok := v.(map[string]interface{}) + if !ok { + return nil + } + fieldVal := m[field] + if fieldVal == nil { + return nil + } + iarr, ok := fieldVal.([]interface{}) + if !ok { + return nil + } + var sarr []string + for _, iv := range iarr { + if sv, ok := iv.(string); ok { + sarr = append(sarr, sv) + } + } + return sarr +} + +func GetBool(v interface{}, field string) bool { + if v == nil { + return false + } + m, ok := v.(map[string]interface{}) + if !ok { + return false + } + fieldVal := m[field] + if fieldVal == nil { + return false + } + bval, ok := fieldVal.(bool) + if !ok { + return false + } + return bval +} + +// minimum maxlen=6 +func ShellQuote(val string, forceQuote bool, maxLen int) string { + if maxLen < 6 { + maxLen = 6 + } + rtn := shellescape.Quote(val) + if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") { + if len(rtn) > maxLen { + return rtn[0:maxLen-4] + "..." + rtn[0:1] + } + return rtn + } + if forceQuote { + if len(rtn) > maxLen-2 { + return "\"" + rtn[0:maxLen-5] + "...\"" + } + return "\"" + rtn + "\"" + } else { + if len(rtn) > maxLen { + return rtn[0:maxLen-3] + "..." + } + return rtn + } +} From 2ab87956435b32144b39c8c17b96f9aa2c00dd5c Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 10 Nov 2022 15:28:39 -0800 Subject: [PATCH 166/397] comp checkpoint --- pkg/cmdrunner/cmdrunner.go | 31 +----------------- pkg/comp/comp.go | 66 ++++++++++++++++++++++++++++++++++---- pkg/utilfn/utilfn.go | 38 ++++++++++++++++++++++ 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f788e918f..ac9041b33 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1081,7 +1081,7 @@ func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore if hasMore { return nil } - lcp := longestPrefix(prefix, comps) + lcp := utilfn.LongestPrefix(prefix, comps) if lcp == prefix || len(lcp) < len(prefix) || !strings.HasPrefix(lcp, prefix) { return nil } @@ -1090,35 +1090,6 @@ func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore return sstore.ModelUpdate{CmdLine: clu} } -func longestPrefix(root string, comps []string) string { - if len(comps) == 0 { - return root - } - if len(comps) == 1 { - comp := comps[0] - if len(comp) >= len(root) && strings.HasPrefix(comp, root) { - if strings.HasSuffix(comp, "/") { - return comps[0] - } - return comps[0] + " " - } - } - lcp := comps[0] - for i := 1; i < len(comps); i++ { - s := comps[i] - for j := 0; j < len(lcp); j++ { - if j >= len(s) || lcp[j] != s[j] { - lcp = lcp[0:j] - break - } - } - } - if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) { - return root - } - return lcp -} - func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { compsCmd, _ := comp.DoSimpleComp(ctx, "command", prefix, compCtx, nil) compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil) diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index cc6fffeae..0e29d0e5a 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -43,9 +43,9 @@ type CompContext struct { ForDisplay bool } -type SimpleCompPoint struct { - Word string - Pos int +type StrWithPos struct { + Str string + Pos int } type fullCompPrefix struct { @@ -130,7 +130,10 @@ func compQuoteString(s string, quoteType string, close bool) string { return rtn } if quoteType == QuoteTypeSQ { - rtn := utilfn.ShellQuote(s, true, MaxCompQuoteLen) + rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen) + if len(rtn) > 0 && rtn[0] != '\'' { + rtn = "'" + rtn + "'" + } if !close { rtn = rtn[0 : len(rtn)-1] } @@ -185,18 +188,46 @@ func (p *CompPoint) getCompPrefix() string { return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) } -func (p *CompPoint) extendWord(newWord string, newWordComplete bool) (string, int) { +func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos { pword := p.Words[p.CompWord] wordStr := p.wordAsStr(pword) quotePref := getQuoteTypePref(wordStr) needsClose := newWordComplete && (len(wordStr) == p.CompWordPos) wordSuffix := wordStr[p.CompWordPos:] newQuotedStr := compQuoteString(newWord, quotePref, needsClose) - if needsClose && wordSuffix == "" { + if needsClose && wordSuffix == "" && !strings.HasSuffix(newWord, "/") { newQuotedStr = newQuotedStr + " " } newPos := len(newQuotedStr) - return newQuotedStr + wordSuffix, newPos + return StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos} +} + +func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos { + if crtn == nil || crtn.HasMore { + return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + } + compStrs := crtn.getCompStrs() + compPrefix := p.getCompPrefix() + lcp := utilfn.LongestPrefix(compPrefix, compStrs) + if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) { + return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + } + newStr := p.extendWord(lcp, utilfn.ContainsStr(compStrs, lcp)) + var buf bytes.Buffer + buf.WriteString(p.Prefix) + for idx, w := range p.Words { + if idx == p.CompWord { + buf.WriteString(w.Prefix) + buf.WriteString(newStr.Str) + } else { + buf.WriteString(w.Prefix) + buf.WriteString(p.wordAsStr(w)) + } + } + buf.WriteString(p.Suffix) + compWord := p.Words[p.CompWord] + newPos := len(p.Prefix) + compWord.Offset + len(compWord.Prefix) + newStr.Pos + return StrWithPos{Str: buf.String(), Pos: newPos} } func (p *CompPoint) dump() { @@ -223,6 +254,10 @@ func (p *CompPoint) dump() { var SimpleCompGenFns map[string]SimpleCompGenFnType +func (sp StrWithPos) String() string { + return strWithCursor(sp.Str, sp.Pos) +} + func strWithCursor(str string, pos int) string { if pos < 0 { return "[*]_" + str @@ -384,3 +419,20 @@ func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn { SortCompReturnEntries(&rtn) return &rtn } + +func (c *CompReturn) getCompStrs() []string { + rtn := make([]string, len(c.Entries)) + for idx, entry := range c.Entries { + rtn[idx] = entry.Word + } + return rtn +} + +func (p CompPoint) getOrigPos() int { + pword := p.Words[p.CompWord] + return len(p.Prefix) + pword.Offset + len(pword.Prefix) + p.CompWordPos +} + +func (p CompPoint) getOrigStr() string { + return p.Prefix + p.StmtStr + p.Suffix +} diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index a7fc04de2..b8e4a7c79 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -74,3 +74,41 @@ func ShellQuote(val string, forceQuote bool, maxLen int) string { return rtn } } + +func LongestPrefix(root string, strs []string) string { + if len(strs) == 0 { + return root + } + if len(strs) == 1 { + comp := strs[0] + if len(comp) >= len(root) && strings.HasPrefix(comp, root) { + if strings.HasSuffix(comp, "/") { + return strs[0] + } + return strs[0] + } + } + lcp := strs[0] + for i := 1; i < len(strs); i++ { + s := strs[i] + for j := 0; j < len(lcp); j++ { + if j >= len(s) || lcp[j] != s[j] { + lcp = lcp[0:j] + break + } + } + } + if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) { + return root + } + return lcp +} + +func ContainsStr(strs []string, test string) bool { + for _, s := range strs { + if s == test { + return true + } + } + return false +} From f81bea66584416aa8043ccce200943306ccf47b7 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 10 Nov 2022 15:39:13 -0800 Subject: [PATCH 167/397] check in some tests for comp package --- pkg/comp/comp_test.go | 122 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 pkg/comp/comp_test.go diff --git a/pkg/comp/comp_test.go b/pkg/comp/comp_test.go new file mode 100644 index 000000000..0d01342fc --- /dev/null +++ b/pkg/comp/comp_test.go @@ -0,0 +1,122 @@ +package comp + +import ( + "fmt" + "strings" + "testing" +) + +func parseToSP(s string) StrWithPos { + idx := strings.Index(s, "[*]") + if idx == -1 { + return StrWithPos{Str: s} + } + return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: idx} +} + +func testParse(cmdStr string, pos int) { + fmt.Printf("cmd: %s\n", strWithCursor(cmdStr, pos)) + p, err := ParseCompPoint(cmdStr, pos) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + p.dump() +} + +func _Test1(t *testing.T) { + testParse("ls ", 3) + testParse("ls ", 4) + testParse("ls -l foo", 4) + testParse("ls foo; cd h", 12) + testParse("ls foo; cd h;", 13) + testParse("ls & foo; cd h", 12) + testParse("ls \"he", 6) + testParse("ls;", 3) + testParse("ls;", 2) + testParse("ls; cd x; ls", 8) + testParse("cd \"foo ", 8) + testParse("ls; { ls f", 10) + testParse("ls; { ls -l; ls f", 17) + testParse("ls $(ls f", 9) +} + +func testMiniExtend(t *testing.T, p *CompPoint, newWord string, complete bool, expectedStr string) { + newSP := p.extendWord(newWord, complete) + expectedSP := parseToSP(expectedStr) + if newSP != expectedSP { + t.Fatalf("not equal: [%s] != [%s]", newSP, expectedSP) + } else { + fmt.Printf("extend: %s\n", newSP) + } +} + +func Test2(t *testing.T) { + p, err := ParseCompPoint("ls f", 4) + if err != nil { + t.Fatalf("error: %v", err) + } + testMiniExtend(t, p, "foo", false, "foo[*]") + testMiniExtend(t, p, "foo", true, "foo [*]") + testMiniExtend(t, p, "foo bar", true, "'foo bar' [*]") + testMiniExtend(t, p, "foo'bar", true, `$'foo\'bar' [*]`) + + p, err = ParseCompPoint("ls fmore", 4) + if err != nil { + t.Fatalf("error: %v", err) + } + testMiniExtend(t, p, "foo", false, "foo[*]more") + testMiniExtend(t, p, "foo bar", false, `'foo bar[*]more`) + testMiniExtend(t, p, "foo bar", true, `'foo bar[*]more`) + testMiniExtend(t, p, "foo's", true, `$'foo\'s[*]more`) +} + +func testParseRT(t *testing.T, origSP StrWithPos) { + p, err := ParseCompPoint(origSP.Str, origSP.Pos) + if err != nil { + t.Fatalf("error: %v", err) + } + newSP := StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + if origSP != newSP { + t.Fatalf("not equal: [%s] != [%s]", origSP, newSP) + } +} + +func Test3(t *testing.T) { + testParseRT(t, parseToSP("ls f[*]")) + testParseRT(t, parseToSP("ls f[*]; more $FOO")) + testParseRT(t, parseToSP("hello; ls [*]f")) + testParseRT(t, parseToSP("ls -l; ./foo he[*]ll more; touch foo &")) +} + +func testExtend(t *testing.T, origStr string, compStrs []string, expectedStr string) { + origSP := parseToSP(origStr) + expectedSP := parseToSP(expectedStr) + p, err := ParseCompPoint(origSP.Str, origSP.Pos) + if err != nil { + t.Fatalf("error: %v", err) + } + crtn := compsToCompReturn(compStrs, false) + newSP := p.FullyExtend(crtn) + if newSP != expectedSP { + t.Fatalf("comp-fail: %s + %v => [%s] expected[%s]", origSP, compStrs, newSP, expectedSP) + } else { + fmt.Printf("comp: %s + %v => [%s]\n", origSP, compStrs, newSP) + } +} + +func Test4(t *testing.T) { + testExtend(t, "ls f[*]", []string{"foo"}, "ls foo [*]") + testExtend(t, "ls f[*]", []string{"foox", "fooy"}, "ls foo[*]") + testExtend(t, "w; ls f[*]; touch x", []string{"foo"}, "w; ls foo [*]; touch x") + testExtend(t, "w; ls f[*] more; touch x", []string{"foo"}, "w; ls foo [*] more; touch x") + testExtend(t, "w; ls f[*]oo; touch x", []string{"foo"}, "w; ls foo[*]oo; touch x") + testExtend(t, `ls "f[*]`, []string{"foo"}, `ls "foo" [*]`) + testExtend(t, `ls 'f[*]`, []string{"foo"}, `ls 'foo' [*]`) + testExtend(t, `ls $'f[*]`, []string{"foo"}, `ls $'foo' [*]`) + testExtend(t, `ls f[*]`, []string{"foo/"}, `ls foo/[*]`) + testExtend(t, `ls f[*]`, []string{"foo bar"}, `ls 'foo bar' [*]`) + testExtend(t, `ls f[*]`, []string{"f\x01\x02"}, `ls $'f\x01\x02' [*]`) + testExtend(t, `ls "foo [*]`, []string{"foo bar"}, `ls "foo bar" [*]`) + testExtend(t, `ls f[*]`, []string{"foo's"}, `ls $'foo\'s' [*]`) +} From afbdf644bf5d3bde950551be4ae24cd5a3ceaf02 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 10 Nov 2022 16:00:51 -0800 Subject: [PATCH 168/397] checkpoint on comp --- pkg/cmdrunner/cmdrunner.go | 4 +-- pkg/comp/comp.go | 53 ++++++++++++++++++++++++-------------- pkg/comp/comp_test.go | 26 ++++--------------- pkg/comp/simplecomp.go | 16 ++++++------ 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ac9041b33..71d9aef4c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -35,8 +35,8 @@ const ( ) func init() { - comp.RegisterSimpleCompFn("meta", simpleCompMeta) - comp.RegisterSimpleCompFn("command+meta", simpleCompCommandMeta) + comp.RegisterSimpleCompFn(comp.CGTypeMeta, simpleCompMeta) + comp.RegisterSimpleCompFn(comp.CGTypeCommandMeta, simpleCompCommandMeta) } const DefaultUserId = "sawka" diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 0e29d0e5a..7714c474e 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -20,14 +20,19 @@ import ( const MaxCompQuoteLen = 5000 const ( - SimpleCompGenTypeFile = "file" - SimpleCompGenTypeDir = "dir" - SimpleCompGenTypeCommand = "command" - SimpleCompGenTypeRemote = "remote" - SimpleCompGenTypeRemoteInstance = "remoteinstance" - SimpleCompGenTypeMetaCmd = "metacmd" - SimpleCompGenTypeGlobalCmd = "globalcmd" - SimpleCompGenTypeVariable = "variable" + // local to simplecomp + CGTypeCommand = "command" + CGTypeFile = "file" + CGTypeDir = "directory" + CGTypeVariable = "variable" + + // implemented in cmdrunner + CGTypeMeta = "metacmd" + CGTypeCommandMeta = "command+meta" + + CGTypeRemote = "remote" + CGTypeRemoteInstance = "remoteinstance" + CGTypeGlobalCmd = "globalcmd" ) const ( @@ -48,13 +53,6 @@ type StrWithPos struct { Pos int } -type fullCompPrefix struct { - RawStr string - RawPos int - CompPrefix string - QuoteTypePref string -} - type ParsedWord struct { Offset int Word *syntax.Word @@ -285,7 +283,9 @@ func splitInitialWhitespace(str string) (string, string) { return str, "" } -func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { +func ParseCompPoint(cmdStr StrWithPos) *CompPoint { + fullCmdStr := cmdStr.Str + pos := cmdStr.Pos // fmt.Printf("---\n") // fmt.Printf("cmd: %s\n", strWithCursor(fullCmdStr, pos)) @@ -367,7 +367,7 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) { } } } - return &rtnPoint, nil + return &rtnPoint } func splitCompWord(p *CompPoint) { @@ -387,8 +387,23 @@ func splitCompWord(p *CompPoint) { p.Words = newWords } -func DoCompGen(ctx context.Context, point CompPoint, rptr sstore.RemotePtrType, state *packet.ShellState) (*CompReturn, error) { - return nil, nil +func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) { + compPoint := ParseCompPoint(sp) + compType := CGTypeFile + if compPoint.CompWord == 0 { + compType = CGTypeCommandMeta + } + // TODO lookup special types + compPrefix := compPoint.getCompPrefix() + crtn, err := DoSimpleComp(ctx, compType, compPrefix, compCtx, nil) + if err != nil { + return nil, nil, err + } + if compCtx.ForDisplay { + return crtn, nil, nil + } + rtnSP := compPoint.FullyExtend(crtn) + return crtn, &rtnSP, nil } func SortCompReturnEntries(c *CompReturn) { diff --git a/pkg/comp/comp_test.go b/pkg/comp/comp_test.go index 0d01342fc..31b4ed751 100644 --- a/pkg/comp/comp_test.go +++ b/pkg/comp/comp_test.go @@ -16,11 +16,7 @@ func parseToSP(s string) StrWithPos { func testParse(cmdStr string, pos int) { fmt.Printf("cmd: %s\n", strWithCursor(cmdStr, pos)) - p, err := ParseCompPoint(cmdStr, pos) - if err != nil { - fmt.Printf("err: %v\n", err) - return - } + p := ParseCompPoint(StrWithPos{Str: cmdStr, Pos: pos}) p.dump() } @@ -52,19 +48,13 @@ func testMiniExtend(t *testing.T, p *CompPoint, newWord string, complete bool, e } func Test2(t *testing.T) { - p, err := ParseCompPoint("ls f", 4) - if err != nil { - t.Fatalf("error: %v", err) - } + p := ParseCompPoint(parseToSP("ls f[*]")) testMiniExtend(t, p, "foo", false, "foo[*]") testMiniExtend(t, p, "foo", true, "foo [*]") testMiniExtend(t, p, "foo bar", true, "'foo bar' [*]") testMiniExtend(t, p, "foo'bar", true, `$'foo\'bar' [*]`) - p, err = ParseCompPoint("ls fmore", 4) - if err != nil { - t.Fatalf("error: %v", err) - } + p = ParseCompPoint(parseToSP("ls f[*]more")) testMiniExtend(t, p, "foo", false, "foo[*]more") testMiniExtend(t, p, "foo bar", false, `'foo bar[*]more`) testMiniExtend(t, p, "foo bar", true, `'foo bar[*]more`) @@ -72,10 +62,7 @@ func Test2(t *testing.T) { } func testParseRT(t *testing.T, origSP StrWithPos) { - p, err := ParseCompPoint(origSP.Str, origSP.Pos) - if err != nil { - t.Fatalf("error: %v", err) - } + p := ParseCompPoint(origSP) newSP := StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} if origSP != newSP { t.Fatalf("not equal: [%s] != [%s]", origSP, newSP) @@ -92,10 +79,7 @@ func Test3(t *testing.T) { func testExtend(t *testing.T, origStr string, compStrs []string, expectedStr string) { origSP := parseToSP(origStr) expectedSP := parseToSP(expectedStr) - p, err := ParseCompPoint(origSP.Str, origSP.Pos) - if err != nil { - t.Fatalf("error: %v", err) - } + p := ParseCompPoint(origSP) crtn := compsToCompReturn(compStrs, false) newSP := p.FullyExtend(crtn) if newSP != expectedSP { diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index ddae3f46e..bdc0c5c45 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -13,10 +13,10 @@ import ( var globalLock = &sync.Mutex{} var simpleCompMap = map[string]SimpleCompGenFnType{ - "file": simpleCompFile, - "directory": simpleCompDir, - "variable": simpleCompVar, - "command": simpleCompCommand, + CGTypeCommand: simpleCompCommand, + CGTypeFile: simpleCompFile, + CGTypeDir: simpleCompDir, + CGTypeVariable: simpleCompVar, } type SimpleCompGenFnType = func(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) @@ -79,17 +79,17 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp } func simpleCompFile(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { - return doCompGen(ctx, prefix, "file", compCtx) + return doCompGen(ctx, prefix, CGTypeFile, compCtx) } func simpleCompDir(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { - return doCompGen(ctx, prefix, "directory", compCtx) + return doCompGen(ctx, prefix, CGTypeDir, compCtx) } func simpleCompVar(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { - return doCompGen(ctx, prefix, "variable", compCtx) + return doCompGen(ctx, prefix, CGTypeVariable, compCtx) } func simpleCompCommand(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) { - return doCompGen(ctx, prefix, "command", compCtx) + return doCompGen(ctx, prefix, CGTypeCommand, compCtx) } From 90bcb09b3e2875afe42269f5377105e272301875 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 10 Nov 2022 18:51:20 -0800 Subject: [PATCH 169/397] hook up new command completion --- pkg/cmdrunner/cmdrunner.go | 87 +++++++++++++++++++++++--------------- pkg/comp/comp.go | 26 +++++++++--- pkg/comp/simplecomp.go | 7 ++- pkg/sstore/updatebus.go | 4 +- 4 files changed, 82 insertions(+), 42 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 71d9aef4c..17edd0b4a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1077,23 +1077,14 @@ func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.Upd return update } -func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore bool) sstore.UpdatePacket { - if hasMore { - return nil - } - lcp := utilfn.LongestPrefix(prefix, comps) - if lcp == prefix || len(lcp) < len(prefix) || !strings.HasPrefix(lcp, prefix) { - return nil - } - insertChars := lcp[len(prefix):] - clu := &sstore.CmdLineType{InsertChars: insertChars, InsertPos: pos} - return sstore.ModelUpdate{CmdLine: clu} -} - func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { - compsCmd, _ := comp.DoSimpleComp(ctx, "command", prefix, compCtx, nil) - compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil) - return comp.CombineCompReturn(compsCmd, compsMeta), nil + if strings.HasPrefix(prefix, "/") { + compsCmd, _ := comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil) + compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil) + return comp.CombineCompReturn(comp.CGTypeCommandMeta, compsCmd, compsMeta), nil + } else { + return comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil) + } } func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { @@ -1158,7 +1149,13 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str return comps, hasMore, nil } +// func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) + func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, 0) // best-effort + if err != nil { + return nil, fmt.Errorf("/compgen error: %w", err) + } cmdLine := firstArg(pk) pos := len(cmdLine) if pk.Kwargs["comppos"] != "" { @@ -1175,28 +1172,52 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto pos = len(cmdLine) } showComps := resolveBool(pk.Kwargs["compshow"], false) - prefix := cmdLine[:pos] - parts := strings.Split(prefix, " ") - compType := "file" - if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "/") { - compType = "metacommand" - } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { - compType = "directory" - } else if len(parts) <= 1 { - compType = "command" + cmdSP := comp.StrWithPos{Str: cmdLine, Pos: pos} + compCtx := comp.CompContext{} + if ids.Remote != nil { + rptr := ids.Remote.RemotePtr + compCtx.RemotePtr = &rptr + compCtx.State = ids.Remote.RemoteState } - lastPart := "" - if len(parts) > 0 { - lastPart = parts[len(parts)-1] - } - comps, hasMore, err := doCompGen(ctx, pk, lastPart, compType, showComps) + compCtx.ForDisplay = showComps + crtn, newSP, err := comp.DoCompGen(ctx, cmdSP, compCtx) if err != nil { return nil, err } - if showComps { - return makeInfoFromComps(compType, comps, hasMore), nil + if crtn == nil { + return nil, fmt.Errorf("no return value from DoCompGen") } - return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil + if showComps || newSP == nil { + compStrs := crtn.GetCompDisplayStrs() + return makeInfoFromComps(crtn.CompType, compStrs, crtn.HasMore), nil + } + update := sstore.ModelUpdate{ + CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos}, + } + return update, nil + + // prefix := cmdLine[:pos] + // parts := strings.Split(prefix, " ") + // compType := "file" + // if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "/") { + // compType = "metacommand" + // } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { + // compType = "directory" + // } else if len(parts) <= 1 { + // compType = "command" + // } + // lastPart := "" + // if len(parts) > 0 { + // lastPart = parts[len(parts)-1] + // } + // comps, hasMore, err := doCompGen(ctx, pk, lastPart, compType, showComps) + // if err != nil { + // return nil, err + // } + // if showComps { + // return makeInfoFromComps(compType, comps, hasMore), nil + // } + // return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 7714c474e..b446450df 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -43,7 +43,7 @@ const ( ) type CompContext struct { - RemotePtr sstore.RemotePtrType + RemotePtr *sstore.RemotePtrType State *packet.ShellState ForDisplay bool } @@ -76,8 +76,9 @@ type CompEntry struct { } type CompReturn struct { - Entries []CompEntry - HasMore bool + CompType string + Entries []CompEntry + HasMore bool } func compQuoteDQString(s string, close bool) string { @@ -204,7 +205,7 @@ func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos { if crtn == nil || crtn.HasMore { return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} } - compStrs := crtn.getCompStrs() + compStrs := crtn.GetCompStrs() compPrefix := p.getCompPrefix() lcp := utilfn.LongestPrefix(compPrefix, compStrs) if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) { @@ -420,7 +421,7 @@ func SortCompReturnEntries(c *CompReturn) { }) } -func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn { +func CombineCompReturn(compType string, c1 *CompReturn, c2 *CompReturn) *CompReturn { if c1 == nil { return c2 } @@ -428,6 +429,7 @@ func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn { return c1 } var rtn CompReturn + rtn.CompType = compType rtn.HasMore = c1.HasMore || c2.HasMore rtn.Entries = append([]CompEntry{}, c1.Entries...) rtn.Entries = append(rtn.Entries, c2.Entries...) @@ -435,7 +437,7 @@ func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn { return &rtn } -func (c *CompReturn) getCompStrs() []string { +func (c *CompReturn) GetCompStrs() []string { rtn := make([]string, len(c.Entries)) for idx, entry := range c.Entries { rtn[idx] = entry.Word @@ -443,6 +445,18 @@ func (c *CompReturn) getCompStrs() []string { return rtn } +func (c *CompReturn) GetCompDisplayStrs() []string { + rtn := make([]string, len(c.Entries)) + for idx, entry := range c.Entries { + if entry.IsMetaCmd { + rtn[idx] = "^" + entry.Word + } else { + rtn[idx] = entry.Word + } + } + return rtn +} + func (p CompPoint) getOrigPos() int { pword := p.Words[p.CompWord] return len(p.Prefix) + pword.Offset + len(pword.Prefix) + p.CompWordPos diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index bdc0c5c45..f3b93d076 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -41,7 +41,12 @@ func DoSimpleComp(ctx context.Context, compType string, prefix string, compCtx C if compFn == nil { return nil, fmt.Errorf("no simple comp fn for %q", compType) } - return compFn(ctx, prefix, compCtx, args) + crtn, err := compFn(ctx, prefix, compCtx, args) + if err != nil { + return nil, err + } + crtn.CompType = compType + return crtn, nil } func compsToCompReturn(comps []string, hasMore bool) *CompReturn { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index a224ab40d..302064517 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -113,8 +113,8 @@ type HistoryInfoType struct { } type CmdLineType struct { - InsertChars string `json:"insertchars"` - InsertPos int64 `json:"insertpos"` + CmdLine string `json:"cmdline"` + CursorPos int `json:"cursorpos"` } type UpdateChannel struct { From 24f17ff5e9c6b9a116b0f115ebe4a1c8df7b2816 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 14 Nov 2022 13:56:28 -0800 Subject: [PATCH 170/397] checkpoint shparse --- pkg/shparse/shparse.go | 320 ++++++++++++++++++++++++++++++++++++ pkg/shparse/shparse_test.go | 24 +++ 2 files changed, 344 insertions(+) create mode 100644 pkg/shparse/shparse.go create mode 100644 pkg/shparse/shparse_test.go diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go new file mode 100644 index 000000000..ce1a10b8a --- /dev/null +++ b/pkg/shparse/shparse.go @@ -0,0 +1,320 @@ +package shparse + +import ( + "fmt" + "strings" +) + +// +// cmds := cmd (sep cmd)* +// sep := ';' | '&' | '&&' | '||' | '|' | '\n' +// cmd := simple-cmd | compound-command redirect-list? +// compound-command := brace-group | subshell | for-clause | case-clause | if-clause | while-clause | until-clause +// brace-group := '{' cmds '}' +// subshell := '(' cmds ')' +// simple-command := cmd-prefix cmd-word (io-redirect)* +// cmd-prefix := (io-redirect | assignment)* +// cmd-suffix := (io-redirect | word)* +// cmd-name := word +// cmd-word := word +// io-redirect := (io-number? io-file) | (io-number? io-here) +// io-file := ('<' | '<&' | '>' | '>&' | '>>' | '>|' ) filename +// io-here := ('<<' | '<<-') here_end +// here-end := word +// if-clause := 'if' compound-list 'then' compound-list else-part 'fi' +// else-part := 'elif' compound-list 'then' compound-list +// | 'elif' compount-list 'then' compound-list else-part +// | 'else' compound-list +// compound-list := linebreak term sep? +// +// +// +// $var +// ${var} +// ${var op word?} +// op := '-' | '=' | '?' | '+' | ':-' | ':=' | ':?' | ':+' | '%' | '%%' | '#' | '##' +// ${ '#' var } +// +// $(command) +// `command` +// $(( arith )) +// +// " ... " +// ' ... ' +// $' ... ' +// $" ... ' + +// " => $, ", `, \ +// ' => ' +// (process quotes) +// mark as escaped +// split into commands (use ';' as separator) +// parse special operators +// perform expansions (vars, globs, commands) +// split command into name and arguments + +// A correctly-formed brace expansion must contain unquoted opening and closing braces, and at least one unquoted comma or a valid sequence expression +// Any incorrectly formed brace expansion is left unchanged. + +// word: char *word; flags +// bash aliases are lexical + +// [[, ((, $(( <- DQ + +// $ -> expansion +// $(...) +// (...) +// $((...)) +// ((...)) +// ${...} +// {...} +// X=(...) + +// ambiguity between $((...)) and $((ls); ls) +// ambiguity between foo=([0]=hell) and foo=([abc) + +const ( + WordTypeRaw = "raw" + WordTypeLit = "lit" + WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- + WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ + WordTypeDQ = "dq" // " + WordTypeSQ = "sq" // ' + WordTypeBQ = "bq" // ` + WordTypeDSQ = "dsq" // $' + WordTypeDDQ = "ddq" // $" + WordTypeVar = "var" // $ + WordTypeVarBrace = "varbrace" // ${ + WordTypeDP = "dp" // $( + WordTypeDPP = "dpp" // $(( + WordTypeP = "p" // ( + WordTypeDB = "db" // $[ + WordTypeDBB = "dbb" // $[[ +) + +type parseContext struct { + Input []rune + Pos int +} + +type wordType struct { + Offset int + End int + Type string + Complete bool + Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions) + Prefix []rune + Subs []*wordType +} + +func (c *parseContext) at(offset int) rune { + pos := c.Pos + offset + if pos < 0 || pos >= len(c.Input) { + return 0 + } + return c.Input[pos] +} + +func (c *parseContext) eof() bool { + return c.Pos >= len(c.Input) +} + +func (c *parseContext) cur() rune { + return c.at(0) +} + +func (c *parseContext) match(ch rune) bool { + return c.at(0) == ch +} + +func (c *parseContext) match2(ch rune, ch2 rune) bool { + return c.at(0) == ch && c.at(1) == ch2 +} + +func (c *parseContext) newOp(length int) *wordType { + rtn := &wordType{Offset: c.Pos} + rtn.Type = WordTypeOp + rtn.Complete = true + rtn.Val = string(c.Input[c.Pos : c.Pos+length]) + c.Pos += length + rtn.End = c.Pos + return rtn +} + +func (c *parseContext) parseOp() *wordType { + ch := c.cur() + if ch == '&' || ch == ';' || ch == '|' || ch == '\n' || ch == '<' || ch == '>' || ch == '!' || ch == '(' { + ch2 := c.at(1) + if ch2 == 0 { + return c.newOp(1) + } + r2 := string([]rune{ch, ch2}) + if r2 == "<<" { + ch3 := c.at(2) + if ch3 == '-' { + return c.newOp(3) // "<<-" + } + return c.newOp(2) // "<<" + } + if r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == ">>" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { + return c.newOp(2) + } + return c.newOp(1) + } + return nil +} + +// returns (new-offset, complete) +func (c *parseContext) skipToChar(offset int, endCh rune, allowEsc bool) (int, bool) { + for { + ch := c.at(offset) + if ch == 0 { + return offset, false + } + if allowEsc && ch == '\\' { + if c.at(offset+1) == 0 { + return offset + 1, false + } + offset += 2 + continue + } + if ch == endCh { + return offset + 1, true + } + offset++ + } +} + +func (c *parseContext) parseStrSQ() *wordType { + if !c.match('\'') { + return nil + } + w := &wordType{ + Offset: c.Pos, + Type: WordTypeDQ, + } + w.End, w.Complete = c.skipToChar(1, '\'', false) + c.Pos = w.End + return w +} + +func (c *parseContext) parseStrDQ() *wordType { + if !c.match('"') { + return nil + } + w := &wordType{ + Offset: c.Pos, + Type: WordTypeDQ, + } + w.End, w.Complete = c.skipToChar(1, '"', true) + c.Pos = w.End + return w +} + +func (c *parseContext) parseStrBQ() *wordType { + if c.match('`') { + return nil + } + w := &wordType{ + Offset: c.Pos, + Type: WordTypeBQ, + } + w.End, w.Complete = c.skipToChar(1, '`', true) + c.Pos = w.End + return w +} + +func (c *parseContext) parseStrANSI() *wordType { + if !c.match2('$', '\'') { + return nil + } + w := &wordType{ + Offset: c.Pos, + Type: WordTypeDSQ, + } + w.End, w.Complete = c.skipToChar(1, '\'', true) + c.Pos = w.End + return w +} + +func (c *parseContext) parseStrDDQ() *wordType { + if !c.match2('$', '"') { + return nil + } + w := &wordType{ + Offset: c.Pos, + Type: WordTypeDDQ, + } + w.End, w.Complete = c.skipToChar(1, '"', true) + c.Pos = w.End + return w +} + +func (c *parseContext) parseVar() *wordType { + if !c.match('$') { + return nil + } + return nil +} + +func (c *parseContext) parseQuotes() []*wordType { + var rtn []*wordType + var litWord *wordType + for { + var quoteWord *wordType + ch := c.cur() + fmt.Printf("ch: %d %q\n", c.Pos, string([]rune{ch})) + startPos := c.Pos + if ch == 0 { + break + } + switch ch { + case '\'': + quoteWord = c.parseStrSQ() + + case '"': + quoteWord = c.parseStrDQ() + + case '$': + quoteWord = c.parseStrANSI() + if quoteWord == nil { + quoteWord = c.parseStrDDQ() + } + } + if quoteWord != nil { + if litWord != nil { + litWord.End = startPos + rtn = append(rtn, litWord) + litWord = nil + } + rtn = append(rtn, quoteWord) + continue + } + if litWord == nil { + litWord = &wordType{Offset: c.Pos, Type: WordTypeLit, Complete: true} + } + if ch == '\\' && c.at(1) != 0 { + c.Pos += 2 + continue + } + c.Pos++ + } + if litWord != nil { + litWord.End = c.Pos + rtn = append(rtn, litWord) + } + return rtn +} + +func (c *parseContext) RawString(w *wordType) string { + return fmt.Sprintf("%s[%q]", w.Type, string(c.Input[w.Offset:w.End])) +} + +func (c *parseContext) dumpWords(words []*wordType) { + var strs []string + for _, word := range words { + strs = append(strs, c.RawString(word)) + } + output := strings.Join(strs, " ") + fmt.Printf("%s\n", output) +} diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go new file mode 100644 index 000000000..b88c323b4 --- /dev/null +++ b/pkg/shparse/shparse_test.go @@ -0,0 +1,24 @@ +package shparse + +import ( + "testing" +) + +// $(ls f[*]); ./x +// ls f => raw["ls f"] -> lit["ls f"] -> lit["ls"] lit["f"]p +// w; ls foo; => raw["w; ls foo;"] +// ls&"ls" => raw["ls&ls"] => lit["ls&"] dq["ls"] => lit["ls"] key["&"] dq["ls"] +// ls $x; echo `ls f => raw["ls $x; echo `ls f"] +// > echo $foo{x,y} + +func testParse(t *testing.T, s string) { + c := &parseContext{Input: []rune(s)} + words := c.parseQuotes() + c.dumpWords(words) +} + +func Test1(t *testing.T) { + testParse(t, "ls") + testParse(t, "ls 'foo'") + testParse(t, `ls "hello" $'\''`) +} From 55767e32562caa50a41e5afc186ed3d23c58b9d1 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 14 Nov 2022 13:56:50 -0800 Subject: [PATCH 171/397] remove shellescape dependency --- pkg/utilfn/utilfn.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index b8e4a7c79..35db1c4df 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -1,9 +1,8 @@ package utilfn import ( + "regexp" "strings" - - "github.com/alessio/shellescape" ) func GetStrArr(v interface{}, field string) []string { @@ -50,12 +49,17 @@ func GetBool(v interface{}, field string) bool { return bval } +var needsQuoteRe = regexp.MustCompile(`[^\w@%:,./=+-]`) + // minimum maxlen=6 func ShellQuote(val string, forceQuote bool, maxLen int) string { if maxLen < 6 { maxLen = 6 } - rtn := shellescape.Quote(val) + rtn := val + if needsQuoteRe.MatchString(val) { + rtn = "'" + strings.ReplaceAll(val, "'", `'"'"'`) + "'" + } if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") { if len(rtn) > maxLen { return rtn[0:maxLen-4] + "..." + rtn[0:1] From 9da092816a93c76abdcdb94ca268622bfc032d40 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 14 Nov 2022 19:57:29 -0800 Subject: [PATCH 172/397] checkpoint --- pkg/shparse/shparse.go | 228 ++++++++++++++++++++++++++++-------- pkg/shparse/shparse_test.go | 13 +- 2 files changed, 191 insertions(+), 50 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index ce1a10b8a..a1b485739 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -3,6 +3,7 @@ package shparse import ( "fmt" "strings" + "unicode" ) // @@ -73,10 +74,12 @@ import ( // ambiguity between $((...)) and $((ls); ls) // ambiguity between foo=([0]=hell) and foo=([abc) +// tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003 + const ( WordTypeRaw = "raw" WordTypeLit = "lit" - WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- + WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ WordTypeDQ = "dq" // " WordTypeSQ = "sq" // ' @@ -98,9 +101,9 @@ type parseContext struct { } type wordType struct { - Offset int - End int Type string + Offset int + Raw []rune Complete bool Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions) Prefix []rune @@ -131,37 +134,43 @@ func (c *parseContext) match2(ch rune, ch2 rune) bool { return c.at(0) == ch && c.at(1) == ch2 } +func (c *parseContext) match3(ch rune, ch2 rune, ch3 rune) bool { + return c.at(0) == ch && c.at(1) == ch2 && c.at(2) == ch3 +} + func (c *parseContext) newOp(length int) *wordType { - rtn := &wordType{Offset: c.Pos} - rtn.Type = WordTypeOp + rtn := &wordType{Type: WordTypeOp} + rtn.Offset = c.Pos + rtn.Raw = c.Input[c.Pos : c.Pos+length] + rtn.Val = string(rtn.Raw) rtn.Complete = true - rtn.Val = string(c.Input[c.Pos : c.Pos+length]) c.Pos += length - rtn.End = c.Pos return rtn } -func (c *parseContext) parseOp() *wordType { - ch := c.cur() +// returns (found, newOffset) +func (c *parseContext) parseOp(offset int) (bool, int) { + ch := c.at(offset) if ch == '&' || ch == ';' || ch == '|' || ch == '\n' || ch == '<' || ch == '>' || ch == '!' || ch == '(' { - ch2 := c.at(1) + ch2 := c.at(offset + 1) if ch2 == 0 { - return c.newOp(1) + return true, offset + 1 } r2 := string([]rune{ch, ch2}) if r2 == "<<" { - ch3 := c.at(2) + ch3 := c.at(offset + 2) if ch3 == '-' { - return c.newOp(3) // "<<-" + return true, offset + 3 // "<<-" } - return c.newOp(2) // "<<" + return true, offset + 2 // "<<" } if r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == ">>" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { + // we don't return '((' here (requires special processing) return c.newOp(2) } return c.newOp(1) } - return nil + return false, 0 } // returns (new-offset, complete) @@ -189,12 +198,14 @@ func (c *parseContext) parseStrSQ() *wordType { if !c.match('\'') { return nil } + newOffset, complete := c.skipToChar(1, '\'', false) w := &wordType{ - Offset: c.Pos, - Type: WordTypeDQ, + Type: WordTypeDQ, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: complete, } - w.End, w.Complete = c.skipToChar(1, '\'', false) - c.Pos = w.End + c.Pos = c.Pos + newOffset return w } @@ -202,12 +213,14 @@ func (c *parseContext) parseStrDQ() *wordType { if !c.match('"') { return nil } + newOffset, complete := c.skipToChar(1, '"', false) w := &wordType{ - Offset: c.Pos, - Type: WordTypeDQ, + Type: WordTypeDQ, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: complete, } - w.End, w.Complete = c.skipToChar(1, '"', true) - c.Pos = w.End + c.Pos = c.Pos + newOffset return w } @@ -215,12 +228,14 @@ func (c *parseContext) parseStrBQ() *wordType { if c.match('`') { return nil } + newOffset, complete := c.skipToChar(1, '`', true) w := &wordType{ - Offset: c.Pos, - Type: WordTypeBQ, + Type: WordTypeBQ, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: complete, } - w.End, w.Complete = c.skipToChar(1, '`', true) - c.Pos = w.End + c.Pos = c.Pos + newOffset return w } @@ -228,12 +243,14 @@ func (c *parseContext) parseStrANSI() *wordType { if !c.match2('$', '\'') { return nil } + newOffset, complete := c.skipToChar(2, '\'', true) w := &wordType{ - Offset: c.Pos, - Type: WordTypeDSQ, + Type: WordTypeDSQ, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: complete, } - w.End, w.Complete = c.skipToChar(1, '\'', true) - c.Pos = w.End + c.Pos = c.Pos + newOffset return w } @@ -241,30 +258,103 @@ func (c *parseContext) parseStrDDQ() *wordType { if !c.match2('$', '"') { return nil } + newOffset, complete := c.skipToChar(2, '"', true) w := &wordType{ - Offset: c.Pos, - Type: WordTypeDDQ, + Type: WordTypeDDQ, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: complete, } - w.End, w.Complete = c.skipToChar(1, '"', true) - c.Pos = w.End + c.Pos = c.Pos + newOffset return w } -func (c *parseContext) parseVar() *wordType { +func (c *parseContext) parseExpansion() *wordType { if !c.match('$') { return nil } + if c.match3('$', '(', '(') { + // arith expansion + return nil + } + if c.match2('$', '(') { + // subshell + return nil + } + if c.match2('$', '[') { + // deprecated arith expansion + return nil + } + if c.match2('$', '{') { + // variable expansion + return nil + } + ch2 := c.at(1) + if ch2 == 0 || unicode.IsSpace(ch2) { + // no expansion + return nil + } + newOffset := c.parseSimpleVarName(1) + if newOffset > 1 { + // simple variable name + rtn := &wordType{ + Type: WordTypeVar, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+newOffset], + Complete: true, + } + c.Pos = c.Pos + newOffset + return rtn + } + // single character variable name, e.g. $@, $_, $1, etc. + rtn := &wordType{ + Type: WordTypeVar, + Offset: c.Pos, + Raw: c.Input[c.Pos : c.Pos+2], + Complete: true, + } + c.Pos += 2 + return rtn +} + +func (c *parseContext) parseShellTest() *wordType { + if !c.match2('[', '[') { + return nil + } return nil } -func (c *parseContext) parseQuotes() []*wordType { +func (c *parseContext) parseProcessSubstitution() *wordType { + if !c.match2('<', '(') && !c.match2('>', '(') { + return nil + } + return nil +} + +// returns newOffset +func (c *parseContext) parseSimpleVarName(offset int) int { + first := true + for { + ch := c.at(offset) + if ch == 0 { + return offset + } + if (ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) || (!first && ch >= '0' && ch <= '9') { + first = false + offset++ + continue + } + return offset + } +} + +func parseInput(inputStr string) []*wordType { + c := &parseContext{Input: []rune(inputStr)} var rtn []*wordType var litWord *wordType for { var quoteWord *wordType ch := c.cur() - fmt.Printf("ch: %d %q\n", c.Pos, string([]rune{ch})) - startPos := c.Pos if ch == 0 { break } @@ -280,10 +370,12 @@ func (c *parseContext) parseQuotes() []*wordType { if quoteWord == nil { quoteWord = c.parseStrDDQ() } + if quoteWord == nil { + quoteWord = c.parseExpansion() + } } if quoteWord != nil { if litWord != nil { - litWord.End = startPos rtn = append(rtn, litWord) litWord = nil } @@ -291,29 +383,73 @@ func (c *parseContext) parseQuotes() []*wordType { continue } if litWord == nil { - litWord = &wordType{Offset: c.Pos, Type: WordTypeLit, Complete: true} + litWord = &wordType{Type: WordTypeLit, Offset: c.Pos, Complete: true} } if ch == '\\' && c.at(1) != 0 { + litWord.Raw = append(litWord.Raw, ch, c.at(1)) c.Pos += 2 continue } + litWord.Raw = append(litWord.Raw, ch) c.Pos++ } if litWord != nil { - litWord.End = c.Pos rtn = append(rtn, litWord) } + + // now we want to expand ops + rtn := expandAllOps(rtn) + + return rtn +} + +func expandOps(word *wordType) []*wordType { + if word.Type != WordTypeLit { + return nil + } + var lastBackSlash bool + var rtn []*wordType + for idx, ch := range word.Raw { + if ch == 0 { + break + } + if ch == '\\' { + lastBackSlash = true + continue + } + if lastBackSlash { + lastBackSlash = false + continue + } + + } +} + +func expandAllOps(words []*wordType) []*wordType { + var rtn []*wordType + for _, word := range words { + exWords := expandOps(word) + if len(exWords) == 0 { + rtn = append(rtn, word) + } else { + rtn = append(rtn, exWords...) + } + } return rtn } -func (c *parseContext) RawString(w *wordType) string { - return fmt.Sprintf("%s[%q]", w.Type, string(c.Input[w.Offset:w.End])) +func (w *wordType) String() string { + var notCompleteFlag string + if !w.Complete { + notCompleteFlag = "*" + } + return fmt.Sprintf("%s[%d:%q]%s", w.Type, w.Offset, string(w.Raw), notCompleteFlag) } -func (c *parseContext) dumpWords(words []*wordType) { +func dumpWords(words []*wordType) { var strs []string for _, word := range words { - strs = append(strs, c.RawString(word)) + strs = append(strs, word.String()) } output := strings.Join(strs, " ") fmt.Printf("%s\n", output) diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index b88c323b4..ff9128671 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -1,24 +1,29 @@ package shparse import ( + "fmt" "testing" ) // $(ls f[*]); ./x -// ls f => raw["ls f"] -> lit["ls f"] -> lit["ls"] lit["f"]p +// ls f => raw["ls f"] -> lit["ls f"] -> lit["ls"] lit["f"] // w; ls foo; => raw["w; ls foo;"] // ls&"ls" => raw["ls&ls"] => lit["ls&"] dq["ls"] => lit["ls"] key["&"] dq["ls"] // ls $x; echo `ls f => raw["ls $x; echo `ls f"] // > echo $foo{x,y} func testParse(t *testing.T, s string) { - c := &parseContext{Input: []rune(s)} - words := c.parseQuotes() - c.dumpWords(words) + words := parseInput(s) + + fmt.Printf("%s\n", s) + dumpWords(words) + fmt.Printf("\n") } func Test1(t *testing.T) { testParse(t, "ls") testParse(t, "ls 'foo'") testParse(t, `ls "hello" $'\''`) + testParse(t, `ls "foo`) + testParse(t, `echo $11 $xyz $ `) } From 06724528cc15c3f4cb85a2b7888b53591a430226 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 15 Nov 2022 00:36:30 -0800 Subject: [PATCH 173/397] checkpoint --- pkg/shparse/shparse.go | 114 +++++++++++++++++++++++++----------- pkg/shparse/shparse_test.go | 4 +- pkg/shparse/tokenize.go | 110 ++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 pkg/shparse/tokenize.go diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index a1b485739..908f3c48a 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -2,7 +2,6 @@ package shparse import ( "fmt" - "strings" "unicode" ) @@ -77,27 +76,46 @@ import ( // tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003 const ( - WordTypeRaw = "raw" - WordTypeLit = "lit" - WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) - WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ - WordTypeDQ = "dq" // " - WordTypeSQ = "sq" // ' - WordTypeBQ = "bq" // ` - WordTypeDSQ = "dsq" // $' - WordTypeDDQ = "ddq" // $" - WordTypeVar = "var" // $ - WordTypeVarBrace = "varbrace" // ${ - WordTypeDP = "dp" // $( - WordTypeDPP = "dpp" // $(( - WordTypeP = "p" // ( - WordTypeDB = "db" // $[ - WordTypeDBB = "dbb" // $[[ + WordTypeRaw = "raw" + WordTypeLit = "lit" + WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) + WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ + WordTypeSimpleVar = "svar" // simplevar $ + + // each of these can also be used as an entry in quoteContext + WordTypeDQ = "dq" // " + WordTypeSQ = "sq" // ' + WordTypeBQ = "bq" // ` + WordTypeDSQ = "dsq" // $' + WordTypeDDQ = "ddq" // $" + WordTypeVarBrace = "varb" // ${ + WordTypeDP = "dp" // $( + WordTypeDPP = "dpp" // $(( + WordTypeP = "p" // ( + WordTypePP = "pp" // (( + WordTypeDB = "db" // $[ ) +type quoteContext []string + +func (qc quoteContext) push(q string) quoteContext { + rtn := make([]string, 0, len(qc)+1) + rtn = append(rtn, qc...) + rtn = append(rtn, q) + return rtn +} + +func (qc quoteContext) cur() string { + if len(qc) == 0 { + return "" + } + return qc[len(qc)-1] +} + type parseContext struct { Input []rune Pos int + QC quoteContext } type wordType struct { @@ -110,6 +128,15 @@ type wordType struct { Subs []*wordType } +func (c *parseContext) clone(pos int, newQuote string) *parseContext { + rtn := *c + if newQuote != "" { + rtn.QC = append(rtn.QC, newQuote) + } + rtn.Input = rtn.Input[pos:] + return &rtn +} + func (c *parseContext) at(offset int) rune { pos := c.Pos + offset if pos < 0 || pos >= len(c.Input) { @@ -149,9 +176,11 @@ func (c *parseContext) newOp(length int) *wordType { } // returns (found, newOffset) +// shell_meta_chars "()<>;&|" +// possible to maybe add ;;& &>> &> |& ;& func (c *parseContext) parseOp(offset int) (bool, int) { ch := c.at(offset) - if ch == '&' || ch == ';' || ch == '|' || ch == '\n' || ch == '<' || ch == '>' || ch == '!' || ch == '(' { + if ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == ';' || ch == '&' || ch == '|' { ch2 := c.at(offset + 1) if ch2 == 0 { return true, offset + 1 @@ -159,16 +188,16 @@ func (c *parseContext) parseOp(offset int) (bool, int) { r2 := string([]rune{ch, ch2}) if r2 == "<<" { ch3 := c.at(offset + 2) - if ch3 == '-' { - return true, offset + 3 // "<<-" + if ch3 == '-' || ch3 == '<' { + return true, offset + 3 // "<<-" or "<<<" } return true, offset + 2 // "<<" } - if r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == ">>" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { + if r2 == ">>" || r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { // we don't return '((' here (requires special processing) - return c.newOp(2) + return true, offset + 2 } - return c.newOp(1) + return true, offset + 1 } return false, 0 } @@ -298,7 +327,7 @@ func (c *parseContext) parseExpansion() *wordType { if newOffset > 1 { // simple variable name rtn := &wordType{ - Type: WordTypeVar, + Type: WordTypeSimpleVar, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: true, @@ -308,7 +337,7 @@ func (c *parseContext) parseExpansion() *wordType { } // single character variable name, e.g. $@, $_, $1, etc. rtn := &wordType{ - Type: WordTypeVar, + Type: WordTypeSimpleVar, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+2], Complete: true, @@ -398,7 +427,7 @@ func parseInput(inputStr string) []*wordType { } // now we want to expand ops - rtn := expandAllOps(rtn) + rtn = expandAllOps(rtn) return rtn } @@ -409,7 +438,7 @@ func expandOps(word *wordType) []*wordType { } var lastBackSlash bool var rtn []*wordType - for idx, ch := range word.Raw { + for _, ch := range word.Raw { if ch == 0 { break } @@ -421,8 +450,9 @@ func expandOps(word *wordType) []*wordType { lastBackSlash = false continue } - } + rtn = append(rtn, word) + return rtn } func expandAllOps(words []*wordType) []*wordType { @@ -438,19 +468,33 @@ func expandAllOps(words []*wordType) []*wordType { return rtn } +func makeSpaceStr(slen int) string { + if slen == 0 { + return "" + } + if slen == 1 { + return " " + } + rtn := make([]byte, slen) + for i := 0; i < slen; i++ { + rtn[i] = ' ' + } + return string(rtn) +} + func (w *wordType) String() string { - var notCompleteFlag string + notCompleteFlag := " " if !w.Complete { notCompleteFlag = "*" } - return fmt.Sprintf("%s[%d:%q]%s", w.Type, w.Offset, string(w.Raw), notCompleteFlag) + return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeSpaceStr(len(w.Prefix)), string(w.Raw)) } -func dumpWords(words []*wordType) { - var strs []string +func dumpWords(words []*wordType, indentStr string) { for _, word := range words { - strs = append(strs, word.String()) + fmt.Printf("%s%s\n", indentStr, word.String()) + if len(word.Subs) > 0 { + dumpWords(word.Subs, indentStr+" ") + } } - output := strings.Join(strs, " ") - fmt.Printf("%s\n", output) } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index ff9128671..c81ccd801 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -13,10 +13,10 @@ import ( // > echo $foo{x,y} func testParse(t *testing.T, s string) { - words := parseInput(s) + words := Tokenize(s) fmt.Printf("%s\n", s) - dumpWords(words) + dumpWords(words, " ") fmt.Printf("\n") } diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go new file mode 100644 index 000000000..dd6e0d265 --- /dev/null +++ b/pkg/shparse/tokenize.go @@ -0,0 +1,110 @@ +package shparse + +import ( + "unicode" +) + +// from bash source +// +// shell_meta_chars "()<>;&|" +// + +type tokenizeOutputState struct { + Rtn []*wordType + CurWord *wordType + SavedPrefix []rune +} + +func (state *tokenizeOutputState) appendWord(word *wordType) { + state.delimitCurWord() + if len(state.SavedPrefix) > 0 { + word.Prefix = state.SavedPrefix + state.SavedPrefix = nil + } + state.Rtn = append(state.Rtn, word) +} + +func (state *tokenizeOutputState) ensureCurWord(pc *parseContext) { + if state.CurWord != nil { + return + } + state.CurWord = &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true, Prefix: state.SavedPrefix} + state.SavedPrefix = nil +} + +func (state *tokenizeOutputState) delimitCurWord() { + if state.CurWord != nil { + state.Rtn = append(state.Rtn, state.CurWord) + state.CurWord = nil + } +} + +func (state *tokenizeOutputState) delimitWithSpace(spaceCh rune) { + state.delimitCurWord() + state.SavedPrefix = append(state.SavedPrefix, spaceCh) +} + +func (state *tokenizeOutputState) finish(pc *parseContext) { + state.delimitCurWord() + if len(state.SavedPrefix) > 0 { + state.ensureCurWord(pc) + state.delimitCurWord() + } +} + +func Tokenize(cmd string) []*wordType { + c := &parseContext{Input: []rune(cmd)} + state := &tokenizeOutputState{} + for { + ch := c.cur() + if ch == 0 { + break + } + // fmt.Printf("ch %d %q\n", c.Pos, string([]rune{ch})) + foundOp, newOffset := c.parseOp(0) + if foundOp { + opWord := &wordType{Type: WordTypeOp, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: true} + opWord.Val = string(opWord.Raw) + c.Pos = c.Pos + newOffset + state.appendWord(opWord) + continue + } + var quoteWord *wordType + switch ch { + case '\'': + quoteWord = c.parseStrSQ() + + case '"': + quoteWord = c.parseStrDQ() + + case '$': + quoteWord = c.parseStrANSI() + if quoteWord == nil { + quoteWord = c.parseStrDDQ() + } + if quoteWord == nil { + quoteWord = c.parseExpansion() + } + } + if quoteWord != nil { + state.appendWord(quoteWord) + continue + } + if ch == '\\' && c.at(1) != 0 { + state.ensureCurWord(c) + state.CurWord.Raw = append(state.CurWord.Raw, ch, c.at(1)) + c.Pos += 2 + continue + } + if unicode.IsSpace(ch) { + state.delimitWithSpace(ch) + c.Pos++ + continue + } + state.ensureCurWord(c) + state.CurWord.Raw = append(state.CurWord.Raw, ch) + c.Pos++ + } + state.finish(c) + return state.Rtn +} From b9fd4df60fca2edb3e759a40b0f8e743e62e81de Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 15 Nov 2022 00:39:53 -0800 Subject: [PATCH 174/397] words -> str. test roundtrip --- pkg/shparse/shparse_test.go | 5 +++++ pkg/shparse/tokenize.go | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index c81ccd801..206b225e2 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -18,6 +18,11 @@ func testParse(t *testing.T, s string) { fmt.Printf("%s\n", s) dumpWords(words, " ") fmt.Printf("\n") + + outStr := wordsToStr(words) + if outStr != s { + t.Errorf("tokenization output does not match input: %q => %q", s, outStr) + } } func Test1(t *testing.T) { diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index dd6e0d265..9d447b89f 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -1,6 +1,7 @@ package shparse import ( + "bytes" "unicode" ) @@ -108,3 +109,14 @@ func Tokenize(cmd string) []*wordType { state.finish(c) return state.Rtn } + +func wordsToStr(words []*wordType) string { + var buf bytes.Buffer + for _, word := range words { + if len(word.Prefix) > 0 { + buf.WriteString(string(word.Prefix)) + } + buf.WriteString(string(word.Raw)) + } + return buf.String() +} From d44242fe7139cd3cefc62ac477e77c039b1baf19 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 15 Nov 2022 10:27:36 -0800 Subject: [PATCH 175/397] checkpoint on new shparse --- pkg/shparse/shparse.go | 43 ++++++++++++++++++++++++++++++++----- pkg/shparse/shparse_test.go | 4 ++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 908f3c48a..ec4e966ee 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -223,6 +223,28 @@ func (c *parseContext) skipToChar(offset int, endCh rune, allowEsc bool) (int, b } } +// returns (new-offset, complete) +func (c *parseContext) skipToChar2(offset int, endCh rune, endCh2 rune, allowEsc bool) (int, bool) { + for { + ch := c.at(offset) + ch2 := c.at(offset + 1) + if ch == 0 { + return offset, false + } + if ch2 == 0 { + return offset + 1, false + } + if allowEsc && ch == '\\' { + offset += 2 + continue + } + if ch == endCh && ch2 == endCh2 { + return offset + 2, true + } + offset++ + } +} + func (c *parseContext) parseStrSQ() *wordType { if !c.match('\'') { return nil @@ -303,20 +325,31 @@ func (c *parseContext) parseExpansion() *wordType { return nil } if c.match3('$', '(', '(') { - // arith expansion - return nil + newOffset, complete := c.skipToChar2(3, ')', ')', false) + w := &wordType{Type: WordTypeDPP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + c.Pos = c.Pos + newOffset + return w } if c.match2('$', '(') { // subshell - return nil + newOffset, complete := c.skipToChar(2, ')', false) + w := &wordType{Type: WordTypeDP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + c.Pos = c.Pos + newOffset + return w } if c.match2('$', '[') { // deprecated arith expansion - return nil + newOffset, complete := c.skipToChar(2, ']', false) + w := &wordType{Type: WordTypeDB, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + c.Pos = c.Pos + newOffset + return w } if c.match2('$', '{') { // variable expansion - return nil + newOffset, complete := c.skipToChar(2, '}', false) + w := &wordType{Type: WordTypeVarBrace, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + c.Pos = c.Pos + newOffset + return w } ch2 := c.at(1) if ch2 == 0 || unicode.IsSpace(ch2) { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 206b225e2..afed47707 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -31,4 +31,8 @@ func Test1(t *testing.T) { testParse(t, `ls "hello" $'\''`) testParse(t, `ls "foo`) testParse(t, `echo $11 $xyz $ `) + testParse(t, `echo $(ls ${x:"hello"} foo`) + testParse(t, `ls ${x:"hello"} $[2+2] $((5 * 10)) $(ls; ls&)`) + testParse(t, `ls;ls&./foo > out 2> "out2"`) + testParse(t, `(( x = 5)); ls& cd ~/work/"hello again"`) } From f1958eaac7a64d5ae18ac5d27671f2f6ca60e953 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 16 Nov 2022 00:37:22 -0800 Subject: [PATCH 176/397] recursive parsing for double quotes, subshells, and variable expansions --- pkg/shparse/shparse.go | 43 ++++-- pkg/shparse/shparse_test.go | 8 ++ pkg/shparse/tokenize.go | 261 ++++++++++++++++++++++++++++++++---- 3 files changed, 274 insertions(+), 38 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index ec4e966ee..acb219983 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -81,6 +81,8 @@ const ( WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ WordTypeSimpleVar = "svar" // simplevar $ + WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x + WordTypeArith = "ath" // each of these can also be used as an entry in quoteContext WordTypeDQ = "dq" // " @@ -129,11 +131,10 @@ type wordType struct { } func (c *parseContext) clone(pos int, newQuote string) *parseContext { - rtn := *c + rtn := parseContext{Input: c.Input[pos:], QC: c.QC} if newQuote != "" { - rtn.QC = append(rtn.QC, newQuote) + rtn.QC = rtn.QC.push(newQuote) } - rtn.Input = rtn.Input[pos:] return &rtn } @@ -264,12 +265,15 @@ func (c *parseContext) parseStrDQ() *wordType { if !c.match('"') { return nil } - newOffset, complete := c.skipToChar(1, '"', false) + newContext := c.clone(c.Pos+1, WordTypeDQ) + subWords, eofExit := newContext.tokenizeDQ() + newOffset := newContext.Pos + 1 w := &wordType{ Type: WordTypeDQ, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: complete, + Complete: !eofExit, + Subs: subWords, } c.Pos = c.Pos + newOffset return w @@ -320,6 +324,19 @@ func (c *parseContext) parseStrDDQ() *wordType { return w } +func (c *parseContext) parseArith(mustComplete bool) *wordType { + if !c.match2('(', '(') { + return nil + } + newOffset, complete := c.skipToChar2(2, ')', ')', false) + if mustComplete && !complete { + return nil + } + w := &wordType{Type: WordTypeArith, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + c.Pos = c.Pos + newOffset + return w +} + func (c *parseContext) parseExpansion() *wordType { if !c.match('$') { return nil @@ -332,8 +349,12 @@ func (c *parseContext) parseExpansion() *wordType { } if c.match2('$', '(') { // subshell - newOffset, complete := c.skipToChar(2, ')', false) - w := &wordType{Type: WordTypeDP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + newContext := c.clone(c.Pos+2, WordTypeDP) + subWords, eofExit := newContext.tokenizeRaw() + newOffset := newContext.Pos + 2 + // newOffset, complete := c.skipToChar(2, ')', false) + w := &wordType{Type: WordTypeDP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: !eofExit} + w.Subs = subWords c.Pos = c.Pos + newOffset return w } @@ -346,8 +367,10 @@ func (c *parseContext) parseExpansion() *wordType { } if c.match2('$', '{') { // variable expansion - newOffset, complete := c.skipToChar(2, '}', false) - w := &wordType{Type: WordTypeVarBrace, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} + newContext := c.clone(c.Pos+2, WordTypeVarBrace) + _, eofExit := newContext.tokenizeVarBrace() + newOffset := newContext.Pos + 2 + w := &wordType{Type: WordTypeVarBrace, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: !eofExit} c.Pos = c.Pos + newOffset return w } @@ -520,7 +543,7 @@ func (w *wordType) String() string { if !w.Complete { notCompleteFlag = "*" } - return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeSpaceStr(len(w.Prefix)), string(w.Raw)) + return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeSpaceStr(len(w.Prefix)), string(w.FullRawString())) } func dumpWords(words []*wordType, indentStr string) { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index afed47707..187c50cfb 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -35,4 +35,12 @@ func Test1(t *testing.T) { testParse(t, `ls ${x:"hello"} $[2+2] $((5 * 10)) $(ls; ls&)`) testParse(t, `ls;ls&./foo > out 2> "out2"`) testParse(t, `(( x = 5)); ls& cd ~/work/"hello again"`) + testParse(t, `echo "hello"abc$(ls)$x${y:foo}`) + testParse(t, `echo $(ls; ./x "foo")`) + testParse(t, `echo $(ls; (cd foo; ls); (cd bar; ls))xyz`) + testParse(t, `echo "$x ${y:-foo}"`) + testParse(t, `command="$(echo "$input" | sed -e "s/^[ \t]*\([^ \t]*\)[ \t]*.*$/\1/g")"`) + testParse(t, `echo $(ls $)`) + testParse(t, `echo ${x:-hello\}"}"} 2nd`) + testParse(t, `echo "$(ls "foo") more $x"`) } diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index 9d447b89f..d2b45b683 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -2,6 +2,7 @@ package shparse import ( "bytes" + "fmt" "unicode" ) @@ -16,7 +17,8 @@ type tokenizeOutputState struct { SavedPrefix []rune } -func (state *tokenizeOutputState) appendWord(word *wordType) { +// does not set CurWord +func (state *tokenizeOutputState) appendStandaloneWord(word *wordType) { state.delimitCurWord() if len(state.SavedPrefix) > 0 { word.Prefix = state.SavedPrefix @@ -25,12 +27,72 @@ func (state *tokenizeOutputState) appendWord(word *wordType) { state.Rtn = append(state.Rtn, word) } -func (state *tokenizeOutputState) ensureCurWord(pc *parseContext) { - if state.CurWord != nil { +func (state *tokenizeOutputState) appendWord(word *wordType) { + if len(state.SavedPrefix) > 0 { + word.Prefix = state.SavedPrefix + state.SavedPrefix = nil + } + if state.CurWord == nil { + state.CurWord = word return } - state.CurWord = &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true, Prefix: state.SavedPrefix} - state.SavedPrefix = nil + state.ensureGroupWord() + state.CurWord.Subs = append(state.CurWord.Subs, word) +} + +func (state *tokenizeOutputState) ensureGroupWord() { + if state.CurWord == nil { + panic("invalid state, cannot make group word when CurWord is nil") + } + if state.CurWord.Type == WordTypeGroup { + return + } + // moves the prefix from CurWord to the new group word + groupWord := &wordType{ + Type: WordTypeGroup, + Offset: state.CurWord.Offset, + Complete: true, + Prefix: state.CurWord.Prefix, + } + state.CurWord.Prefix = nil + groupWord.Subs = []*wordType{state.CurWord} + state.CurWord = groupWord +} + +func ungroupWord(w *wordType) []*wordType { + if w.Type != WordTypeGroup { + return []*wordType{w} + } + rtn := w.Subs + if len(w.Prefix) > 0 && len(rtn) > 0 { + newPrefix := append([]rune{}, w.Prefix...) + newPrefix = append(newPrefix, rtn[0].Prefix...) + rtn[0].Prefix = newPrefix + } + return rtn +} + +func (state *tokenizeOutputState) ensureLitCurWord(pc *parseContext) { + if state.CurWord == nil { + state.CurWord = &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true, Prefix: state.SavedPrefix} + state.SavedPrefix = nil + return + } + if state.CurWord.Type == WordTypeLit { + return + } + state.ensureGroupWord() + lastWord := state.CurWord.Subs[len(state.CurWord.Subs)-1] + if lastWord.Type != WordTypeLit { + if len(state.SavedPrefix) > 0 { + dumpWords(state.Rtn, "**") + dumpWords([]*wordType{state.CurWord}, ">>") + fmt.Printf("sp: %q\n", state.SavedPrefix) + panic("invalid state, there can be no saved prefix") + } + litWord := &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true} + state.CurWord.Subs = append(state.CurWord.Subs, litWord) + } } func (state *tokenizeOutputState) delimitCurWord() { @@ -45,40 +107,51 @@ func (state *tokenizeOutputState) delimitWithSpace(spaceCh rune) { state.SavedPrefix = append(state.SavedPrefix, spaceCh) } +func (state *tokenizeOutputState) appendLiteral(pc *parseContext, ch rune) { + state.ensureLitCurWord(pc) + if state.CurWord.Type == WordTypeLit { + state.CurWord.Raw = append(state.CurWord.Raw, ch) + } else if state.CurWord.Type == WordTypeGroup { + lastWord := state.CurWord.Subs[len(state.CurWord.Subs)-1] + if lastWord.Type != WordTypeLit { + panic(fmt.Sprintf("invalid curword type (group) %q", state.CurWord.Type)) + } + lastWord.Raw = append(lastWord.Raw, ch) + } else { + panic(fmt.Sprintf("invalid curword type %q", state.CurWord.Type)) + } +} + func (state *tokenizeOutputState) finish(pc *parseContext) { state.delimitCurWord() if len(state.SavedPrefix) > 0 { - state.ensureCurWord(pc) + state.ensureLitCurWord(pc) state.delimitCurWord() } } -func Tokenize(cmd string) []*wordType { - c := &parseContext{Input: []rune(cmd)} +func (c *parseContext) tokenizeVarBrace() ([]*wordType, bool) { state := &tokenizeOutputState{} + eofExit := false for { ch := c.cur() if ch == 0 { + eofExit = true break } - // fmt.Printf("ch %d %q\n", c.Pos, string([]rune{ch})) - foundOp, newOffset := c.parseOp(0) - if foundOp { - opWord := &wordType{Type: WordTypeOp, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: true} - opWord.Val = string(opWord.Raw) - c.Pos = c.Pos + newOffset - state.appendWord(opWord) - continue + if ch == '}' { + c.Pos++ + break } var quoteWord *wordType - switch ch { - case '\'': + if ch == '\'' { quoteWord = c.parseStrSQ() - - case '"': + } + if quoteWord == nil && ch == '"' { quoteWord = c.parseStrDQ() - - case '$': + } + isNextBrace := c.at(1) == '}' + if quoteWord == nil && ch == '$' && !isNextBrace { quoteWord = c.parseStrANSI() if quoteWord == nil { quoteWord = c.parseStrDDQ() @@ -92,8 +165,124 @@ func Tokenize(cmd string) []*wordType { continue } if ch == '\\' && c.at(1) != 0 { - state.ensureCurWord(c) - state.CurWord.Raw = append(state.CurWord.Raw, ch, c.at(1)) + state.appendLiteral(c, ch) + state.appendLiteral(c, c.at(1)) + c.Pos += 2 + continue + } + state.appendLiteral(c, ch) + c.Pos++ + } + return state.Rtn, eofExit +} + +func (c *parseContext) tokenizeDQ() ([]*wordType, bool) { + state := &tokenizeOutputState{} + eofExit := false + for { + ch := c.cur() + if ch == 0 { + eofExit = true + break + } + if ch == '"' { + c.Pos++ + break + } + if ch == '$' && c.at(1) != 0 { + quoteWord := c.parseStrANSI() + if quoteWord == nil { + quoteWord = c.parseStrDDQ() + } + if quoteWord == nil { + quoteWord = c.parseExpansion() + } + if quoteWord != nil { + state.appendWord(quoteWord) + continue + } + } + if ch == '\\' && c.at(1) != 0 { + state.appendLiteral(c, ch) + state.appendLiteral(c, c.at(1)) + c.Pos += 2 + continue + } + state.appendLiteral(c, ch) + c.Pos++ + } + state.finish(c) + if len(state.Rtn) == 0 { + return nil, eofExit + } + if len(state.Rtn) == 1 && state.Rtn[0].Type == WordTypeGroup { + return ungroupWord(state.Rtn[0]), eofExit + } + return state.Rtn, eofExit +} + +// returns (words, eofexit) +func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { + state := &tokenizeOutputState{} + isExpSubShell := c.QC.cur() == WordTypeDP + parenLevel := 0 + eofExit := false + for { + ch := c.cur() + if ch == 0 { + eofExit = true + break + } + if isExpSubShell && ch == ')' && parenLevel == 0 { + c.Pos++ + break + } + // fmt.Printf("ch %d %q\n", c.Pos, string([]rune{ch})) + foundOp, newOffset := c.parseOp(0) + if foundOp { + rawOp := c.Input[c.Pos : c.Pos+newOffset] + opVal := string(rawOp) + opWord := &wordType{Type: WordTypeOp, Offset: c.Pos, Raw: rawOp, Val: opVal, Complete: true} + if opWord.Val == "(" { + arithWord := c.parseArith(true) + if arithWord != nil { + state.appendStandaloneWord(arithWord) + continue + } else { + parenLevel++ + } + } + if opWord.Val == ")" { + parenLevel-- + } + c.Pos = c.Pos + newOffset + state.appendStandaloneWord(opWord) + continue + } + var quoteWord *wordType + if ch == '\'' { + quoteWord = c.parseStrSQ() + } + if quoteWord == nil && ch == '"' { + quoteWord = c.parseStrDQ() + } + isNextParen := isExpSubShell && c.at(1) == ')' + if quoteWord == nil && ch == '$' && !isNextParen { + quoteWord = c.parseStrANSI() + if quoteWord == nil { + quoteWord = c.parseStrDDQ() + } + if quoteWord == nil { + quoteWord = c.parseExpansion() + } + } + if quoteWord != nil { + state.appendWord(quoteWord) + continue + } + if ch == '\\' && c.at(1) != 0 { + state.appendLiteral(c, ch) + state.appendLiteral(c, c.at(1)) c.Pos += 2 continue } @@ -102,12 +291,28 @@ func Tokenize(cmd string) []*wordType { c.Pos++ continue } - state.ensureCurWord(c) - state.CurWord.Raw = append(state.CurWord.Raw, ch) + state.appendLiteral(c, ch) c.Pos++ } state.finish(c) - return state.Rtn + return state.Rtn, eofExit +} + +func Tokenize(cmd string) []*wordType { + c := &parseContext{Input: []rune(cmd)} + rtn, _ := c.tokenizeRaw() + return rtn +} + +func (w *wordType) FullRawString() []rune { + if w.Type == WordTypeGroup { + var rtn []rune + for _, sw := range w.Subs { + rtn = append(rtn, sw.FullRawString()...) + } + return rtn + } + return w.Raw } func wordsToStr(words []*wordType) string { @@ -116,7 +321,7 @@ func wordsToStr(words []*wordType) string { if len(word.Prefix) > 0 { buf.WriteString(string(word.Prefix)) } - buf.WriteString(string(word.Raw)) + buf.WriteString(string(word.FullRawString())) } return buf.String() } From ec2de4609ba01bc8311af80302381c0d3695e2f3 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 16 Nov 2022 11:13:15 -0800 Subject: [PATCH 177/397] add QC to word, refactor makeWord, fix special variables --- pkg/shparse/shparse.go | 181 +++++----------------------------------- pkg/shparse/tokenize.go | 20 ++--- 2 files changed, 32 insertions(+), 169 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index acb219983..192f6b88c 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -123,6 +123,7 @@ type parseContext struct { type wordType struct { Type string Offset int + QC quoteContext Raw []rune Complete bool Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions) @@ -166,12 +167,12 @@ func (c *parseContext) match3(ch rune, ch2 rune, ch3 rune) bool { return c.at(0) == ch && c.at(1) == ch2 && c.at(2) == ch3 } -func (c *parseContext) newOp(length int) *wordType { - rtn := &wordType{Type: WordTypeOp} +func (c *parseContext) makeWord(t string, length int, complete bool) *wordType { + rtn := &wordType{Type: t} rtn.Offset = c.Pos + rtn.QC = c.QC rtn.Raw = c.Input[c.Pos : c.Pos+length] - rtn.Val = string(rtn.Raw) - rtn.Complete = true + rtn.Complete = complete c.Pos += length return rtn } @@ -251,13 +252,7 @@ func (c *parseContext) parseStrSQ() *wordType { return nil } newOffset, complete := c.skipToChar(1, '\'', false) - w := &wordType{ - Type: WordTypeDQ, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: complete, - } - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeSQ, newOffset, complete) return w } @@ -268,14 +263,8 @@ func (c *parseContext) parseStrDQ() *wordType { newContext := c.clone(c.Pos+1, WordTypeDQ) subWords, eofExit := newContext.tokenizeDQ() newOffset := newContext.Pos + 1 - w := &wordType{ - Type: WordTypeDQ, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: !eofExit, - Subs: subWords, - } - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeDQ, newOffset, !eofExit) + w.Subs = subWords return w } @@ -284,13 +273,7 @@ func (c *parseContext) parseStrBQ() *wordType { return nil } newOffset, complete := c.skipToChar(1, '`', true) - w := &wordType{ - Type: WordTypeBQ, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: complete, - } - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeBQ, newOffset, complete) return w } @@ -299,13 +282,7 @@ func (c *parseContext) parseStrANSI() *wordType { return nil } newOffset, complete := c.skipToChar(2, '\'', true) - w := &wordType{ - Type: WordTypeDSQ, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: complete, - } - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeDSQ, newOffset, complete) return w } @@ -314,13 +291,7 @@ func (c *parseContext) parseStrDDQ() *wordType { return nil } newOffset, complete := c.skipToChar(2, '"', true) - w := &wordType{ - Type: WordTypeDDQ, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: complete, - } - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeDDQ, newOffset, complete) return w } @@ -332,8 +303,7 @@ func (c *parseContext) parseArith(mustComplete bool) *wordType { if mustComplete && !complete { return nil } - w := &wordType{Type: WordTypeArith, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeArith, newOffset, complete) return w } @@ -343,8 +313,7 @@ func (c *parseContext) parseExpansion() *wordType { } if c.match3('$', '(', '(') { newOffset, complete := c.skipToChar2(3, ')', ')', false) - w := &wordType{Type: WordTypeDPP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeDPP, newOffset, complete) return w } if c.match2('$', '(') { @@ -352,17 +321,14 @@ func (c *parseContext) parseExpansion() *wordType { newContext := c.clone(c.Pos+2, WordTypeDP) subWords, eofExit := newContext.tokenizeRaw() newOffset := newContext.Pos + 2 - // newOffset, complete := c.skipToChar(2, ')', false) - w := &wordType{Type: WordTypeDP, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: !eofExit} + w := c.makeWord(WordTypeDP, newOffset, !eofExit) w.Subs = subWords - c.Pos = c.Pos + newOffset return w } if c.match2('$', '[') { // deprecated arith expansion newOffset, complete := c.skipToChar(2, ']', false) - w := &wordType{Type: WordTypeDB, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: complete} - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeDB, newOffset, complete) return w } if c.match2('$', '{') { @@ -370,8 +336,7 @@ func (c *parseContext) parseExpansion() *wordType { newContext := c.clone(c.Pos+2, WordTypeVarBrace) _, eofExit := newContext.tokenizeVarBrace() newOffset := newContext.Pos + 2 - w := &wordType{Type: WordTypeVarBrace, Offset: c.Pos, Raw: c.Input[c.Pos : c.Pos+newOffset], Complete: !eofExit} - c.Pos = c.Pos + newOffset + w := c.makeWord(WordTypeVarBrace, newOffset, !eofExit) return w } ch2 := c.at(1) @@ -382,24 +347,15 @@ func (c *parseContext) parseExpansion() *wordType { newOffset := c.parseSimpleVarName(1) if newOffset > 1 { // simple variable name - rtn := &wordType{ - Type: WordTypeSimpleVar, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+newOffset], - Complete: true, - } - c.Pos = c.Pos + newOffset - return rtn + w := c.makeWord(WordTypeSimpleVar, newOffset, true) + return w } - // single character variable name, e.g. $@, $_, $1, etc. - rtn := &wordType{ - Type: WordTypeSimpleVar, - Offset: c.Pos, - Raw: c.Input[c.Pos : c.Pos+2], - Complete: true, + if ch2 == '*' || ch2 == '@' || ch2 == '#' || ch2 == '?' || ch2 == '-' || ch2 == '$' || ch2 == '!' || (ch2 >= '0' && ch2 <= '9') { + // single character variable name, e.g. $@, $_, $1, etc. + w := c.makeWord(WordTypeSimpleVar, 2, true) + return w } - c.Pos += 2 - return rtn + return nil } func (c *parseContext) parseShellTest() *wordType { @@ -433,97 +389,6 @@ func (c *parseContext) parseSimpleVarName(offset int) int { } } -func parseInput(inputStr string) []*wordType { - c := &parseContext{Input: []rune(inputStr)} - var rtn []*wordType - var litWord *wordType - for { - var quoteWord *wordType - ch := c.cur() - if ch == 0 { - break - } - switch ch { - case '\'': - quoteWord = c.parseStrSQ() - - case '"': - quoteWord = c.parseStrDQ() - - case '$': - quoteWord = c.parseStrANSI() - if quoteWord == nil { - quoteWord = c.parseStrDDQ() - } - if quoteWord == nil { - quoteWord = c.parseExpansion() - } - } - if quoteWord != nil { - if litWord != nil { - rtn = append(rtn, litWord) - litWord = nil - } - rtn = append(rtn, quoteWord) - continue - } - if litWord == nil { - litWord = &wordType{Type: WordTypeLit, Offset: c.Pos, Complete: true} - } - if ch == '\\' && c.at(1) != 0 { - litWord.Raw = append(litWord.Raw, ch, c.at(1)) - c.Pos += 2 - continue - } - litWord.Raw = append(litWord.Raw, ch) - c.Pos++ - } - if litWord != nil { - rtn = append(rtn, litWord) - } - - // now we want to expand ops - rtn = expandAllOps(rtn) - - return rtn -} - -func expandOps(word *wordType) []*wordType { - if word.Type != WordTypeLit { - return nil - } - var lastBackSlash bool - var rtn []*wordType - for _, ch := range word.Raw { - if ch == 0 { - break - } - if ch == '\\' { - lastBackSlash = true - continue - } - if lastBackSlash { - lastBackSlash = false - continue - } - } - rtn = append(rtn, word) - return rtn -} - -func expandAllOps(words []*wordType) []*wordType { - var rtn []*wordType - for _, word := range words { - exWords := expandOps(word) - if len(exWords) == 0 { - rtn = append(rtn, word) - } else { - rtn = append(rtn, exWords...) - } - } - return rtn -} - func makeSpaceStr(slen int) string { if slen == 0 { return "" diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index d2b45b683..67f45688c 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -51,6 +51,7 @@ func (state *tokenizeOutputState) ensureGroupWord() { groupWord := &wordType{ Type: WordTypeGroup, Offset: state.CurWord.Offset, + QC: state.CurWord.QC, Complete: true, Prefix: state.CurWord.Prefix, } @@ -74,7 +75,8 @@ func ungroupWord(w *wordType) []*wordType { func (state *tokenizeOutputState) ensureLitCurWord(pc *parseContext) { if state.CurWord == nil { - state.CurWord = &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true, Prefix: state.SavedPrefix} + state.CurWord = pc.makeWord(WordTypeLit, 0, true) + state.CurWord.Prefix = state.SavedPrefix state.SavedPrefix = nil return } @@ -85,12 +87,9 @@ func (state *tokenizeOutputState) ensureLitCurWord(pc *parseContext) { lastWord := state.CurWord.Subs[len(state.CurWord.Subs)-1] if lastWord.Type != WordTypeLit { if len(state.SavedPrefix) > 0 { - dumpWords(state.Rtn, "**") - dumpWords([]*wordType{state.CurWord}, ">>") - fmt.Printf("sp: %q\n", state.SavedPrefix) panic("invalid state, there can be no saved prefix") } - litWord := &wordType{Type: WordTypeLit, Offset: pc.Pos, Complete: true} + litWord := pc.makeWord(WordTypeLit, 0, true) state.CurWord.Subs = append(state.CurWord.Subs, litWord) } } @@ -240,10 +239,8 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { // fmt.Printf("ch %d %q\n", c.Pos, string([]rune{ch})) foundOp, newOffset := c.parseOp(0) if foundOp { - rawOp := c.Input[c.Pos : c.Pos+newOffset] - opVal := string(rawOp) - opWord := &wordType{Type: WordTypeOp, Offset: c.Pos, Raw: rawOp, Val: opVal, Complete: true} - if opWord.Val == "(" { + opVal := string(c.Input[c.Pos : c.Pos+newOffset]) + if opVal == "(" { arithWord := c.parseArith(true) if arithWord != nil { state.appendStandaloneWord(arithWord) @@ -252,10 +249,11 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { parenLevel++ } } - if opWord.Val == ")" { + if opVal == ")" { parenLevel-- } - c.Pos = c.Pos + newOffset + opWord := c.makeWord(WordTypeOp, newOffset, true) + opWord.Val = opVal state.appendStandaloneWord(opWord) continue } From 475d7cd6479fe951e32294fbf2155d118f29b163 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 16 Nov 2022 12:00:44 -0800 Subject: [PATCH 178/397] subparse backticks and dollar double quote --- pkg/shparse/shparse.go | 72 ++++++++++++++++--------------------- pkg/shparse/shparse_test.go | 2 ++ pkg/shparse/tokenize.go | 10 ++++++ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 192f6b88c..0f55bab8a 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -82,20 +82,18 @@ const ( WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ WordTypeSimpleVar = "svar" // simplevar $ WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x - WordTypeArith = "ath" - // each of these can also be used as an entry in quoteContext - WordTypeDQ = "dq" // " - WordTypeSQ = "sq" // ' - WordTypeBQ = "bq" // ` - WordTypeDSQ = "dsq" // $' - WordTypeDDQ = "ddq" // $" - WordTypeVarBrace = "varb" // ${ - WordTypeDP = "dp" // $( - WordTypeDPP = "dpp" // $(( - WordTypeP = "p" // ( - WordTypePP = "pp" // (( - WordTypeDB = "db" // $[ + WordTypeDQ = "dq" // " (quote-context) + WordTypeDDQ = "ddq" // $" (quote-context) + WordTypeVarBrace = "varb" // ${ (quote-context) + WordTypeDP = "dp" // $( (quote-context) + WordTypeBQ = "bq" // ` (quote-context) + + WordTypeSQ = "sq" // ' + WordTypeDSQ = "dsq" // $' + WordTypeDPP = "dpp" // $(( (internals not parsed) + WordTypePP = "pp" // (( (internals not parsed) + WordTypeDB = "db" // $[ (internals not parsed) ) type quoteContext []string @@ -268,12 +266,27 @@ func (c *parseContext) parseStrDQ() *wordType { return w } -func (c *parseContext) parseStrBQ() *wordType { - if c.match('`') { +func (c *parseContext) parseStrDDQ() *wordType { + if !c.match2('$', '"') { return nil } - newOffset, complete := c.skipToChar(1, '`', true) - w := c.makeWord(WordTypeBQ, newOffset, complete) + newContext := c.clone(c.Pos+2, WordTypeDDQ) + subWords, eofExit := newContext.tokenizeDQ() + newOffset := newContext.Pos + 2 + w := c.makeWord(WordTypeDDQ, newOffset, !eofExit) + w.Subs = subWords + return w +} + +func (c *parseContext) parseStrBQ() *wordType { + if !c.match('`') { + return nil + } + newContext := c.clone(c.Pos+1, WordTypeBQ) + subWords, eofExit := newContext.tokenizeRaw() + newOffset := newContext.Pos + 1 + w := c.makeWord(WordTypeBQ, newOffset, !eofExit) + w.Subs = subWords return w } @@ -286,15 +299,6 @@ func (c *parseContext) parseStrANSI() *wordType { return w } -func (c *parseContext) parseStrDDQ() *wordType { - if !c.match2('$', '"') { - return nil - } - newOffset, complete := c.skipToChar(2, '"', true) - w := c.makeWord(WordTypeDDQ, newOffset, complete) - return w -} - func (c *parseContext) parseArith(mustComplete bool) *wordType { if !c.match2('(', '(') { return nil @@ -303,7 +307,7 @@ func (c *parseContext) parseArith(mustComplete bool) *wordType { if mustComplete && !complete { return nil } - w := c.makeWord(WordTypeArith, newOffset, complete) + w := c.makeWord(WordTypePP, newOffset, complete) return w } @@ -358,20 +362,6 @@ func (c *parseContext) parseExpansion() *wordType { return nil } -func (c *parseContext) parseShellTest() *wordType { - if !c.match2('[', '[') { - return nil - } - return nil -} - -func (c *parseContext) parseProcessSubstitution() *wordType { - if !c.match2('<', '(') && !c.match2('>', '(') { - return nil - } - return nil -} - // returns newOffset func (c *parseContext) parseSimpleVarName(offset int) int { first := true diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 187c50cfb..970318820 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -43,4 +43,6 @@ func Test1(t *testing.T) { testParse(t, `echo $(ls $)`) testParse(t, `echo ${x:-hello\}"}"} 2nd`) testParse(t, `echo "$(ls "foo") more $x"`) + testParse(t, "echo `ls $x \"hello $x\" \\`ls\\`; ./foo`") + testParse(t, `echo $"hello $x $(ls)"`) } diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index 67f45688c..61c8e04d8 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -221,9 +221,12 @@ func (c *parseContext) tokenizeDQ() ([]*wordType, bool) { } // returns (words, eofexit) +// backticks (WordTypeBQ) handle backslash in a special way, but that seems to mainly effect execution (not completion) +// de_backslash => removes initial backslash in \`, \\, and \$ before execution func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { state := &tokenizeOutputState{} isExpSubShell := c.QC.cur() == WordTypeDP + isInBQ := c.QC.cur() == WordTypeBQ parenLevel := 0 eofExit := false for { @@ -236,6 +239,10 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { c.Pos++ break } + if isInBQ && ch == '`' { + c.Pos++ + break + } // fmt.Printf("ch %d %q\n", c.Pos, string([]rune{ch})) foundOp, newOffset := c.parseOp(0) if foundOp { @@ -264,6 +271,9 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { if quoteWord == nil && ch == '"' { quoteWord = c.parseStrDQ() } + if quoteWord == nil && ch == '`' { + quoteWord = c.parseStrBQ() + } isNextParen := isExpSubShell && c.at(1) == ')' if quoteWord == nil && ch == '$' && !isNextParen { quoteWord = c.parseStrANSI() From 7f4e5c0ef13b961f5ce5b60d09fac466c3ef7926 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 16 Nov 2022 22:17:38 -0800 Subject: [PATCH 179/397] checkpoint, starting extend --- pkg/shparse/extend.go | 152 +++++++++++++++++++++++++++++++++++++++++ pkg/shparse/shparse.go | 14 ++-- pkg/utilfn/utilfn.go | 2 + 3 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 pkg/shparse/extend.go diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go new file mode 100644 index 000000000..457a454dd --- /dev/null +++ b/pkg/shparse/extend.go @@ -0,0 +1,152 @@ +package shparse + +import ( + "bytes" + "fmt" + "unicode" + "unicode/utf8" + + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" +) + +var noEscChars []bool +var specialEsc []string + +func init() { + noEscChars = make([]bool, 256) + for ch := 0; ch < 256; ch++ { + if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + ch == '-' || ch == '.' || ch == '/' || ch == ':' || ch == '=' { + noEscChars[byte(ch)] = true + } + } + specialEsc = make([]string, 256) + specialEsc[0x7] = "\\a" + specialEsc[0x8] = "\\b" + specialEsc[0x9] = "\\t" + specialEsc[0xa] = "\\n" + specialEsc[0xb] = "\\v" + specialEsc[0xc] = "\\f" + specialEsc[0xd] = "\\r" + specialEsc[0x1b] = "\\E" +} + +func getUtf8Literal(ch rune) string { + var buf bytes.Buffer + var runeArr [utf8.UTFMax]byte + barr := runeArr[:] + byteLen := utf8.EncodeRune(barr, ch) + for i := 0; i < byteLen; i++ { + buf.WriteString("\\x") + buf.WriteByte(utilfn.HexDigits[barr[i]/16]) + buf.WriteByte(utilfn.HexDigits[barr[i]%16]) + } + return buf.String() +} + +func (w *wordType) writeString(s string) { + for _, ch := range s { + w.writeRune(ch) + } +} + +func (w *wordType) writeRune(ch rune) { + if w.Type == WordTypeLit { + w.Raw = append(w.Raw, ch) + return + } + if w.Type == WordTypeDQ || w.Type == WordTypeDDQ || w.Type == WordTypeSimpleVar || w.Type == WordTypeVarBrace || w.Type == WordTypeSQ || w.Type == WordTypeDSQ { + w.Raw = append(w.Raw[0:len(w.Raw)-1], ch, w.Raw[len(w.Raw)-1]) + return + } + panic(fmt.Sprintf("cannot extend type %q", w.Type)) +} + +func (w *wordType) cloneRaw() { + if len(w.Raw) == 0 { + return + } + buf := make([]rune, 0, len(w.Raw)) + w.Raw = append(buf, w.Raw...) +} + +type extendContext struct { + QC quoteContext + Rtn []*wordType + CurWord *wordType + Intention string +} + +func makeExtendContext(qc quoteContext, w *wordType, intention string) *extendContext { + rtn := &extendContext{QC: qc, Intention: intention} + if w != nil { + w.cloneRaw() + rtn.Rtn = []*wordType{w} + rtn.CurWord = w + } + return rtn +} + +func (ec *extendContext) appendWord(w *wordType) { + ec.Rtn = append(ec.Rtn, w) + ec.CurWord = w +} + +func (ec *extendContext) extend(ch rune) { + if ch == 0 { + return + } + if ec.CurWord == nil { + ec.CurWord = &wordType{Type: WordTypeLit, QC: ec.QC} + ec.Rtn = append(ec.Rtn, ec.CurWord) + } + switch ec.CurWord.Type { + case WordTypeSimpleVar: + + case WordTypeDQ, WordTypeDDQ: + + case WordTypeVarBrace: + + case WordTypeSQ: + ec.extendSQ(ch) + + case WordTypeDSQ: + + case WordTypeLit: + + default: + + } +} + +func getSpecialEscape(ch rune) string { + if ch > unicode.MaxASCII { + return "" + } + return specialEsc[byte(ch)] +} + +func (ec *extendContext) extendSQ(ch rune) { + if ch == 0 { + return + } + if ch == '\'' { + litWord := &wordType{Type: WordTypeLit, QC: ec.QC} + litWord.Raw = []rune{'\\', '\''} + ec.appendWord(litWord) + } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + dsqWord := &wordType{Type: WordTypeDSQ, QC: ec.QC} + dsqWord.Raw = []rune{'$', '\'', '\''} + ec.appendWord(dsqWord) + sesc := getSpecialEscape(ch) + if sesc != "" { + dsqWord.writeString(sesc) + return + } else { + utf8Lit := getUtf8Literal(ch) + dsqWord.writeString(utf8Lit) + } + } + ec.CurWord.writeRune(ch) +} diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 0f55bab8a..2e71fc072 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -77,20 +77,20 @@ import ( const ( WordTypeRaw = "raw" - WordTypeLit = "lit" + WordTypeLit = "lit" // (can-extend) WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ - WordTypeSimpleVar = "svar" // simplevar $ WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x + WordTypeSimpleVar = "svar" // simplevar $ (can-extend) - WordTypeDQ = "dq" // " (quote-context) - WordTypeDDQ = "ddq" // $" (quote-context) - WordTypeVarBrace = "varb" // ${ (quote-context) + WordTypeDQ = "dq" // " (quote-context) (can-extend) + WordTypeDDQ = "ddq" // $" (quote-context) (can-extend) + WordTypeVarBrace = "varb" // ${ (quote-context) (can-extend) WordTypeDP = "dp" // $( (quote-context) WordTypeBQ = "bq" // ` (quote-context) - WordTypeSQ = "sq" // ' - WordTypeDSQ = "dsq" // $' + WordTypeSQ = "sq" // ' (can-extend) + WordTypeDSQ = "dsq" // $' (can-extend) WordTypeDPP = "dpp" // $(( (internals not parsed) WordTypePP = "pp" // (( (internals not parsed) WordTypeDB = "db" // $[ (internals not parsed) diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index 35db1c4df..fcce5845d 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -5,6 +5,8 @@ import ( "strings" ) +var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} + func GetStrArr(v interface{}, field string) []string { if v == nil { return nil From 082b1464096dcc8fb9a020a693d69c4930bc5f64 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 16 Nov 2022 23:52:10 -0800 Subject: [PATCH 180/397] extension functions, word meta --- pkg/shparse/extend.go | 154 ++++++++++++++++++++++++++++++------ pkg/shparse/shparse.go | 48 +++++++++++ pkg/shparse/shparse_test.go | 36 +++++++++ 3 files changed, 213 insertions(+), 25 deletions(-) diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index 457a454dd..a616a9858 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -2,7 +2,6 @@ package shparse import ( "bytes" - "fmt" "unicode" "unicode/utf8" @@ -51,15 +50,18 @@ func (w *wordType) writeString(s string) { } func (w *wordType) writeRune(ch rune) { - if w.Type == WordTypeLit { - w.Raw = append(w.Raw, ch) - return - } - if w.Type == WordTypeDQ || w.Type == WordTypeDDQ || w.Type == WordTypeSimpleVar || w.Type == WordTypeVarBrace || w.Type == WordTypeSQ || w.Type == WordTypeDSQ { + wmeta := wordMetaMap[w.Type] + if w.Complete && wmeta.SuffixLen == 1 { w.Raw = append(w.Raw[0:len(w.Raw)-1], ch, w.Raw[len(w.Raw)-1]) return } - panic(fmt.Sprintf("cannot extend type %q", w.Type)) + if w.Complete && wmeta.SuffixLen == 2 { + w.Raw = append(w.Raw[0:len(w.Raw)-2], ch, w.Raw[len(w.Raw)-2], w.Raw[len(w.Raw)-1]) + return + } + // not complete or SuffixLen == 0 (2+ is not supported) + w.Raw = append(w.Raw, ch) + return } func (w *wordType) cloneRaw() { @@ -77,12 +79,13 @@ type extendContext struct { Intention string } -func makeExtendContext(qc quoteContext, w *wordType, intention string) *extendContext { - rtn := &extendContext{QC: qc, Intention: intention} +func makeExtendContext(qc quoteContext, w *wordType) *extendContext { + rtn := &extendContext{QC: qc, Intention: WordTypeLit} if w != nil { w.cloneRaw() rtn.Rtn = []*wordType{w} rtn.CurWord = w + rtn.Intention = w.Type } return rtn } @@ -92,30 +95,36 @@ func (ec *extendContext) appendWord(w *wordType) { ec.CurWord = w } +func (ec *extendContext) ensureCurWord() { + if ec.CurWord == nil || ec.CurWord.Type != ec.Intention { + ec.CurWord = makeEmptyWord(ec.Intention, ec.QC, 0) + ec.Rtn = append(ec.Rtn, ec.CurWord) + } +} + func (ec *extendContext) extend(ch rune) { if ch == 0 { return } - if ec.CurWord == nil { - ec.CurWord = &wordType{Type: WordTypeLit, QC: ec.QC} - ec.Rtn = append(ec.Rtn, ec.CurWord) - } - switch ec.CurWord.Type { - case WordTypeSimpleVar: + switch ec.Intention { + + case WordTypeSimpleVar, WordTypeVarBrace: + ec.extendVar(ch) case WordTypeDQ, WordTypeDDQ: - - case WordTypeVarBrace: + ec.extendDQ(ch) case WordTypeSQ: ec.extendSQ(ch) case WordTypeDSQ: + ec.extendDSQ(ch) case WordTypeLit: + ec.extendLit(ch) default: - + return } } @@ -126,18 +135,27 @@ func getSpecialEscape(ch rune) string { return specialEsc[byte(ch)] } -func (ec *extendContext) extendSQ(ch rune) { +func isVarNameChar(ch rune) bool { + return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') +} + +func (ec *extendContext) extendVar(ch rune) { if ch == 0 { return } - if ch == '\'' { - litWord := &wordType{Type: WordTypeLit, QC: ec.QC} - litWord.Raw = []rune{'\\', '\''} - ec.appendWord(litWord) + if !isVarNameChar(ch) { + return + } + ec.ensureCurWord() + ec.CurWord.writeRune(ch) +} + +func (ec *extendContext) extendLit(ch rune) { + if ch == 0 { + return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := &wordType{Type: WordTypeDSQ, QC: ec.QC} - dsqWord.Raw = []rune{'$', '\'', '\''} + dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) ec.appendWord(dsqWord) sesc := getSpecialEscape(ch) if sesc != "" { @@ -147,6 +165,92 @@ func (ec *extendContext) extendSQ(ch rune) { utf8Lit := getUtf8Literal(ch) dsqWord.writeString(utf8Lit) } + return + } + var bch = byte(ch) + ec.ensureCurWord() + if noEscChars[bch] { + ec.CurWord.writeRune(ch) + return + } + ec.CurWord.writeRune('\\') + ec.CurWord.writeRune(ch) + return +} + +func (ec *extendContext) extendDSQ(ch rune) { + if ch == 0 { + return + } + ec.ensureCurWord() + if ch == '\'' { + ec.CurWord.writeRune('\\') + ec.CurWord.writeRune(ch) + return + } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + sesc := getSpecialEscape(ch) + if sesc != "" { + ec.CurWord.writeString(sesc) + } else { + utf8Lit := getUtf8Literal(ch) + ec.CurWord.writeString(utf8Lit) + } + return } ec.CurWord.writeRune(ch) + return +} + +func (ec *extendContext) extendSQ(ch rune) { + if ch == 0 { + return + } + if ch == '\'' { + litWord := &wordType{Type: WordTypeLit, QC: ec.QC} + litWord.Raw = []rune{'\\', '\''} + ec.appendWord(litWord) + return + } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) + ec.appendWord(dsqWord) + sesc := getSpecialEscape(ch) + if sesc != "" { + dsqWord.writeString(sesc) + } else { + utf8Lit := getUtf8Literal(ch) + dsqWord.writeString(utf8Lit) + } + return + } + ec.ensureCurWord() + ec.CurWord.writeRune(ch) + return +} + +func (ec *extendContext) extendDQ(ch rune) { + if ch == 0 { + return + } + if ch == '"' || ch == '\\' || ch == '$' || ch == '`' { + ec.ensureCurWord() + ec.CurWord.writeRune('\\') + ec.CurWord.writeRune(ch) + return + } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) + ec.appendWord(dsqWord) + sesc := getSpecialEscape(ch) + if sesc != "" { + dsqWord.writeString(sesc) + } else { + utf8Lit := getUtf8Literal(ch) + dsqWord.writeString(utf8Lit) + } + return + } + ec.CurWord.writeRune(ch) + return } diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 2e71fc072..13030b342 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -75,6 +75,8 @@ import ( // tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003 +// can-extend: WordTypeLit, WordTypeSimpleVar, WordTypeVarBrace, WordTypeDQ, WordTypeDDQ, WordTypeSQ, WordTypeDSQ + const ( WordTypeRaw = "raw" WordTypeLit = "lit" // (can-extend) @@ -96,6 +98,52 @@ const ( WordTypeDB = "db" // $[ (internals not parsed) ) +var wordMetaMap map[string]wordMeta + +type wordMeta struct { + Type string + EmptyWord []rune + SuffixLen int + CanExtend bool + QuoteContext bool +} + +func makeWordMeta(wtype string, emptyWord string, suffixLen int, canExtend bool, quoteContext bool) { + wordMetaMap[wtype] = wordMeta{wtype, []rune(emptyWord), suffixLen, canExtend, quoteContext} +} + +func init() { + wordMetaMap = make(map[string]wordMeta) + makeWordMeta(WordTypeRaw, "", 0, false, false) + makeWordMeta(WordTypeLit, "", 0, true, false) + makeWordMeta(WordTypeOp, "", 0, false, false) + makeWordMeta(WordTypeKey, "", 0, false, false) + makeWordMeta(WordTypeGroup, "", 0, false, false) + makeWordMeta(WordTypeSimpleVar, "$", 0, true, false) + makeWordMeta(WordTypeVarBrace, "${}", 1, true, true) + makeWordMeta(WordTypeDQ, `""`, 1, true, true) + makeWordMeta(WordTypeDDQ, `$""`, 1, true, true) + makeWordMeta(WordTypeDP, "$()", 1, false, false) + makeWordMeta(WordTypeBQ, "``", 1, false, false) + makeWordMeta(WordTypeSQ, "''", 1, true, false) + makeWordMeta(WordTypeDSQ, "$''", 1, true, false) + makeWordMeta(WordTypeDPP, "$(())", 2, false, false) + makeWordMeta(WordTypePP, "(())", 2, false, false) + makeWordMeta(WordTypeDB, "$[]", 1, false, false) +} + +func makeEmptyWord(wtype string, qc quoteContext, offset int) *wordType { + meta := wordMetaMap[wtype] + if meta.Type == "" { + meta = wordMetaMap[WordTypeRaw] + } + rtn := &wordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true} + if len(meta.EmptyWord) > 0 { + rtn.Raw = append([]rune(nil), meta.EmptyWord...) + } + return rtn +} + type quoteContext []string func (qc quoteContext) push(q string) quoteContext { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 970318820..ae2074c92 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -46,3 +46,39 @@ func Test1(t *testing.T) { testParse(t, "echo `ls $x \"hello $x\" \\`ls\\`; ./foo`") testParse(t, `echo $"hello $x $(ls)"`) } + +func lastWord(words []*wordType) *wordType { + if len(words) == 0 { + return nil + } + return words[len(words)-1] +} + +func testExtend(t *testing.T, startStr string, extendStr string, expectedStr string) { + words := Tokenize(startStr) + ec := makeExtendContext(nil, lastWord(words)) + for _, ch := range extendStr { + ec.extend(ch) + } + ec.ensureCurWord() + output := wordsToStr(ec.Rtn) + fmt.Printf("[%s] + [%s] => [%s]\n", startStr, extendStr, output) + if output != expectedStr { + t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, output, expectedStr) + } +} + +func Test2(t *testing.T) { + testExtend(t, `'he'`, "llo", `'hello'`) + testExtend(t, `'he'`, "'", `'he'\'''`) + testExtend(t, `'he'`, "'\x01", `'he'\'$'\x01'''`) + testExtend(t, `he`, "llo", `hello`) + testExtend(t, `he`, "l*l'\x01\x07o", `hel\*l\'$'\x01'$'\a'o`) + testExtend(t, `$x`, "fo|o", `$xfoo`) + testExtend(t, `${x`, "fo|o", `${xfoo`) + testExtend(t, `$'f`, "oo", `$'foo`) + testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`) + testExtend(t, `"f"`, "oo", `"foo"`) + testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`) + testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\x07'""`) +} From a599dc473aece30c9759dbeddae7ffb4742afe27 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 18 Nov 2022 00:09:18 -0800 Subject: [PATCH 181/397] split words into simple commands. identify bash keywords. light parsing of bash language to get command separation --- pkg/shparse/extend.go | 28 +- pkg/shparse/shparse.go | 660 ++++++++++++++++++------------------ pkg/shparse/shparse_test.go | 22 +- pkg/shparse/tokenize.go | 313 ++++++++++++++--- 4 files changed, 646 insertions(+), 377 deletions(-) diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index a616a9858..99b3ead95 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -43,13 +43,13 @@ func getUtf8Literal(ch rune) string { return buf.String() } -func (w *wordType) writeString(s string) { +func (w *WordType) writeString(s string) { for _, ch := range s { w.writeRune(ch) } } -func (w *wordType) writeRune(ch rune) { +func (w *WordType) writeRune(ch rune) { wmeta := wordMetaMap[w.Type] if w.Complete && wmeta.SuffixLen == 1 { w.Raw = append(w.Raw[0:len(w.Raw)-1], ch, w.Raw[len(w.Raw)-1]) @@ -64,7 +64,7 @@ func (w *wordType) writeRune(ch rune) { return } -func (w *wordType) cloneRaw() { +func (w *WordType) cloneRaw() { if len(w.Raw) == 0 { return } @@ -73,31 +73,31 @@ func (w *wordType) cloneRaw() { } type extendContext struct { - QC quoteContext - Rtn []*wordType - CurWord *wordType + QC QuoteContext + Rtn []*WordType + CurWord *WordType Intention string } -func makeExtendContext(qc quoteContext, w *wordType) *extendContext { +func makeExtendContext(qc QuoteContext, w *WordType) *extendContext { rtn := &extendContext{QC: qc, Intention: WordTypeLit} if w != nil { w.cloneRaw() - rtn.Rtn = []*wordType{w} + rtn.Rtn = []*WordType{w} rtn.CurWord = w rtn.Intention = w.Type } return rtn } -func (ec *extendContext) appendWord(w *wordType) { +func (ec *extendContext) appendWord(w *WordType) { ec.Rtn = append(ec.Rtn, w) ec.CurWord = w } func (ec *extendContext) ensureCurWord() { if ec.CurWord == nil || ec.CurWord.Type != ec.Intention { - ec.CurWord = makeEmptyWord(ec.Intention, ec.QC, 0) + ec.CurWord = MakeEmptyWord(ec.Intention, ec.QC, 0) ec.Rtn = append(ec.Rtn, ec.CurWord) } } @@ -155,7 +155,7 @@ func (ec *extendContext) extendLit(ch rune) { return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) + dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) ec.appendWord(dsqWord) sesc := getSpecialEscape(ch) if sesc != "" { @@ -207,13 +207,13 @@ func (ec *extendContext) extendSQ(ch rune) { return } if ch == '\'' { - litWord := &wordType{Type: WordTypeLit, QC: ec.QC} + litWord := &WordType{Type: WordTypeLit, QC: ec.QC} litWord.Raw = []rune{'\\', '\''} ec.appendWord(litWord) return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) + dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) ec.appendWord(dsqWord) sesc := getSpecialEscape(ch) if sesc != "" { @@ -240,7 +240,7 @@ func (ec *extendContext) extendDQ(ch rune) { return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := makeEmptyWord(WordTypeDSQ, ec.QC, 0) + dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) ec.appendWord(dsqWord) sesc := getSpecialEscape(ch) if sesc != "" { diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 13030b342..3ac8a38a8 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -1,8 +1,8 @@ package shparse import ( + "bytes" "fmt" - "unicode" ) // @@ -29,54 +29,14 @@ import ( // // // -// $var -// ${var} -// ${var op word?} -// op := '-' | '=' | '?' | '+' | ':-' | ':=' | ':?' | ':+' | '%' | '%%' | '#' | '##' -// ${ '#' var } -// -// $(command) -// `command` -// $(( arith )) -// -// " ... " -// ' ... ' -// $' ... ' -// $" ... ' - -// " => $, ", `, \ -// ' => ' -// (process quotes) -// mark as escaped -// split into commands (use ';' as separator) -// parse special operators -// perform expansions (vars, globs, commands) -// split command into name and arguments - // A correctly-formed brace expansion must contain unquoted opening and closing braces, and at least one unquoted comma or a valid sequence expression // Any incorrectly formed brace expansion is left unchanged. - -// word: char *word; flags -// bash aliases are lexical - -// [[, ((, $(( <- DQ - -// $ -> expansion -// $(...) -// (...) -// $((...)) -// ((...)) -// ${...} -// {...} -// X=(...) - +// // ambiguity between $((...)) and $((ls); ls) // ambiguity between foo=([0]=hell) and foo=([abc) - // tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003 // can-extend: WordTypeLit, WordTypeSimpleVar, WordTypeVarBrace, WordTypeDQ, WordTypeDDQ, WordTypeSQ, WordTypeDSQ - const ( WordTypeRaw = "raw" WordTypeLit = "lit" // (can-extend) @@ -98,8 +58,48 @@ const ( WordTypeDB = "db" // $[ (internals not parsed) ) +const ( + CmdTypeNone = "none" // holds control structures: '(' ')' 'for' 'while' etc. + CmdTypeSimple = "simple" // holds real commands +) + +type WordType struct { + Type string + Offset int + QC QuoteContext + Raw []rune + Complete bool + Prefix []rune + Subs []*WordType +} + +type CmdType struct { + Type string + AssignmentWords []*WordType + Words []*WordType +} + +type QuoteContext []string + var wordMetaMap map[string]wordMeta +// same order as https://www.gnu.org/software/bash/manual/html_node/Reserved-Words.html +var bashReservedWords = []string{ + "if", "then", "elif", "else", "fi", "time", + "for", "in", "until", "while", "do", "done", + "case", "esac", "coproc", "select", "function", + "{", "}", "[[", "]]", "!", +} + +// special reserved words: "for", "in", "case", "select", "function", "[[", and "]]" + +var bashNoneRW = []string{ + "if", "then", "elif", "else", "fi", "time", + "until", "while", "do", "done", + "esac", "coproc", + "{", "}", "!", +} + type wordMeta struct { Type string EmptyWord []rune @@ -132,324 +132,52 @@ func init() { makeWordMeta(WordTypeDB, "$[]", 1, false, false) } -func makeEmptyWord(wtype string, qc quoteContext, offset int) *wordType { +func MakeEmptyWord(wtype string, qc QuoteContext, offset int) *WordType { meta := wordMetaMap[wtype] if meta.Type == "" { meta = wordMetaMap[WordTypeRaw] } - rtn := &wordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true} + rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true} if len(meta.EmptyWord) > 0 { rtn.Raw = append([]rune(nil), meta.EmptyWord...) } return rtn } -type quoteContext []string - -func (qc quoteContext) push(q string) quoteContext { +func (qc QuoteContext) push(q string) QuoteContext { rtn := make([]string, 0, len(qc)+1) rtn = append(rtn, qc...) rtn = append(rtn, q) return rtn } -func (qc quoteContext) cur() string { +func (qc QuoteContext) cur() string { if len(qc) == 0 { return "" } return qc[len(qc)-1] } -type parseContext struct { - Input []rune - Pos int - QC quoteContext -} - -type wordType struct { - Type string - Offset int - QC quoteContext - Raw []rune - Complete bool - Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions) - Prefix []rune - Subs []*wordType -} - -func (c *parseContext) clone(pos int, newQuote string) *parseContext { - rtn := parseContext{Input: c.Input[pos:], QC: c.QC} - if newQuote != "" { - rtn.QC = rtn.QC.push(newQuote) - } - return &rtn -} - -func (c *parseContext) at(offset int) rune { - pos := c.Pos + offset - if pos < 0 || pos >= len(c.Input) { - return 0 - } - return c.Input[pos] -} - -func (c *parseContext) eof() bool { - return c.Pos >= len(c.Input) -} - -func (c *parseContext) cur() rune { - return c.at(0) -} - -func (c *parseContext) match(ch rune) bool { - return c.at(0) == ch -} - -func (c *parseContext) match2(ch rune, ch2 rune) bool { - return c.at(0) == ch && c.at(1) == ch2 -} - -func (c *parseContext) match3(ch rune, ch2 rune, ch3 rune) bool { - return c.at(0) == ch && c.at(1) == ch2 && c.at(2) == ch3 -} - -func (c *parseContext) makeWord(t string, length int, complete bool) *wordType { - rtn := &wordType{Type: t} - rtn.Offset = c.Pos - rtn.QC = c.QC - rtn.Raw = c.Input[c.Pos : c.Pos+length] - rtn.Complete = complete - c.Pos += length - return rtn -} - -// returns (found, newOffset) -// shell_meta_chars "()<>;&|" -// possible to maybe add ;;& &>> &> |& ;& -func (c *parseContext) parseOp(offset int) (bool, int) { - ch := c.at(offset) - if ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == ';' || ch == '&' || ch == '|' { - ch2 := c.at(offset + 1) - if ch2 == 0 { - return true, offset + 1 - } - r2 := string([]rune{ch, ch2}) - if r2 == "<<" { - ch3 := c.at(offset + 2) - if ch3 == '-' || ch3 == '<' { - return true, offset + 3 // "<<-" or "<<<" - } - return true, offset + 2 // "<<" - } - if r2 == ">>" || r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { - // we don't return '((' here (requires special processing) - return true, offset + 2 - } - return true, offset + 1 - } - return false, 0 -} - -// returns (new-offset, complete) -func (c *parseContext) skipToChar(offset int, endCh rune, allowEsc bool) (int, bool) { - for { - ch := c.at(offset) - if ch == 0 { - return offset, false - } - if allowEsc && ch == '\\' { - if c.at(offset+1) == 0 { - return offset + 1, false - } - offset += 2 - continue - } - if ch == endCh { - return offset + 1, true - } - offset++ - } -} - -// returns (new-offset, complete) -func (c *parseContext) skipToChar2(offset int, endCh rune, endCh2 rune, allowEsc bool) (int, bool) { - for { - ch := c.at(offset) - ch2 := c.at(offset + 1) - if ch == 0 { - return offset, false - } - if ch2 == 0 { - return offset + 1, false - } - if allowEsc && ch == '\\' { - offset += 2 - continue - } - if ch == endCh && ch2 == endCh2 { - return offset + 2, true - } - offset++ - } -} - -func (c *parseContext) parseStrSQ() *wordType { - if !c.match('\'') { - return nil - } - newOffset, complete := c.skipToChar(1, '\'', false) - w := c.makeWord(WordTypeSQ, newOffset, complete) - return w -} - -func (c *parseContext) parseStrDQ() *wordType { - if !c.match('"') { - return nil - } - newContext := c.clone(c.Pos+1, WordTypeDQ) - subWords, eofExit := newContext.tokenizeDQ() - newOffset := newContext.Pos + 1 - w := c.makeWord(WordTypeDQ, newOffset, !eofExit) - w.Subs = subWords - return w -} - -func (c *parseContext) parseStrDDQ() *wordType { - if !c.match2('$', '"') { - return nil - } - newContext := c.clone(c.Pos+2, WordTypeDDQ) - subWords, eofExit := newContext.tokenizeDQ() - newOffset := newContext.Pos + 2 - w := c.makeWord(WordTypeDDQ, newOffset, !eofExit) - w.Subs = subWords - return w -} - -func (c *parseContext) parseStrBQ() *wordType { - if !c.match('`') { - return nil - } - newContext := c.clone(c.Pos+1, WordTypeBQ) - subWords, eofExit := newContext.tokenizeRaw() - newOffset := newContext.Pos + 1 - w := c.makeWord(WordTypeBQ, newOffset, !eofExit) - w.Subs = subWords - return w -} - -func (c *parseContext) parseStrANSI() *wordType { - if !c.match2('$', '\'') { - return nil - } - newOffset, complete := c.skipToChar(2, '\'', true) - w := c.makeWord(WordTypeDSQ, newOffset, complete) - return w -} - -func (c *parseContext) parseArith(mustComplete bool) *wordType { - if !c.match2('(', '(') { - return nil - } - newOffset, complete := c.skipToChar2(2, ')', ')', false) - if mustComplete && !complete { - return nil - } - w := c.makeWord(WordTypePP, newOffset, complete) - return w -} - -func (c *parseContext) parseExpansion() *wordType { - if !c.match('$') { - return nil - } - if c.match3('$', '(', '(') { - newOffset, complete := c.skipToChar2(3, ')', ')', false) - w := c.makeWord(WordTypeDPP, newOffset, complete) - return w - } - if c.match2('$', '(') { - // subshell - newContext := c.clone(c.Pos+2, WordTypeDP) - subWords, eofExit := newContext.tokenizeRaw() - newOffset := newContext.Pos + 2 - w := c.makeWord(WordTypeDP, newOffset, !eofExit) - w.Subs = subWords - return w - } - if c.match2('$', '[') { - // deprecated arith expansion - newOffset, complete := c.skipToChar(2, ']', false) - w := c.makeWord(WordTypeDB, newOffset, complete) - return w - } - if c.match2('$', '{') { - // variable expansion - newContext := c.clone(c.Pos+2, WordTypeVarBrace) - _, eofExit := newContext.tokenizeVarBrace() - newOffset := newContext.Pos + 2 - w := c.makeWord(WordTypeVarBrace, newOffset, !eofExit) - return w - } - ch2 := c.at(1) - if ch2 == 0 || unicode.IsSpace(ch2) { - // no expansion - return nil - } - newOffset := c.parseSimpleVarName(1) - if newOffset > 1 { - // simple variable name - w := c.makeWord(WordTypeSimpleVar, newOffset, true) - return w - } - if ch2 == '*' || ch2 == '@' || ch2 == '#' || ch2 == '?' || ch2 == '-' || ch2 == '$' || ch2 == '!' || (ch2 >= '0' && ch2 <= '9') { - // single character variable name, e.g. $@, $_, $1, etc. - w := c.makeWord(WordTypeSimpleVar, 2, true) - return w - } - return nil -} - -// returns newOffset -func (c *parseContext) parseSimpleVarName(offset int) int { - first := true - for { - ch := c.at(offset) - if ch == 0 { - return offset - } - if (ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) || (!first && ch >= '0' && ch <= '9') { - first = false - offset++ - continue - } - return offset - } -} - -func makeSpaceStr(slen int) string { +func makeRepeatStr(ch byte, slen int) string { if slen == 0 { return "" } - if slen == 1 { - return " " - } rtn := make([]byte, slen) for i := 0; i < slen; i++ { - rtn[i] = ' ' + rtn[i] = ch } return string(rtn) } -func (w *wordType) String() string { +func (w *WordType) String() string { notCompleteFlag := " " if !w.Complete { notCompleteFlag = "*" } - return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeSpaceStr(len(w.Prefix)), string(w.FullRawString())) + return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), string(w.FullRawString())) } -func dumpWords(words []*wordType, indentStr string) { +func dumpWords(words []*WordType, indentStr string) { for _, word := range words { fmt.Printf("%s%s\n", indentStr, word.String()) if len(word.Subs) > 0 { @@ -457,3 +185,289 @@ func dumpWords(words []*wordType, indentStr string) { } } } + +func dumpCommands(cmds []*CmdType, indentStr string) { + for _, cmd := range cmds { + fmt.Printf("%sCMD: %s [%d]\n", indentStr, cmd.Type, len(cmd.Words)) + dumpWords(cmd.Words, indentStr+" ") + } +} + +func (w *WordType) FullRawString() []rune { + if w.Type == WordTypeGroup { + var rtn []rune + for _, sw := range w.Subs { + rtn = append(rtn, sw.FullRawString()...) + } + return rtn + } + return w.Raw +} + +func wordsToStr(words []*WordType) string { + var buf bytes.Buffer + for _, word := range words { + if len(word.Prefix) > 0 { + buf.WriteString(string(word.Prefix)) + } + buf.WriteString(string(word.FullRawString())) + } + return buf.String() +} + +// recognizes reserved words in first position +func convertToAnyReservedWord(w *WordType) bool { + if w == nil || w.Type != WordTypeLit { + return false + } + rawVal := string(w.Raw) + for _, rw := range bashReservedWords { + if rawVal == rw { + w.Type = WordTypeKey + return true + } + } + return false +} + +// recognizes the specific reserved-word given only ('in' and 'do' in 'for', 'case', and 'select' commands) +func convertToReservedWord(w *WordType, reservedWord string) { + if w == nil || w.Type != WordTypeLit { + return + } + if string(w.Raw) == reservedWord { + w.Type = WordTypeKey + } +} + +func isNoneReservedWord(w *WordType) bool { + if w.Type != WordTypeKey { + return false + } + rawVal := string(w.Raw) + for _, rw := range bashNoneRW { + if rawVal == rw { + return true + } + } + return false +} + +type parseCmdState struct { + Input []*WordType + InputPos int + + Rtn []*CmdType + Cur *CmdType +} + +func (state *parseCmdState) isEof() bool { + return state.InputPos >= len(state.Input) +} + +func (state *parseCmdState) curWord() *WordType { + if state.isEof() { + return nil + } + return state.Input[state.InputPos] +} + +func (state *parseCmdState) lastCmd() *CmdType { + if len(state.Rtn) == 0 { + return nil + } + return state.Rtn[len(state.Rtn)-1] +} + +func (state *parseCmdState) makeNoneCmd() { + lastCmd := state.lastCmd() + if lastCmd == nil || lastCmd.Type != CmdTypeNone { + lastCmd = &CmdType{Type: CmdTypeNone} + state.Rtn = append(state.Rtn, lastCmd) + } + lastCmd.Words = append(lastCmd.Words, state.curWord()) + state.Cur = nil + state.InputPos++ +} + +func (state *parseCmdState) handleKeyword(word *WordType) bool { + if word.Type != WordTypeKey { + return false + } + if isNoneReservedWord(word) { + state.makeNoneCmd() + return true + } + rw := string(word.Raw) + if rw == "[[" { + // just ignore everything between [[ and ]] + for !state.isEof() { + curWord := state.curWord() + if curWord.Type == WordTypeLit && string(curWord.Raw) == "]]" { + convertToReservedWord(curWord, "]]") + state.makeNoneCmd() + break + } + state.makeNoneCmd() + } + return true + } + if rw == "case" { + // ignore everything between "case" and "esac" + for !state.isEof() { + curWord := state.curWord() + if curWord.Type == WordTypeKey && string(curWord.Raw) == "esac" { + state.makeNoneCmd() + break + } + state.makeNoneCmd() + } + return true + } + if rw == "for" || rw == "select" { + // ignore until a "do" + for !state.isEof() { + curWord := state.curWord() + if curWord.Type == WordTypeKey && string(curWord.Raw) == "do" { + state.makeNoneCmd() + break + } + state.makeNoneCmd() + } + return true + } + if rw == "in" { + // the "for" and "case" clauses should skip "in". so encountering an "in" here is a syntax error. + // just treat it as a none and allow a new command after. + state.makeNoneCmd() + return true + } + if rw == "function" { + // ignore until '{' + for !state.isEof() { + curWord := state.curWord() + if curWord.Type == WordTypeKey && string(curWord.Raw) == "{" { + state.makeNoneCmd() + break + } + state.makeNoneCmd() + } + return true + } + state.makeNoneCmd() + return true +} + +func isCmdSeparatorOp(word *WordType) bool { + if word.Type != WordTypeOp { + return false + } + opVal := string(word.Raw) + return opVal == ";" || opVal == "\n" || opVal == "&" || opVal == "|" || opVal == "|&" || opVal == "&&" || opVal == "||" || opVal == "(" || opVal == ")" +} + +func (state *parseCmdState) handleOp(word *WordType) bool { + opVal := string(word.Raw) + // sequential separators + if opVal == ";" || opVal == "\n" { + state.makeNoneCmd() + return true + } + // separator + if opVal == "&" { + state.makeNoneCmd() + return true + } + // pipelines + if opVal == "|" || opVal == "|&" { + state.makeNoneCmd() + return true + } + // lists + if opVal == "&&" || opVal == "||" { + state.makeNoneCmd() + return true + } + // subshell + if opVal == "(" || opVal == ")" { + state.makeNoneCmd() + return true + } + return false +} + +func wordSliceBoundedIdx(words []*WordType, idx int) *WordType { + if idx >= len(words) { + return nil + } + return words[idx] +} + +// note that a newline "op" can appear in the third position of "for" or "case". the "in" keyword is still converted because of wordNum == 0 +func identifyReservedWords(words []*WordType) { + wordNum := 0 + lastReserved := false + for idx, word := range words { + if wordNum == 0 || lastReserved { + convertToAnyReservedWord(word) + } + if word.Type == WordTypeKey { + rwVal := string(word.Raw) + switch rwVal { + case "for": + lastReserved = false + third := wordSliceBoundedIdx(words, idx+2) + convertToReservedWord(third, "in") + convertToReservedWord(third, "do") + + case "case": + lastReserved = false + third := wordSliceBoundedIdx(words, idx+2) + convertToReservedWord(third, "in") + + case "in": + lastReserved = false + + default: + lastReserved = true + } + continue + } + lastReserved = false + if isCmdSeparatorOp(word) { + wordNum = 0 + continue + } + wordNum++ + } +} + +func ParseCommands(words []*WordType) []*CmdType { + identifyReservedWords(words) + state := parseCmdState{Input: words} + for { + if state.isEof() { + break + } + word := state.curWord() + if word.Type == WordTypeKey { + done := state.handleKeyword(word) + if done { + continue + } + } + if word.Type == WordTypeOp { + done := state.handleOp(word) + if done { + continue + } + } + if state.Cur == nil { + state.Cur = &CmdType{Type: CmdTypeSimple} + state.Rtn = append(state.Rtn, state.Cur) + } + state.Cur.Words = append(state.Cur.Words, word) + state.InputPos++ + } + return state.Rtn +} diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index ae2074c92..f276a0364 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -45,9 +45,10 @@ func Test1(t *testing.T) { testParse(t, `echo "$(ls "foo") more $x"`) testParse(t, "echo `ls $x \"hello $x\" \\`ls\\`; ./foo`") testParse(t, `echo $"hello $x $(ls)"`) + testParse(t, "echo 'hello'\nls\n") } -func lastWord(words []*wordType) *wordType { +func lastWord(words []*WordType) *WordType { if len(words) == 0 { return nil } @@ -80,5 +81,22 @@ func Test2(t *testing.T) { testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`) testExtend(t, `"f"`, "oo", `"foo"`) testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`) - testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\x07'""`) + testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\a'""`) +} + +func testParseCommands(t *testing.T, str string) { + fmt.Printf("parse: %q\n", str) + words := Tokenize(str) + cmds := ParseCommands(words) + dumpCommands(cmds, " ") + fmt.Printf("\n") +} + +func TestCmd(t *testing.T) { + testParseCommands(t, "ls foo") + testParseCommands(t, "ls foo && ls bar; ./run $x hello | xargs foo; ") + testParseCommands(t, "if [[ 2 > 1 ]]; then echo hello\nelse echo world; echo next; done") + testParseCommands(t, "case lots of stuff; i don\\'t know how to parse; esac; ls foo") + testParseCommands(t, "(ls & ./x); for x in $vars 3; do { echo $x; ls foo; } done") + testParseCommands(t, "function foo () { echo hello; }") } diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index 61c8e04d8..1381e0ede 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -1,7 +1,6 @@ package shparse import ( - "bytes" "fmt" "unicode" ) @@ -12,13 +11,13 @@ import ( // type tokenizeOutputState struct { - Rtn []*wordType - CurWord *wordType + Rtn []*WordType + CurWord *WordType SavedPrefix []rune } // does not set CurWord -func (state *tokenizeOutputState) appendStandaloneWord(word *wordType) { +func (state *tokenizeOutputState) appendStandaloneWord(word *WordType) { state.delimitCurWord() if len(state.SavedPrefix) > 0 { word.Prefix = state.SavedPrefix @@ -27,7 +26,7 @@ func (state *tokenizeOutputState) appendStandaloneWord(word *wordType) { state.Rtn = append(state.Rtn, word) } -func (state *tokenizeOutputState) appendWord(word *wordType) { +func (state *tokenizeOutputState) appendWord(word *WordType) { if len(state.SavedPrefix) > 0 { word.Prefix = state.SavedPrefix state.SavedPrefix = nil @@ -48,7 +47,7 @@ func (state *tokenizeOutputState) ensureGroupWord() { return } // moves the prefix from CurWord to the new group word - groupWord := &wordType{ + groupWord := &WordType{ Type: WordTypeGroup, Offset: state.CurWord.Offset, QC: state.CurWord.QC, @@ -56,13 +55,13 @@ func (state *tokenizeOutputState) ensureGroupWord() { Prefix: state.CurWord.Prefix, } state.CurWord.Prefix = nil - groupWord.Subs = []*wordType{state.CurWord} + groupWord.Subs = []*WordType{state.CurWord} state.CurWord = groupWord } -func ungroupWord(w *wordType) []*wordType { +func ungroupWord(w *WordType) []*WordType { if w.Type != WordTypeGroup { - return []*wordType{w} + return []*WordType{w} } rtn := w.Subs if len(w.Prefix) > 0 && len(rtn) > 0 { @@ -129,7 +128,7 @@ func (state *tokenizeOutputState) finish(pc *parseContext) { } } -func (c *parseContext) tokenizeVarBrace() ([]*wordType, bool) { +func (c *parseContext) tokenizeVarBrace() ([]*WordType, bool) { state := &tokenizeOutputState{} eofExit := false for { @@ -142,7 +141,7 @@ func (c *parseContext) tokenizeVarBrace() ([]*wordType, bool) { c.Pos++ break } - var quoteWord *wordType + var quoteWord *WordType if ch == '\'' { quoteWord = c.parseStrSQ() } @@ -175,7 +174,7 @@ func (c *parseContext) tokenizeVarBrace() ([]*wordType, bool) { return state.Rtn, eofExit } -func (c *parseContext) tokenizeDQ() ([]*wordType, bool) { +func (c *parseContext) tokenizeDQ() ([]*WordType, bool) { state := &tokenizeOutputState{} eofExit := false for { @@ -223,7 +222,7 @@ func (c *parseContext) tokenizeDQ() ([]*wordType, bool) { // returns (words, eofexit) // backticks (WordTypeBQ) handle backslash in a special way, but that seems to mainly effect execution (not completion) // de_backslash => removes initial backslash in \`, \\, and \$ before execution -func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { +func (c *parseContext) tokenizeRaw() ([]*WordType, bool) { state := &tokenizeOutputState{} isExpSubShell := c.QC.cur() == WordTypeDP isInBQ := c.QC.cur() == WordTypeBQ @@ -260,11 +259,10 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { parenLevel-- } opWord := c.makeWord(WordTypeOp, newOffset, true) - opWord.Val = opVal state.appendStandaloneWord(opWord) continue } - var quoteWord *wordType + var quoteWord *WordType if ch == '\'' { quoteWord = c.parseStrSQ() } @@ -294,6 +292,11 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { c.Pos += 2 continue } + if ch == '\n' { + newlineWord := c.makeWord(WordTypeOp, 1, true) + state.appendStandaloneWord(newlineWord) + continue + } if unicode.IsSpace(ch) { state.delimitWithSpace(ch) c.Pos++ @@ -306,30 +309,264 @@ func (c *parseContext) tokenizeRaw() ([]*wordType, bool) { return state.Rtn, eofExit } -func Tokenize(cmd string) []*wordType { +type parseContext struct { + Input []rune + Pos int + QC QuoteContext +} + +func (c *parseContext) clone(pos int, newQuote string) *parseContext { + rtn := parseContext{Input: c.Input[pos:], QC: c.QC} + if newQuote != "" { + rtn.QC = rtn.QC.push(newQuote) + } + return &rtn +} + +func (c *parseContext) at(offset int) rune { + pos := c.Pos + offset + if pos < 0 || pos >= len(c.Input) { + return 0 + } + return c.Input[pos] +} + +func (c *parseContext) eof() bool { + return c.Pos >= len(c.Input) +} + +func (c *parseContext) cur() rune { + return c.at(0) +} + +func (c *parseContext) match(ch rune) bool { + return c.at(0) == ch +} + +func (c *parseContext) match2(ch rune, ch2 rune) bool { + return c.at(0) == ch && c.at(1) == ch2 +} + +func (c *parseContext) match3(ch rune, ch2 rune, ch3 rune) bool { + return c.at(0) == ch && c.at(1) == ch2 && c.at(2) == ch3 +} + +func (c *parseContext) makeWord(t string, length int, complete bool) *WordType { + rtn := &WordType{Type: t} + rtn.Offset = c.Pos + rtn.QC = c.QC + rtn.Raw = c.Input[c.Pos : c.Pos+length] + rtn.Complete = complete + c.Pos += length + return rtn +} + +// returns (found, newOffset) +// shell_meta_chars "()<>;&|" +// possible to maybe add ;;& &>> &> |& ;& +func (c *parseContext) parseOp(offset int) (bool, int) { + ch := c.at(offset) + if ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == ';' || ch == '&' || ch == '|' { + ch2 := c.at(offset + 1) + if ch2 == 0 { + return true, offset + 1 + } + r2 := string([]rune{ch, ch2}) + if r2 == "<<" { + ch3 := c.at(offset + 2) + if ch3 == '-' || ch3 == '<' { + return true, offset + 3 // "<<-" or "<<<" + } + return true, offset + 2 // "<<" + } + if r2 == ">>" || r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { + // we don't return '((' here (requires special processing) + return true, offset + 2 + } + return true, offset + 1 + } + return false, 0 +} + +// returns (new-offset, complete) +func (c *parseContext) skipToChar(offset int, endCh rune, allowEsc bool) (int, bool) { + for { + ch := c.at(offset) + if ch == 0 { + return offset, false + } + if allowEsc && ch == '\\' { + if c.at(offset+1) == 0 { + return offset + 1, false + } + offset += 2 + continue + } + if ch == endCh { + return offset + 1, true + } + offset++ + } +} + +// returns (new-offset, complete) +func (c *parseContext) skipToChar2(offset int, endCh rune, endCh2 rune, allowEsc bool) (int, bool) { + for { + ch := c.at(offset) + ch2 := c.at(offset + 1) + if ch == 0 { + return offset, false + } + if ch2 == 0 { + return offset + 1, false + } + if allowEsc && ch == '\\' { + offset += 2 + continue + } + if ch == endCh && ch2 == endCh2 { + return offset + 2, true + } + offset++ + } +} + +func (c *parseContext) parseStrSQ() *WordType { + if !c.match('\'') { + return nil + } + newOffset, complete := c.skipToChar(1, '\'', false) + w := c.makeWord(WordTypeSQ, newOffset, complete) + return w +} + +func (c *parseContext) parseStrDQ() *WordType { + if !c.match('"') { + return nil + } + newContext := c.clone(c.Pos+1, WordTypeDQ) + subWords, eofExit := newContext.tokenizeDQ() + newOffset := newContext.Pos + 1 + w := c.makeWord(WordTypeDQ, newOffset, !eofExit) + w.Subs = subWords + return w +} + +func (c *parseContext) parseStrDDQ() *WordType { + if !c.match2('$', '"') { + return nil + } + newContext := c.clone(c.Pos+2, WordTypeDDQ) + subWords, eofExit := newContext.tokenizeDQ() + newOffset := newContext.Pos + 2 + w := c.makeWord(WordTypeDDQ, newOffset, !eofExit) + w.Subs = subWords + return w +} + +func (c *parseContext) parseStrBQ() *WordType { + if !c.match('`') { + return nil + } + newContext := c.clone(c.Pos+1, WordTypeBQ) + subWords, eofExit := newContext.tokenizeRaw() + newOffset := newContext.Pos + 1 + w := c.makeWord(WordTypeBQ, newOffset, !eofExit) + w.Subs = subWords + return w +} + +func (c *parseContext) parseStrANSI() *WordType { + if !c.match2('$', '\'') { + return nil + } + newOffset, complete := c.skipToChar(2, '\'', true) + w := c.makeWord(WordTypeDSQ, newOffset, complete) + return w +} + +func (c *parseContext) parseArith(mustComplete bool) *WordType { + if !c.match2('(', '(') { + return nil + } + newOffset, complete := c.skipToChar2(2, ')', ')', false) + if mustComplete && !complete { + return nil + } + w := c.makeWord(WordTypePP, newOffset, complete) + return w +} + +func (c *parseContext) parseExpansion() *WordType { + if !c.match('$') { + return nil + } + if c.match3('$', '(', '(') { + newOffset, complete := c.skipToChar2(3, ')', ')', false) + w := c.makeWord(WordTypeDPP, newOffset, complete) + return w + } + if c.match2('$', '(') { + // subshell + newContext := c.clone(c.Pos+2, WordTypeDP) + subWords, eofExit := newContext.tokenizeRaw() + newOffset := newContext.Pos + 2 + w := c.makeWord(WordTypeDP, newOffset, !eofExit) + w.Subs = subWords + return w + } + if c.match2('$', '[') { + // deprecated arith expansion + newOffset, complete := c.skipToChar(2, ']', false) + w := c.makeWord(WordTypeDB, newOffset, complete) + return w + } + if c.match2('$', '{') { + // variable expansion + newContext := c.clone(c.Pos+2, WordTypeVarBrace) + _, eofExit := newContext.tokenizeVarBrace() + newOffset := newContext.Pos + 2 + w := c.makeWord(WordTypeVarBrace, newOffset, !eofExit) + return w + } + ch2 := c.at(1) + if ch2 == 0 || unicode.IsSpace(ch2) { + // no expansion + return nil + } + newOffset := c.parseSimpleVarName(1) + if newOffset > 1 { + // simple variable name + w := c.makeWord(WordTypeSimpleVar, newOffset, true) + return w + } + if ch2 == '*' || ch2 == '@' || ch2 == '#' || ch2 == '?' || ch2 == '-' || ch2 == '$' || ch2 == '!' || (ch2 >= '0' && ch2 <= '9') { + // single character variable name, e.g. $@, $_, $1, etc. + w := c.makeWord(WordTypeSimpleVar, 2, true) + return w + } + return nil +} + +// returns newOffset +func (c *parseContext) parseSimpleVarName(offset int) int { + first := true + for { + ch := c.at(offset) + if ch == 0 { + return offset + } + if (ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) || (!first && ch >= '0' && ch <= '9') { + first = false + offset++ + continue + } + return offset + } +} + +func Tokenize(cmd string) []*WordType { c := &parseContext{Input: []rune(cmd)} rtn, _ := c.tokenizeRaw() return rtn } - -func (w *wordType) FullRawString() []rune { - if w.Type == WordTypeGroup { - var rtn []rune - for _, sw := range w.Subs { - rtn = append(rtn, sw.FullRawString()...) - } - return rtn - } - return w.Raw -} - -func wordsToStr(words []*wordType) string { - var buf bytes.Buffer - for _, word := range words { - if len(word.Prefix) > 0 { - buf.WriteString(string(word.Prefix)) - } - buf.WriteString(string(word.FullRawString())) - } - return buf.String() -} From a6306060204fe2fdbb1b16e1882f2c9376132e7b Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 18 Nov 2022 14:26:52 -0800 Subject: [PATCH 182/397] working on integrating position. identify cmd assignment words. make group words consistent. always copy Raw runes. fixup command whitespace --- pkg/shparse/extend.go | 28 ++--- pkg/shparse/shparse.go | 235 ++++++++++++++++++++++++++++++------ pkg/shparse/shparse_test.go | 16 +-- pkg/shparse/tokenize.go | 45 +++++-- pkg/utilfn/utilfn.go | 23 ++++ 5 files changed, 279 insertions(+), 68 deletions(-) diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index 99b3ead95..5761d0bd2 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -64,30 +64,26 @@ func (w *WordType) writeRune(ch rune) { return } -func (w *WordType) cloneRaw() { - if len(w.Raw) == 0 { - return - } - buf := make([]rune, 0, len(w.Raw)) - w.Raw = append(buf, w.Raw...) -} - type extendContext struct { + Input []*WordType + InputPos int QC QuoteContext Rtn []*WordType CurWord *WordType Intention string } -func makeExtendContext(qc QuoteContext, w *WordType) *extendContext { - rtn := &extendContext{QC: qc, Intention: WordTypeLit} - if w != nil { - w.cloneRaw() - rtn.Rtn = []*WordType{w} - rtn.CurWord = w - rtn.Intention = w.Type +func makeExtendContext(qc QuoteContext, word *WordType) *extendContext { + rtn := &extendContext{QC: qc} + if word == nil { + rtn.Intention = WordTypeLit + return rtn + } else { + rtn.Intention = word.Type + rtn.Rtn = []*WordType{word} + rtn.CurWord = word + return rtn } - return rtn } func (ec *extendContext) appendWord(w *WordType) { diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 3ac8a38a8..8eafcd7a7 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -3,6 +3,8 @@ package shparse import ( "bytes" "fmt" + + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" ) // @@ -103,33 +105,37 @@ var bashNoneRW = []string{ type wordMeta struct { Type string EmptyWord []rune + PrefixLen int SuffixLen int CanExtend bool QuoteContext bool } -func makeWordMeta(wtype string, emptyWord string, suffixLen int, canExtend bool, quoteContext bool) { - wordMetaMap[wtype] = wordMeta{wtype, []rune(emptyWord), suffixLen, canExtend, quoteContext} +func makeWordMeta(wtype string, emptyWord string, prefixLen int, suffixLen int, canExtend bool, quoteContext bool) { + if len(emptyWord) != prefixLen+suffixLen { + panic(fmt.Sprintf("invalid empty word %s %d %d", emptyWord, prefixLen, suffixLen)) + } + wordMetaMap[wtype] = wordMeta{wtype, []rune(emptyWord), prefixLen, suffixLen, canExtend, quoteContext} } func init() { wordMetaMap = make(map[string]wordMeta) - makeWordMeta(WordTypeRaw, "", 0, false, false) - makeWordMeta(WordTypeLit, "", 0, true, false) - makeWordMeta(WordTypeOp, "", 0, false, false) - makeWordMeta(WordTypeKey, "", 0, false, false) - makeWordMeta(WordTypeGroup, "", 0, false, false) - makeWordMeta(WordTypeSimpleVar, "$", 0, true, false) - makeWordMeta(WordTypeVarBrace, "${}", 1, true, true) - makeWordMeta(WordTypeDQ, `""`, 1, true, true) - makeWordMeta(WordTypeDDQ, `$""`, 1, true, true) - makeWordMeta(WordTypeDP, "$()", 1, false, false) - makeWordMeta(WordTypeBQ, "``", 1, false, false) - makeWordMeta(WordTypeSQ, "''", 1, true, false) - makeWordMeta(WordTypeDSQ, "$''", 1, true, false) - makeWordMeta(WordTypeDPP, "$(())", 2, false, false) - makeWordMeta(WordTypePP, "(())", 2, false, false) - makeWordMeta(WordTypeDB, "$[]", 1, false, false) + makeWordMeta(WordTypeRaw, "", 0, 0, false, false) + makeWordMeta(WordTypeLit, "", 0, 0, true, false) + makeWordMeta(WordTypeOp, "", 0, 0, false, false) + makeWordMeta(WordTypeKey, "", 0, 0, false, false) + makeWordMeta(WordTypeGroup, "", 0, 0, false, false) + makeWordMeta(WordTypeSimpleVar, "$", 1, 0, true, false) + makeWordMeta(WordTypeVarBrace, "${}", 2, 1, true, true) + makeWordMeta(WordTypeDQ, `""`, 1, 1, true, true) + makeWordMeta(WordTypeDDQ, `$""`, 2, 1, true, true) + makeWordMeta(WordTypeDP, "$()", 2, 1, false, false) + makeWordMeta(WordTypeBQ, "``", 1, 1, false, false) + makeWordMeta(WordTypeSQ, "''", 1, 1, true, false) + makeWordMeta(WordTypeDSQ, "$''", 2, 1, true, false) + makeWordMeta(WordTypeDPP, "$(())", 3, 2, false, false) + makeWordMeta(WordTypePP, "(())", 2, 2, false, false) + makeWordMeta(WordTypeDB, "$[]", 2, 1, false, false) } func MakeEmptyWord(wtype string, qc QuoteContext, offset int) *WordType { @@ -169,48 +175,72 @@ func makeRepeatStr(ch byte, slen int) string { return string(rtn) } +func (w *WordType) isBlank() bool { + return w.Type == WordTypeLit && len(w.Raw) == 0 +} + +func (w *WordType) stringWithPos(pos int) string { + notCompleteFlag := " " + if !w.Complete { + notCompleteFlag = "*" + } + str := string(w.Raw) + if pos != -1 { + str = utilfn.StrWithPos{Str: str, Pos: pos}.String() + } + return fmt.Sprintf("%-4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), str) +} + func (w *WordType) String() string { notCompleteFlag := " " if !w.Complete { notCompleteFlag = "*" } - return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), string(w.FullRawString())) + return fmt.Sprintf("%-4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeRepeatStr('_', len(w.Prefix)), string(w.Raw)) } -func dumpWords(words []*WordType, indentStr string) { +// offset = -1 for don't show +func dumpWords(words []*WordType, indentStr string, offset int) { + wrotePos := false for _, word := range words { - fmt.Printf("%s%s\n", indentStr, word.String()) + posInWord := false + if !wrotePos && offset != -1 && offset <= word.Offset { + fmt.Printf("%s* [%3d] [*]\n", indentStr, offset) + wrotePos = true + } + if !wrotePos && offset != -1 && offset < word.Offset+len(word.Raw) { + fmt.Printf("%s%s\n", indentStr, word.stringWithPos(offset-word.Offset)) + wrotePos = true + posInWord = true + } else { + fmt.Printf("%s%s\n", indentStr, word.String()) + } if len(word.Subs) > 0 { - dumpWords(word.Subs, indentStr+" ") + if posInWord { + wmeta := wordMetaMap[word.Type] + dumpWords(word.Subs, indentStr+" ", offset-word.Offset-wmeta.PrefixLen) + } else { + dumpWords(word.Subs, indentStr+" ", -1) + } } } } -func dumpCommands(cmds []*CmdType, indentStr string) { +func dumpCommands(cmds []*CmdType, indentStr string, pos *CmdPos) { for _, cmd := range cmds { fmt.Printf("%sCMD: %s [%d]\n", indentStr, cmd.Type, len(cmd.Words)) - dumpWords(cmd.Words, indentStr+" ") + dumpWords(cmd.AssignmentWords, indentStr+" *", -1) + dumpWords(cmd.Words, indentStr+" ", -1) } } -func (w *WordType) FullRawString() []rune { - if w.Type == WordTypeGroup { - var rtn []rune - for _, sw := range w.Subs { - rtn = append(rtn, sw.FullRawString()...) - } - return rtn - } - return w.Raw -} - func wordsToStr(words []*WordType) string { var buf bytes.Buffer for _, word := range words { if len(word.Prefix) > 0 { buf.WriteString(string(word.Prefix)) } - buf.WriteString(string(word.FullRawString())) + buf.WriteString(string(word.Raw)) } return buf.String() } @@ -442,6 +472,132 @@ func identifyReservedWords(words []*WordType) { } } +type CmdPos struct { + CmdPos int + CmdOffset int + + CurWord *WordType // nil if between words + CurWordOffset int + + CmdWordPos int + OffsetInWord int // if BetweenWords is set, this offset can be negative (position is inside of prefix) + BetweenWords bool +} + +// func FindCmdPos(cmds []*CmdType, offset int) CmdPos { +// if len(words) == 0 { +// return WordsPos{[]int{0}, 0, true} +// } +// pos := 0 +// for idx, word := range words { +// if offset <= word.Offset+len(word.Raw) { +// if offset <= word.Offset { +// // in the prefix, so we are between-words with a possibly negative offset +// return WordPos{WordPos: idx, OffsetInWord: offset - word.Offset, BetweenWords: true} +// } +// if offset == pos+fullWordLen { +// return WordPos{WordPos: idx + 1, OffsetInWord: 0, BetweenWords: true} +// } +// return WordPos{WordPos: idx, OffsetInWord: offset - word.Offset, BetweenWords: false} +// } +// pos += fullWordLen +// } +// return WordPos{WordPos: []int{len(words)}, OffsetInWord: 0, BetweenWords: true} +// } + +func ResetWordOffsets(words []*WordType) { + pos := 0 + for _, word := range words { + pos += len(word.Prefix) + word.Offset = pos + if len(word.Subs) > 0 { + ResetWordOffsets(word.Subs) + } + pos += len(word.Raw) + } +} + +func CommandsToWords(cmds []*CmdType) []*WordType { + var rtn []*WordType + for _, cmd := range cmds { + rtn = append(rtn, cmd.Words...) + } + return rtn +} + +func (c *CmdType) stripPrefix() []rune { + if len(c.AssignmentWords) > 0 { + w := c.AssignmentWords[0] + prefix := w.Prefix + w.Prefix = nil + return prefix + } + if len(c.Words) > 0 { + w := c.Words[0] + prefix := w.Prefix + w.Prefix = nil + return prefix + } + return nil +} + +func (c *CmdType) isEmpty() bool { + return len(c.AssignmentWords) == 0 && len(c.Words) == 0 +} + +func (c *CmdType) lastWord() *WordType { + if len(c.Words) > 0 { + return c.Words[len(c.Words)-1] + } + if len(c.AssignmentWords) > 0 { + return c.AssignmentWords[len(c.AssignmentWords)-1] + } + return nil +} + +func (c *CmdType) endOffset() int { + lastWord := c.lastWord() + if lastWord == nil { + return 0 + } + return lastWord.Offset + len(lastWord.Raw) +} + +func indexInRunes(arr []rune, ch rune) int { + for idx, r := range arr { + if r == ch { + return idx + } + } + return -1 +} + +func isAssignmentWord(w *WordType) bool { + if w.Type == WordTypeLit || w.Type == WordTypeGroup { + eqIdx := indexInRunes(w.Raw, '=') + if eqIdx == -1 { + return false + } + prefix := w.Raw[0:eqIdx] + return isSimpleVarName(prefix) + } + return false +} + +// simple commands steal whitespace from subsequent commands +func cmdWhitespaceFixup(cmds []*CmdType) { + for idx := 0; idx < len(cmds)-1; idx++ { + cmd := cmds[idx] + if cmd.Type != CmdTypeSimple || cmd.isEmpty() { + continue + } + nextCmd := cmds[idx+1] + nextPrefix := nextCmd.stripPrefix() + blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset(), Prefix: nextPrefix, Complete: true} + cmd.Words = append(cmd.Words, blankWord) + } +} + func ParseCommands(words []*WordType) []*CmdType { identifyReservedWords(words) state := parseCmdState{Input: words} @@ -466,8 +622,13 @@ func ParseCommands(words []*WordType) []*CmdType { state.Cur = &CmdType{Type: CmdTypeSimple} state.Rtn = append(state.Rtn, state.Cur) } - state.Cur.Words = append(state.Cur.Words, word) + if len(state.Cur.Words) == 0 && isAssignmentWord(word) { + state.Cur.AssignmentWords = append(state.Cur.AssignmentWords, word) + } else { + state.Cur.Words = append(state.Cur.Words, word) + } state.InputPos++ } + cmdWhitespaceFixup(state.Rtn) return state.Rtn } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index f276a0364..1d6f8d737 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -15,14 +15,13 @@ import ( func testParse(t *testing.T, s string) { words := Tokenize(s) - fmt.Printf("%s\n", s) - dumpWords(words, " ") - fmt.Printf("\n") - + fmt.Printf("parse <<\n%s\n>>\n", s) + dumpWords(words, " ", 8) outStr := wordsToStr(words) if outStr != s { t.Errorf("tokenization output does not match input: %q => %q", s, outStr) } + fmt.Printf("------\n\n") } func Test1(t *testing.T) { @@ -46,6 +45,7 @@ func Test1(t *testing.T) { testParse(t, "echo `ls $x \"hello $x\" \\`ls\\`; ./foo`") testParse(t, `echo $"hello $x $(ls)"`) testParse(t, "echo 'hello'\nls\n") + testParse(t, "echo 'hello'abc$'\a'") } func lastWord(words []*WordType) *WordType { @@ -88,15 +88,17 @@ func testParseCommands(t *testing.T, str string) { fmt.Printf("parse: %q\n", str) words := Tokenize(str) cmds := ParseCommands(words) - dumpCommands(cmds, " ") + dumpCommands(cmds, " ", nil) fmt.Printf("\n") } func TestCmd(t *testing.T) { testParseCommands(t, "ls foo") + testParseCommands(t, "function foo () { echo hello; }") testParseCommands(t, "ls foo && ls bar; ./run $x hello | xargs foo; ") testParseCommands(t, "if [[ 2 > 1 ]]; then echo hello\nelse echo world; echo next; done") testParseCommands(t, "case lots of stuff; i don\\'t know how to parse; esac; ls foo") - testParseCommands(t, "(ls & ./x); for x in $vars 3; do { echo $x; ls foo; } done") - testParseCommands(t, "function foo () { echo hello; }") + testParseCommands(t, "(ls & ./x \n\n); for x in $vars 3; do { echo $x; ls foo; } done") + testParseCommands(t, `ls f"oo" "${x:"hello$y"}"`) + testParseCommands(t, `x="foo $y" z=10 ls`) } diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index 1381e0ede..52ecbd143 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -16,6 +16,13 @@ type tokenizeOutputState struct { SavedPrefix []rune } +func copyRunes(rarr []rune) []rune { + if len(rarr) == 0 { + return nil + } + return append([]rune(nil), rarr...) +} + // does not set CurWord func (state *tokenizeOutputState) appendStandaloneWord(word *WordType) { state.delimitCurWord() @@ -36,7 +43,9 @@ func (state *tokenizeOutputState) appendWord(word *WordType) { return } state.ensureGroupWord() + word.Offset = word.Offset - state.CurWord.Offset state.CurWord.Subs = append(state.CurWord.Subs, word) + state.CurWord.Raw = append(state.CurWord.Raw, word.Raw...) } func (state *tokenizeOutputState) ensureGroupWord() { @@ -46,29 +55,34 @@ func (state *tokenizeOutputState) ensureGroupWord() { if state.CurWord.Type == WordTypeGroup { return } - // moves the prefix from CurWord to the new group word + // moves the prefix from CurWord to the new group word, resets offsets groupWord := &WordType{ Type: WordTypeGroup, Offset: state.CurWord.Offset, QC: state.CurWord.QC, + Raw: copyRunes(state.CurWord.Raw), Complete: true, Prefix: state.CurWord.Prefix, } state.CurWord.Prefix = nil + state.CurWord.Offset = 0 groupWord.Subs = []*WordType{state.CurWord} state.CurWord = groupWord } -func ungroupWord(w *WordType) []*WordType { - if w.Type != WordTypeGroup { - return []*WordType{w} +func ungroupWord(groupWord *WordType) []*WordType { + if groupWord.Type != WordTypeGroup { + return []*WordType{groupWord} } - rtn := w.Subs - if len(w.Prefix) > 0 && len(rtn) > 0 { - newPrefix := append([]rune{}, w.Prefix...) + rtn := groupWord.Subs + if len(groupWord.Prefix) > 0 && len(rtn) > 0 { + newPrefix := append([]rune{}, groupWord.Prefix...) newPrefix = append(newPrefix, rtn[0].Prefix...) rtn[0].Prefix = newPrefix } + for _, word := range rtn { + word.Offset = word.Offset + groupWord.Offset + } return rtn } @@ -89,6 +103,7 @@ func (state *tokenizeOutputState) ensureLitCurWord(pc *parseContext) { panic("invalid state, there can be no saved prefix") } litWord := pc.makeWord(WordTypeLit, 0, true) + litWord.Offset = litWord.Offset - state.CurWord.Offset state.CurWord.Subs = append(state.CurWord.Subs, litWord) } } @@ -115,6 +130,7 @@ func (state *tokenizeOutputState) appendLiteral(pc *parseContext, ch rune) { panic(fmt.Sprintf("invalid curword type (group) %q", state.CurWord.Type)) } lastWord.Raw = append(lastWord.Raw, ch) + state.CurWord.Raw = append(state.CurWord.Raw, ch) } else { panic(fmt.Sprintf("invalid curword type %q", state.CurWord.Type)) } @@ -355,7 +371,7 @@ func (c *parseContext) makeWord(t string, length int, complete bool) *WordType { rtn := &WordType{Type: t} rtn.Offset = c.Pos rtn.QC = c.QC - rtn.Raw = c.Input[c.Pos : c.Pos+length] + rtn.Raw = copyRunes(c.Input[c.Pos : c.Pos+length]) rtn.Complete = complete c.Pos += length return rtn @@ -565,6 +581,19 @@ func (c *parseContext) parseSimpleVarName(offset int) int { } } +func isSimpleVarName(rstr []rune) bool { + if len(rstr) == 0 { + return false + } + for idx, ch := range rstr { + if (ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) || ((idx != 0) && ch >= '0' && ch <= '9') { + continue + } + return false + } + return true +} + func Tokenize(cmd string) []*WordType { c := &parseContext{Input: []rune(cmd)} rtn, _ := c.tokenizeRaw() diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index fcce5845d..2dace7fe8 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -118,3 +118,26 @@ func ContainsStr(strs []string, test string) bool { } return false } + +type StrWithPos struct { + Str string + Pos int +} + +func (sp StrWithPos) String() string { + return strWithCursor(sp.Str, sp.Pos) +} + +func strWithCursor(str string, pos int) string { + if pos < 0 { + return "[*]_" + str + } + if pos >= len(str) { + if pos > len(str) { + return str + "_[*]" + } + return str + "[*]" + } else { + return str[:pos] + "[*]" + str[pos:] + } +} From 3474177f287ed5ce730343da2ab1b00c877c57e3 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 18 Nov 2022 14:57:25 -0800 Subject: [PATCH 183/397] split cmdnone on command separators --- pkg/shparse/shparse.go | 95 ++++++++++++++++--------------------- pkg/shparse/shparse_test.go | 2 +- 2 files changed, 41 insertions(+), 56 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 8eafcd7a7..a26f868b9 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -96,7 +96,8 @@ var bashReservedWords = []string{ // special reserved words: "for", "in", "case", "select", "function", "[[", and "]]" var bashNoneRW = []string{ - "if", "then", "elif", "else", "fi", "time", + "if", "then", + "elif", "else", "fi", "time", "until", "while", "do", "done", "esac", "coproc", "{", "}", "!", @@ -309,14 +310,15 @@ func (state *parseCmdState) lastCmd() *CmdType { return state.Rtn[len(state.Rtn)-1] } -func (state *parseCmdState) makeNoneCmd() { - lastCmd := state.lastCmd() - if lastCmd == nil || lastCmd.Type != CmdTypeNone { - lastCmd = &CmdType{Type: CmdTypeNone} - state.Rtn = append(state.Rtn, lastCmd) +func (state *parseCmdState) makeNoneCmd(sep bool) { + if state.Cur == nil || state.Cur.Type != CmdTypeNone { + state.Cur = &CmdType{Type: CmdTypeNone} + state.Rtn = append(state.Rtn, state.Cur) + } + state.Cur.Words = append(state.Cur.Words, state.curWord()) + if sep { + state.Cur = nil } - lastCmd.Words = append(lastCmd.Words, state.curWord()) - state.Cur = nil state.InputPos++ } @@ -325,7 +327,7 @@ func (state *parseCmdState) handleKeyword(word *WordType) bool { return false } if isNoneReservedWord(word) { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } rw := string(word.Raw) @@ -335,10 +337,10 @@ func (state *parseCmdState) handleKeyword(word *WordType) bool { curWord := state.curWord() if curWord.Type == WordTypeLit && string(curWord.Raw) == "]]" { convertToReservedWord(curWord, "]]") - state.makeNoneCmd() + state.makeNoneCmd(false) break } - state.makeNoneCmd() + state.makeNoneCmd(false) } return true } @@ -347,10 +349,10 @@ func (state *parseCmdState) handleKeyword(word *WordType) bool { for !state.isEof() { curWord := state.curWord() if curWord.Type == WordTypeKey && string(curWord.Raw) == "esac" { - state.makeNoneCmd() + state.makeNoneCmd(false) break } - state.makeNoneCmd() + state.makeNoneCmd(false) } return true } @@ -359,17 +361,17 @@ func (state *parseCmdState) handleKeyword(word *WordType) bool { for !state.isEof() { curWord := state.curWord() if curWord.Type == WordTypeKey && string(curWord.Raw) == "do" { - state.makeNoneCmd() + state.makeNoneCmd(true) break } - state.makeNoneCmd() + state.makeNoneCmd(false) } return true } if rw == "in" { // the "for" and "case" clauses should skip "in". so encountering an "in" here is a syntax error. // just treat it as a none and allow a new command after. - state.makeNoneCmd() + state.makeNoneCmd(false) return true } if rw == "function" { @@ -377,14 +379,14 @@ func (state *parseCmdState) handleKeyword(word *WordType) bool { for !state.isEof() { curWord := state.curWord() if curWord.Type == WordTypeKey && string(curWord.Raw) == "{" { - state.makeNoneCmd() + state.makeNoneCmd(true) break } - state.makeNoneCmd() + state.makeNoneCmd(false) } return true } - state.makeNoneCmd() + state.makeNoneCmd(true) return true } @@ -400,27 +402,27 @@ func (state *parseCmdState) handleOp(word *WordType) bool { opVal := string(word.Raw) // sequential separators if opVal == ";" || opVal == "\n" { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } // separator if opVal == "&" { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } // pipelines if opVal == "|" || opVal == "|&" { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } // lists if opVal == "&&" || opVal == "||" { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } // subshell if opVal == "(" || opVal == ")" { - state.makeNoneCmd() + state.makeNoneCmd(true) return true } return false @@ -473,37 +475,18 @@ func identifyReservedWords(words []*WordType) { } type CmdPos struct { - CmdPos int - CmdOffset int + CmdPos int // index into cmd array + Cmd *CmdType // nil if between commands (only if CmdPos == 0 || CmdPos == len(cmds), otherwise should be a valid entry into a command) + CmdOffset int // offset within the command - CurWord *WordType // nil if between words - CurWordOffset int - - CmdWordPos int - OffsetInWord int // if BetweenWords is set, this offset can be negative (position is inside of prefix) - BetweenWords bool + CmdWordPos int // (index into cmd) 0 = command-word, negative numbers are assignment-words. can be past the end of Words (means start new word) + CmdWord *WordType // nil if between words + CmdWordOffset int // offset into the word. when cmdword is nil, positive offset would mean in the prefix of next word } -// func FindCmdPos(cmds []*CmdType, offset int) CmdPos { -// if len(words) == 0 { -// return WordsPos{[]int{0}, 0, true} -// } -// pos := 0 -// for idx, word := range words { -// if offset <= word.Offset+len(word.Raw) { -// if offset <= word.Offset { -// // in the prefix, so we are between-words with a possibly negative offset -// return WordPos{WordPos: idx, OffsetInWord: offset - word.Offset, BetweenWords: true} -// } -// if offset == pos+fullWordLen { -// return WordPos{WordPos: idx + 1, OffsetInWord: 0, BetweenWords: true} -// } -// return WordPos{WordPos: idx, OffsetInWord: offset - word.Offset, BetweenWords: false} -// } -// pos += fullWordLen -// } -// return WordPos{WordPos: []int{len(words)}, OffsetInWord: 0, BetweenWords: true} -// } +func FindCmdPos(cmds []*CmdType, offset int) CmdPos { + return CmdPos{} +} func ResetWordOffsets(words []*WordType) { pos := 0 @@ -593,8 +576,10 @@ func cmdWhitespaceFixup(cmds []*CmdType) { } nextCmd := cmds[idx+1] nextPrefix := nextCmd.stripPrefix() - blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset(), Prefix: nextPrefix, Complete: true} - cmd.Words = append(cmd.Words, blankWord) + if len(nextPrefix) > 0 { + blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset(), Prefix: nextPrefix, Complete: true} + cmd.Words = append(cmd.Words, blankWord) + } } } @@ -618,7 +603,7 @@ func ParseCommands(words []*WordType) []*CmdType { continue } } - if state.Cur == nil { + if state.Cur == nil || state.Cur.Type != CmdTypeSimple { state.Cur = &CmdType{Type: CmdTypeSimple} state.Rtn = append(state.Rtn, state.Cur) } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 1d6f8d737..7c863bd24 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -98,7 +98,7 @@ func TestCmd(t *testing.T) { testParseCommands(t, "ls foo && ls bar; ./run $x hello | xargs foo; ") testParseCommands(t, "if [[ 2 > 1 ]]; then echo hello\nelse echo world; echo next; done") testParseCommands(t, "case lots of stuff; i don\\'t know how to parse; esac; ls foo") - testParseCommands(t, "(ls & ./x \n\n); for x in $vars 3; do { echo $x; ls foo; } done") + testParseCommands(t, "(ls & ./x \n \n); for x in $vars 3; do { echo $x; ls foo ; } done") testParseCommands(t, `ls f"oo" "${x:"hello$y"}"`) testParseCommands(t, `x="foo $y" z=10 ls`) } From 54e42ad10e85d4c7ac9191b639ce0d9eee195344 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 18 Nov 2022 16:16:31 -0800 Subject: [PATCH 184/397] checkpoint on finding command completion point --- pkg/shparse/shparse.go | 182 ++++++++++++++++++++++++++++++++---- pkg/shparse/shparse_test.go | 2 +- 2 files changed, 167 insertions(+), 17 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index a26f868b9..601e983c0 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -44,14 +44,14 @@ const ( WordTypeLit = "lit" // (can-extend) WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ - WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x + WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs) WordTypeSimpleVar = "svar" // simplevar $ (can-extend) - WordTypeDQ = "dq" // " (quote-context) (can-extend) - WordTypeDDQ = "ddq" // $" (quote-context) (can-extend) - WordTypeVarBrace = "varb" // ${ (quote-context) (can-extend) - WordTypeDP = "dp" // $( (quote-context) - WordTypeBQ = "bq" // ` (quote-context) + WordTypeDQ = "dq" // " (quote-context) (can-extend) (has-subs) + WordTypeDDQ = "ddq" // $" (quote-context) (can-extend) (has-subs) + WordTypeVarBrace = "varb" // ${ (quote-context) (can-extend) (internals not parsed) + WordTypeDP = "dp" // $( (quote-context) (has-subs) + WordTypeBQ = "bq" // ` (quote-context) (has-subs) WordTypeSQ = "sq" // ' (can-extend) WordTypeDSQ = "dsq" // $' (can-extend) @@ -79,6 +79,7 @@ type CmdType struct { Type string AssignmentWords []*WordType Words []*WordType + NoneComplete bool // set to true when last-word is a "separator" } type QuoteContext []string @@ -180,6 +181,16 @@ func (w *WordType) isBlank() bool { return w.Type == WordTypeLit && len(w.Raw) == 0 } +func (w *WordType) uncompletable() bool { + switch w.Type { + case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB: + return false + + default: + return true + } +} + func (w *WordType) stringWithPos(pos int) string { notCompleteFlag := " " if !w.Complete { @@ -227,7 +238,7 @@ func dumpWords(words []*WordType, indentStr string, offset int) { } } -func dumpCommands(cmds []*CmdType, indentStr string, pos *CmdPos) { +func dumpCommands(cmds []*CmdType, indentStr string, pos int) { for _, cmd := range cmds { fmt.Printf("%sCMD: %s [%d]\n", indentStr, cmd.Type, len(cmd.Words)) dumpWords(cmd.AssignmentWords, indentStr+" *", -1) @@ -317,6 +328,7 @@ func (state *parseCmdState) makeNoneCmd(sep bool) { } state.Cur.Words = append(state.Cur.Words, state.curWord()) if sep { + state.Cur.NoneComplete = true state.Cur = nil } state.InputPos++ @@ -474,18 +486,138 @@ func identifyReservedWords(words []*WordType) { } } -type CmdPos struct { - CmdPos int // index into cmd array - Cmd *CmdType // nil if between commands (only if CmdPos == 0 || CmdPos == len(cmds), otherwise should be a valid entry into a command) - CmdOffset int // offset within the command +type CompletionPos struct { + RawPos int // the raw position of cursor + Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand) - CmdWordPos int // (index into cmd) 0 = command-word, negative numbers are assignment-words. can be past the end of Words (means start new word) - CmdWord *WordType // nil if between words - CmdWordOffset int // offset into the word. when cmdword is nil, positive offset would mean in the prefix of next word + // index into cmd.Words (only useful when Cmd is not nil, otherwise we look at CompCommand) + // 0 means command-word + // negative means assignment-words. + // can be past the end of Words (means start new word). + CmdWordPos int + + CmdWord *WordType // set to the word we are completing (nil if we are starting a new word) + CmdWordOffset int // offset into cmdword (only if CmdWord is not nil) + CompInvalid bool // some words cannot be completed (e.g. in the middle of an operator, inside a control structure, etc.) + CompCommand bool // set when we think we are the first word of an existing or new command. otherwise we default to file completion } -func FindCmdPos(cmds []*CmdType, offset int) CmdPos { - return CmdPos{} +func (cmd *CmdType) findCompletionPos_simple(pos int) CompletionPos { + if cmd.Type != CmdTypeSimple { + panic("findCompletetionPos_simple only works for CmdTypeSimple") + } + for idx, word := range cmd.AssignmentWords { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + // starting a new word at this position (before the current assignment word) + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1} + } + if pos <= endOffset { + // completing an assignment word + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset} + } + } + var foundWord *WordType + var foundWordIdx int + for idx, word := range cmd.Words { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + // starting a new word at this position + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx} + } + if pos == endOffset && word.uncompletable() { + continue + } + if pos <= endOffset { + foundWord = word + foundWordIdx = idx + break + } + } + if foundWord != nil { + if foundWord.uncompletable() { + // invalid completion point + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true} + } + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset} + } + // past the end, so we're starting a new word in Cmd + return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: len(cmd.Words)} +} + +func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { + if cmd.Type != CmdTypeNone { + panic("findWordAtPos_none only works for CmdTypeNone") + } + for _, word := range cmd.Words { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + return nil + } + if pos <= endOffset { + if word.uncompletable() { + // only return an uncompletable word if we are really in the middle of it + if pos == endOffset { + continue + } + return word + } + return word + } + } + return nil +} + +// returns the context for completion +// if we are completing in a simple-command, the returns the Cmd. the Cmd can be used for specialized completion (command name, arg position, etc.) +// if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) +// otherwise we are going to create a new word to insert at offset (so the context does not matter) +func FindCompletionPos(cmds []*CmdType, pos int) CompletionPos { + if len(cmds) == 0 { + // set CompCommand because we're starting a new command + return CompletionPos{RawPos: pos, CompCommand: true} + } + for _, cmd := range cmds { + endOffset := cmd.endOffset() + if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) { + continue + } + startOffset := cmd.offset() + if cmd.Type == CmdTypeSimple { + if pos <= startOffset { + return CompletionPos{RawPos: pos, CompCommand: true} + } + return cmd.findCompletionPos_simple(pos) + } else { + // not in a simple-command + // if we're before the none-command, just start a new command + if pos <= startOffset { + return CompletionPos{RawPos: pos, CompCommand: true} + } + word := cmd.findWordAtPos_none(pos) + if word == nil { + // just revert to a file completion + return CompletionPos{RawPos: pos, CompCommand: false} + } + if word.uncompletable() { + // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid + return CompletionPos{RawPos: pos, CompInvalid: true} + } + // revert to file completion + return CompletionPos{RawPos: pos, CmdWord: word, CmdWordOffset: pos - word.Offset} + } + } + // past the end + lastCmd := cmds[len(cmds)-1] + if lastCmd.Type == CmdTypeSimple { + // just extend last command + return CompletionPos{RawPos: pos, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)} + } + // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand + return CompletionPos{RawPos: pos, CompCommand: lastCmd.NoneComplete} } func ResetWordOffsets(words []*WordType) { @@ -538,6 +670,24 @@ func (c *CmdType) lastWord() *WordType { return nil } +func (c *CmdType) firstWord() *WordType { + if len(c.AssignmentWords) > 0 { + return c.AssignmentWords[0] + } + if len(c.Words) > 0 { + return c.Words[0] + } + return nil +} + +func (c *CmdType) offset() int { + firstWord := c.firstWord() + if firstWord == nil { + return 0 + } + return firstWord.Offset +} + func (c *CmdType) endOffset() int { lastWord := c.lastWord() if lastWord == nil { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 7c863bd24..11f9a6cd8 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -88,7 +88,7 @@ func testParseCommands(t *testing.T, str string) { fmt.Printf("parse: %q\n", str) words := Tokenize(str) cmds := ParseCommands(words) - dumpCommands(cmds, " ", nil) + dumpCommands(cmds, " ", -1) fmt.Printf("\n") } From d469fed490ce5936b36047bbe16ce13302660c11 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 18 Nov 2022 19:05:03 -0800 Subject: [PATCH 185/397] working on getting comppos to work recursively --- pkg/shparse/shparse.go | 150 +++++++++++++++++++++++++++--------- pkg/shparse/shparse_test.go | 55 +++++++++++++ pkg/utilfn/utilfn.go | 8 ++ 3 files changed, 176 insertions(+), 37 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 601e983c0..2a658bc64 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -166,6 +166,13 @@ func (qc QuoteContext) cur() string { return qc[len(qc)-1] } +func (qc QuoteContext) clone() QuoteContext { + if len(qc) == 0 { + return nil + } + return append([]string(nil), qc...) +} + func makeRepeatStr(ch byte, slen int) string { if slen == 0 { return "" @@ -184,10 +191,10 @@ func (w *WordType) isBlank() bool { func (w *WordType) uncompletable() bool { switch w.Type { case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB: - return false + return true default: - return true + return false } } @@ -240,9 +247,9 @@ func dumpWords(words []*WordType, indentStr string, offset int) { func dumpCommands(cmds []*CmdType, indentStr string, pos int) { for _, cmd := range cmds { - fmt.Printf("%sCMD: %s [%d]\n", indentStr, cmd.Type, len(cmd.Words)) - dumpWords(cmd.AssignmentWords, indentStr+" *", -1) - dumpWords(cmd.Words, indentStr+" ", -1) + fmt.Printf("%sCMD: %s [%d] pos:%d\n", indentStr, cmd.Type, len(cmd.Words), pos) + dumpWords(cmd.AssignmentWords, indentStr+" *", pos) + dumpWords(cmd.Words, indentStr+" ", pos) } } @@ -487,8 +494,9 @@ func identifyReservedWords(words []*WordType) { } type CompletionPos struct { - RawPos int // the raw position of cursor - Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand) + RawPos int // the raw position of cursor + SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset + Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand) // index into cmd.Words (only useful when Cmd is not nil, otherwise we look at CompCommand) // 0 means command-word @@ -502,7 +510,7 @@ type CompletionPos struct { CompCommand bool // set when we think we are the first word of an existing or new command. otherwise we default to file completion } -func (cmd *CmdType) findCompletionPos_simple(pos int) CompletionPos { +func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos { if cmd.Type != CmdTypeSimple { panic("findCompletetionPos_simple only works for CmdTypeSimple") } @@ -511,11 +519,11 @@ func (cmd *CmdType) findCompletionPos_simple(pos int) CompletionPos { endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position (before the current assignment word) - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1} } if pos <= endOffset { // completing an assignment word - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset} } } var foundWord *WordType @@ -525,9 +533,10 @@ func (cmd *CmdType) findCompletionPos_simple(pos int) CompletionPos { endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: idx} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx} } - if pos == endOffset && word.uncompletable() { + if pos == endOffset && word.Type == WordTypeOp { + // operators are special, they can allow a full-word completion at endpos continue } if pos <= endOffset { @@ -539,12 +548,12 @@ func (cmd *CmdType) findCompletionPos_simple(pos int) CompletionPos { if foundWord != nil { if foundWord.uncompletable() { // invalid completion point - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true} } - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset} } // past the end, so we're starting a new word in Cmd - return CompletionPos{RawPos: pos, Cmd: cmd, CmdWordPos: len(cmd.Words)} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: len(cmd.Words)} } func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { @@ -558,12 +567,9 @@ func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { return nil } if pos <= endOffset { - if word.uncompletable() { - // only return an uncompletable word if we are really in the middle of it - if pos == endOffset { - continue - } - return word + if pos == endOffset && word.Type == WordTypeOp { + // operators are special, they can allow a full-word completion at endpos + continue } return word } @@ -571,14 +577,61 @@ func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { return nil } +func findWordAtPos(words []*WordType, pos int) *WordType { + for _, word := range words { + if pos > word.Offset && pos < word.Offset+len(word.Raw) { + return word + } + } + return nil +} + +// recursively descend down the word, parse commands and find a sub completion point if any. +// return nil if there is no sub completion point in this word +func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos { + if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ { + // need to descend further + wmeta := wordMetaMap[word.Type] + if pos <= wmeta.PrefixLen { + return nil + } + endPos := len(word.Raw) + if word.Complete { + endPos = endPos - wmeta.SuffixLen + } + if pos >= endPos { + return nil + } + subWord := findWordAtPos(word.Subs, pos-wmeta.PrefixLen) + if subWord == nil { + return nil + } + fullOffset := subWord.Offset + wmeta.PrefixLen + return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset) + } + if word.Type == WordTypeDP || word.Type == WordTypeBQ { + wmeta := wordMetaMap[word.Type] + if pos < wmeta.PrefixLen { + return nil + } + if word.Complete && pos > len(word.Raw)-wmeta.SuffixLen { + return nil + } + subCmds := ParseCommands(word.Subs) + newPos := FindCompletionPos(subCmds, pos-wmeta.PrefixLen, superOffset+wmeta.PrefixLen) + return &newPos + } + return nil +} + // returns the context for completion // if we are completing in a simple-command, the returns the Cmd. the Cmd can be used for specialized completion (command name, arg position, etc.) // if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) // otherwise we are going to create a new word to insert at offset (so the context does not matter) -func FindCompletionPos(cmds []*CmdType, pos int) CompletionPos { +func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos { if len(cmds) == 0 { // set CompCommand because we're starting a new command - return CompletionPos{RawPos: pos, CompCommand: true} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} } for _, cmd := range cmds { endOffset := cmd.endOffset() @@ -588,45 +641,58 @@ func FindCompletionPos(cmds []*CmdType, pos int) CompletionPos { startOffset := cmd.offset() if cmd.Type == CmdTypeSimple { if pos <= startOffset { - return CompletionPos{RawPos: pos, CompCommand: true} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} } - return cmd.findCompletionPos_simple(pos) + return cmd.findCompletionPos_simple(pos, superOffset) } else { // not in a simple-command // if we're before the none-command, just start a new command if pos <= startOffset { - return CompletionPos{RawPos: pos, CompCommand: true} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} } word := cmd.findWordAtPos_none(pos) if word == nil { // just revert to a file completion - return CompletionPos{RawPos: pos, CompCommand: false} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: false} } if word.uncompletable() { // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid - return CompletionPos{RawPos: pos, CompInvalid: true} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset, CompInvalid: true} } // revert to file completion - return CompletionPos{RawPos: pos, CmdWord: word, CmdWordOffset: pos - word.Offset} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset} } } // past the end lastCmd := cmds[len(cmds)-1] if lastCmd.Type == CmdTypeSimple { // just extend last command - return CompletionPos{RawPos: pos, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)} } // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand - return CompletionPos{RawPos: pos, CompCommand: lastCmd.NoneComplete} + return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: lastCmd.NoneComplete} } -func ResetWordOffsets(words []*WordType) { - pos := 0 +func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { + cpos := findCompletionPosCmds(cmds, pos, superOffset) + if cpos.CmdWord == nil { + return cpos + } + subPos := findCompletionPosInWord(cpos.CmdWord, cpos.CmdWordOffset, superOffset+cpos.CmdWord.Offset) + if subPos == nil { + return cpos + } else { + return *subPos + } +} + +func ResetWordOffsets(words []*WordType, startIdx int) { + pos := startIdx for _, word := range words { pos += len(word.Prefix) word.Offset = pos if len(word.Subs) > 0 { - ResetWordOffsets(word.Subs) + ResetWordOffsets(word.Subs, 0) } pos += len(word.Raw) } @@ -644,13 +710,23 @@ func (c *CmdType) stripPrefix() []rune { if len(c.AssignmentWords) > 0 { w := c.AssignmentWords[0] prefix := w.Prefix - w.Prefix = nil + if len(prefix) == 0 { + return nil + } + newWord := *w + newWord.Prefix = nil + c.AssignmentWords[0] = &newWord return prefix } if len(c.Words) > 0 { w := c.Words[0] prefix := w.Prefix - w.Prefix = nil + if len(prefix) == 0 { + return nil + } + newWord := *w + newWord.Prefix = nil + c.Words[0] = &newWord return prefix } return nil @@ -727,7 +803,7 @@ func cmdWhitespaceFixup(cmds []*CmdType) { nextCmd := cmds[idx+1] nextPrefix := nextCmd.stripPrefix() if len(nextPrefix) > 0 { - blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset(), Prefix: nextPrefix, Complete: true} + blankWord := &WordType{Type: WordTypeLit, QC: cmd.lastWord().QC, Offset: cmd.endOffset() + len(nextPrefix), Prefix: nextPrefix, Complete: true} cmd.Words = append(cmd.Words, blankWord) } } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 11f9a6cd8..a445dd45c 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -3,6 +3,8 @@ package shparse import ( "fmt" "testing" + + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" ) // $(ls f[*]); ./x @@ -102,3 +104,56 @@ func TestCmd(t *testing.T) { testParseCommands(t, `ls f"oo" "${x:"hello$y"}"`) testParseCommands(t, `x="foo $y" z=10 ls`) } + +func testCompPos(t *testing.T, cmdStr string, hasCommand bool, cmdWordPos int, hasWord bool, compInvalid bool, compCommand bool) { + cmdSP := utilfn.ParseToSP(cmdStr) + words := Tokenize(cmdSP.Str) + cmds := ParseCommands(words) + cpos := FindCompletionPos(cmds, cmdSP.Pos, 0) + fmt.Printf("testCompPos [%d] %q => %v\n", cmdSP.Pos, cmdStr, cpos) + if cpos.CmdWord != nil { + fmt.Printf(" found-word: %d %s\n", cpos.CmdWordOffset, cpos.CmdWord.stringWithPos(cpos.CmdWordOffset)) + } + if cpos.Cmd != nil { + fmt.Printf(" found-cmd: ") + dumpCommands([]*CmdType{cpos.Cmd}, " ", cpos.RawPos) + } + dumpCommands(cmds, " ", cmdSP.Pos) + fmt.Printf("\n") + if cpos.RawPos+cpos.SuperOffset != cmdSP.Pos { + t.Errorf("testCompPos %q => bad rawpos:%d superoffset:%d expected:%d", cmdStr, cpos.RawPos, cpos.SuperOffset, cmdSP.Pos) + } + if (cpos.Cmd != nil) != hasCommand { + t.Errorf("testCompPos %q => bad has-command exp:%v", cmdStr, hasCommand) + } + if (cpos.CmdWord != nil) != hasWord { + t.Errorf("testCompPos %q => bad has-word exp:%v", cmdStr, hasWord) + } + if cpos.CmdWordPos != cmdWordPos { + t.Errorf("testCompPos %q => bad cmd-word-pos got:%d exp:%d", cmdStr, cpos.CmdWordPos, cmdWordPos) + } + if cpos.CompInvalid != compInvalid { + t.Errorf("testCompPos %q => bad comp-invalid exp:%v", cmdStr, compInvalid) + } + if cpos.CompCommand != compCommand { + t.Errorf("testCompPos %q => bad comp-command exp:%v", cmdStr, compCommand) + } +} + +func TestCompPos(t *testing.T) { + testCompPos(t, "ls [*]foo", true, 1, false, false, false) + testCompPos(t, "ls foo [*];", true, 2, false, false, false) + testCompPos(t, "ls foo ;[*]", false, 0, false, false, true) + testCompPos(t, "ls foo >[*]> ./bar", true, 2, true, true, false) + testCompPos(t, "l[*]s", true, 0, true, false, false) + testCompPos(t, "ls[*]", true, 0, true, false, false) + testCompPos(t, "x=10 { (ls ./f[*] more); ls }", true, 1, true, false, false) + testCompPos(t, "for x in 1[*] 2 3; do ", false, 0, true, false, false) + testCompPos(t, "for[*] x in 1 2 3;", false, 0, true, true, false) + + testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", true, 2, true, false, false) + + testCompPos(t, "ls ${abc:$(ls -l [*])}", true, 1, true, false, false) + + testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, true, 1, true, false, false) +} diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index 2dace7fe8..e3f392a08 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -128,6 +128,14 @@ func (sp StrWithPos) String() string { return strWithCursor(sp.Str, sp.Pos) } +func ParseToSP(s string) StrWithPos { + idx := strings.Index(s, "[*]") + if idx == -1 { + return StrWithPos{Str: s} + } + return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: idx} +} + func strWithCursor(str string, pos int) string { if pos < 0 { return "[*]_" + str From 39ac41c448baf3ca38b4cabcf5b0505830bb2645 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 19 Nov 2022 14:05:38 -0800 Subject: [PATCH 186/397] working on expand --- pkg/shparse/expand.go | 205 ++++++++++++++++++++++++++++++++++++ pkg/shparse/shparse.go | 15 ++- pkg/shparse/shparse_test.go | 31 ++++++ 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 pkg/shparse/expand.go diff --git a/pkg/shparse/expand.go b/pkg/shparse/expand.go new file mode 100644 index 000000000..2f8d89108 --- /dev/null +++ b/pkg/shparse/expand.go @@ -0,0 +1,205 @@ +package shparse + +import ( + "bytes" + "fmt" + + "mvdan.cc/sh/v3/expand" +) + +const MaxExpandLen = 64 * 1024 + +type ExpandInfo struct { + HasTilde bool // only ~ as the first character when SimpleExpandContext.HomeDir is set + HasVar bool // $x, $$, ${...} + HasGlob bool // *, ?, [, { + HasExtGlob bool // ?(...) ... ?*+@! + HasHistory bool // ! (anywhere) + HasSpecial bool // subshell, arith +} + +type ExpandContext struct { + HomeDir string +} + +func expandSQ(buf *bytes.Buffer, rawLit []rune) { + // no info specials + buf.WriteString(string(rawLit)) +} + +// TODO implement our own ANSI single quote formatter +func expandANSISQ(buf *bytes.Buffer, rawLit []rune) { + // no info specials + str, _, _ := expand.Format(nil, string(rawLit), nil) + buf.WriteString(str) +} + +func expandLiteral(buf *bytes.Buffer, info *ExpandInfo, rawLit []rune) { + var lastBackSlash bool + var lastExtGlob bool + var lastDollar bool + for _, ch := range rawLit { + if ch == 0 { + break + } + if lastBackSlash { + lastBackSlash = false + if ch == '\n' { + // special case, backslash *and* newline are ignored + continue + } + buf.WriteRune(ch) + continue + } + if ch == '\\' { + lastBackSlash = true + lastExtGlob = false + lastDollar = false + continue + } + if ch == '*' || ch == '?' || ch == '[' || ch == '{' { + info.HasGlob = true + } + if ch == '`' { + info.HasSpecial = true + } + if ch == '!' { + info.HasHistory = true + } + if lastExtGlob && ch == '(' { + info.HasExtGlob = true + } + if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') { + info.HasVar = true + } + if lastDollar && (ch == '(' || ch == '[') { + info.HasSpecial = true + } + lastExtGlob = (ch == '?' || ch == '*' || ch == '+' || ch == '@' || ch == '!') + lastDollar = (ch == '$') + buf.WriteRune(ch) + } + if lastBackSlash { + buf.WriteByte('\\') + } +} + +// will also work for partial double quoted strings +func expandDQLiteral(buf *bytes.Buffer, info *ExpandInfo, rawVal []rune) { + var lastBackSlash bool + var lastDollar bool + for _, ch := range rawVal { + if ch == 0 { + break + } + if lastBackSlash { + lastBackSlash = false + if ch == '"' || ch == '\\' || ch == '$' || ch == '`' { + buf.WriteRune(ch) + continue + } + buf.WriteRune('\\') + buf.WriteRune(ch) + continue + } + if ch == '\\' { + lastBackSlash = true + lastDollar = false + continue + } + + // similar to expandLiteral, but no globbing + if ch == '`' { + info.HasSpecial = true + } + if ch == '!' { + info.HasHistory = true + } + if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') { + info.HasVar = true + } + if lastDollar && (ch == '(' || ch == '[') { + info.HasSpecial = true + } + lastDollar = (ch == '$') + buf.WriteRune(ch) + } + // in a valid parsed DQ string, you cannot have a trailing backslash (because \" would not end the string) + // still putting the case here though in case we ever deal with incomplete strings (e.g. completion) + if lastBackSlash { + buf.WriteByte('\\') + } +} + +func simpleExpandSubs(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) { + fmt.Printf("expand subs: %v\n", word) + parts := word.Subs + startPos := word.contentStartPos() + for _, part := range parts { + remainingLen := pos - startPos + if remainingLen <= 0 { + break + } + simpleExpandWord(buf, info, ectx, part, remainingLen) + startPos += len(part.Raw) + } +} + +func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) { + if pos >= word.contentEndPos() { + pos = len(word.Raw) + } + if pos <= word.contentStartPos() { + return + } + + switch word.Type { + case WordTypeLit: + if word.QC.cur() == WordTypeDQ { + expandDQLiteral(buf, info, word.Raw[:pos]) + return + } + expandLiteral(buf, info, word.Raw[:pos]) + + case WordTypeSQ: + expandSQ(buf, word.Raw[word.contentStartPos():pos]) + return + + case WordTypeDSQ: + expandANSISQ(buf, word.Raw[word.contentStartPos():pos]) + return + + case WordTypeDQ, WordTypeDDQ: + simpleExpandSubs(buf, info, ectx, word, pos) + return + + case WordTypeGroup: + simpleExpandSubs(buf, info, ectx, word, pos) + return + + case WordTypeSimpleVar: + return + + case WordTypeVarBrace: + return + + default: + if pos > len(word.Raw) { + pos = len(word.Raw) + } + info.HasSpecial = true + buf.WriteString(string(word.Raw[:pos])) + return + } +} + +func SimpleExpandPrefix(ectx ExpandContext, word *WordType, pos int) (string, ExpandInfo) { + var buf bytes.Buffer + var info ExpandInfo + simpleExpandWord(&buf, &info, ectx, word, pos) + return buf.String(), info +} + +func SimpleExpand(ectx ExpandContext, word *WordType) (string, ExpandInfo) { + return SimpleExpandPrefix(ectx, word, len(word.Raw)) +} diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 2a658bc64..e0f49eb5c 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -188,9 +188,22 @@ func (w *WordType) isBlank() bool { return w.Type == WordTypeLit && len(w.Raw) == 0 } +func (w *WordType) contentEndPos() int { + if !w.Complete { + return len(w.Raw) + } + wmeta := wordMetaMap[w.Type] + return len(w.Raw) - wmeta.SuffixLen +} + +func (w *WordType) contentStartPos() int { + wmeta := wordMetaMap[w.Type] + return wmeta.PrefixLen +} + func (w *WordType) uncompletable() bool { switch w.Type { - case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB: + case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB, WordTypeBQ, WordTypeDP: return true default: diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index a445dd45c..030c2ae5d 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -157,3 +157,34 @@ func TestCompPos(t *testing.T) { testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, true, 1, true, false, false) } + +func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { + ectx := ExpandContext{HomeDir: "/Users/mike"} + words := Tokenize(str) + if len(words) == 0 { + t.Errorf("could not tokenize any words from %q", str) + return + } + word := words[0] + output, info := SimpleExpandPrefix(ectx, word, pos) + if output != expStr { + t.Errorf("error expanding %q, output:%q exp:%q", str, output, expStr) + } else { + fmt.Printf("expand: %q (%d) => %q\n", str, pos, output) + } + if expInfo != nil { + if info != *expInfo { + t.Errorf("error expanding %q, info:%v exp:%v", str, info, expInfo) + } + } +} + +func TestExpand(t *testing.T) { + testExpand(t, "hello", 3, "hel", nil) + testExpand(t, "he\\$xabc", 6, "he$xa", nil) + testExpand(t, "he${x}abc", 6, "he$xa", nil) + testExpand(t, "'hello\"mike'", 8, "hello\"m", nil) + testExpand(t, `$'abc\x01def`, 10, "abc\x01d", nil) + testExpand(t, `$((2 + 2))`, 6, "$((2 +", &ExpandInfo{HasSpecial: true}) + testExpand(t, `abc"def"`, 6, "abcde", nil) +} From ff11be592253684c8dfe69bbfec8de0f1762f909 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 12:55:53 -0800 Subject: [PATCH 187/397] checkpoint on more completion/expansion --- pkg/shparse/expand.go | 69 +++++++++++++++++++++--- pkg/shparse/shparse.go | 101 +++++++++++++++++++++++++++--------- pkg/shparse/shparse_test.go | 50 ++++++++---------- 3 files changed, 160 insertions(+), 60 deletions(-) diff --git a/pkg/shparse/expand.go b/pkg/shparse/expand.go index 2f8d89108..9f8723118 100644 --- a/pkg/shparse/expand.go +++ b/pkg/shparse/expand.go @@ -145,12 +145,26 @@ func simpleExpandSubs(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, w } } +func canExpand(ectx ExpandContext, wtype string) bool { + return wtype == WordTypeLit || wtype == WordTypeSQ || wtype == WordTypeDSQ || + wtype == WordTypeDQ || wtype == WordTypeDDQ || wtype == WordTypeGroup +} + func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) { - if pos >= word.contentEndPos() { - pos = len(word.Raw) - } - if pos <= word.contentStartPos() { - return + if canExpand(ectx, word.Type) { + if pos >= word.contentEndPos() { + pos = word.contentEndPos() + } + if pos <= word.contentStartPos() { + return + } + } else { + if pos >= len(word.Raw) { + pos = len(word.Raw) + } + if pos <= 0 { + return + } } switch word.Type { @@ -177,16 +191,19 @@ func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, w simpleExpandSubs(buf, info, ectx, word, pos) return + // not expanded case WordTypeSimpleVar: + info.HasVar = true + buf.WriteString(string(word.Raw[:pos])) return + // not expanded case WordTypeVarBrace: + info.HasVar = true + buf.WriteString(string(word.Raw[:pos])) return default: - if pos > len(word.Raw) { - pos = len(word.Raw) - } info.HasSpecial = true buf.WriteString(string(word.Raw[:pos])) return @@ -203,3 +220,39 @@ func SimpleExpandPrefix(ectx ExpandContext, word *WordType, pos int) (string, Ex func SimpleExpand(ectx ExpandContext, word *WordType) (string, ExpandInfo) { return SimpleExpandPrefix(ectx, word, len(word.Raw)) } + +// returns varname (no '$') and ok (whether this is a valid varname expansion) +func SimpleVarNamePrefix(ectx ExpandContext, word *WordType, pos int) (string, bool) { + if word.Type != WordTypeSimpleVar && word.Type != WordTypeVarBrace { + return "", false + } + if word.Type == WordTypeSimpleVar { + if pos == 0 { + return "", false + } + if pos == 1 { + return "", true + } + if pos > len(word.Raw) { + pos = len(word.Raw) + } + return string(word.Raw[1:pos]), true + } + + // word.Type == WordTypeVarBrace + // knock '${' off the front, then see if the rest is a valid var name. + if pos == 0 || pos == 1 { + return "", false + } + if pos == 2 { + return "", true + } + if pos > word.contentEndPos() { + pos = word.contentEndPos() + } + rawVarName := word.Raw[2:pos] + if isSimpleVarName(rawVarName) { + return string(rawVarName), true + } + return "", false +} diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index e0f49eb5c..c3c23dc57 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -65,6 +65,15 @@ const ( CmdTypeSimple = "simple" // holds real commands ) +const ( + CompTypeCommand = "command" + CompTypeArg = "command-arg" + CompTypeInvalid = "invalid" + CompTypeVar = "var" + CompTypeAssignment = "assignment" + CompTypeBasic = "basic" +) + type WordType struct { Type string Offset int @@ -507,36 +516,52 @@ func identifyReservedWords(words []*WordType) { } type CompletionPos struct { - RawPos int // the raw position of cursor - SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset - Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand) + RawPos int // the raw position of cursor + SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset - // index into cmd.Words (only useful when Cmd is not nil, otherwise we look at CompCommand) + CompType string // see CompType* constants + Cmd *CmdType // nil if between commands or a special completion (otherwise will be a SimpleCommand) + // index into cmd.Words (only set when Cmd is not nil, otherwise we look at CompCommand) // 0 means command-word // negative means assignment-words. // can be past the end of Words (means start new word). - CmdWordPos int + CmdWordPos int + CompWord *WordType // set to the word we are completing (nil if we are starting a new word) + CompWordOffset int // offset into compword (only if CmdWord is not nil) - CmdWord *WordType // set to the word we are completing (nil if we are starting a new word) - CmdWordOffset int // offset into cmdword (only if CmdWord is not nil) - CompInvalid bool // some words cannot be completed (e.g. in the middle of an operator, inside a control structure, etc.) - CompCommand bool // set when we think we are the first word of an existing or new command. otherwise we default to file completion +} + +func compTypeFromPos(cmdWordPos int) string { + if cmdWordPos == 0 { + return CompTypeCommand + } + if cmdWordPos < 0 { + return CompTypeAssignment + } + return CompTypeArg } func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos { if cmd.Type != CmdTypeSimple { panic("findCompletetionPos_simple only works for CmdTypeSimple") } + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd} for idx, word := range cmd.AssignmentWords { startOffset := word.Offset endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position (before the current assignment word) - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1} + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompType = CompTypeAssignment + return rtn } if pos <= endOffset { // completing an assignment word - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset} + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompWord = word + rtn.CompWordOffset = pos - word.Offset + rtn.CompType = CompTypeAssignment + return rtn } } var foundWord *WordType @@ -546,7 +571,9 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx} + rtn.CmdWordPos = idx + rtn.CompType = compTypeFromPos(idx) + return rtn } if pos == endOffset && word.Type == WordTypeOp { // operators are special, they can allow a full-word completion at endpos @@ -559,14 +586,21 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio } } if foundWord != nil { + rtn.CmdWordPos = foundWordIdx + rtn.CompWord = foundWord + rtn.CompWordOffset = pos - foundWord.Offset if foundWord.uncompletable() { // invalid completion point - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true} + rtn.CompType = CompTypeInvalid + return rtn } - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset} + rtn.CompType = compTypeFromPos(foundWordIdx) + return rtn } // past the end, so we're starting a new word in Cmd - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: len(cmd.Words)} + rtn.CmdWordPos = len(cmd.Words) + rtn.CompType = CompTypeArg + return rtn } func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { @@ -642,9 +676,11 @@ func findCompletionPosInWord(word *WordType, pos int, superOffset int) *Completi // if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) // otherwise we are going to create a new word to insert at offset (so the context does not matter) func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos { + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} if len(cmds) == 0 { // set CompCommand because we're starting a new command - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } for _, cmd := range cmds { endOffset := cmd.endOffset() @@ -654,44 +690,59 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion startOffset := cmd.offset() if cmd.Type == CmdTypeSimple { if pos <= startOffset { - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } return cmd.findCompletionPos_simple(pos, superOffset) } else { // not in a simple-command // if we're before the none-command, just start a new command if pos <= startOffset { - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } word := cmd.findWordAtPos_none(pos) if word == nil { // just revert to a file completion - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: false} + rtn.CompType = CompTypeBasic + return rtn } + rtn.CompWord = word + rtn.CompWordOffset = pos - word.Offset if word.uncompletable() { // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset, CompInvalid: true} + rtn.CompType = CompTypeInvalid + return rtn } // revert to file completion - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset} + rtn.CompType = CompTypeBasic + return rtn } } // past the end lastCmd := cmds[len(cmds)-1] if lastCmd.Type == CmdTypeSimple { // just extend last command - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)} + rtn.Cmd = lastCmd + rtn.CmdWordPos = len(lastCmd.Words) + rtn.CompType = CompTypeArg + return rtn } // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: lastCmd.NoneComplete} + if lastCmd.NoneComplete { + rtn.CompType = CompTypeCommand + } else { + rtn.CompType = CompTypeBasic + } + return rtn } func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { cpos := findCompletionPosCmds(cmds, pos, superOffset) - if cpos.CmdWord == nil { + if cpos.CompWord == nil { return cpos } - subPos := findCompletionPosInWord(cpos.CmdWord, cpos.CmdWordOffset, superOffset+cpos.CmdWord.Offset) + subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset) if subPos == nil { return cpos } else { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 030c2ae5d..e4d7cc622 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -105,14 +105,17 @@ func TestCmd(t *testing.T) { testParseCommands(t, `x="foo $y" z=10 ls`) } -func testCompPos(t *testing.T, cmdStr string, hasCommand bool, cmdWordPos int, hasWord bool, compInvalid bool, compCommand bool) { +func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool) { cmdSP := utilfn.ParseToSP(cmdStr) words := Tokenize(cmdSP.Str) cmds := ParseCommands(words) cpos := FindCompletionPos(cmds, cmdSP.Pos, 0) - fmt.Printf("testCompPos [%d] %q => %v\n", cmdSP.Pos, cmdStr, cpos) - if cpos.CmdWord != nil { - fmt.Printf(" found-word: %d %s\n", cpos.CmdWordOffset, cpos.CmdWord.stringWithPos(cpos.CmdWordOffset)) + fmt.Printf("testCompPos [%d] %q => [%s] %v\n", cmdSP.Pos, cmdStr, cpos.CompType, cpos) + if cpos.CompType != compType { + t.Errorf("testCompPos %q => invalid comp-type %q, expected %q", cmdStr, cpos.CompType, compType) + } + if cpos.CompWord != nil { + fmt.Printf(" found-word: %d %s\n", cpos.CompWordOffset, cpos.CompWord.stringWithPos(cpos.CompWordOffset)) } if cpos.Cmd != nil { fmt.Printf(" found-cmd: ") @@ -126,36 +129,27 @@ func testCompPos(t *testing.T, cmdStr string, hasCommand bool, cmdWordPos int, h if (cpos.Cmd != nil) != hasCommand { t.Errorf("testCompPos %q => bad has-command exp:%v", cmdStr, hasCommand) } - if (cpos.CmdWord != nil) != hasWord { + if (cpos.CompWord != nil) != hasWord { t.Errorf("testCompPos %q => bad has-word exp:%v", cmdStr, hasWord) } if cpos.CmdWordPos != cmdWordPos { t.Errorf("testCompPos %q => bad cmd-word-pos got:%d exp:%d", cmdStr, cpos.CmdWordPos, cmdWordPos) } - if cpos.CompInvalid != compInvalid { - t.Errorf("testCompPos %q => bad comp-invalid exp:%v", cmdStr, compInvalid) - } - if cpos.CompCommand != compCommand { - t.Errorf("testCompPos %q => bad comp-command exp:%v", cmdStr, compCommand) - } } func TestCompPos(t *testing.T) { - testCompPos(t, "ls [*]foo", true, 1, false, false, false) - testCompPos(t, "ls foo [*];", true, 2, false, false, false) - testCompPos(t, "ls foo ;[*]", false, 0, false, false, true) - testCompPos(t, "ls foo >[*]> ./bar", true, 2, true, true, false) - testCompPos(t, "l[*]s", true, 0, true, false, false) - testCompPos(t, "ls[*]", true, 0, true, false, false) - testCompPos(t, "x=10 { (ls ./f[*] more); ls }", true, 1, true, false, false) - testCompPos(t, "for x in 1[*] 2 3; do ", false, 0, true, false, false) - testCompPos(t, "for[*] x in 1 2 3;", false, 0, true, true, false) - - testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", true, 2, true, false, false) - - testCompPos(t, "ls ${abc:$(ls -l [*])}", true, 1, true, false, false) - - testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, true, 1, true, false, false) + testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false) + testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false) + testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false) + testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true) + testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true) + testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true) + testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true) + testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true) + testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true) + testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", CompTypeArg, true, 2, true) + testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeArg, true, 1, true) // we don't sub-parse inside of ${} + testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true) } func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { @@ -182,9 +176,11 @@ func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *Expan func TestExpand(t *testing.T) { testExpand(t, "hello", 3, "hel", nil) testExpand(t, "he\\$xabc", 6, "he$xa", nil) - testExpand(t, "he${x}abc", 6, "he$xa", nil) + testExpand(t, "he${x}abc", 6, "he${x}", nil) testExpand(t, "'hello\"mike'", 8, "hello\"m", nil) testExpand(t, `$'abc\x01def`, 10, "abc\x01d", nil) testExpand(t, `$((2 + 2))`, 6, "$((2 +", &ExpandInfo{HasSpecial: true}) testExpand(t, `abc"def"`, 6, "abcde", nil) + testExpand(t, `"abc$x$'"'""`, 12, "abc$x\"", nil) + testExpand(t, `'he'\''s'`, 9, "he's", nil) } From ff11290fa08b219efe94386922027d78b79cb7b0 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 14:25:02 -0800 Subject: [PATCH 188/397] checkpoint, none/simple both need to recurse --- pkg/shparse/shparse.go | 52 ++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index c3c23dc57..744260f06 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -210,6 +210,16 @@ func (w *WordType) contentStartPos() int { return wmeta.PrefixLen } +func (w *WordType) canHaveSubs() bool { + switch w.Type { + case WordTypeGroup, WordTypeDQ, WordTypeDDQ, WordTypeDP, WordTypeBQ: + return true + + default: + return false + } +} + func (w *WordType) uncompletable() bool { switch w.Type { case WordTypeRaw, WordTypeOp, WordTypeKey, WordTypeDPP, WordTypePP, WordTypeDB, WordTypeBQ, WordTypeDP: @@ -603,25 +613,42 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio return rtn } -func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { +func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos { + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} if cmd.Type != CmdTypeNone { panic("findWordAtPos_none only works for CmdTypeNone") } + var foundWord *WordType for _, word := range cmd.Words { startOffset := word.Offset endOffset := word.Offset + len(word.Raw) if pos <= startOffset { - return nil + break } if pos <= endOffset { if pos == endOffset && word.Type == WordTypeOp { // operators are special, they can allow a full-word completion at endpos continue } - return word + foundWord = word + break } } - return nil + if foundWord == nil { + // just revert to a file completion + rtn.CompType = CompTypeBasic + return rtn + } + rtn.CompWord = foundWord + rtn.CompWordOffset = pos - foundWord.Offset + if foundWord.uncompletable() { + // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid + rtn.CompType = CompTypeInvalid + return rtn + } + // revert to file completion + rtn.CompType = CompTypeBasic + return rtn } func findWordAtPos(words []*WordType, pos int) *WordType { @@ -701,22 +728,7 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion rtn.CompType = CompTypeCommand return rtn } - word := cmd.findWordAtPos_none(pos) - if word == nil { - // just revert to a file completion - rtn.CompType = CompTypeBasic - return rtn - } - rtn.CompWord = word - rtn.CompWordOffset = pos - word.Offset - if word.uncompletable() { - // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid - rtn.CompType = CompTypeInvalid - return rtn - } - // revert to file completion - rtn.CompType = CompTypeBasic - return rtn + return cmd.findCompletionWordAtPos_none(pos, superOffset) } } // past the end From 9f7b5c82266872054695116b48cad3784419d933 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 14:47:00 -0800 Subject: [PATCH 189/397] checkpoint, return comptype 'var' when we're in a var word --- pkg/shparse/comp.go | 253 ++++++++++++++++++++++++++++++++++++ pkg/shparse/shparse.go | 246 ----------------------------------- pkg/shparse/shparse_test.go | 3 +- 3 files changed, 255 insertions(+), 247 deletions(-) create mode 100644 pkg/shparse/comp.go diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go new file mode 100644 index 000000000..176378af4 --- /dev/null +++ b/pkg/shparse/comp.go @@ -0,0 +1,253 @@ +package shparse + +const ( + CompTypeCommand = "command" + CompTypeArg = "command-arg" + CompTypeInvalid = "invalid" + CompTypeVar = "var" + CompTypeAssignment = "assignment" + CompTypeBasic = "basic" +) + +type CompletionPos struct { + RawPos int // the raw position of cursor + SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset + + CompType string // see CompType* constants + Cmd *CmdType // nil if between commands or a special completion (otherwise will be a SimpleCommand) + // index into cmd.Words (only set when Cmd is not nil, otherwise we look at CompCommand) + // 0 means command-word + // negative means assignment-words. + // can be past the end of Words (means start new word). + CmdWordPos int + CompWord *WordType // set to the word we are completing (nil if we are starting a new word) + CompWordOffset int // offset into compword (only if CmdWord is not nil) + +} + +func compTypeFromPos(cmdWordPos int) string { + if cmdWordPos == 0 { + return CompTypeCommand + } + if cmdWordPos < 0 { + return CompTypeAssignment + } + return CompTypeArg +} + +func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos { + if cmd.Type != CmdTypeSimple { + panic("findCompletetionPos_simple only works for CmdTypeSimple") + } + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd} + for idx, word := range cmd.AssignmentWords { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + // starting a new word at this position (before the current assignment word) + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompType = CompTypeAssignment + return rtn + } + if pos <= endOffset { + // completing an assignment word + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompWord = word + rtn.CompWordOffset = pos - word.Offset + rtn.CompType = CompTypeAssignment + return rtn + } + } + var foundWord *WordType + var foundWordIdx int + for idx, word := range cmd.Words { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + // starting a new word at this position + rtn.CmdWordPos = idx + rtn.CompType = compTypeFromPos(idx) + return rtn + } + if pos == endOffset && word.Type == WordTypeOp { + // operators are special, they can allow a full-word completion at endpos + continue + } + if pos <= endOffset { + foundWord = word + foundWordIdx = idx + break + } + } + if foundWord != nil { + rtn.CmdWordPos = foundWordIdx + rtn.CompWord = foundWord + rtn.CompWordOffset = pos - foundWord.Offset + if foundWord.uncompletable() { + // invalid completion point + rtn.CompType = CompTypeInvalid + return rtn + } + rtn.CompType = compTypeFromPos(foundWordIdx) + return rtn + } + // past the end, so we're starting a new word in Cmd + rtn.CmdWordPos = len(cmd.Words) + rtn.CompType = CompTypeArg + return rtn +} + +func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos { + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} + if cmd.Type != CmdTypeNone { + panic("findCompletionWordAtPos_none only works for CmdTypeNone") + } + var foundWord *WordType + for _, word := range cmd.Words { + startOffset := word.Offset + endOffset := word.Offset + len(word.Raw) + if pos <= startOffset { + break + } + if pos <= endOffset { + if pos == endOffset && word.Type == WordTypeOp { + // operators are special, they can allow a full-word completion at endpos + continue + } + foundWord = word + break + } + } + if foundWord == nil { + // just revert to a file completion + rtn.CompType = CompTypeBasic + return rtn + } + rtn.CompWord = foundWord + rtn.CompWordOffset = pos - foundWord.Offset + if foundWord.uncompletable() { + // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid + rtn.CompType = CompTypeInvalid + return rtn + } + // revert to file completion + rtn.CompType = CompTypeBasic + return rtn +} + +func findCompletionWordAtPos(words []*WordType, pos int) *WordType { + // WordTypeSimpleVar is special, if cursor is at the end of SimpleVar it is returned + for _, word := range words { + if pos > word.Offset && pos < word.Offset+len(word.Raw) { + return word + } + if word.Type == WordTypeSimpleVar && pos == word.Offset+len(word.Raw) { + return word + } + } + return nil +} + +// recursively descend down the word, parse commands and find a sub completion point if any. +// return nil if there is no sub completion point in this word +func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos { + if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ { + // need to descend further + if pos <= word.contentStartPos() { + return nil + } + if pos > word.contentEndPos() { + return nil + } + subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos()) + if subWord == nil { + return nil + } + fullOffset := subWord.Offset + word.contentStartPos() + return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset) + } + if word.Type == WordTypeDP || word.Type == WordTypeBQ { + if pos < word.contentStartPos() { + return nil + } + if pos > word.contentEndPos() { + return nil + } + subCmds := ParseCommands(word.Subs) + newPos := FindCompletionPos(subCmds, pos-word.contentStartPos(), superOffset+word.contentStartPos()) + return &newPos + } + if word.Type == WordTypeSimpleVar || word.Type == WordTypeVarBrace { + // special "var" completion + rtn := &CompletionPos{RawPos: pos, SuperOffset: superOffset} + rtn.CompType = CompTypeVar + rtn.CompWordOffset = pos + rtn.CompWord = word + return rtn + } + return nil +} + +// returns the context for completion +// if we are completing in a simple-command, the returns the Cmd. the Cmd can be used for specialized completion (command name, arg position, etc.) +// if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) +// otherwise we are going to create a new word to insert at offset (so the context does not matter) +func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos { + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} + if len(cmds) == 0 { + // set CompCommand because we're starting a new command + rtn.CompType = CompTypeCommand + return rtn + } + for _, cmd := range cmds { + endOffset := cmd.endOffset() + if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) { + continue + } + startOffset := cmd.offset() + if cmd.Type == CmdTypeSimple { + if pos <= startOffset { + rtn.CompType = CompTypeCommand + return rtn + } + return cmd.findCompletionPos_simple(pos, superOffset) + } else { + // not in a simple-command + // if we're before the none-command, just start a new command + if pos <= startOffset { + rtn.CompType = CompTypeCommand + return rtn + } + return cmd.findCompletionWordAtPos_none(pos, superOffset) + } + } + // past the end + lastCmd := cmds[len(cmds)-1] + if lastCmd.Type == CmdTypeSimple { + // just extend last command + rtn.Cmd = lastCmd + rtn.CmdWordPos = len(lastCmd.Words) + rtn.CompType = CompTypeArg + return rtn + } + // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand + if lastCmd.NoneComplete { + rtn.CompType = CompTypeCommand + } else { + rtn.CompType = CompTypeBasic + } + return rtn +} + +func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { + cpos := findCompletionPosCmds(cmds, pos, superOffset) + if cpos.CompWord == nil { + return cpos + } + subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset) + if subPos == nil { + return cpos + } else { + return *subPos + } +} diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index 744260f06..ac09762f1 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -65,15 +65,6 @@ const ( CmdTypeSimple = "simple" // holds real commands ) -const ( - CompTypeCommand = "command" - CompTypeArg = "command-arg" - CompTypeInvalid = "invalid" - CompTypeVar = "var" - CompTypeAssignment = "assignment" - CompTypeBasic = "basic" -) - type WordType struct { Type string Offset int @@ -525,243 +516,6 @@ func identifyReservedWords(words []*WordType) { } } -type CompletionPos struct { - RawPos int // the raw position of cursor - SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset - - CompType string // see CompType* constants - Cmd *CmdType // nil if between commands or a special completion (otherwise will be a SimpleCommand) - // index into cmd.Words (only set when Cmd is not nil, otherwise we look at CompCommand) - // 0 means command-word - // negative means assignment-words. - // can be past the end of Words (means start new word). - CmdWordPos int - CompWord *WordType // set to the word we are completing (nil if we are starting a new word) - CompWordOffset int // offset into compword (only if CmdWord is not nil) - -} - -func compTypeFromPos(cmdWordPos int) string { - if cmdWordPos == 0 { - return CompTypeCommand - } - if cmdWordPos < 0 { - return CompTypeAssignment - } - return CompTypeArg -} - -func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos { - if cmd.Type != CmdTypeSimple { - panic("findCompletetionPos_simple only works for CmdTypeSimple") - } - rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd} - for idx, word := range cmd.AssignmentWords { - startOffset := word.Offset - endOffset := word.Offset + len(word.Raw) - if pos <= startOffset { - // starting a new word at this position (before the current assignment word) - rtn.CmdWordPos = idx - len(cmd.AssignmentWords) - rtn.CompType = CompTypeAssignment - return rtn - } - if pos <= endOffset { - // completing an assignment word - rtn.CmdWordPos = idx - len(cmd.AssignmentWords) - rtn.CompWord = word - rtn.CompWordOffset = pos - word.Offset - rtn.CompType = CompTypeAssignment - return rtn - } - } - var foundWord *WordType - var foundWordIdx int - for idx, word := range cmd.Words { - startOffset := word.Offset - endOffset := word.Offset + len(word.Raw) - if pos <= startOffset { - // starting a new word at this position - rtn.CmdWordPos = idx - rtn.CompType = compTypeFromPos(idx) - return rtn - } - if pos == endOffset && word.Type == WordTypeOp { - // operators are special, they can allow a full-word completion at endpos - continue - } - if pos <= endOffset { - foundWord = word - foundWordIdx = idx - break - } - } - if foundWord != nil { - rtn.CmdWordPos = foundWordIdx - rtn.CompWord = foundWord - rtn.CompWordOffset = pos - foundWord.Offset - if foundWord.uncompletable() { - // invalid completion point - rtn.CompType = CompTypeInvalid - return rtn - } - rtn.CompType = compTypeFromPos(foundWordIdx) - return rtn - } - // past the end, so we're starting a new word in Cmd - rtn.CmdWordPos = len(cmd.Words) - rtn.CompType = CompTypeArg - return rtn -} - -func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos { - rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} - if cmd.Type != CmdTypeNone { - panic("findWordAtPos_none only works for CmdTypeNone") - } - var foundWord *WordType - for _, word := range cmd.Words { - startOffset := word.Offset - endOffset := word.Offset + len(word.Raw) - if pos <= startOffset { - break - } - if pos <= endOffset { - if pos == endOffset && word.Type == WordTypeOp { - // operators are special, they can allow a full-word completion at endpos - continue - } - foundWord = word - break - } - } - if foundWord == nil { - // just revert to a file completion - rtn.CompType = CompTypeBasic - return rtn - } - rtn.CompWord = foundWord - rtn.CompWordOffset = pos - foundWord.Offset - if foundWord.uncompletable() { - // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid - rtn.CompType = CompTypeInvalid - return rtn - } - // revert to file completion - rtn.CompType = CompTypeBasic - return rtn -} - -func findWordAtPos(words []*WordType, pos int) *WordType { - for _, word := range words { - if pos > word.Offset && pos < word.Offset+len(word.Raw) { - return word - } - } - return nil -} - -// recursively descend down the word, parse commands and find a sub completion point if any. -// return nil if there is no sub completion point in this word -func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos { - if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ { - // need to descend further - wmeta := wordMetaMap[word.Type] - if pos <= wmeta.PrefixLen { - return nil - } - endPos := len(word.Raw) - if word.Complete { - endPos = endPos - wmeta.SuffixLen - } - if pos >= endPos { - return nil - } - subWord := findWordAtPos(word.Subs, pos-wmeta.PrefixLen) - if subWord == nil { - return nil - } - fullOffset := subWord.Offset + wmeta.PrefixLen - return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset) - } - if word.Type == WordTypeDP || word.Type == WordTypeBQ { - wmeta := wordMetaMap[word.Type] - if pos < wmeta.PrefixLen { - return nil - } - if word.Complete && pos > len(word.Raw)-wmeta.SuffixLen { - return nil - } - subCmds := ParseCommands(word.Subs) - newPos := FindCompletionPos(subCmds, pos-wmeta.PrefixLen, superOffset+wmeta.PrefixLen) - return &newPos - } - return nil -} - -// returns the context for completion -// if we are completing in a simple-command, the returns the Cmd. the Cmd can be used for specialized completion (command name, arg position, etc.) -// if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) -// otherwise we are going to create a new word to insert at offset (so the context does not matter) -func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos { - rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} - if len(cmds) == 0 { - // set CompCommand because we're starting a new command - rtn.CompType = CompTypeCommand - return rtn - } - for _, cmd := range cmds { - endOffset := cmd.endOffset() - if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) { - continue - } - startOffset := cmd.offset() - if cmd.Type == CmdTypeSimple { - if pos <= startOffset { - rtn.CompType = CompTypeCommand - return rtn - } - return cmd.findCompletionPos_simple(pos, superOffset) - } else { - // not in a simple-command - // if we're before the none-command, just start a new command - if pos <= startOffset { - rtn.CompType = CompTypeCommand - return rtn - } - return cmd.findCompletionWordAtPos_none(pos, superOffset) - } - } - // past the end - lastCmd := cmds[len(cmds)-1] - if lastCmd.Type == CmdTypeSimple { - // just extend last command - rtn.Cmd = lastCmd - rtn.CmdWordPos = len(lastCmd.Words) - rtn.CompType = CompTypeArg - return rtn - } - // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand - if lastCmd.NoneComplete { - rtn.CompType = CompTypeCommand - } else { - rtn.CompType = CompTypeBasic - } - return rtn -} - -func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { - cpos := findCompletionPosCmds(cmds, pos, superOffset) - if cpos.CompWord == nil { - return cpos - } - subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset) - if subPos == nil { - return cpos - } else { - return *subPos - } -} - func ResetWordOffsets(words []*WordType, startIdx int) { pos := startIdx for _, word := range words { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index e4d7cc622..371791cdf 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -148,8 +148,9 @@ func TestCompPos(t *testing.T) { testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true) testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true) testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", CompTypeArg, true, 2, true) - testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeArg, true, 1, true) // we don't sub-parse inside of ${} + testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true) // we don't sub-parse inside of ${} (so this returns "var" right now) testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true) + testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true) } func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { From 8729d1f4919134e58b5322238f75fb34d252f6c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 16:37:09 -0800 Subject: [PATCH 190/397] strwithpos uses rune position not byte position --- pkg/utilfn/utilfn.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index e3f392a08..62195f970 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -3,6 +3,7 @@ package utilfn import ( "regexp" "strings" + "unicode/utf8" ) var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} @@ -121,7 +122,7 @@ func ContainsStr(strs []string, test string) bool { type StrWithPos struct { Str string - Pos int + Pos int // this is a 'rune' position (not a byte position) } func (sp StrWithPos) String() string { @@ -133,7 +134,7 @@ func ParseToSP(s string) StrWithPos { if idx == -1 { return StrWithPos{Str: s} } - return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: idx} + return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: utf8.RuneCountInString(s[0:idx])} } func strWithCursor(str string, pos int) string { @@ -145,7 +146,14 @@ func strWithCursor(str string, pos int) string { return str + "_[*]" } return str + "[*]" - } else { - return str[:pos] + "[*]" + str[pos:] } + + var rtn []rune + for _, ch := range str { + if len(rtn) == pos { + rtn = append(rtn, '[', '*', ']') + } + rtn = append(rtn, ch) + } + return string(rtn) } From 75f662a188577bf0229eaab5bcfe6bf6022c9abd Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 19:06:59 -0800 Subject: [PATCH 191/397] checkpoint -- extension --- pkg/shparse/comp.go | 36 +++--- pkg/shparse/extend.go | 241 ++++++++++++++++++++++-------------- pkg/shparse/shparse.go | 26 +++- pkg/shparse/shparse_test.go | 59 +++++---- 4 files changed, 231 insertions(+), 131 deletions(-) diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index 176378af4..136bf365a 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -1,12 +1,13 @@ package shparse const ( - CompTypeCommand = "command" - CompTypeArg = "command-arg" - CompTypeInvalid = "invalid" - CompTypeVar = "var" - CompTypeAssignment = "assignment" - CompTypeBasic = "basic" + CompTypeCommandMeta = "command-meta" + CompTypeCommand = "command" + CompTypeArg = "command-arg" + CompTypeInvalid = "invalid" + CompTypeVar = "var" + CompTypeAssignment = "assignment" + CompTypeBasic = "basic" ) type CompletionPos struct { @@ -22,7 +23,6 @@ type CompletionPos struct { CmdWordPos int CompWord *WordType // set to the word we are completing (nil if we are starting a new word) CompWordOffset int // offset into compword (only if CmdWord is not nil) - } func compTypeFromPos(cmdWordPos int) string { @@ -97,10 +97,10 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio return rtn } -func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos { +func (cmd *CmdType) findCompletionPos_none(pos int, superOffset int) CompletionPos { rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} if cmd.Type != CmdTypeNone { - panic("findCompletionWordAtPos_none only works for CmdTypeNone") + panic("findCompletionPos_none only works for CmdTypeNone") } var foundWord *WordType for _, word := range cmd.Words { @@ -123,25 +123,31 @@ func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) Compl rtn.CompType = CompTypeBasic return rtn } + foundWordOffset := pos - foundWord.Offset rtn.CompWord = foundWord - rtn.CompWordOffset = pos - foundWord.Offset + rtn.CompWordOffset = foundWordOffset if foundWord.uncompletable() { // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid rtn.CompType = CompTypeInvalid return rtn } + if foundWordOffset > 0 && foundWordOffset < foundWord.contentStartPos() { + // cursor is in a weird position, between characters of a multi-char prefix (e.g. "$[*]{hello}" or $[*]'hello'). cannot complete. + rtn.CompType = CompTypeInvalid + return rtn + } // revert to file completion rtn.CompType = CompTypeBasic return rtn } -func findCompletionWordAtPos(words []*WordType, pos int) *WordType { - // WordTypeSimpleVar is special, if cursor is at the end of SimpleVar it is returned +func findCompletionWordAtPos(words []*WordType, pos int, allowEndMatch bool) *WordType { + // WordTypeSimpleVar is special (always allowEndMatch), if cursor is at the end of SimpleVar it is returned for _, word := range words { if pos > word.Offset && pos < word.Offset+len(word.Raw) { return word } - if word.Type == WordTypeSimpleVar && pos == word.Offset+len(word.Raw) { + if (allowEndMatch || word.Type == WordTypeSimpleVar) && pos == word.Offset+len(word.Raw) { return word } } @@ -159,7 +165,7 @@ func findCompletionPosInWord(word *WordType, pos int, superOffset int) *Completi if pos > word.contentEndPos() { return nil } - subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos()) + subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos(), false) if subWord == nil { return nil } @@ -218,7 +224,7 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion rtn.CompType = CompTypeCommand return rtn } - return cmd.findCompletionWordAtPos_none(pos, superOffset) + return cmd.findCompletionPos_none(pos, superOffset) } } // past the end diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index 5761d0bd2..f7bbd8f0e 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -93,35 +93,108 @@ func (ec *extendContext) appendWord(w *WordType) { func (ec *extendContext) ensureCurWord() { if ec.CurWord == nil || ec.CurWord.Type != ec.Intention { - ec.CurWord = MakeEmptyWord(ec.Intention, ec.QC, 0) + ec.CurWord = MakeEmptyWord(ec.Intention, ec.QC, 0, true) ec.Rtn = append(ec.Rtn, ec.CurWord) } } +// grp, dq, ddq +func extendWithSubs(buf *bytes.Buffer, word *WordType, wordPos int, extStr string) { + +} + +// lit, svar, varb, sq, dsq +func extendLeafCh(buf *bytes.Buffer, wordOpen *bool, wtype string, qc QuoteContext, ch rune) { + switch wtype { + case WordTypeSimpleVar, WordTypeVarBrace: + extendVar(buf, ch) + + case WordTypeLit: + if qc.cur() == WordTypeDQ { + extendDQLit(buf, wordOpen, ch) + } else { + extendLit(buf, ch) + } + + case WordTypeSQ: + extendSQ(buf, wordOpen, ch) + + case WordTypeDSQ: + extendDSQ(buf, wordOpen, ch) + + default: + return + } +} + +// lit, svar, varb sq, dsq +func extendLeaf(buf *bytes.Buffer, wordOpen *bool, word *WordType, wordPos int, extStr string) { + for _, ch := range extStr { + extendLeafCh(buf, wordOpen, word.Type, word.QC, ch) + } +} + +// lit, grp, svar, dq, ddq, varb, sq, dsq +func Extend(word *WordType, wordPos int, extStr string, complete bool) utilfn.StrWithPos { + if extStr == "" { + return utilfn.StrWithPos{Str: string(word.Raw), Pos: wordPos} + } + var buf bytes.Buffer + isEOW := wordPos >= word.contentEndPos() + if isEOW { + wordPos = word.contentEndPos() + } + if wordPos > 0 && wordPos < word.contentStartPos() { + wordPos = word.contentStartPos() + } + wordOpen := false + if wordPos >= word.contentStartPos() { + wordOpen = true + } + buf.WriteString(string(word.Raw[0:wordPos])) // write the prefix + if word.canHaveSubs() { + extendWithSubs(&buf, word, wordPos, extStr) + } else { + extendLeaf(&buf, &wordOpen, word, wordPos, extStr) + } + if isEOW { + // end-of-word, write the suffix (and optional ' '). return the end of the string + wmeta := wordMetaMap[word.Type] + buf.WriteString(wmeta.getSuffix()) + var rtnPos int + if complete { + buf.WriteRune(' ') + rtnPos = utf8.RuneCount(buf.Bytes()) + } else { + rtnPos = utf8.RuneCount(buf.Bytes()) - wmeta.SuffixLen + } + return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos} + } + // completion in the middle of a word (no ' ') + rtnPos := utf8.RuneCount(buf.Bytes()) + buf.WriteString(string(word.Raw[wordPos:])) // write the suffix + return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos} +} + func (ec *extendContext) extend(ch rune) { if ch == 0 { return } - switch ec.Intention { + return +} - case WordTypeSimpleVar, WordTypeVarBrace: - ec.extendVar(ch) +func isVarNameChar(ch rune) bool { + return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') +} - case WordTypeDQ, WordTypeDDQ: - ec.extendDQ(ch) - - case WordTypeSQ: - ec.extendSQ(ch) - - case WordTypeDSQ: - ec.extendDSQ(ch) - - case WordTypeLit: - ec.extendLit(ch) - - default: +func extendVar(buf *bytes.Buffer, ch rune) { + if ch == 0 { return } + if !isVarNameChar(ch) { + return + } + buf.WriteRune(ch) } func getSpecialEscape(ch rune) string { @@ -131,122 +204,110 @@ func getSpecialEscape(ch rune) string { return specialEsc[byte(ch)] } -func isVarNameChar(ch rune) bool { - return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') +func writeSpecial(buf *bytes.Buffer, ch rune) { + sesc := getSpecialEscape(ch) + if sesc != "" { + buf.WriteString(sesc) + } else { + utf8Lit := getUtf8Literal(ch) + buf.WriteString(utf8Lit) + } } -func (ec *extendContext) extendVar(ch rune) { - if ch == 0 { - return - } - if !isVarNameChar(ch) { - return - } - ec.ensureCurWord() - ec.CurWord.writeRune(ch) -} - -func (ec *extendContext) extendLit(ch rune) { +func extendLit(buf *bytes.Buffer, ch rune) { if ch == 0 { return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) - ec.appendWord(dsqWord) - sesc := getSpecialEscape(ch) - if sesc != "" { - dsqWord.writeString(sesc) - return - } else { - utf8Lit := getUtf8Literal(ch) - dsqWord.writeString(utf8Lit) - } + writeSpecial(buf, ch) return } var bch = byte(ch) - ec.ensureCurWord() if noEscChars[bch] { - ec.CurWord.writeRune(ch) + buf.WriteRune(ch) return } - ec.CurWord.writeRune('\\') - ec.CurWord.writeRune(ch) + buf.WriteRune('\\') + buf.WriteRune(ch) return } -func (ec *extendContext) extendDSQ(ch rune) { +func extendDSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) { if ch == 0 { return } - ec.ensureCurWord() - if ch == '\'' { - ec.CurWord.writeRune('\\') - ec.CurWord.writeRune(ch) - return - } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - sesc := getSpecialEscape(ch) - if sesc != "" { - ec.CurWord.writeString(sesc) - } else { - utf8Lit := getUtf8Literal(ch) - ec.CurWord.writeString(utf8Lit) + if *wordOpen { + buf.WriteRune('\'') + *wordOpen = false } + writeSpecial(buf, ch) return } - ec.CurWord.writeRune(ch) + if *wordOpen { + buf.WriteRune('$') + buf.WriteRune('\'') + *wordOpen = true + } + if ch == '\'' { + buf.WriteRune('\\') + buf.WriteRune(ch) + return + } + buf.WriteRune(ch) return } -func (ec *extendContext) extendSQ(ch rune) { +func extendSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) { if ch == 0 { return } if ch == '\'' { - litWord := &WordType{Type: WordTypeLit, QC: ec.QC} - litWord.Raw = []rune{'\\', '\''} - ec.appendWord(litWord) + if *wordOpen { + buf.WriteRune('\'') + *wordOpen = false + } + buf.WriteRune('\\') + buf.WriteRune('\'') return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) - ec.appendWord(dsqWord) - sesc := getSpecialEscape(ch) - if sesc != "" { - dsqWord.writeString(sesc) - } else { - utf8Lit := getUtf8Literal(ch) - dsqWord.writeString(utf8Lit) + if *wordOpen { + buf.WriteRune('\'') + *wordOpen = false } + writeSpecial(buf, ch) return } - ec.ensureCurWord() - ec.CurWord.writeRune(ch) + if !*wordOpen { + buf.WriteRune('\'') + *wordOpen = true + } + buf.WriteRune(ch) return } -func (ec *extendContext) extendDQ(ch rune) { +func extendDQLit(buf *bytes.Buffer, wordOpen *bool, ch rune) { if ch == 0 { return } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + if *wordOpen { + buf.WriteRune('"') + *wordOpen = false + } + writeSpecial(buf, ch) + return + } + if !*wordOpen { + buf.WriteRune('"') + *wordOpen = true + } if ch == '"' || ch == '\\' || ch == '$' || ch == '`' { - ec.ensureCurWord() - ec.CurWord.writeRune('\\') - ec.CurWord.writeRune(ch) + buf.WriteRune('\\') + buf.WriteRune(ch) return } - if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0) - ec.appendWord(dsqWord) - sesc := getSpecialEscape(ch) - if sesc != "" { - dsqWord.writeString(sesc) - } else { - utf8Lit := getUtf8Literal(ch) - dsqWord.writeString(utf8Lit) - } - return - } - ec.CurWord.writeRune(ch) + buf.WriteRune(ch) return } diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index ac09762f1..ecf9b9476 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -44,7 +44,7 @@ const ( WordTypeLit = "lit" // (can-extend) WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing) WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[ - WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs) + WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs) (can-extend) WordTypeSimpleVar = "svar" // simplevar $ (can-extend) WordTypeDQ = "dq" // " (quote-context) (can-extend) (has-subs) @@ -113,6 +113,20 @@ type wordMeta struct { QuoteContext bool } +func (m wordMeta) getSuffix() string { + if m.SuffixLen == 0 { + return "" + } + return string(m.EmptyWord[len(m.EmptyWord)-m.SuffixLen:]) +} + +func (m wordMeta) getPrefix() string { + if m.PrefixLen == 0 { + return "" + } + return string(m.EmptyWord[:m.PrefixLen]) +} + func makeWordMeta(wtype string, emptyWord string, prefixLen int, suffixLen int, canExtend bool, quoteContext bool) { if len(emptyWord) != prefixLen+suffixLen { panic(fmt.Sprintf("invalid empty word %s %d %d", emptyWord, prefixLen, suffixLen)) @@ -140,14 +154,18 @@ func init() { makeWordMeta(WordTypeDB, "$[]", 2, 1, false, false) } -func MakeEmptyWord(wtype string, qc QuoteContext, offset int) *WordType { +func MakeEmptyWord(wtype string, qc QuoteContext, offset int, complete bool) *WordType { meta := wordMetaMap[wtype] if meta.Type == "" { meta = wordMetaMap[WordTypeRaw] } - rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true} + rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: complete} if len(meta.EmptyWord) > 0 { - rtn.Raw = append([]rune(nil), meta.EmptyWord...) + if complete { + rtn.Raw = append([]rune(nil), meta.EmptyWord...) + } else { + rtn.Raw = append([]rune(nil), []rune(meta.getPrefix())...) + } } return rtn } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 371791cdf..1740e2dcf 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -57,33 +57,46 @@ func lastWord(words []*WordType) *WordType { return words[len(words)-1] } -func testExtend(t *testing.T, startStr string, extendStr string, expectedStr string) { - words := Tokenize(startStr) - ec := makeExtendContext(nil, lastWord(words)) - for _, ch := range extendStr { - ec.extend(ch) +func testExtend(t *testing.T, startStr string, extendStr string, complete bool, expStr string) { + startSP := utilfn.ParseToSP(startStr) + words := Tokenize(startSP.Str) + word := findCompletionWordAtPos(words, startSP.Pos, true) + if word == nil { + word = MakeEmptyWord(WordTypeLit, nil, startSP.Pos, true) } - ec.ensureCurWord() - output := wordsToStr(ec.Rtn) - fmt.Printf("[%s] + [%s] => [%s]\n", startStr, extendStr, output) - if output != expectedStr { - t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, output, expectedStr) + outSP := Extend(word, startSP.Pos-word.Offset, extendStr, complete) + expSP := utilfn.ParseToSP(expStr) + fmt.Printf("extend: [%s] + [%s] => [%s]\n", startStr, extendStr, outSP) + if outSP != expSP { + t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, outSP, expSP) } } func Test2(t *testing.T) { - testExtend(t, `'he'`, "llo", `'hello'`) - testExtend(t, `'he'`, "'", `'he'\'''`) - testExtend(t, `'he'`, "'\x01", `'he'\'$'\x01'''`) - testExtend(t, `he`, "llo", `hello`) - testExtend(t, `he`, "l*l'\x01\x07o", `hel\*l\'$'\x01'$'\a'o`) - testExtend(t, `$x`, "fo|o", `$xfoo`) - testExtend(t, `${x`, "fo|o", `${xfoo`) - testExtend(t, `$'f`, "oo", `$'foo`) - testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`) - testExtend(t, `"f"`, "oo", `"foo"`) - testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`) - testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\a'""`) + testExtend(t, `he[*]`, "llo", false, "hello[*]") + testExtend(t, `he[*]`, "llo", true, "hello [*]") + testExtend(t, `'mi[*]e`, "k", false, "'mik[*]e") + testExtend(t, `'mi[*]e`, "k", true, "'mik[*]e") + testExtend(t, `'mi[*]'`, "ke", true, "'mike' [*]") + testExtend(t, `'mi'[*]`, "ke", true, "'mike' [*]") + testExtend(t, `'mi[*]'`, "ke", false, "'mike[*]'") + testExtend(t, `'mi'[*]`, "ke", false, "'mike[*]'") + testExtend(t, `$f[*]`, "oo", false, "$foo[*]") + testExtend(t, `${f}[*]`, "oo", false, "${foo[*]}") + testExtend(t, `${f[*]}`, "oo", true, "${foo} [*]") + + // testExtend(t, `'he'`, "llo", `'hello'`) + // testExtend(t, `'he'`, "'", `'he'\'''`) + // testExtend(t, `'he'`, "'\x01", `'he'\'$'\x01'''`) + // testExtend(t, `he`, "llo", `hello`) + // testExtend(t, `he`, "l*l'\x01\x07o", `hel\*l\'$'\x01'$'\a'o`) + // testExtend(t, `$x`, "fo|o", `$xfoo`) + // testExtend(t, `${x`, "fo|o", `${xfoo`) + // testExtend(t, `$'f`, "oo", `$'foo`) + // testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`) + // testExtend(t, `"f"`, "oo", `"foo"`) + // testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`) + // testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\a'""`) } func testParseCommands(t *testing.T, str string) { @@ -151,6 +164,8 @@ func TestCompPos(t *testing.T) { testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true) // we don't sub-parse inside of ${} (so this returns "var" right now) testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true) testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true) + testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true) + testCompPos(t, `ls $[*]'foo`, CompTypeInvalid, false, 0, false) } func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { From bb3e12fee7603441b296313dc11af00aadaab867 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 23:06:58 -0800 Subject: [PATCH 192/397] checkpoint, extend working with all the crazy quote balancing for subs --- pkg/shparse/extend.go | 159 +++++++++++++++++++++++++++++------- pkg/shparse/shparse.go | 2 +- pkg/shparse/shparse_test.go | 17 +++- pkg/shparse/tokenize.go | 2 +- pkg/utilfn/utilfn.go | 8 ++ 5 files changed, 153 insertions(+), 35 deletions(-) diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index f7bbd8f0e..e1641d5b5 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -99,8 +99,74 @@ func (ec *extendContext) ensureCurWord() { } // grp, dq, ddq -func extendWithSubs(buf *bytes.Buffer, word *WordType, wordPos int, extStr string) { - +func extendWithSubs(word *WordType, wordPos int, extStr string, complete bool) utilfn.StrWithPos { + wmeta := wordMetaMap[word.Type] + if word.Type == WordTypeGroup { + atEnd := (wordPos == len(word.Raw)) + subWord := findCompletionWordAtPos(word.Subs, wordPos, true) + if subWord == nil { + strPos := Extend(MakeEmptyWord(WordTypeLit, word.QC, 0, true), 0, extStr, atEnd) + strPos = strPos.Prepend(string(word.Raw[0:wordPos])) + strPos = strPos.Append(string(word.Raw[wordPos:])) + return strPos + } else { + subComplete := complete && atEnd + strPos := Extend(subWord, wordPos-subWord.Offset, extStr, subComplete) + strPos = strPos.Prepend(string(word.Raw[0:subWord.Offset])) + strPos = strPos.Append(string(word.Raw[subWord.Offset+len(subWord.Raw):])) + return strPos + } + } else if word.Type == WordTypeDQ || word.Type == WordTypeDDQ { + if wordPos < word.contentStartPos() { + wordPos = word.contentStartPos() + } + atEnd := (wordPos >= len(word.Raw)-wmeta.SuffixLen) + subWord := findCompletionWordAtPos(word.Subs, wordPos-wmeta.PrefixLen, true) + quoteBalance := !atEnd + if subWord == nil { + realOffset := wordPos + strPos, wordOpen := extendInternal(MakeEmptyWord(WordTypeLit, word.QC.push(WordTypeDQ), 0, true), 0, extStr, false, quoteBalance) + strPos = strPos.Prepend(string(word.Raw[0:realOffset])) + var requiredSuffix string + if wordOpen { + requiredSuffix = wmeta.getSuffix() + } + if atEnd { + if complete { + return utilfn.StrWithPos{Str: strPos.Str + requiredSuffix + " ", Pos: strPos.Pos + len(requiredSuffix) + 1} + } else { + if word.Complete && requiredSuffix != "" { + return strPos.Append(requiredSuffix) + } + return strPos + } + } + strPos = strPos.Append(string(word.Raw[wordPos:])) + return strPos + } else { + realOffset := subWord.Offset + wmeta.PrefixLen + strPos, wordOpen := extendInternal(subWord, wordPos-realOffset, extStr, false, quoteBalance) + strPos = strPos.Prepend(string(word.Raw[0:realOffset])) + var requiredSuffix string + if wordOpen { + requiredSuffix = wmeta.getSuffix() + } + if atEnd { + if complete { + return utilfn.StrWithPos{Str: strPos.Str + requiredSuffix + " ", Pos: strPos.Pos + len(requiredSuffix) + 1} + } else { + if word.Complete && requiredSuffix != "" { + return strPos.Append(requiredSuffix) + } + return strPos + } + } + strPos = strPos.Append(string(word.Raw[realOffset+len(subWord.Raw):])) + return strPos + } + } else { + return utilfn.StrWithPos{Str: string(word.Raw), Pos: wordPos} + } } // lit, svar, varb, sq, dsq @@ -127,6 +193,18 @@ func extendLeafCh(buf *bytes.Buffer, wordOpen *bool, wtype string, qc QuoteConte } } +func getWordOpenStr(wtype string, qc QuoteContext) string { + if wtype == WordTypeLit { + if qc.cur() == WordTypeDQ { + return "\"" + } else { + return "" + } + } + wmeta := wordMetaMap[wtype] + return wmeta.getPrefix() +} + // lit, svar, varb sq, dsq func extendLeaf(buf *bytes.Buffer, wordOpen *bool, word *WordType, wordPos int, extStr string) { for _, ch := range extStr { @@ -135,45 +213,61 @@ func extendLeaf(buf *bytes.Buffer, wordOpen *bool, word *WordType, wordPos int, } // lit, grp, svar, dq, ddq, varb, sq, dsq -func Extend(word *WordType, wordPos int, extStr string, complete bool) utilfn.StrWithPos { +// returns (strwithpos, dq-closed) +func extendInternal(word *WordType, wordPos int, extStr string, complete bool, requiresQuoteBalance bool) (utilfn.StrWithPos, bool) { if extStr == "" { - return utilfn.StrWithPos{Str: string(word.Raw), Pos: wordPos} + return utilfn.StrWithPos{Str: string(word.Raw), Pos: wordPos}, true + } + if word.canHaveSubs() { + return extendWithSubs(word, wordPos, extStr, complete), true } var buf bytes.Buffer isEOW := wordPos >= word.contentEndPos() if isEOW { wordPos = word.contentEndPos() } - if wordPos > 0 && wordPos < word.contentStartPos() { + if wordPos < word.contentStartPos() { wordPos = word.contentStartPos() } - wordOpen := false - if wordPos >= word.contentStartPos() { - wordOpen = true + if wordPos > 0 { + buf.WriteString(string(word.Raw[0:word.contentStartPos()])) // write the prefix } - buf.WriteString(string(word.Raw[0:wordPos])) // write the prefix - if word.canHaveSubs() { - extendWithSubs(&buf, word, wordPos, extStr) - } else { - extendLeaf(&buf, &wordOpen, word, wordPos, extStr) + if wordPos > word.contentStartPos() { + buf.WriteString(string(word.Raw[word.contentStartPos():wordPos])) } + wordOpen := true + extendLeaf(&buf, &wordOpen, word, wordPos, extStr) if isEOW { // end-of-word, write the suffix (and optional ' '). return the end of the string wmeta := wordMetaMap[word.Type] + rtnPos := utf8.RuneCount(buf.Bytes()) buf.WriteString(wmeta.getSuffix()) - var rtnPos int + if !wordOpen && requiresQuoteBalance { + buf.WriteString(getWordOpenStr(word.Type, word.QC)) + wordOpen = true + } if complete { buf.WriteRune(' ') - rtnPos = utf8.RuneCount(buf.Bytes()) + return utilfn.StrWithPos{Str: buf.String(), Pos: utf8.RuneCount(buf.Bytes())}, wordOpen } else { - rtnPos = utf8.RuneCount(buf.Bytes()) - wmeta.SuffixLen + return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos}, wordOpen } - return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos} } // completion in the middle of a word (no ' ') rtnPos := utf8.RuneCount(buf.Bytes()) + if !wordOpen { + // always required since there is a suffix + buf.WriteString(getWordOpenStr(word.Type, word.QC)) + wordOpen = true + } buf.WriteString(string(word.Raw[wordPos:])) // write the suffix - return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos} + return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos}, wordOpen +} + +// lit, grp, svar, dq, ddq, varb, sq, dsq +func Extend(word *WordType, wordPos int, extStr string, complete bool) utilfn.StrWithPos { + rtn, _ := extendInternal(word, wordPos, extStr, complete, false) + return rtn } func (ec *extendContext) extend(ch rune) { @@ -204,7 +298,11 @@ func getSpecialEscape(ch rune) string { return specialEsc[byte(ch)] } -func writeSpecial(buf *bytes.Buffer, ch rune) { +func writeSpecial(buf *bytes.Buffer, ch rune, wrap bool) { + if wrap { + buf.WriteRune('$') + buf.WriteRune('\'') + } sesc := getSpecialEscape(ch) if sesc != "" { buf.WriteString(sesc) @@ -212,6 +310,9 @@ func writeSpecial(buf *bytes.Buffer, ch rune) { utf8Lit := getUtf8Literal(ch) buf.WriteString(utf8Lit) } + if wrap { + buf.WriteRune('\'') + } } func extendLit(buf *bytes.Buffer, ch rune) { @@ -219,7 +320,7 @@ func extendLit(buf *bytes.Buffer, ch rune) { return } if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - writeSpecial(buf, ch) + writeSpecial(buf, ch, true) return } var bch = byte(ch) @@ -236,19 +337,15 @@ func extendDSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) { if ch == 0 { return } - if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { - if *wordOpen { - buf.WriteRune('\'') - *wordOpen = false - } - writeSpecial(buf, ch) - return - } - if *wordOpen { + if !*wordOpen { buf.WriteRune('$') buf.WriteRune('\'') *wordOpen = true } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + writeSpecial(buf, ch, false) + return + } if ch == '\'' { buf.WriteRune('\\') buf.WriteRune(ch) @@ -276,7 +373,7 @@ func extendSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) { buf.WriteRune('\'') *wordOpen = false } - writeSpecial(buf, ch) + writeSpecial(buf, ch, true) return } if !*wordOpen { @@ -296,7 +393,7 @@ func extendDQLit(buf *bytes.Buffer, wordOpen *bool, ch rune) { buf.WriteRune('"') *wordOpen = false } - writeSpecial(buf, ch) + writeSpecial(buf, ch, true) return } if !*wordOpen { diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index ecf9b9476..d2e31827d 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -48,7 +48,7 @@ const ( WordTypeSimpleVar = "svar" // simplevar $ (can-extend) WordTypeDQ = "dq" // " (quote-context) (can-extend) (has-subs) - WordTypeDDQ = "ddq" // $" (quote-context) (can-extend) (has-subs) + WordTypeDDQ = "ddq" // $" (can-extend) (has-subs) (for quotecontext, uses WordTypeDQ) WordTypeVarBrace = "varb" // ${ (quote-context) (can-extend) (internals not parsed) WordTypeDP = "dp" // $( (quote-context) (has-subs) WordTypeBQ = "bq" // ` (quote-context) (has-subs) diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 1740e2dcf..13d48c72d 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -66,9 +66,9 @@ func testExtend(t *testing.T, startStr string, extendStr string, complete bool, } outSP := Extend(word, startSP.Pos-word.Offset, extendStr, complete) expSP := utilfn.ParseToSP(expStr) - fmt.Printf("extend: [%s] + [%s] => [%s]\n", startStr, extendStr, outSP) + fmt.Printf("extend: [%s] + %q => [%s]\n", startStr, extendStr, outSP) if outSP != expSP { - t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, outSP, expSP) + t.Errorf("extension does not match: [%s] + %q => [%s] expected [%s]\n", startStr, extendStr, outSP, expSP) } } @@ -84,6 +84,19 @@ func Test2(t *testing.T) { testExtend(t, `$f[*]`, "oo", false, "$foo[*]") testExtend(t, `${f}[*]`, "oo", false, "${foo[*]}") testExtend(t, `${f[*]}`, "oo", true, "${foo} [*]") + testExtend(t, `[*]`, "more stuff", false, `more\ stuff[*]`) + testExtend(t, `[*]`, "hello\amike", false, `hello$'\a'mike[*]`) + testExtend(t, `$'he[*]'`, "\x01\x02\x0a", true, `$'he\x01\x02\n' [*]`) + testExtend(t, `${x}\ [*]ll$y`, "e", false, `${x}\ e[*]ll$y`) + testExtend(t, `"he[*]"`, "$$o", true, `"he\$\$o" [*]`) + testExtend(t, `"h[*]llo"`, "e", false, `"he[*]llo"`) + testExtend(t, `"h[*]llo"`, "e", true, `"he[*]llo"`) + testExtend(t, `"[*]${h}llo"`, "e\x01", true, `"e"$'\x01'[*]"${h}llo"`) + testExtend(t, `"${h}llo[*]"`, "e\x01", true, `"${h}lloe"$'\x01' [*]`) + testExtend(t, `"${h}llo[*]"`, "e\x01", false, `"${h}lloe"$'\x01'[*]`) + testExtend(t, `"${h}ll[*]o"`, "e\x01", false, `"${h}lle"$'\x01'[*]"o"`) + testExtend(t, `"ab[*]c${x}def"`, "\x01", false, `"ab"$'\x01'[*]"c${x}def"`) + testExtend(t, `'ab[*]ef'`, "\x01", false, `'ab'$'\x01'[*]'ef'`) // testExtend(t, `'he'`, "llo", `'hello'`) // testExtend(t, `'he'`, "'", `'he'\'''`) diff --git a/pkg/shparse/tokenize.go b/pkg/shparse/tokenize.go index 52ecbd143..825e73870 100644 --- a/pkg/shparse/tokenize.go +++ b/pkg/shparse/tokenize.go @@ -472,7 +472,7 @@ func (c *parseContext) parseStrDDQ() *WordType { if !c.match2('$', '"') { return nil } - newContext := c.clone(c.Pos+2, WordTypeDDQ) + newContext := c.clone(c.Pos+2, WordTypeDQ) // use WordTypeDQ (not DDQ) subWords, eofExit := newContext.tokenizeDQ() newOffset := newContext.Pos + 2 w := c.makeWord(WordTypeDDQ, newOffset, !eofExit) diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index 62195f970..752684d90 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -157,3 +157,11 @@ func strWithCursor(str string, pos int) string { } return string(rtn) } + +func (sp StrWithPos) Prepend(str string) StrWithPos { + return StrWithPos{Str: str + sp.Str, Pos: utf8.RuneCountInString(str) + sp.Pos} +} + +func (sp StrWithPos) Append(str string) StrWithPos { + return StrWithPos{Str: sp.Str + str, Pos: sp.Pos} +} From bd3595c95428abce5e0f5313b9fe9529ae86860a Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 22 Nov 2022 00:26:41 -0800 Subject: [PATCH 193/397] checkpoint, testing new compgen. fixed superoffset bug --- pkg/cmdrunner/cmdrunner.go | 32 +---- pkg/cmdrunner/shparse.go | 9 +- pkg/comp/comp.go | 273 +++++++++++++++++++++++++++++------- pkg/shparse/comp.go | 48 +++++-- pkg/shparse/shparse_test.go | 36 ++--- 5 files changed, 291 insertions(+), 107 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 17edd0b4a..c41337136 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1172,7 +1172,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto pos = len(cmdLine) } showComps := resolveBool(pk.Kwargs["compshow"], false) - cmdSP := comp.StrWithPos{Str: cmdLine, Pos: pos} + cmdSP := utilfn.StrWithPos{Str: cmdLine, Pos: pos} compCtx := comp.CompContext{} if ids.Remote != nil { rptr := ids.Remote.RemotePtr @@ -1185,39 +1185,19 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, err } if crtn == nil { - return nil, fmt.Errorf("no return value from DoCompGen") + return nil, nil } - if showComps || newSP == nil { + if showComps { compStrs := crtn.GetCompDisplayStrs() return makeInfoFromComps(crtn.CompType, compStrs, crtn.HasMore), nil } + if newSP == nil || cmdSP == *newSP { + return nil, nil + } update := sstore.ModelUpdate{ CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos}, } return update, nil - - // prefix := cmdLine[:pos] - // parts := strings.Split(prefix, " ") - // compType := "file" - // if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "/") { - // compType = "metacommand" - // } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { - // compType = "directory" - // } else if len(parts) <= 1 { - // compType = "command" - // } - // lastPart := "" - // if len(parts) > 0 { - // lastPart = parts[len(parts)-1] - // } - // comps, hasMore, err := doCompGen(ctx, pk, lastPart, compType, showComps) - // if err != nil { - // return nil, err - // } - // if showComps { - // return makeInfoFromComps(compType, comps, hasMore), nil - // } - // return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 941c9e9da..3ad497ebc 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/mshell/pkg/simpleexpand" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/utilfn" "mvdan.cc/sh/v3/expand" @@ -124,9 +125,9 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { strReader := strings.NewReader(bracketStr) parser := syntax.NewParser(syntax.Variant(syntax.LangBash)) var wordErr error - var ectx shexec.SimpleExpandContext // do not set HomeDir (we don't expand ~ in bracket args) + var ectx simpleexpand.SimpleExpandContext // do not set HomeDir (we don't expand ~ in bracket args) err := parser.Words(strReader, func(w *syntax.Word) bool { - litStr := shexec.SimpleExpandWord(ectx, w, bracketStr) + litStr, _ := simpleexpand.SimpleExpandWord(ectx, w, bracketStr) eqIdx := strings.Index(litStr, "=") var varName, varVal string if eqIdx == -1 { @@ -285,8 +286,8 @@ func parseAliasStmt(stmt *syntax.Stmt, sourceStr string) (string, string, error) return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')") } secondWord := callExpr.Args[1] - var ectx shexec.SimpleExpandContext // no homedir, do not want ~ expansion - val := shexec.SimpleExpandWord(ectx, secondWord, sourceStr) + 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") diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index b446450df..445ee53df 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -9,9 +9,11 @@ import ( "strconv" "strings" "unicode" + "unicode/utf8" "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/shparse" "github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/utilfn" "mvdan.cc/sh/v3/syntax" @@ -48,11 +50,6 @@ type CompContext struct { ForDisplay bool } -type StrWithPos struct { - Str string - Pos int -} - type ParsedWord struct { Offset int Word *syntax.Word @@ -81,6 +78,28 @@ type CompReturn struct { HasMore bool } +var noEscChars []bool +var specialEsc []string + +func init() { + noEscChars = make([]bool, 256) + for ch := 0; ch < 256; ch++ { + if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + ch == '-' || ch == '.' || ch == '/' || ch == ':' || ch == '=' { + noEscChars[byte(ch)] = true + } + } + specialEsc = make([]string, 256) + specialEsc[0x7] = "\\a" + specialEsc[0x8] = "\\b" + specialEsc[0x9] = "\\t" + specialEsc[0xa] = "\\n" + specialEsc[0xb] = "\\v" + specialEsc[0xc] = "\\f" + specialEsc[0xd] = "\\r" + specialEsc[0x1b] = "\\E" +} + func compQuoteDQString(s string, close bool) string { var buf bytes.Buffer buf.WriteByte('"') @@ -98,15 +117,108 @@ func compQuoteDQString(s string, close bool) string { return buf.String() } +func hasGlob(s string) bool { + var lastExtGlob bool + for _, ch := range s { + if ch == '*' || ch == '?' || ch == '[' || ch == '{' { + return true + } + if ch == '+' || ch == '@' || ch == '!' { + lastExtGlob = true + continue + } + if lastExtGlob && ch == '(' { + return true + } + lastExtGlob = false + } + return false +} + +func writeUtf8Literal(buf *bytes.Buffer, ch rune) { + var runeArr [utf8.UTFMax]byte + buf.WriteString("$'") + barr := runeArr[:] + byteLen := utf8.EncodeRune(barr, ch) + for i := 0; i < byteLen; i++ { + buf.WriteString("\\x") + buf.WriteByte(utilfn.HexDigits[barr[i]/16]) + buf.WriteByte(utilfn.HexDigits[barr[i]%16]) + } + buf.WriteByte('\'') +} + +func compQuoteLiteralString(s string) string { + var buf bytes.Buffer + for idx, ch := range s { + if ch == 0 { + break + } + if idx == 0 && ch == '~' { + buf.WriteRune(ch) + continue + } + if ch > unicode.MaxASCII { + writeUtf8Literal(&buf, ch) + continue + } + var bch = byte(ch) + if noEscChars[bch] { + buf.WriteRune(ch) + continue + } + if specialEsc[bch] != "" { + buf.WriteString(specialEsc[bch]) + continue + } + if !unicode.IsPrint(ch) { + writeUtf8Literal(&buf, ch) + continue + } + buf.WriteByte('\\') + buf.WriteByte(bch) + } + return buf.String() +} + +func compQuoteSQString(s string) string { + var buf bytes.Buffer + for _, ch := range s { + if ch == 0 { + break + } + if ch == '\'' { + buf.WriteString("'\\''") + continue + } + var bch byte + if ch <= unicode.MaxASCII { + bch = byte(ch) + } + if ch > unicode.MaxASCII || !unicode.IsPrint(ch) { + buf.WriteByte('\'') + if bch != 0 && specialEsc[bch] != "" { + buf.WriteString(specialEsc[bch]) + } else { + writeUtf8Literal(&buf, ch) + } + buf.WriteByte('\'') + continue + } + buf.WriteByte(bch) + } + return buf.String() +} + func compQuoteString(s string, quoteType string, close bool) string { - if quoteType != QuoteTypeANSI { + if quoteType != QuoteTypeANSI && quoteType != QuoteTypeLiteral { for _, ch := range s { if ch > unicode.MaxASCII || !unicode.IsPrint(ch) || ch == '!' { quoteType = QuoteTypeANSI break } if ch == '\'' { - if quoteType == QuoteTypeSQ || quoteType == QuoteTypeLiteral { + if quoteType == QuoteTypeSQ { quoteType = QuoteTypeANSI break } @@ -122,11 +234,7 @@ func compQuoteString(s string, quoteType string, close bool) string { return rtn } if quoteType == QuoteTypeLiteral { - rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen) - if len(rtn) > 0 && rtn[0] == '\'' && !close { - rtn = rtn[0 : len(rtn)-1] - } - return rtn + return compQuoteLiteralString(s) } if quoteType == QuoteTypeSQ { rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen) @@ -149,12 +257,12 @@ func (p *CompPoint) wordAsStr(w ParsedWord) string { return w.PartialWord } -func (p *CompPoint) simpleExpandWord(w ParsedWord) string { - ectx := shexec.SimpleExpandContext{} +func (p *CompPoint) simpleExpandWord(w ParsedWord) (string, simpleexpand.SimpleExpandInfo) { + ectx := simpleexpand.SimpleExpandContext{} if w.Word != nil { - return shexec.SimpleExpandWord(ectx, w.Word, p.StmtStr) + return simpleexpand.SimpleExpandWord(ectx, w.Word, p.StmtStr) } - return shexec.SimpleExpandPartialWord(ectx, w.PartialWord, false) + return simpleexpand.SimpleExpandPartialWord(ectx, w.PartialWord, false) } func getQuoteTypePref(str string) string { @@ -170,9 +278,9 @@ func getQuoteTypePref(str string) string { return QuoteTypeLiteral } -func (p *CompPoint) getCompPrefix() string { +func (p *CompPoint) getCompPrefix() (string, simpleexpand.SimpleExpandInfo) { if p.CompWordPos == 0 { - return "" + return "", simpleexpand.SimpleExpandInfo{} } pword := p.Words[p.CompWord] wordStr := p.wordAsStr(pword) @@ -184,10 +292,10 @@ func (p *CompPoint) getCompPrefix() string { // and a partial on just the current part. this is an uncommon case though // and has very little upside (even bash does not expand multipart words correctly) partialWordStr := wordStr[:p.CompWordPos] - return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) + return simpleexpand.SimpleExpandPartialWord(simpleexpand.SimpleExpandContext{}, partialWordStr, false) } -func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos { +func (p *CompPoint) extendWord(newWord string, newWordComplete bool) utilfn.StrWithPos { pword := p.Words[p.CompWord] wordStr := p.wordAsStr(pword) quotePref := getQuoteTypePref(wordStr) @@ -198,18 +306,31 @@ func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos newQuotedStr = newQuotedStr + " " } newPos := len(newQuotedStr) - return StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos} + return utilfn.StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos} } -func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos { +// returns (extension, complete) +func computeCompExtension(compPrefix string, crtn *CompReturn) (string, bool) { if crtn == nil || crtn.HasMore { - return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + return "", false } compStrs := crtn.GetCompStrs() - compPrefix := p.getCompPrefix() lcp := utilfn.LongestPrefix(compPrefix, compStrs) if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) { - return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + return "", false + } + return lcp[len(compPrefix):], utilfn.ContainsStr(compStrs, lcp) +} + +func (p *CompPoint) FullyExtend(crtn *CompReturn) utilfn.StrWithPos { + if crtn == nil || crtn.HasMore { + return utilfn.StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} + } + compStrs := crtn.GetCompStrs() + compPrefix, _ := p.getCompPrefix() + lcp := utilfn.LongestPrefix(compPrefix, compStrs) + if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) { + return utilfn.StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()} } newStr := p.extendWord(lcp, utilfn.ContainsStr(compStrs, lcp)) var buf bytes.Buffer @@ -226,7 +347,7 @@ func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos { buf.WriteString(p.Suffix) compWord := p.Words[p.CompWord] newPos := len(p.Prefix) + compWord.Offset + len(compWord.Prefix) + newStr.Pos - return StrWithPos{Str: buf.String(), Pos: newPos} + return utilfn.StrWithPos{Str: buf.String(), Pos: newPos} } func (p *CompPoint) dump() { @@ -240,7 +361,7 @@ func (p *CompPoint) dump() { fmt.Printf("{%s}", w.Prefix) } if idx == p.CompWord { - fmt.Printf("%s\n", strWithCursor(p.wordAsStr(w), p.CompWordPos)) + fmt.Printf("%s\n", utilfn.StrWithPos{Str: p.wordAsStr(w), Pos: p.CompWordPos}) } else { fmt.Printf("%s\n", p.wordAsStr(w)) } @@ -253,24 +374,6 @@ func (p *CompPoint) dump() { var SimpleCompGenFns map[string]SimpleCompGenFnType -func (sp StrWithPos) String() string { - return strWithCursor(sp.Str, sp.Pos) -} - -func strWithCursor(str string, pos int) string { - if pos < 0 { - return "[*]_" + str - } - if pos >= len(str) { - if pos > len(str) { - return str + "_[*]" - } - return str + "[*]" - } else { - return str[:pos] + "[*]" + str[pos:] - } -} - func isWhitespace(str string) bool { return strings.TrimSpace(str) == "" } @@ -284,7 +387,7 @@ func splitInitialWhitespace(str string) (string, string) { return str, "" } -func ParseCompPoint(cmdStr StrWithPos) *CompPoint { +func ParseCompPoint(cmdStr utilfn.StrWithPos) *CompPoint { fullCmdStr := cmdStr.Str pos := cmdStr.Pos // fmt.Printf("---\n") @@ -388,14 +491,86 @@ func splitCompWord(p *CompPoint) { p.Words = newWords } -func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) { +func getCompType(compPos shparse.CompletionPos) string { + switch compPos.CompType { + case shparse.CompTypeCommandMeta: + return CGTypeCommandMeta + + case shparse.CompTypeCommand: + return CGTypeCommand + + case shparse.CompTypeVar: + return CGTypeVariable + + case shparse.CompTypeArg, shparse.CompTypeBasic, shparse.CompTypeAssignment: + return CGTypeFile + + default: + return CGTypeFile + } +} + +func fixupVarPrefix(varPrefix string) string { + if strings.HasPrefix(varPrefix, "${") { + varPrefix = varPrefix[2:] + if strings.HasSuffix(varPrefix, "}") { + varPrefix = varPrefix[:len(varPrefix)-1] + } + } else if strings.HasPrefix(varPrefix, "$") { + varPrefix = varPrefix[1:] + } + return varPrefix +} + +func DoCompGen(ctx context.Context, cmdStr utilfn.StrWithPos, compCtx CompContext) (*CompReturn, *utilfn.StrWithPos, error) { + words := shparse.Tokenize(cmdStr.Str) + cmds := shparse.ParseCommands(words) + compPos := shparse.FindCompletionPos(cmds, cmdStr.Pos, 0) + fmt.Printf("comppos: %v\n", compPos) + if compPos.CompType == shparse.CompTypeInvalid { + return nil, nil, nil + } + var compPrefix string + if compPos.CompWord != nil { + var info shparse.ExpandInfo + compPrefix, info = shparse.SimpleExpandPrefix(shparse.ExpandContext{}, compPos.CompWord, compPos.CompWordOffset) + if info.HasGlob || info.HasExtGlob || info.HasHistory || info.HasSpecial { + return nil, nil, nil + } + if compPos.CompType != shparse.CompTypeVar && info.HasVar { + return nil, nil, nil + } + if compPos.CompType == shparse.CompTypeVar { + compPrefix = fixupVarPrefix(compPrefix) + } + } + scType := getCompType(compPos) + crtn, err := DoSimpleComp(ctx, scType, compPrefix, compCtx, nil) + if err != nil { + return nil, nil, err + } + if compCtx.ForDisplay { + return crtn, nil, nil + } + extensionStr, extensionComplete := computeCompExtension(compPrefix, crtn) + if extensionStr == "" { + return crtn, nil, nil + } + rtnSP := compPos.Extend(cmdStr, extensionStr, extensionComplete) + return crtn, &rtnSP, nil +} + +func DoCompGenOld(ctx context.Context, sp utilfn.StrWithPos, compCtx CompContext) (*CompReturn, *utilfn.StrWithPos, error) { compPoint := ParseCompPoint(sp) compType := CGTypeFile if compPoint.CompWord == 0 { compType = CGTypeCommandMeta } // TODO lookup special types - compPrefix := compPoint.getCompPrefix() + compPrefix, info := compPoint.getCompPrefix() + if info.HasVar || info.HasGlob || info.HasExtGlob || info.HasHistory || info.HasSpecial { + return nil, nil, nil + } crtn, err := DoSimpleComp(ctx, compType, compPrefix, compCtx, nil) if err != nil { return nil, nil, err diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index 136bf365a..c0efe82c7 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -1,5 +1,12 @@ package shparse +import ( + "fmt" + "strings" + + "github.com/scripthaus-dev/sh2-server/pkg/utilfn" +) + const ( CompTypeCommandMeta = "command-meta" CompTypeCommand = "command" @@ -156,38 +163,38 @@ func findCompletionWordAtPos(words []*WordType, pos int, allowEndMatch bool) *Wo // recursively descend down the word, parse commands and find a sub completion point if any. // return nil if there is no sub completion point in this word -func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos { +func findCompletionPosInWord(word *WordType, posInWord int, superOffset int) *CompletionPos { + rawPos := word.Offset + posInWord if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ { // need to descend further - if pos <= word.contentStartPos() { + if posInWord <= word.contentStartPos() { return nil } - if pos > word.contentEndPos() { + if posInWord > word.contentEndPos() { return nil } - subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos(), false) + subWord := findCompletionWordAtPos(word.Subs, posInWord-word.contentStartPos(), false) if subWord == nil { return nil } - fullOffset := subWord.Offset + word.contentStartPos() - return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset) + return findCompletionPosInWord(subWord, posInWord-(subWord.Offset+word.contentStartPos()), superOffset+(word.Offset+word.contentStartPos())) } if word.Type == WordTypeDP || word.Type == WordTypeBQ { - if pos < word.contentStartPos() { + if posInWord < word.contentStartPos() { return nil } - if pos > word.contentEndPos() { + if posInWord > word.contentEndPos() { return nil } subCmds := ParseCommands(word.Subs) - newPos := FindCompletionPos(subCmds, pos-word.contentStartPos(), superOffset+word.contentStartPos()) + newPos := FindCompletionPos(subCmds, posInWord-word.contentStartPos(), superOffset+(word.Offset+word.contentStartPos())) return &newPos } if word.Type == WordTypeSimpleVar || word.Type == WordTypeVarBrace { // special "var" completion - rtn := &CompletionPos{RawPos: pos, SuperOffset: superOffset} + rtn := &CompletionPos{RawPos: rawPos, SuperOffset: superOffset} rtn.CompType = CompTypeVar - rtn.CompWordOffset = pos + rtn.CompWordOffset = posInWord rtn.CompWord = word return rtn } @@ -250,10 +257,27 @@ func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos if cpos.CompWord == nil { return cpos } - subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset) + subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset) if subPos == nil { return cpos } else { return *subPos } } + +func (cpos CompletionPos) Extend(origStr utilfn.StrWithPos, extensionStr string, extensionComplete bool) utilfn.StrWithPos { + compWord := cpos.CompWord + if compWord == nil { + compWord = MakeEmptyWord(WordTypeLit, nil, cpos.RawPos, true) + } + realOffset := compWord.Offset + cpos.SuperOffset + fmt.Printf("cpos-extend: %d[%s] ext[%s] cword[%v] off:%d super:%d real:%d\n", len([]rune(origStr.Str)), origStr, extensionStr, compWord, compWord.Offset, cpos.SuperOffset, realOffset) + if strings.HasSuffix(extensionStr, "/") { + extensionComplete = false + } + rtnSP := Extend(compWord, cpos.CompWordOffset, extensionStr, extensionComplete) + origRunes := []rune(origStr.Str) + rtnSP = rtnSP.Prepend(string(origRunes[0:realOffset])) + rtnSP = rtnSP.Append(string(origRunes[realOffset+len(compWord.Raw):])) + return rtnSP +} diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 13d48c72d..e98064c10 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -131,7 +131,7 @@ func TestCmd(t *testing.T) { testParseCommands(t, `x="foo $y" z=10 ls`) } -func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool) { +func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool, superOffset int) { cmdSP := utilfn.ParseToSP(cmdStr) words := Tokenize(cmdSP.Str) cmds := ParseCommands(words) @@ -161,24 +161,28 @@ func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, if cpos.CmdWordPos != cmdWordPos { t.Errorf("testCompPos %q => bad cmd-word-pos got:%d exp:%d", cmdStr, cpos.CmdWordPos, cmdWordPos) } + if cpos.SuperOffset != superOffset { + t.Errorf("testCompPos %q => bad super-offset got:%d exp:%d", cmdStr, cpos.SuperOffset, superOffset) + } } func TestCompPos(t *testing.T) { - testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false) - testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false) - testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false) - testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true) - testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true) - testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true) - testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true) - testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true) - testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true) - testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", CompTypeArg, true, 2, true) - testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true) // we don't sub-parse inside of ${} (so this returns "var" right now) - testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true) - testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true) - testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true) - testCompPos(t, `ls $[*]'foo`, CompTypeInvalid, false, 0, false) + testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false, 0) + testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false, 0) + testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false, 0) + testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true, 0) + testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true, 0) + testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true, 0) + testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true, 0) + testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true, 0) + testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true, 0) + testCompPos(t, `ls "abc $(ls -l t[*])" && foo`, CompTypeArg, true, 2, true, 10) + testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true, 0) // we don't sub-parse inside of ${} (so this returns "var" right now) + testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true, 21) + testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true, 4) + testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true, 0) + testCompPos(t, `ls $[*]'foo`, CompTypeArg, true, 1, true, 0) + testCompPos(t, `echo $TE[*]`, CompTypeVar, false, 0, true, 0) } func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { From 00356fc29729376812d4d04a89cd0df520fc112f Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 22 Nov 2022 00:32:27 -0800 Subject: [PATCH 194/397] fix comp complete --- pkg/comp/comp.go | 2 +- pkg/utilfn/utilfn.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 445ee53df..9531479bf 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -319,7 +319,7 @@ func computeCompExtension(compPrefix string, crtn *CompReturn) (string, bool) { if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) { return "", false } - return lcp[len(compPrefix):], utilfn.ContainsStr(compStrs, lcp) + return lcp[len(compPrefix):], (utilfn.ContainsStr(compStrs, lcp) && !utilfn.IsPrefix(compStrs, lcp)) } func (p *CompPoint) FullyExtend(crtn *CompReturn) utilfn.StrWithPos { diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index 752684d90..e5b245f26 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -120,6 +120,15 @@ func ContainsStr(strs []string, test string) bool { return false } +func IsPrefix(strs []string, test string) bool { + for _, s := range strs { + if len(s) > len(test) && strings.HasPrefix(s, test) { + return true + } + } + return false +} + type StrWithPos struct { Str string Pos int // this is a 'rune' position (not a byte position) From ac8180a1ae4f08ae4086fec300b50f8f3aa9a5cf Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 22 Nov 2022 13:52:31 -0800 Subject: [PATCH 195/397] fix default anchor value --- pkg/sstore/dbops.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 787e5eb4b..2958d9156 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -495,7 +495,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, "", "input") + tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, SWAnchorType{}, "input") if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` tx.ExecWrap(query, newScreenId, sessionId) From 7882232a0e1709e9a35431589e05eb3755646106 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 23 Nov 2022 11:12:05 -0800 Subject: [PATCH 196/397] meta+command compgen working. hide compgen command from completion (underscores) --- pkg/cmdrunner/cmdrunner.go | 21 ++++++++++++--------- pkg/cmdrunner/shparse.go | 2 +- pkg/comp/simplecomp.go | 2 +- pkg/shparse/comp.go | 17 ++++++++++++----- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c41337136..705ce378c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -49,7 +49,7 @@ var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cy var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "alias", "unalias", "function", "reset"} -var NoHistCmds = []string{"compgen", "line", "history"} +var NoHistCmds = []string{"_compgen", "line", "history"} var GlobalCmds = []string{"session", "screen", "remote", "killserver"} var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") @@ -83,7 +83,7 @@ func init() { registerCmdFn("comment", CommentCommand) // registerCmdFn("cd", CdCommand) registerCmdFn("cr", CrCommand) - registerCmdFn("compgen", CompGenCommand) + registerCmdFn("_compgen", CompGenCommand) registerCmdFn("clear", ClearCommand) registerCmdFn("reset", ResetCommand) @@ -1077,20 +1077,23 @@ func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.Upd return update } -func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { +func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { if strings.HasPrefix(prefix, "/") { compsCmd, _ := comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil) - compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil) + compsMeta, _ := simpleCompMeta(ctx, prefix, compCtx, nil) return comp.CombineCompReturn(comp.CGTypeCommandMeta, compsCmd, compsMeta), nil } else { return comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil) } } -func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { +func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) { rtn := comp.CompReturn{} validCommands := getValidCommands() for _, cmd := range validCommands { + if strings.HasPrefix(cmd, "/_") && !strings.HasPrefix(prefix, "/_") { + continue + } if strings.HasPrefix(cmd, prefix) { rtn.Entries = append(rtn.Entries, comp.CompEntry{Word: cmd, IsMetaCmd: true}) } @@ -1126,11 +1129,11 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str return doMetaCompGen(ctx, pk, prefix, forDisplay) } if !packet.IsValidCompGenType(compType) { - return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, false, fmt.Errorf("/_compgen invalid type '%s'", compType) } ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) if err != nil { - return nil, false, fmt.Errorf("compgen error: %w", err) + return nil, false, fmt.Errorf("/_compgen error: %w", err) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() @@ -1154,14 +1157,14 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, 0) // best-effort if err != nil { - return nil, fmt.Errorf("/compgen error: %w", err) + return nil, fmt.Errorf("/_compgen error: %w", err) } cmdLine := firstArg(pk) pos := len(cmdLine) if pk.Kwargs["comppos"] != "" { posArg, err := strconv.Atoi(pk.Kwargs["comppos"]) if err != nil { - return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) + return nil, fmt.Errorf("/_compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) } pos = posArg } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 3ad497ebc..e6504e58a 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -14,7 +14,7 @@ import ( "mvdan.cc/sh/v3/syntax" ) -var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(?::([a-z][a-z0-9_-]*))?$") +var ValidMetaCmdRe = regexp.MustCompile("^/([a-z_][a-z0-9_-]*)(?::([a-z][a-z0-9_-]*))?$") type BareMetaCmdDecl struct { CmdStr string diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index f3b93d076..5c83df37c 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -60,7 +60,7 @@ func compsToCompReturn(comps []string, hasMore bool) *CompReturn { func doCompGen(ctx context.Context, prefix string, compType string, compCtx CompContext) (*CompReturn, error) { if !packet.IsValidCompGenType(compType) { - return nil, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, fmt.Errorf("/_compgen invalid type '%s'", compType) } msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId) if msh == nil { diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index c0efe82c7..fbc6065c1 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -187,7 +187,7 @@ func findCompletionPosInWord(word *WordType, posInWord int, superOffset int) *Co return nil } subCmds := ParseCommands(word.Subs) - newPos := FindCompletionPos(subCmds, posInWord-word.contentStartPos(), superOffset+(word.Offset+word.contentStartPos())) + newPos := findCompletionPosInternal(subCmds, posInWord-word.contentStartPos(), superOffset+(word.Offset+word.contentStartPos())) return &newPos } if word.Type == WordTypeSimpleVar || word.Type == WordTypeVarBrace { @@ -252,17 +252,24 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion return rtn } -func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { +func findCompletionPosInternal(cmds []*CmdType, pos int, superOffset int) CompletionPos { cpos := findCompletionPosCmds(cmds, pos, superOffset) if cpos.CompWord == nil { return cpos } subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset) - if subPos == nil { - return cpos - } else { + if subPos != nil { return *subPos } + return cpos +} + +func FindCompletionPos(cmds []*CmdType, pos int) CompletionPos { + cpos := findCompletionPosInternal(cmds, pos, 0) + if cpos.CompType == CompTypeCommand && cpos.SuperOffset == 0 && cpos.CompWord != nil && cpos.CompWord.Offset == 0 && strings.HasPrefix(string(cpos.CompWord.Raw), "/") { + cpos.CompType = CompTypeCommandMeta + } + return cpos } func (cpos CompletionPos) Extend(origStr utilfn.StrWithPos, extensionStr string, extensionComplete bool) utilfn.StrWithPos { From d286f4d6ab2d846d4b08674a260b783c701fe074 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 23 Nov 2022 14:34:49 -0800 Subject: [PATCH 197/397] checkpoint, add winsize to uicontext --- pkg/comp/comp.go | 2 +- pkg/scpacket/scpacket.go | 1 + pkg/shparse/shparse_test.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 9531479bf..e1fdf720d 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -525,7 +525,7 @@ func fixupVarPrefix(varPrefix string) string { func DoCompGen(ctx context.Context, cmdStr utilfn.StrWithPos, compCtx CompContext) (*CompReturn, *utilfn.StrWithPos, error) { words := shparse.Tokenize(cmdStr.Str) cmds := shparse.ParseCommands(words) - compPos := shparse.FindCompletionPos(cmds, cmdStr.Pos, 0) + compPos := shparse.FindCompletionPos(cmds, cmdStr.Pos) fmt.Printf("comppos: %v\n", compPos) if compPos.CompType == shparse.CompTypeInvalid { return nil, nil, nil diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index c27d367c5..292cc85ea 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -55,6 +55,7 @@ type UIContextType struct { WindowId string `json:"windowid"` Remote *sstore.RemotePtrType `json:"remote,omitempty"` TermOpts *packet.TermOpts `json:"termopts,omitempty"` + WinSize *WinSize `json:"winsize,omitempty"` } type FeInputPacketType struct { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index e98064c10..8ca32cc21 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -135,7 +135,7 @@ func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdSP := utilfn.ParseToSP(cmdStr) words := Tokenize(cmdSP.Str) cmds := ParseCommands(words) - cpos := FindCompletionPos(cmds, cmdSP.Pos, 0) + cpos := FindCompletionPos(cmds, cmdSP.Pos) fmt.Printf("testCompPos [%d] %q => [%s] %v\n", cmdSP.Pos, cmdStr, cpos.CompType, cpos) if cpos.CompType != compType { t.Errorf("testCompPos %q => invalid comp-type %q, expected %q", cmdStr, cpos.CompType, compType) From 576d02c98fb7d35aa3dc32d6b9951a8382dc67bd Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 24 Nov 2022 15:05:08 -0800 Subject: [PATCH 198/397] make a simple linediff --- pkg/utilfn/linediff.go | 132 ++++++++++++++++++++++++++++++++++++++ pkg/utilfn/utilfn_test.go | 48 ++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 pkg/utilfn/linediff.go create mode 100644 pkg/utilfn/utilfn_test.go diff --git a/pkg/utilfn/linediff.go b/pkg/utilfn/linediff.go new file mode 100644 index 000000000..5d4a2e9e0 --- /dev/null +++ b/pkg/utilfn/linediff.go @@ -0,0 +1,132 @@ +package utilfn + +import ( + "bytes" + "encoding/binary" + "fmt" + "strings" +) + +const LineDiffVersion = 0 + +type LineDiffType struct { + Lines []int + NewData []string +} + +// simple encoding +// a 0 means read a line from NewData +// a non-zero number means read the 1-indexed line from OldData +func applyDiff(oldData []string, diff LineDiffType) ([]string, error) { + rtn := make([]string, 0, len(diff.Lines)) + newDataPos := 0 + for i := 0; i < len(diff.Lines); i++ { + if diff.Lines[i] == 0 { + if newDataPos >= len(diff.NewData) { + return nil, fmt.Errorf("not enough newdata for diff") + } + rtn = append(rtn, diff.NewData[newDataPos]) + newDataPos++ + } else { + idx := diff.Lines[i] - 1 // 1-indexed + if idx < 0 || idx >= len(oldData) { + return nil, fmt.Errorf("diff index out of bounds %d old-data-len:%d", idx, len(oldData)) + } + rtn = append(rtn, oldData[idx]) + } + } + return rtn, nil +} + +func putUVarint(buf *bytes.Buffer, viBuf []byte, ival int) { + l := binary.PutUvarint(viBuf, uint64(ival)) + buf.Write(viBuf[0:l]) +} + +// simple encoding +// write varints. first version, then len, then len-number-of-varints, then fill the rest with newdata +// [version] [len-varint] [varint]xlen... newdata (bytes) +func encodeDiff(diff LineDiffType) []byte { + var buf bytes.Buffer + viBuf := make([]byte, binary.MaxVarintLen64) + putUVarint(&buf, viBuf, 0) + putUVarint(&buf, viBuf, len(diff.Lines)) + for _, val := range diff.Lines { + putUVarint(&buf, viBuf, val) + } + for _, str := range diff.NewData { + buf.WriteString(str) + buf.WriteByte('\n') + } + return buf.Bytes() +} + +func decodeDiff(diffBytes []byte) (LineDiffType, error) { + var rtn LineDiffType + r := bytes.NewBuffer(diffBytes) + version, err := binary.ReadUvarint(r) + if err != nil { + return rtn, fmt.Errorf("invalid diff, cannot read version: %v", err) + } + if version != LineDiffVersion { + return rtn, fmt.Errorf("invalid diff, bad version: %d", version) + } + linesLen64, err := binary.ReadUvarint(r) + if err != nil { + return rtn, fmt.Errorf("invalid diff, cannot read lines length: %v", err) + } + linesLen := int(linesLen64) + rtn.Lines = make([]int, linesLen) + for idx := 0; idx < linesLen; idx++ { + vi, err := binary.ReadUvarint(r) + if err != nil { + return rtn, fmt.Errorf("invalid diff, cannot read line %d: %v", idx, err) + } + rtn.Lines[idx] = int(vi) + } + restOfInput := string(r.Bytes()) + rtn.NewData = strings.Split(restOfInput, "\n") + return rtn, nil +} + +func makeDiff(oldData []string, newData []string) LineDiffType { + var rtn LineDiffType + oldDataMap := make(map[string]int) // 1-indexed + for idx, str := range oldData { + if _, found := oldDataMap[str]; found { + continue + } + oldDataMap[str] = idx + 1 + } + rtn.Lines = make([]int, len(newData)) + for idx, str := range newData { + oldIdx, found := oldDataMap[str] + if found { + rtn.Lines[idx] = oldIdx + } else { + rtn.Lines[idx] = 0 + rtn.NewData = append(rtn.NewData, str) + } + } + return rtn +} + +func MakeDiff(str1 string, str2 string) []byte { + str1Arr := strings.Split(str1, "\n") + str2Arr := strings.Split(str2, "\n") + diff := makeDiff(str1Arr, str2Arr) + return encodeDiff(diff) +} + +func ApplyDiff(str1 string, diffBytes []byte) (string, error) { + diff, err := decodeDiff(diffBytes) + if err != nil { + return "", err + } + str1Arr := strings.Split(str1, "\n") + str2Arr, err := applyDiff(str1Arr, diff) + if err != nil { + return "", err + } + return strings.Join(str2Arr, "\n"), nil +} diff --git a/pkg/utilfn/utilfn_test.go b/pkg/utilfn/utilfn_test.go new file mode 100644 index 000000000..d5d7c81ba --- /dev/null +++ b/pkg/utilfn/utilfn_test.go @@ -0,0 +1,48 @@ +package utilfn + +import ( + "fmt" + "testing" +) + +const Str1 = ` +hello +line #2 +more +stuff +apple +` + +const Str2 = ` +line #2 +apple +grapes +banana +` + +const Str3 = ` +more +stuff +banana +coconut +` + +func testDiff(t *testing.T, str1 string, str2 string) { + diffBytes := MakeDiff(str1, str2) + fmt.Printf("diff-len: %d\n", len(diffBytes)) + out, err := ApplyDiff(str1, diffBytes) + if err != nil { + t.Errorf("error in diff: %v", err) + return + } + if out != str2 { + t.Errorf("bad diff output") + } +} + +func TestDiff(t *testing.T) { + testDiff(t, Str1, Str2) + testDiff(t, Str2, Str3) + testDiff(t, Str1, Str3) + testDiff(t, Str3, Str1) +} From 301bfaa0be958b3b9ac76527c1c1f446fe2f9ff7 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 24 Nov 2022 15:16:18 -0800 Subject: [PATCH 199/397] write a quick set of functions to pack and unpack byte arrays --- pkg/utilfn/binpack.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pkg/utilfn/binpack.go diff --git a/pkg/utilfn/binpack.go b/pkg/utilfn/binpack.go new file mode 100644 index 000000000..94cc12a85 --- /dev/null +++ b/pkg/utilfn/binpack.go @@ -0,0 +1,34 @@ +package utilfn + +import ( + "bufio" + "encoding/binary" + "io" +) + +func PackValue(w io.Writer, barr []byte) error { + viBuf := make([]byte, binary.MaxVarintLen64) + viLen := binary.PutUvarint(viBuf, uint64(len(barr))) + _, err := w.Write(viBuf[0:viLen]) + if err != nil { + return err + } + _, err = w.Write(barr) + if err != nil { + return err + } + return nil +} + +func UnpackValue(r *bufio.Reader) ([]byte, error) { + lenVal, err := binary.ReadUvarint(r) + if err != nil { + return nil, err + } + rtnBuf := make([]byte, int(lenVal)) + _, err = io.ReadFull(r, rtnBuf) + if err != nil { + return nil, err + } + return rtnBuf, nil +} From d5ea9e022108f1b7a34eadd5cfa5c7e8fc4e310d Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 27 Nov 2022 14:12:15 -0800 Subject: [PATCH 200/397] implement cmdfinal (hangup) from server --- pkg/remote/remote.go | 40 ++++++++++++++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 21 +++++++++++++++++++++ pkg/utilfn/binpack.go | 34 ---------------------------------- 3 files changed, 61 insertions(+), 34 deletions(-) delete mode 100644 pkg/utilfn/binpack.go diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 6583710a7..eb151f8a8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "os" "os/exec" "path" @@ -1401,10 +1402,38 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { } } } + if donePk.FinalStateDiff != nil { + fmt.Printf("** final state diff! %v\n", donePk.FinalStateDiff) + } sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) return } +func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) { + defer msh.RemoveRunningCmd(finalPk.CK) + rtnCmd, err := sstore.GetCmdById(context.Background(), finalPk.CK.GetSessionId(), finalPk.CK.GetCmdId()) + if err != nil { + log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err) + return + } + if rtnCmd == nil || rtnCmd.DonePk != nil { + return + } + log.Printf("finalpk %s (hangup): %s\n", finalPk.CK, finalPk.Error) + sstore.HangupCmd(context.Background(), finalPk.CK) + rtnCmd, err = sstore.GetCmdById(context.Background(), finalPk.CK.GetSessionId(), finalPk.CK.GetCmdId()) + if err != nil { + log.Printf("error getting cmd(2) in handleCmdFinalPacket: %v\n", err) + return + } + if rtnCmd == nil { + log.Printf("error getting cmd(2) in handleCmdFinalPacket (not found)\n") + return + } + update := &sstore.ModelUpdate{Cmd: rtnCmd} + sstore.MainBus.SendUpdate(finalPk.CK.GetSessionId(), update) +} + // TODO notify FE about cmd errors func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) { err := sstore.AppendCmdErrorPk(context.Background(), errPk) @@ -1454,6 +1483,12 @@ func (msh *MShellProc) makeHandleCmdDonePacketClosure(donePk *packet.CmdDonePack } } +func (msh *MShellProc) makeHandleCmdFinalPacketClosure(finalPk *packet.CmdFinalPacketType) func() { + return func() { + msh.handleCmdFinalPacket(finalPk) + } +} + func (msh *MShellProc) ProcessPackets() { defer msh.WithLock(func() { if msh.Status == StatusConnected { @@ -1488,6 +1523,11 @@ func (msh *MShellProc) ProcessPackets() { runCmdUpdateFn(donePk.CK, msh.makeHandleCmdDonePacketClosure(donePk)) continue } + if pk.GetType() == packet.CmdFinalPacketStr { + finalPk := pk.(*packet.CmdFinalPacketType) + runCmdUpdateFn(finalPk.CK, msh.makeHandleCmdFinalPacketClosure(finalPk)) + continue + } if pk.GetType() == packet.CmdErrorPacketStr { msh.handleCmdErrorPacket(pk.(*packet.CmdErrorPacketType)) continue diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 2958d9156..1544af636 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) @@ -681,6 +682,18 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, return cmd, nil } +func HasDonePk(ctx context.Context, ck base.CommandKey) (bool, error) { + var found bool + txErr := WithTx(ctx, func(tx *TxWrap) error { + found = tx.Exists(`SELECT sessionid FROM cmd WHERE sessionid = ? AND cmdid = ? AND donepk is NOT NULL`, ck.GetSessionId(), ck.GetCmdId()) + return nil + }) + if txErr != nil { + return false, txErr + } + return found, nil +} + func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (*ModelUpdate, error) { if donePk == nil || donePk.CK.IsEmpty() { return nil, fmt.Errorf("invalid cmddone packet (no ck)") @@ -732,6 +745,14 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { }) } +func HangupCmd(ctx context.Context, ck base.CommandKey) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET status = ? WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, CmdStatusHangup, ck.GetSessionId(), ck.GetCmdId()) + return nil + }) +} + func getNextId(ids []string, delId string) string { if len(ids) == 0 { return "" diff --git a/pkg/utilfn/binpack.go b/pkg/utilfn/binpack.go deleted file mode 100644 index 94cc12a85..000000000 --- a/pkg/utilfn/binpack.go +++ /dev/null @@ -1,34 +0,0 @@ -package utilfn - -import ( - "bufio" - "encoding/binary" - "io" -) - -func PackValue(w io.Writer, barr []byte) error { - viBuf := make([]byte, binary.MaxVarintLen64) - viLen := binary.PutUvarint(viBuf, uint64(len(barr))) - _, err := w.Write(viBuf[0:viLen]) - if err != nil { - return err - } - _, err = w.Write(barr) - if err != nil { - return err - } - return nil -} - -func UnpackValue(r *bufio.Reader) ([]byte, error) { - lenVal, err := binary.ReadUvarint(r) - if err != nil { - return nil, err - } - rtnBuf := make([]byte, int(lenVal)) - _, err = io.ReadFull(r, rtnBuf) - if err != nil { - return nil, err - } - return rtnBuf, nil -} From b2dc52e1665dddcc6142a6344f10d59d817f12ab Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 28 Nov 2022 00:13:00 -0800 Subject: [PATCH 201/397] checkpoint --- db/migrations/000001_init.up.sql | 20 +++- db/schema.sql | 21 +++- pkg/cmdrunner/cmdrunner.go | 188 +++++++++++++++-------------- pkg/cmdrunner/resolver.go | 21 ++-- pkg/cmdrunner/shparse.go | 2 +- pkg/remote/remote.go | 151 ++++++++++++++++++------ pkg/sstore/dbops.go | 196 ++++++++++++++++++++++++++++--- pkg/sstore/quick.go | 23 ++++ pkg/sstore/sstore.go | 124 +++++++++++++------ pkg/utilfn/utilfn.go | 9 ++ 10 files changed, 560 insertions(+), 195 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index bc54d74d0..7f05513f6 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -63,7 +63,24 @@ CREATE TABLE remote_instance ( windowid varchar(36) NOT NULL, remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, - state json NOT NULL + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL +); + +CREATE TABLE state_base ( + basehash varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + version varchar(200) NOT NULL, + data blob NOT NULL +); + +CREATE TABLE state_diff ( + diffhash varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + basehash varchar(36) NOT NULL, + diffhasharr json NOT NULL, + data blob NOT NULL ); CREATE TABLE line ( @@ -94,7 +111,6 @@ CREATE TABLE remote ( remotehost varchar(200) NOT NULL, connectmode varchar(20) NOT NULL, autoinstall boolean NOT NULL, - initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, diff --git a/db/schema.sql b/db/schema.sql index c0d09ed5f..34d8bbcc4 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -60,7 +60,22 @@ CREATE TABLE remote_instance ( windowid varchar(36) NOT NULL, remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, - state json NOT NULL + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL +); +CREATE TABLE state_base ( + basehash varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + version varchar(200) NOT NULL, + data blob NOT NULL +); +CREATE TABLE state_diff ( + diffhash varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + basehash varchar(36) NOT NULL, + diffhasharr json NOT NULL, + data blob NOT NULL ); CREATE TABLE line ( sessionid varchar(36) NOT NULL, @@ -75,6 +90,7 @@ CREATE TABLE line ( text text NOT NULL, cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, + contentheight int NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -88,7 +104,6 @@ CREATE TABLE remote ( remotehost varchar(200) NOT NULL, connectmode varchar(20) NOT NULL, autoinstall boolean NOT NULL, - initpk json NOT NULL, sshopts json NOT NULL, remoteopts json NOT NULL, lastconnectts bigint NOT NULL, @@ -110,7 +125,7 @@ CREATE TABLE cmd ( startpk json NOT NULL, donepk json NOT NULL, runout json NOT NULL, - usedrows int NOT NULL, + rtnstate bool NOT NULL, PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 705ce378c..124b52c36 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -6,8 +6,6 @@ import ( "fmt" "log" "os" - "path" - "path/filepath" "regexp" "sort" "strconv" @@ -43,6 +41,7 @@ const DefaultUserId = "sawka" const MaxNameLen = 50 const MaxRemoteAliasLen = 50 const PasswordUnchangedSentinel = "--unchanged--" +const DefaultPTERM = "MxM" var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -50,7 +49,27 @@ var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoins var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "alias", "unalias", "function", "reset"} var NoHistCmds = []string{"_compgen", "line", "history"} -var GlobalCmds = []string{"session", "screen", "remote", "killserver"} +var GlobalCmds = []string{"session", "screen", "remote", "killserver", "set"} + +var SetVarNameMap map[string]string = map[string]string{ + "tabcolor": "screen.tabcolor", + "pterm": "window.pterm", + "anchor": "sw.anchor", + "focus": "sw.focus", + "line": "sw.line", +} + +var SetVarScopes = []SetVarScope{ + SetVarScope{ScopeName: "global", VarNames: []string{}}, + SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}}, + SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos"}}, + SetVarScope{ScopeName: "window", VarNames: []string{"pterm"}}, + SetVarScope{ScopeName: "sw", VarNames: []string{"anchor", "focus", "line"}}, + SetVarScope{ScopeName: "line", VarNames: []string{}}, + // connection = remote, remote = remoteinstance + SetVarScope{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}}, + SetVarScope{ScopeName: "remote", VarNames: []string{}}, +} var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") @@ -63,6 +82,11 @@ type contextType string var historyContextKey = contextType("history") +type SetVarScope struct { + ScopeName string + VarNames []string +} + type historyContextType struct { LineId string CmdId string @@ -81,7 +105,6 @@ func init() { registerCmdFn("run", RunCommand) registerCmdFn("eval", EvalCommand) registerCmdFn("comment", CommentCommand) - // registerCmdFn("cd", CdCommand) registerCmdFn("cr", CrCommand) registerCmdFn("_compgen", CompGenCommand) registerCmdFn("clear", ClearCommand) @@ -122,6 +145,8 @@ func init() { registerCmdFn("history", HistoryCommand) registerCmdFn("killserver", KillServerCommand) + + registerCmdFn("set", SetCommand) } func getValidCommands() []string { @@ -185,6 +210,13 @@ func resolveBool(arg string, def bool) bool { return true } +func defaultStr(arg string, def string) string { + if arg == "" { + return def + } + return arg +} + func resolveFile(arg string) (string, error) { if arg == "" { return "", nil @@ -231,20 +263,6 @@ func resolveNonNegInt(arg string, def int) (int, error) { return ival, nil } -func getUITermOpts(uiContext *scpacket.UIContextType) *packet.TermOpts { - termOpts := &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} - if uiContext != nil && uiContext.TermOpts != nil { - pkOpts := uiContext.TermOpts - if pkOpts.Cols > 0 { - termOpts.Cols = base.BoundInt(pkOpts.Cols, shexec.MinTermCols, shexec.MaxTermCols) - } - if pkOpts.MaxPtySize > 0 { - termOpts.MaxPtySize = base.BoundInt64(pkOpts.MaxPtySize, shexec.MinMaxPtySize, shexec.MaxMaxPtySize) - } - } - return termOpts -} - func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) if err != nil { @@ -252,12 +270,16 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } cmdStr := firstArg(pk) isRtnStateCmd := IsReturnStateCommand(cmdStr) + // runPacket.State is set in remote.RunCommand() runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenSCUUID()) - // runPacket.State is set in remote.RunCommand() runPacket.UsePty = true - runPacket.TermOpts = getUITermOpts(pk.UIContext) + ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) + if err != nil { + return nil, fmt.Errorf("/run error, invalid 'pterm' value %q: %v", ptermVal, err) + } runPacket.Command = strings.TrimSpace(cmdStr) runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd) cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, runPacket) @@ -883,7 +905,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if newRemote == "" { return nil, nil } - remoteName, rptr, _, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + remoteName, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { return nil, err } @@ -911,70 +933,6 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return update, nil } -func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) - if err != nil { - return nil, fmt.Errorf("/cd error: %w", err) - } - newDir := firstArg(pk) - if newDir == "" { - return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, ids.Remote.RemoteState.Cwd), - }, - }, nil - } - newDir, err = ids.Remote.RState.ExpandHomeDir(newDir) - if err != nil { - return nil, err - } - if !strings.HasPrefix(newDir, "/") { - if ids.Remote.RemoteState == nil { - return nil, fmt.Errorf("/cd error: cannot get current remote directory (can only cd with absolute path)") - } - newDir = path.Join(ids.Remote.RemoteState.Cwd, newDir) - newDir, err = filepath.Abs(newDir) - if err != nil { - return nil, fmt.Errorf("/cd error: error canonicalizing new directory: %w", err) - } - } - cdPacket := packet.MakeCdPacket() - cdPacket.ReqId = uuid.New().String() - cdPacket.Dir = newDir - resp, err := ids.Remote.MShell.PacketRpc(ctx, cdPacket) - if err != nil { - return nil, err - } - if err = resp.Err(); err != nil { - return nil, err - } - state := *ids.Remote.RemoteState - state.Cwd = newDir - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, state) - if err != nil { - return nil, err - } - var cmdOutput bytes.Buffer - displayStateUpdateDiff(&cmdOutput, *ids.Remote.RemoteState, remoteInst.State) - cmd, err := makeStaticCmd(ctx, "cd", ids, pk.GetRawStr(), cmdOutput.Bytes()) - if err != nil { - // TODO tricky error since the command was a success, but we can't show the output - return nil, err - } - update, err := addLineForCmd(ctx, "/cd", false, ids, cmd) - if err != nil { - // TODO tricky error since the command was a success, but we can't show the output - return nil, err - } - update.Interactive = pk.Interactive - update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst) - //update.Info = &sstore.InfoMsgType{ - // InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.Remote.DisplayName, newDir), - // TimeoutMs: 2000, - //} - return update, nil -} - func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ SessionId: ids.SessionId, @@ -1409,7 +1367,8 @@ func ResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if initPk == nil || initPk.State == nil { return nil, fmt.Errorf("invalid initpk received from remote (no remote state)") } - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, *initPk.State) + feState := sstore.FeStateFromShellState(initPk.State) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, *feState, initPk.State, nil) if err != nil { return nil, err } @@ -1618,6 +1577,33 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return update, nil } +func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + var setMap map[string]map[string]string + setMap = make(map[string]map[string]string) + _, err := resolveUiIds(ctx, pk, 0) // best effort + if err != nil { + return nil, err + } + for argIdx, rawArgVal := range pk.Args { + eqIdx := strings.Index(rawArgVal, "=") + if eqIdx == -1 { + return nil, fmt.Errorf("/set invalid argument %d, does not contain an '='", argIdx) + } + argName := rawArgVal[:eqIdx] + argVal := rawArgVal[eqIdx+1:] + ok, scopeName, varName := resolveSetArg(argName) + if !ok { + return nil, fmt.Errorf("/set invalid setvar %q", argName) + } + if _, ok := setMap[scopeName]; !ok { + setMap[scopeName] = make(map[string]string) + } + setMap[scopeName][varName] = argVal + } + fmt.Printf("setmap: %#v\n", setMap) + return nil, nil +} + func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { go func() { log.Printf("received /killserver, shutting down\n") @@ -1693,7 +1679,7 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt oldEnvMap := shexec.DeclMapFromState(&oldState) for key, newVal := range newEnvMap { oldVal, found := oldEnvMap[key] - if !found || !shexec.DeclsEqual(oldVal, newVal) { + if !found || !shexec.DeclsEqual(false, oldVal, newVal) { var exportStr string if newVal.IsExport() { exportStr = "export " @@ -1760,3 +1746,31 @@ func GetRtnStateDiff(ctx context.Context, sessionId string, cmdId string) ([]byt displayStateUpdateDiff(&outputBytes, cmd.RemoteState, *cmd.DonePk.FinalState) return outputBytes.Bytes(), nil } + +func isValidInScope(scopeName string, varName string) bool { + for _, varScope := range SetVarScopes { + if varScope.ScopeName == scopeName { + return utilfn.ContainsStr(varScope.VarNames, varName) + } + } + return false +} + +// returns (is-valid, scope, name) +// TODO write a full resolver to allow for indexed arguments. e.g. session[1].screen[1].window.pterm="25x80" +func resolveSetArg(argName string) (bool, string, string) { + dotIdx := strings.Index(argName, ".") + if dotIdx == -1 { + argName = SetVarNameMap[argName] + dotIdx = strings.Index(argName, ".") + } + if argName == "" { + return false, "", "" + } + scopeName := argName[0:dotIdx] + varName := argName[dotIdx+1:] + if !isValidInScope(scopeName, varName) { + return false, "", "" + } + return true, scopeName, varName +} diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 2ab269151..34fff2f90 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -464,7 +464,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi return nil, fmt.Errorf("cannot resolve remote state '%s': %w", displayName, err) } if state == nil { - state = rstate.DefaultState + state = msh.GetDefaultState() } rtn.RemoteState = state } @@ -472,26 +472,22 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi } // returns (remoteDisplayName, remoteptr, state, rstate, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *packet.ShellState, *remote.RemoteRuntimeState, error) { +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *remote.RemoteRuntimeState, error) { if fullRemoteRef == "" { - return "", nil, nil, nil, nil + return "", nil, nil, nil } userRef, remoteRef, remoteName, err := parseFullRemoteRef(fullRemoteRef) if err != nil { - return "", nil, nil, nil, err + return "", nil, nil, err } if userRef != "" { - return "", nil, nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) + return "", nil, nil, fmt.Errorf("invalid remote '%s', cannot resolve remote userid '%s'", fullRemoteRef, userRef) } rstate := remote.ResolveRemoteRef(remoteRef) if rstate == nil { - return "", nil, nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) + return "", nil, nil, fmt.Errorf("cannot resolve remote '%s': not found", fullRemoteRef) } rptr := sstore.RemotePtrType{RemoteId: rstate.RemoteId, Name: remoteName} - state, err := sstore.GetRemoteState(ctx, sessionId, windowId, rptr) - if err != nil { - return "", nil, nil, nil, fmt.Errorf("cannot resolve remote state '%s': %w", fullRemoteRef, err) - } rname := rstate.RemoteCanonicalName if rstate.RemoteAlias != "" { rname = rstate.RemoteAlias @@ -499,8 +495,5 @@ func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, if rptr.Name != "" { rname = fmt.Sprintf("%s:%s", rname, rptr.Name) } - if state == nil { - return rname, &rptr, rstate.DefaultState, rstate, nil - } - return rname, &rptr, state, rstate, nil + return rname, &rptr, rstate, nil } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index e6504e58a..3aa9a08c4 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -110,7 +110,7 @@ func parseMetaCmd(origCommandStr string) (string, string, string) { } func onlyPositionalArgs(metaCmd string, metaSubCmd string) bool { - return (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" + return (metaCmd == "setenv" || metaCmd == "unset" || metaCmd == "set") && metaSubCmd == "" } func onlyRawArgs(metaCmd string, metaSubCmd string) bool { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index eb151f8a8..e993b29fc 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -90,7 +90,8 @@ type MShellProc struct { ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc - DefaultState *packet.ShellState + StateMap map[string]*packet.ShellState // sha1->state + CurrentState string // sha1 // install InstallStatus string @@ -111,26 +112,26 @@ type RunCmdType struct { } type RemoteRuntimeState struct { - RemoteType string `json:"remotetype"` - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalremoteid"` - RemoteAlias string `json:"remotealias,omitempty"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteVars map[string]string `json:"remotevars"` - Status string `json:"status"` - ErrorStr string `json:"errorstr,omitempty"` - InstallStatus string `json:"installstatus"` - InstallErrorStr string `json:"installerrorstr,omitempty"` - NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` - DefaultState *packet.ShellState `json:"defaultstate"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - Archived bool `json:"archived,omitempty"` - RemoteIdx int64 `json:"remoteidx"` - UName string `json:"uname"` - MShellVersion string `json:"mshellversion"` - WaitingForPassword bool `json:"waitingforpassword,omitempty"` - Local bool `json:"local,omitempty"` + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalremoteid"` + RemoteAlias string `json:"remotealias,omitempty"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteVars map[string]string `json:"remotevars"` + DefaultFeState *sstore.FeStateType `json:"defaultfestate"` + Status string `json:"status"` + ErrorStr string `json:"errorstr,omitempty"` + InstallStatus string `json:"installstatus"` + InstallErrorStr string `json:"installerrorstr,omitempty"` + NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + Archived bool `json:"archived,omitempty"` + RemoteIdx int64 `json:"remoteidx"` + UName string `json:"uname"` + MShellVersion string `json:"mshellversion"` + WaitingForPassword bool `json:"waitingforpassword,omitempty"` + Local bool `json:"local,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -146,7 +147,13 @@ func (msh *MShellProc) GetStatus() string { func (msh *MShellProc) GetDefaultState() *packet.ShellState { msh.Lock.Lock() defer msh.Lock.Unlock() - return msh.DefaultState + return msh.StateMap[msh.CurrentState] +} + +func (msh *MShellProc) GetStateByHash(hval string) *packet.ShellState { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.StateMap[hval] } func (msh *MShellProc) GetRemoteId() string { @@ -484,7 +491,6 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["color"] = msh.Remote.RemoteOpts.Color } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { - state.DefaultState = msh.DefaultState state.MShellVersion = msh.ServerProc.InitPk.Version vars["home"] = msh.ServerProc.InitPk.HomeDir vars["remoteuser"] = msh.ServerProc.InitPk.User @@ -494,6 +500,11 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["besthost"] = vars["remotehost"] vars["bestshorthost"] = vars["remoteshorthost"] } + curState := msh.StateMap[msh.CurrentState] + if curState != nil { + state.DefaultFeState = sstore.FeStateFromShellState(curState) + vars["cwd"] = curState.Cwd + } if msh.Remote.Local && msh.Remote.RemoteSudo { vars["bestuser"] = "sudo" } else if msh.Remote.RemoteSudo { @@ -557,6 +568,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { InstallStatus: StatusDisconnected, RunningCmds: make(map[base.CommandKey]RunCmdType), PendingStateCmds: make(map[string]base.CommandKey), + StateMap: make(map[string]*packet.ShellState), } rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) return rtn @@ -913,9 +925,10 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro if initPk.State == nil { return nil, fmt.Errorf("invalid reinit response initpk does not contain remote state") } + hval := initPk.State.GetHashVal(false) msh.WithLock(func() { - msh.Remote.InitPk = initPk - msh.DefaultState = initPk.State + msh.CurrentState = hval + msh.StateMap[hval] = initPk.State }) return initPk, nil } @@ -999,11 +1012,10 @@ func (msh *MShellProc) Launch() { cproc, initPk, err := shexec.MakeClientProc(makeClientCtx, ecmd) // TODO check if initPk.State is not nil var mshellVersion string + var stateBaseHash string msh.WithLock(func() { msh.MakeClientCancelFn = nil if initPk != nil { - msh.DefaultState = initPk.State - msh.Remote.InitPk = initPk msh.UName = initPk.UName mshellVersion = initPk.Version if semver.Compare(mshellVersion, MShellVersion) < 0 { @@ -1011,6 +1023,15 @@ func (msh *MShellProc) Launch() { msh.NeedsMShellUpgrade = true } } + if initPk != nil && initPk.State != nil { + hval := initPk.State.GetHashVal(false) + msh.CurrentState = hval + msh.StateMap[hval] = initPk.State + sstore.StoreStateBase(context.Background(), initPk.State) + stateBaseHash = hval + } else { + msh.CurrentState = "" + } // no notify here, because we'll call notify in either case below }) if err == context.Canceled { @@ -1029,7 +1050,7 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("*error connecting to remote: %v\n", err) return } - msh.WriteToPtyBuffer("connected\n") + msh.WriteToPtyBuffer("connected state:%s\n", stateBaseHash) msh.WithLock(func() { msh.ServerProc = cproc msh.Status = StatusConnected @@ -1389,10 +1410,33 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { // fall-through (nothing to do) } update.ScreenWindows = sws - if donePk.FinalState != nil { - rct := msh.GetRunningCmd(donePk.CK) - if rct != nil { - remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *donePk.FinalState) + rct := msh.GetRunningCmd(donePk.CK) + if donePk.FinalState != nil && rct != nil { + fmt.Printf("** FINALSTATE!\n") + feState := sstore.FeStateFromShellState(donePk.FinalState) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, donePk.FinalState, nil) + if err != nil { + msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) + // fall-through (nothing to do) + } + if remoteInst != nil { + update.Sessions = sstore.MakeSessionsUpdateForRemote(rct.SessionId, remoteInst) + } + } else if donePk.FinalStateDiff != nil && rct != nil { + fmt.Printf("** STATEDIFF! %#v\n", donePk.FinalStateDiff) + fullState, err := msh.getFullState(donePk.FinalStateDiff) + if err != nil { + fmt.Printf("**ERR: %v\n", err) + } + donePk.FinalStateDiff.Dump() + shexec.DumpVarMapFromState(fullState) + feState, err := msh.getFeStateFromDiff(donePk.FinalStateDiff) + if err != nil { + msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) + // fall-through (nothing to do) + } else { + fmt.Printf("** festate = %#v\n", feState) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, nil, donePk.FinalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) @@ -1402,9 +1446,6 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { } } } - if donePk.FinalStateDiff != nil { - fmt.Printf("** final state diff! %v\n", donePk.FinalStateDiff) - } sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) return } @@ -1692,3 +1733,43 @@ func evalPromptEsc(escCode string, vars map[string]string, state *packet.ShellSt // we don't support date/time escapes (d, t, T, @), version escapes (v, V), cmd number (#, !), terminal device (l), jobs (j) return "(" + escCode + ")" } + +func (msh *MShellProc) getFullState(stateDiff *packet.ShellStateDiff) (*packet.ShellState, error) { + baseState := msh.GetStateByHash(stateDiff.BaseHash) + if baseState != nil && len(stateDiff.DiffHashArr) == 0 { + newState, err := shexec.ApplyShellStateDiff(*baseState, *stateDiff) + if err != nil { + return nil, err + } + return &newState, nil + } else { + fullState, err := sstore.GetFullState(context.Background(), stateDiff.BaseHash, stateDiff.DiffHashArr) + if err != nil { + return nil, err + } + newState, err := shexec.ApplyShellStateDiff(*fullState, *stateDiff) + return &newState, nil + } +} + +// internal func, first tries the StateMap, otherwise will fallback on sstore.GetFullState +func (msh *MShellProc) getFeStateFromDiff(stateDiff *packet.ShellStateDiff) (*sstore.FeStateType, error) { + baseState := msh.GetStateByHash(stateDiff.BaseHash) + if baseState != nil && len(stateDiff.DiffHashArr) == 0 { + newState, err := shexec.ApplyShellStateDiff(*baseState, *stateDiff) + if err != nil { + return nil, err + } + return sstore.FeStateFromShellState(&newState), nil + } else { + fullState, err := sstore.GetFullState(context.Background(), stateDiff.BaseHash, stateDiff.DiffHashArr) + if err != nil { + return nil, err + } + newState, err := shexec.ApplyShellStateDiff(*fullState, *stateDiff) + if err != nil { + return nil, err + } + return sstore.FeStateFromShellState(&newState), nil + } +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1544af636..f18dbc3ae 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -5,10 +5,12 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) @@ -141,8 +143,8 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, initpk, sshopts, remoteopts, lastconnectts, archived, remoteidx, local) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:initpk,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local)` + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local)` tx.NamedExecWrap(query, r.ToMap()) return nil }) @@ -822,18 +824,25 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat } func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, error) { - var remoteState *packet.ShellState + var state *packet.ShellState txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) - ri := RIFromMap(m) - if ri != nil { - remoteState = &ri.State + ri, err := GetRemoteInstance(tx.Context(), sessionId, windowId, remotePtr) + if err != nil { + return err + } + if ri == nil { return nil } + state, err = GetFullState(tx.Context(), ri.StateBaseHash, ri.StateDiffHashArr) + if err != nil { + return err + } return nil }) - return remoteState, txErr + if txErr != nil { + return nil, txErr + } + return state, nil } func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error { @@ -852,7 +861,50 @@ func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error } } -func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, state packet.ShellState) (*RemoteInstance, error) { +func GetRemoteInstance(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*RemoteInstance, error) { + if remotePtr.IsSessionScope() { + windowId = "" + } + var ri *RemoteInstance + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + ri = RIFromMap(m) + return nil + }) + if txErr != nil { + return nil, txErr + } + return ri, nil +} + +// internal function for UpdateRemoteState +func updateRIWithState(ctx context.Context, ri *RemoteInstance, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) error { + if stateBase != nil { + ri.StateBaseHash = stateBase.GetHashVal(false) + err := StoreStateBase(ctx, stateBase) + if err != nil { + return err + } + } else if stateDiff != nil { + ri.StateBaseHash = stateDiff.BaseHash + ri.StateDiffHashArr = append(stateDiff.DiffHashArr, stateDiff.GetHashVal(false)) + err := StoreStateDiff(ctx, stateDiff) + if err != nil { + return err + } + } + return nil +} + +// TODO - statediff +func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, feState FeStateType, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) (*RemoteInstance, error) { + if stateBase == nil && stateDiff == nil { + return nil, fmt.Errorf("UpdateRemoteState, must set state or diff") + } + if stateBase != nil && stateDiff != nil { + return nil, fmt.Errorf("UpdateRemoteState, cannot set state and diff") + } if remotePtr.IsSessionScope() { windowId = "" } @@ -860,7 +912,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r txErr := WithTx(ctx, func(tx *TxWrap) error { err := validateSessionWindow(tx, sessionId, windowId) if err != nil { - return fmt.Errorf("cannot update remote instance cwd: %w", err) + return fmt.Errorf("cannot update remote instance state: %w", err) } query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) @@ -873,17 +925,26 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r WindowId: windowId, RemoteOwnerId: remotePtr.OwnerId, RemoteId: remotePtr.RemoteId, - State: state, + FeState: feState, } - query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, state) - VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:state)` + err = updateRIWithState(tx.Context(), ri, stateBase, stateDiff) + if err != nil { + return err + } + query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, festate, statebasehash, statediffhasharr) + VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:festate,:statebasehash,:statediffhasharr)` tx.NamedExecWrap(query, ri.ToMap()) return nil + } else { + query = `UPDATE remote_instance SET festate = ? WHERE riid = ?` + ri.FeState = feState + err = updateRIWithState(tx.Context(), ri, stateBase, stateDiff) + if err != nil { + return err + } + tx.ExecWrap(query, quickJson(ri.FeState), ri.RIId) + return nil } - query = `UPDATE remote_instance SET state = ? WHERE riid = ?` - ri.State = state - tx.ExecWrap(query, quickJson(ri.State), ri.RIId) - return nil }) return ri, txErr } @@ -1255,3 +1316,102 @@ func UpdateSWsWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([] } return rtn, nil } + +func StoreStateBase(ctx context.Context, state *packet.ShellState) error { + stateBase := &StateBase{ + Version: state.Version, + Ts: time.Now().UnixMilli(), + } + stateBase.BaseHash, stateBase.Data = state.EncodeAndHash() + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT basehash FROM state_base WHERE basehash = ?` + if tx.Exists(query, stateBase.BaseHash) { + return nil + } + query = `INSERT INTO state_base (basehash, ts, version, data) VALUES (:basehash,:ts,:version,:data)` + tx.NamedExecWrap(query, stateBase) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +func StoreStateDiff(ctx context.Context, diff *packet.ShellStateDiff) error { + stateDiff := &StateDiff{ + BaseHash: diff.BaseHash, + Ts: time.Now().UnixMilli(), + DiffHashArr: diff.DiffHashArr, + } + stateDiff.DiffHash, stateDiff.Data = diff.EncodeAndHash() + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT basehash FROM state_base WHERE basehash = ?` + if stateDiff.BaseHash == "" || !tx.Exists(query, stateDiff.BaseHash) { + return fmt.Errorf("cannot store statediff, basehash:%s does not exist", stateDiff.BaseHash) + } + query = `SELECT diffhash FROM state_diff WHERE diffhash = ?` + for idx, diffHash := range stateDiff.DiffHashArr { + if !tx.Exists(query, diffHash) { + return fmt.Errorf("cannot store statediff, diffhash[%d]:%s does not exist", idx, diffHash) + } + } + if tx.Exists(query, stateDiff.DiffHash) { + return nil + } + query = `INSERT INTO state_diff (diffhash, ts, basehash, diffhasharr, data) VALUES (:diffhash,:ts,:basehash,:diffhasharr,:data)` + tx.NamedExecWrap(query, stateDiff.ToMap()) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +// returns error when not found +func GetFullState(ctx context.Context, baseHash string, diffHashArr []string) (*packet.ShellState, error) { + var state *packet.ShellState + if baseHash == "" { + return nil, fmt.Errorf("invalid empty basehash") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + var stateBase StateBase + query := `SELECT * FROM state_base WHERE basehash = ?` + found := tx.GetWrap(&stateBase, query, baseHash) + if !found { + return fmt.Errorf("ShellState %s not found", baseHash) + } + state = &packet.ShellState{} + err := state.DecodeShellState(stateBase.Data) + if err != nil { + return err + } + for idx, diffHash := range diffHashArr { + query = `SELECT * FROM state_diff WHERE diffhash = ?` + m := tx.GetMap(query, diffHash) + stateDiff := StateDiffFromMap(m) + if stateDiff == nil { + return fmt.Errorf("ShellStateDiff %s not found", diffHash) + } + var ssDiff packet.ShellStateDiff + err = ssDiff.DecodeShellStateDiff(stateDiff.Data) + if err != nil { + return err + } + newState, err := shexec.ApplyShellStateDiff(*state, ssDiff) + if err != nil { + return fmt.Errorf("GetFullState, diff[%d]:%s: %v", idx, diffHash, err) + } + state = &newState + } + return nil + }) + if txErr != nil { + return nil, txErr + } + if state == nil { + return nil, fmt.Errorf("ShellState not found") + } + return state, nil +} diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index df1882b76..e92c81f53 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -80,6 +80,21 @@ func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { json.Unmarshal([]byte(str), ptr) } +func quickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + if str == "" { + str = "[]" + } + json.Unmarshal([]byte(str), ptr) +} + func quickJson(v interface{}) string { if v == nil { return "{}" @@ -88,6 +103,14 @@ func quickJson(v interface{}) string { return string(barr) } +func quickJsonArr(v interface{}) string { + if v == nil { + return "[]" + } + barr, _ := json.Marshal(v) + return string(barr) +} + func quickScanJson(ptr interface{}, val interface{}) error { barrVal, ok := val.([]byte) if !ok { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 772d7d59a..a994795e8 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -170,6 +170,7 @@ type SessionStatsType struct { } type WindowOptsType struct { + PTerm string `json:"pterm,omitempty"` } func (opts *WindowOptsType) Scan(val interface{}) error { @@ -458,30 +459,70 @@ func (opts TermOpts) Value() (driver.Value, error) { } type RemoteInstance struct { - RIId string `json:"riid"` - Name string `json:"name"` - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - RemoteOwnerId string `json:"remoteownerid"` - RemoteId string `json:"remoteid"` - State packet.ShellState `json:"state"` + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + WindowId string `json:"windowid"` + RemoteOwnerId string `json:"remoteownerid"` + RemoteId string `json:"remoteid"` + FeState FeStateType `json:"festate"` + StateBaseHash string `json:"-"` + StateDiffHashArr []string `json:"-"` // only for updates Remove bool `json:"remove,omitempty"` } -func (ri *RemoteInstance) ToMap() map[string]interface{} { +type StateBase struct { + BaseHash string + Version string + Ts int64 + Data []byte +} + +type StateDiff struct { + DiffHash string + Ts int64 + BaseHash string + DiffHashArr []string + Data []byte +} + +func StateDiffFromMap(m map[string]interface{}) *StateDiff { + if len(m) == 0 { + return nil + } + var sd StateDiff + quickSetStr(&sd.DiffHash, m, "diffhash") + quickSetInt64(&sd.Ts, m, "ts") + quickSetStr(&sd.BaseHash, m, "basehash") + quickSetJsonArr(&sd.DiffHashArr, m, "diffhasharr") + quickSetBytes(&sd.Data, m, "data") + return &sd +} + +func (sd *StateDiff) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) - rtn["riid"] = ri.RIId - rtn["name"] = ri.Name - rtn["sessionid"] = ri.SessionId - rtn["windowid"] = ri.WindowId - rtn["remoteownerid"] = ri.RemoteOwnerId - rtn["remoteid"] = ri.RemoteId - rtn["state"] = quickJson(ri.State) + rtn["diffhash"] = sd.DiffHash + rtn["ts"] = sd.Ts + rtn["basehash"] = sd.BaseHash + rtn["diffhasharr"] = quickJsonArr(sd.DiffHashArr) + rtn["data"] = sd.Data return rtn } +type FeStateType struct { + Cwd string `json:"cwd"` + // maybe later we can add some vars +} + +func FeStateFromShellState(state *packet.ShellState) *FeStateType { + if state == nil { + return nil + } + return &FeStateType{Cwd: state.Cwd} +} + func RIFromMap(m map[string]interface{}) *RemoteInstance { if len(m) == 0 { return nil @@ -493,10 +534,26 @@ func RIFromMap(m map[string]interface{}) *RemoteInstance { quickSetStr(&ri.WindowId, m, "windowid") quickSetStr(&ri.RemoteOwnerId, m, "remoteownerid") quickSetStr(&ri.RemoteId, m, "remoteid") - quickSetJson(&ri.State, m, "state") + quickSetJson(&ri.FeState, m, "festate") + quickSetStr(&ri.StateBaseHash, m, "statebasehash") + quickSetJsonArr(&ri.StateDiffHashArr, m, "statediffhasharr") return &ri } +func (ri *RemoteInstance) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["riid"] = ri.RIId + rtn["name"] = ri.Name + rtn["sessionid"] = ri.SessionId + rtn["windowid"] = ri.WindowId + rtn["remoteownerid"] = ri.RemoteOwnerId + rtn["remoteid"] = ri.RemoteId + rtn["festate"] = quickJson(ri.FeState) + rtn["statebasehash"] = ri.StateBaseHash + rtn["statediffhasharr"] = quickJsonArr(ri.StateDiffHashArr) + return rtn +} + type LineType struct { SessionId string `json:"sessionid"` WindowId string `json:"windowid"` @@ -543,23 +600,22 @@ func (opts RemoteOptsType) Value() (driver.Value, error) { } type RemoteType struct { - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalid"` - RemoteType string `json:"remotetype"` - RemoteAlias string `json:"remotealias"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteSudo bool `json:"remotesudo"` - RemoteUser string `json:"remoteuser"` - RemoteHost string `json:"remotehost"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - InitPk *packet.InitPacketType `json:"inipk"` - SSHOpts *SSHOpts `json:"sshopts"` - RemoteOpts *RemoteOptsType `json:"remoteopts"` - LastConnectTs int64 `json:"lastconnectts"` - Archived bool `json:"archived"` - RemoteIdx int64 `json:"remoteidx"` - Local bool `json:"local"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalid"` + RemoteType string `json:"remotetype"` + RemoteAlias string `json:"remotealias"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteSudo bool `json:"remotesudo"` + RemoteUser string `json:"remoteuser"` + RemoteHost string `json:"remotehost"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + SSHOpts *SSHOpts `json:"sshopts"` + RemoteOpts *RemoteOptsType `json:"remoteopts"` + LastConnectTs int64 `json:"lastconnectts"` + Archived bool `json:"archived"` + RemoteIdx int64 `json:"remoteidx"` + Local bool `json:"local"` } func (r *RemoteType) GetName() string { @@ -597,7 +653,6 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["remotehost"] = r.RemoteHost rtn["connectmode"] = r.ConnectMode rtn["autoinstall"] = r.AutoInstall - rtn["initpk"] = quickJson(r.InitPk) rtn["sshopts"] = quickJson(r.SSHOpts) rtn["remoteopts"] = quickJson(r.RemoteOpts) rtn["lastconnectts"] = r.LastConnectTs @@ -622,7 +677,6 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetStr(&r.RemoteHost, m, "remotehost") quickSetStr(&r.ConnectMode, m, "connectmode") quickSetBool(&r.AutoInstall, m, "autoinstall") - quickSetJson(&r.InitPk, m, "initpk") quickSetJson(&r.SSHOpts, m, "sshopts") quickSetJson(&r.RemoteOpts, m, "remoteopts") quickSetInt64(&r.LastConnectTs, m, "lastconnectts") diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index e5b245f26..d74da80b2 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -1,6 +1,8 @@ package utilfn import ( + "crypto/sha1" + "encoding/base64" "regexp" "strings" "unicode/utf8" @@ -174,3 +176,10 @@ func (sp StrWithPos) Prepend(str string) StrWithPos { func (sp StrWithPos) Append(str string) StrWithPos { return StrWithPos{Str: sp.Str + str, Pos: sp.Pos} } + +// returns base64 hash of data +func Sha1Hash(data []byte) string { + hvalRaw := sha1.Sum(data) + hval := base64.StdEncoding.EncodeToString(hvalRaw[:]) + return hval +} From 61a07d40253db9d7eac62e4e5156987cf263a808 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 28 Nov 2022 12:01:03 -0800 Subject: [PATCH 202/397] fix down migration --- db/migrations/000001_init.down.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db/migrations/000001_init.down.sql b/db/migrations/000001_init.down.sql index 8896331f6..4892a38ed 100644 --- a/db/migrations/000001_init.down.sql +++ b/db/migrations/000001_init.down.sql @@ -8,3 +8,6 @@ DROP TABLE line; DROP TABLE remote; DROP TABLE cmd; DROP TABLE history; +DROP TABLE state_base; +DROP TABLE state_diff; + From c6a2118451258676ae601d6141579ebf27446646 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 28 Nov 2022 18:03:02 -0800 Subject: [PATCH 203/397] big update, got statediff and state_base working. updates to remote_instance and cmd tables/structures --- cmd/main-server.go | 11 +++ db/migrations/000001_init.up.sql | 8 ++- pkg/cmdrunner/cmdrunner.go | 50 +++++++++----- pkg/cmdrunner/resolver.go | 27 +++++--- pkg/cmdrunner/shparse.go | 2 +- pkg/cmdrunner/termopts.go | 110 +++++++++++++++++++++++++++++ pkg/comp/comp.go | 3 +- pkg/comp/simplecomp.go | 2 +- pkg/remote/remote.go | 114 ++++++++++++++++++++++--------- pkg/scpacket/scpacket.go | 3 +- pkg/shparse/extend.go | 2 +- pkg/sstore/dbops.go | 82 +++++++++++++++------- pkg/sstore/sstore.go | 40 +++++++++-- pkg/utilfn/utilfn.go | 10 +++ 14 files changed, 362 insertions(+), 102 deletions(-) create mode 100644 pkg/cmdrunner/termopts.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 77868f944..aa0191067 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -185,6 +185,17 @@ func HandleGetWindow(w http.ResponseWriter, r *http.Request) { } func HandleRtnState(w http.ResponseWriter, r *http.Request) { + defer func() { + r := recover() + if r == nil { + return + } + log.Printf("[error] in handlertnstate: %v\n", r) + debug.PrintStack() + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("panic: %v", r))) + return + }() qvals := r.URL.Query() sessionId := qvals.Get("sessionid") cmdId := qvals.Get("cmdid") diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 7f05513f6..b214f60ba 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -126,14 +126,18 @@ CREATE TABLE cmd ( remoteid varchar(36) NOT NULL, remotename varchar(50) NOT NULL, cmdstr text NOT NULL, - remotestate json NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, termopts json NOT NULL, origtermopts json NOT NULL, status varchar(10) NOT NULL, startpk json NOT NULL, - donepk json NOT NULL, + doneinfo json NOT NULL, runout json NOT NULL, rtnstate bool NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, PRIMARY KEY (sessionid, cmdid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 124b52c36..e516bd369 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -942,11 +942,14 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}, Status: sstore.CmdStatusDone, StartPk: nil, - DonePk: nil, + DoneInfo: nil, RunOut: nil, } - if ids.Remote.RemoteState != nil { - cmd.RemoteState = *ids.Remote.RemoteState + if ids.Remote.StatePtr != nil { + cmd.StatePtr = *ids.Remote.StatePtr + } + if ids.Remote.FeState != nil { + cmd.FeState = *ids.Remote.FeState } err := sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { @@ -1097,7 +1100,7 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix - cgPacket.Cwd = ids.Remote.RemoteState.Cwd + cgPacket.Cwd = ids.Remote.FeState.Cwd resp, err := ids.Remote.MShell.PacketRpc(ctx, cgPacket) if err != nil { return nil, false, err @@ -1110,8 +1113,6 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str return comps, hasMore, nil } -// func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) - func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, 0) // best-effort if err != nil { @@ -1138,7 +1139,9 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if ids.Remote != nil { rptr := ids.Remote.RemotePtr compCtx.RemotePtr = &rptr - compCtx.State = ids.Remote.RemoteState + if ids.Remote.FeState != nil { + compCtx.Cwd = ids.Remote.FeState.Cwd + } } compCtx.ForDisplay = showComps crtn, newSP, err := comp.DoCompGen(ctx, cmdSP, compCtx) @@ -1378,7 +1381,7 @@ func ResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/cd", false, ids, cmd) + update, err := addLineForCmd(ctx, "/reset", false, ids, cmd) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1557,8 +1560,8 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef())) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", cmd.Status)) - if cmd.RemoteState.Cwd != "" { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.RemoteState.Cwd)) + if cmd.FeState.Cwd != "" { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.FeState.Cwd)) } buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", formatTermOpts(cmd.TermOpts))) if cmd.TermOpts != cmd.OrigTermOpts { @@ -1670,6 +1673,9 @@ 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)) @@ -1684,13 +1690,13 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt if newVal.IsExport() { exportStr = "export " } - buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, utilfn.ShellQuote(newVal.Value, false, 50))) + 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", key)) + buf.WriteString(fmt.Sprintf("unset %s\n", utilfn.EllipsisStr(key, MaxDiffKeyLen))) } } } @@ -1700,13 +1706,13 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt for aliasName, newAliasVal := range newAliasMap { oldAliasVal, found := oldAliasMap[aliasName] if !found || newAliasVal != oldAliasVal { - buf.WriteString(fmt.Sprintf("alias %s\n", shellescape.Quote(aliasName))) + 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", shellescape.Quote(aliasName))) + buf.WriteString(fmt.Sprintf("unalias %s\n", utilfn.EllipsisStr(shellescape.Quote(aliasName), MaxDiffKeyLen))) } } } @@ -1716,13 +1722,13 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt for funcName, newFuncVal := range newFuncMap { oldFuncVal, found := oldFuncMap[funcName] if !found || newFuncVal != oldFuncVal { - buf.WriteString(fmt.Sprintf("function %s\n", shellescape.Quote(funcName))) + 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", shellescape.Quote(funcName))) + buf.WriteString(fmt.Sprintf("unset -f %s\n", utilfn.EllipsisStr(shellescape.Quote(funcName), MaxDiffKeyLen))) } } } @@ -1739,11 +1745,19 @@ func GetRtnStateDiff(ctx context.Context, sessionId string, cmdId string) ([]byt if !cmd.RtnState { return nil, nil } - if cmd.DonePk == nil || cmd.DonePk.FinalState == nil { + if cmd.RtnStatePtr.IsEmpty() { return nil, nil } var outputBytes bytes.Buffer - displayStateUpdateDiff(&outputBytes, cmd.RemoteState, *cmd.DonePk.FinalState) + 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/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 34fff2f90..b29337345 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -3,12 +3,12 @@ package cmdrunner import ( "context" "fmt" + "log" "regexp" "strconv" "strings" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -34,8 +34,9 @@ type ResolvedRemote struct { RemotePtr sstore.RemotePtrType MShell *remote.MShellProc RState remote.RemoteRuntimeState - RemoteState *packet.ShellState RemoteCopy *sstore.RemoteType + StatePtr *sstore.ShellStatePtr + FeState *sstore.FeStateType } type ResolveItem = sstore.ResolveItem @@ -264,7 +265,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if !rtn.Remote.RState.IsConnected() { return rtn, fmt.Errorf("remote '%s' is not connected", rtn.Remote.DisplayName) } - if rtn.Remote.RemoteState == nil { + if rtn.Remote.StatePtr == nil || rtn.Remote.FeState == nil { return rtn, fmt.Errorf("remote '%s' state is not available", rtn.Remote.DisplayName) } } @@ -453,20 +454,26 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi rtn := &ResolvedRemote{ DisplayName: displayName, RemotePtr: *rptr, - RemoteState: nil, RState: rstate, MShell: msh, RemoteCopy: &rcopy, + StatePtr: nil, + FeState: nil, } if sessionId != "" && windowId != "" { - state, err := sstore.GetRemoteState(ctx, sessionId, windowId, *rptr) + ri, err := sstore.GetRemoteInstance(ctx, sessionId, windowId, *rptr) if err != nil { - return nil, fmt.Errorf("cannot resolve remote state '%s': %w", displayName, err) + log.Printf("ERROR resolving remote state '%s': %v\n", displayName, err) + // continue with state set to nil + } else { + if ri == nil { + rtn.StatePtr = msh.GetDefaultStatePtr() + rtn.FeState = msh.GetDefaultFeState() + } else { + rtn.StatePtr = &sstore.ShellStatePtr{BaseHash: ri.StateBaseHash, DiffHashArr: ri.StateDiffHashArr} + rtn.FeState = &ri.FeState + } } - if state == nil { - state = msh.GetDefaultState() - } - rtn.RemoteState = state } return rtn, nil } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 3aa9a08c4..8ffe08f2f 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -171,7 +171,7 @@ func IsReturnStateCommand(cmdStr string) bool { if len(callExpr.Args) > 0 && len(callExpr.Args[0].Parts) > 0 { lit, ok := callExpr.Args[0].Parts[0].(*syntax.Lit) if ok { - if lit.Value == "." || lit.Value == "source" || lit.Value == "unset" || lit.Value == "cd" { + if lit.Value == "." || lit.Value == "source" || lit.Value == "unset" || lit.Value == "cd" || lit.Value == "alias" || lit.Value == "unalias" { return true } } diff --git a/pkg/cmdrunner/termopts.go b/pkg/cmdrunner/termopts.go new file mode 100644 index 000000000..34c35f59e --- /dev/null +++ b/pkg/cmdrunner/termopts.go @@ -0,0 +1,110 @@ +package cmdrunner + +import ( + "fmt" + "strconv" + "strings" + + "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/remote" +) + +// PTERM=WxH,Wx25 +// PTERM="Wx25!" +// PTERM=80x25,80x35 + +type PTermOptsType struct { + Rows string + RowsFlex bool + Cols string + ColsFlex bool +} + +const PTermMax = "M" + +func isDigits(s string) bool { + for _, ch := range s { + if ch < '0' || ch > '9' { + return false + } + } + return true +} + +func atoiDefault(s string, def int) int { + ival, err := strconv.Atoi(s) + if err != nil { + return def + } + return ival +} + +func parseTermPart(part string, partType string) (string, bool, error) { + flex := true + if strings.HasSuffix(part, "!") { + part = part[:len(part)-1] + flex = false + } + if part == "" { + return PTermMax, flex, nil + } + if part == PTermMax { + return PTermMax, flex, nil + } + if !isDigits(part) { + return "", false, fmt.Errorf("invalid PTERM %s: must be '%s' or [number]", partType, PTermMax) + } + return part, flex, nil +} + +func parseSingleTermStr(s string) (*PTermOptsType, error) { + s = strings.TrimSpace(s) + xIdx := strings.Index(s, "x") + if xIdx == -1 { + return nil, fmt.Errorf("invalid PTERM, must include 'x' to separate width and height (e.g. WxH)") + } + rowsPart := s[0:xIdx] + colsPart := s[xIdx+1:] + rows, rowsFlex, err := parseTermPart(rowsPart, "rows") + if err != nil { + return nil, err + } + cols, colsFlex, err := parseTermPart(colsPart, "cols") + if err != nil { + return nil, err + } + return &PTermOptsType{Rows: rows, RowsFlex: rowsFlex, Cols: cols, ColsFlex: colsFlex}, nil +} + +func GetUITermOpts(winSize *packet.WinSize, ptermStr string) (*packet.TermOpts, error) { + opts, err := parseSingleTermStr(ptermStr) + if err != nil { + return nil, err + } + termOpts := &packet.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, Term: remote.DefaultTerm, MaxPtySize: shexec.DefaultMaxPtySize} + if winSize == nil { + winSize = &packet.WinSize{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols} + } + if winSize.Rows == 0 { + winSize.Rows = shexec.DefaultTermRows + } + if winSize.Cols == 0 { + winSize.Cols = shexec.DefaultTermCols + } + if opts.Rows == PTermMax { + termOpts.Rows = winSize.Rows + } else { + termOpts.Rows = atoiDefault(opts.Rows, termOpts.Rows) + } + if opts.Cols == PTermMax { + termOpts.Cols = winSize.Cols + } else { + termOpts.Cols = atoiDefault(opts.Cols, termOpts.Cols) + } + termOpts.MaxPtySize = base.BoundInt64(termOpts.MaxPtySize, shexec.MinMaxPtySize, shexec.MaxMaxPtySize) + termOpts.Cols = base.BoundInt(termOpts.Cols, shexec.MinTermCols, shexec.MaxTermCols) + termOpts.Rows = base.BoundInt(termOpts.Rows, shexec.MinTermRows, shexec.MaxTermRows) + return termOpts, nil +} diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index e1fdf720d..245254799 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -11,7 +11,6 @@ import ( "unicode" "unicode/utf8" - "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/simpleexpand" "github.com/scripthaus-dev/sh2-server/pkg/shparse" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -46,7 +45,7 @@ const ( type CompContext struct { RemotePtr *sstore.RemotePtrType - State *packet.ShellState + Cwd string ForDisplay bool } diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index 5c83df37c..2a225113d 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -70,7 +70,7 @@ func doCompGen(ctx context.Context, prefix string, compType string, compCtx Comp cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix - cgPacket.Cwd = compCtx.State.Cwd + cgPacket.Cwd = compCtx.Cwd resp, err := msh.PacketRpc(ctx, cgPacket) if err != nil { return nil, err diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index e993b29fc..c5e8fd930 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -24,6 +24,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/mshell/pkg/statediff" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -150,6 +151,20 @@ func (msh *MShellProc) GetDefaultState() *packet.ShellState { return msh.StateMap[msh.CurrentState] } +func (msh *MShellProc) GetDefaultStatePtr() *sstore.ShellStatePtr { + msh.Lock.Lock() + defer msh.Lock.Unlock() + if msh.CurrentState == "" { + return nil + } + return &sstore.ShellStatePtr{BaseHash: msh.CurrentState} +} + +func (msh *MShellProc) GetDefaultFeState() *sstore.FeStateType { + state := msh.GetDefaultState() + return sstore.FeStateFromShellState(state) +} + func (msh *MShellProc) GetStateByHash(hval string) *packet.ShellState { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -926,6 +941,7 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro return nil, fmt.Errorf("invalid reinit response initpk does not contain remote state") } hval := initPk.State.GetHashVal(false) + sstore.StoreStateBase(ctx, initPk.State) msh.WithLock(func() { msh.CurrentState = hval msh.StateMap[hval] = initPk.State @@ -950,6 +966,7 @@ func stripScVarsFromState(state *packet.ShellState) *packet.ShellState { return nil } rtn := *state + rtn.HashVal = "" envMap := shexec.DeclMapFromState(&rtn) delete(envMap, "SCRIPTHAUS") delete(envMap, "SCRIPTHAUS_VERSION") @@ -957,6 +974,23 @@ func stripScVarsFromState(state *packet.ShellState) *packet.ShellState { return &rtn } +func stripScVarsFromStateDiff(stateDiff *packet.ShellStateDiff) *packet.ShellStateDiff { + if stateDiff == nil || len(stateDiff.VarsDiff) == 0 { + return stateDiff + } + rtn := *stateDiff + rtn.HashVal = "" + var mapDiff statediff.MapDiffType + err := mapDiff.Decode(stateDiff.VarsDiff) + if err != nil { + return stateDiff + } + delete(mapDiff.ToAdd, "SCRIPTHAUS") + delete(mapDiff.ToAdd, "SCRIPTHAUS_VERSION") + rtn.VarsDiff = mapDiff.Encode() + return &rtn +} + func (msh *MShellProc) Launch() { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { @@ -1214,16 +1248,20 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt } }() // get current remote-instance state - currentState, err := sstore.GetRemoteState(ctx, sessionId, windowId, remotePtr) + statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, windowId, remotePtr) if err != nil { + return nil, nil, fmt.Errorf("cannot get current remote stateptr: %w", err) + } + if statePtr == nil { + statePtr = msh.GetDefaultStatePtr() + } + if statePtr == nil { + return nil, nil, fmt.Errorf("cannot run command, no valid remote stateptr") + } + currentState, err := sstore.GetFullState(ctx, *statePtr) + if err != nil || currentState == nil { return nil, nil, fmt.Errorf("cannot get current remote state: %w", err) } - if currentState == nil { - currentState = msh.GetDefaultState() - } - if currentState == nil { - return nil, nil, fmt.Errorf("cannot run command, no valid remote state") - } runPacket.State = addScVarsToState(currentState) runPacket.StateComplete = true msh.ServerProc.Output.RegisterRpc(runPacket.ReqId) @@ -1250,19 +1288,19 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt if runPacket.Detached { status = sstore.CmdStatusDetached } - cmdState := stripScVarsFromState(runPacket.State) cmd := &sstore.CmdType{ - SessionId: runPacket.CK.GetSessionId(), - CmdId: runPacket.CK.GetCmdId(), - CmdStr: runPacket.Command, - Remote: remotePtr, - RemoteState: *cmdState, - TermOpts: makeTermOpts(runPacket), - Status: status, - StartPk: startPk, - DonePk: nil, - RunOut: nil, - RtnState: runPacket.ReturnState, + SessionId: runPacket.CK.GetSessionId(), + CmdId: runPacket.CK.GetCmdId(), + CmdStr: runPacket.Command, + Remote: remotePtr, + FeState: *sstore.FeStateFromShellState(currentState), + StatePtr: *statePtr, + TermOpts: makeTermOpts(runPacket), + Status: status, + StartPk: startPk, + DoneInfo: nil, + RunOut: nil, + RtnState: runPacket.ReturnState, } err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { @@ -1398,8 +1436,15 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { if donePk.FinalState != nil { donePk.FinalState = stripScVarsFromState(donePk.FinalState) } - - update, err := sstore.UpdateCmdDonePk(context.Background(), donePk) + if donePk.FinalStateDiff != nil { + donePk.FinalStateDiff = stripScVarsFromStateDiff(donePk.FinalStateDiff) + } + doneInfo := &sstore.CmdDoneInfo{ + Ts: donePk.Ts, + ExitCode: int64(donePk.ExitCode), + DurationMs: donePk.DurationMs, + } + update, err := sstore.UpdateCmdDoneInfo(context.Background(), donePk.CK, doneInfo) if err != nil { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return @@ -1411,8 +1456,8 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { } update.ScreenWindows = sws rct := msh.GetRunningCmd(donePk.CK) + var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { - fmt.Printf("** FINALSTATE!\n") feState := sstore.FeStateFromShellState(donePk.FinalState) remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, donePk.FinalState, nil) if err != nil { @@ -1422,20 +1467,13 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { if remoteInst != nil { update.Sessions = sstore.MakeSessionsUpdateForRemote(rct.SessionId, remoteInst) } + statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalState.GetHashVal(false)} } else if donePk.FinalStateDiff != nil && rct != nil { - fmt.Printf("** STATEDIFF! %#v\n", donePk.FinalStateDiff) - fullState, err := msh.getFullState(donePk.FinalStateDiff) - if err != nil { - fmt.Printf("**ERR: %v\n", err) - } - donePk.FinalStateDiff.Dump() - shexec.DumpVarMapFromState(fullState) feState, err := msh.getFeStateFromDiff(donePk.FinalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) } else { - fmt.Printf("** festate = %#v\n", feState) remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, nil, donePk.FinalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) @@ -1444,6 +1482,16 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { if remoteInst != nil { update.Sessions = sstore.MakeSessionsUpdateForRemote(rct.SessionId, remoteInst) } + diffHashArr := append(([]string)(nil), donePk.FinalStateDiff.DiffHashArr...) + diffHashArr = append(diffHashArr, donePk.FinalStateDiff.GetHashVal(false)) + statePtr = &sstore.ShellStatePtr{BaseHash: donePk.FinalStateDiff.BaseHash, DiffHashArr: diffHashArr} + } + } + if statePtr != nil { + err = sstore.UpdateCmdRtnState(context.Background(), donePk.CK, *statePtr) + if err != nil { + msh.WriteToPtyBuffer("*error trying to update cmd rtnstate: %v\n", err) + // fall-through (nothing to do) } } sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) @@ -1457,7 +1505,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err) return } - if rtnCmd == nil || rtnCmd.DonePk != nil { + if rtnCmd == nil || rtnCmd.DoneInfo != nil { return } log.Printf("finalpk %s (hangup): %s\n", finalPk.CK, finalPk.Error) @@ -1743,7 +1791,7 @@ func (msh *MShellProc) getFullState(stateDiff *packet.ShellStateDiff) (*packet.S } return &newState, nil } else { - fullState, err := sstore.GetFullState(context.Background(), stateDiff.BaseHash, stateDiff.DiffHashArr) + fullState, err := sstore.GetFullState(context.Background(), sstore.ShellStatePtr{stateDiff.BaseHash, stateDiff.DiffHashArr}) if err != nil { return nil, err } @@ -1762,7 +1810,7 @@ func (msh *MShellProc) getFeStateFromDiff(stateDiff *packet.ShellStateDiff) (*ss } return sstore.FeStateFromShellState(&newState), nil } else { - fullState, err := sstore.GetFullState(context.Background(), stateDiff.BaseHash, stateDiff.DiffHashArr) + fullState, err := sstore.GetFullState(context.Background(), sstore.ShellStatePtr{stateDiff.BaseHash, stateDiff.DiffHashArr}) if err != nil { return nil, err } diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 292cc85ea..7ea1bb396 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -54,8 +54,7 @@ type UIContextType struct { ScreenId string `json:"screenid"` WindowId string `json:"windowid"` Remote *sstore.RemotePtrType `json:"remote,omitempty"` - TermOpts *packet.TermOpts `json:"termopts,omitempty"` - WinSize *WinSize `json:"winsize,omitempty"` + WinSize *packet.WinSize `json:"winsize,omitempty"` } type FeInputPacketType struct { diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index e1641d5b5..487a3808a 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -15,7 +15,7 @@ func init() { noEscChars = make([]bool, 256) for ch := 0; ch < 256; ch++ { if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - ch == '-' || ch == '.' || ch == '/' || ch == ':' || ch == '=' { + ch == '-' || ch == '.' || ch == '/' || ch == ':' || ch == '=' || ch == '_' { noEscChars[byte(ch)] = true } } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f18dbc3ae..039887555 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -661,8 +661,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, remotestate, termopts, origtermopts, status, startpk, donepk, rtnstate, runout) - VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:remotestate,:termopts,:origtermopts,:status,:startpk,:donepk,:rtnstate,:runout) +INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExecWrap(query, cmdMap) } @@ -684,10 +684,10 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, return cmd, nil } -func HasDonePk(ctx context.Context, ck base.CommandKey) (bool, error) { +func HasDoneInfo(ctx context.Context, ck base.CommandKey) (bool, error) { var found bool txErr := WithTx(ctx, func(tx *TxWrap) error { - found = tx.Exists(`SELECT sessionid FROM cmd WHERE sessionid = ? AND cmdid = ? AND donepk is NOT NULL`, ck.GetSessionId(), ck.GetCmdId()) + found = tx.Exists(`SELECT sessionid FROM cmd WHERE sessionid = ? AND cmdid = ? AND doneinfo is NOT NULL`, ck.GetSessionId(), ck.GetCmdId()) return nil }) if txErr != nil { @@ -696,16 +696,19 @@ func HasDonePk(ctx context.Context, ck base.CommandKey) (bool, error) { return found, nil } -func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (*ModelUpdate, error) { - if donePk == nil || donePk.CK.IsEmpty() { - return nil, fmt.Errorf("invalid cmddone packet (no ck)") +func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDoneInfo) (*ModelUpdate, error) { + if doneInfo == nil { + return nil, fmt.Errorf("invalid cmddone packet") + } + if ck.IsEmpty() { + return nil, fmt.Errorf("cannot update cmddoneinfo, empty ck") } var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ?, donepk = ? WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, CmdStatusDone, quickJson(donePk), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, CmdStatusDone, quickJson(doneInfo), ck.GetSessionId(), ck.GetCmdId()) var err error - rtnCmd, err = GetCmdById(tx.Context(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + rtnCmd, err = GetCmdById(tx.Context(), ck.GetSessionId(), ck.GetCmdId()) if err != nil { return err } @@ -715,11 +718,26 @@ func UpdateCmdDonePk(ctx context.Context, donePk *packet.CmdDonePacketType) (*Mo return nil, txErr } if rtnCmd == nil { - return nil, fmt.Errorf("cmd data not found for ck[%s]", donePk.CK) + return nil, fmt.Errorf("cmd data not found for ck[%s]", ck) } return &ModelUpdate{Cmd: rtnCmd}, nil } +func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellStatePtr) error { + if ck.IsEmpty() { + return fmt.Errorf("cannot update cmdrtnstate, empty ck") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetSessionId(), ck.GetCmdId()) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) error { if errPk == nil || errPk.CK.IsEmpty() { return fmt.Errorf("invalid cmderror packet (no ck)") @@ -823,8 +841,23 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat return update, nil } -func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, error) { - var state *packet.ShellState +func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, *ShellStatePtr, error) { + ssptr, err := GetRemoteStatePtr(ctx, sessionId, windowId, remotePtr) + if err != nil { + return nil, nil, err + } + if ssptr == nil { + return nil, nil, nil + } + state, err := GetFullState(ctx, *ssptr) + if err != nil { + return nil, nil, err + } + return state, ssptr, err +} + +func GetRemoteStatePtr(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*ShellStatePtr, error) { + var ssptr *ShellStatePtr txErr := WithTx(ctx, func(tx *TxWrap) error { ri, err := GetRemoteInstance(tx.Context(), sessionId, windowId, remotePtr) if err != nil { @@ -833,16 +866,13 @@ func GetRemoteState(ctx context.Context, sessionId string, windowId string, remo if ri == nil { return nil } - state, err = GetFullState(tx.Context(), ri.StateBaseHash, ri.StateDiffHashArr) - if err != nil { - return err - } + ssptr = &ShellStatePtr{ri.StateBaseHash, ri.StateDiffHashArr} return nil }) if txErr != nil { return nil, txErr } - return state, nil + return ssptr, nil } func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error { @@ -882,6 +912,7 @@ func GetRemoteInstance(ctx context.Context, sessionId string, windowId string, r func updateRIWithState(ctx context.Context, ri *RemoteInstance, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) error { if stateBase != nil { ri.StateBaseHash = stateBase.GetHashVal(false) + ri.StateDiffHashArr = nil err := StoreStateBase(ctx, stateBase) if err != nil { return err @@ -897,7 +928,6 @@ func updateRIWithState(ctx context.Context, ri *RemoteInstance, stateBase *packe return nil } -// TODO - statediff func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, feState FeStateType, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) (*RemoteInstance, error) { if stateBase == nil && stateDiff == nil { return nil, fmt.Errorf("UpdateRemoteState, must set state or diff") @@ -936,13 +966,13 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r tx.NamedExecWrap(query, ri.ToMap()) return nil } else { - query = `UPDATE remote_instance SET festate = ? WHERE riid = ?` + query = `UPDATE remote_instance SET festate = ?, statebasehash = ?, statediffhasharr = ? WHERE riid = ?` ri.FeState = feState err = updateRIWithState(tx.Context(), ri, stateBase, stateDiff) if err != nil { return err } - tx.ExecWrap(query, quickJson(ri.FeState), ri.RIId) + tx.ExecWrap(query, quickJson(ri.FeState), ri.StateBaseHash, quickJsonArr(ri.StateDiffHashArr), ri.RIId) return nil } }) @@ -1370,24 +1400,24 @@ func StoreStateDiff(ctx context.Context, diff *packet.ShellStateDiff) error { } // returns error when not found -func GetFullState(ctx context.Context, baseHash string, diffHashArr []string) (*packet.ShellState, error) { +func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, error) { var state *packet.ShellState - if baseHash == "" { + if ssPtr.BaseHash == "" { return nil, fmt.Errorf("invalid empty basehash") } txErr := WithTx(ctx, func(tx *TxWrap) error { var stateBase StateBase query := `SELECT * FROM state_base WHERE basehash = ?` - found := tx.GetWrap(&stateBase, query, baseHash) + found := tx.GetWrap(&stateBase, query, ssPtr.BaseHash) if !found { - return fmt.Errorf("ShellState %s not found", baseHash) + return fmt.Errorf("ShellState %s not found", ssPtr.BaseHash) } state = &packet.ShellState{} err := state.DecodeShellState(stateBase.Data) if err != nil { return err } - for idx, diffHash := range diffHashArr { + for idx, diffHash := range ssPtr.DiffHashArr { query = `SELECT * FROM state_diff WHERE diffhash = ?` m := tx.GetMap(query, diffHash) stateDiff := StateDiffFromMap(m) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index a994795e8..688b1a932 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -458,6 +458,18 @@ func (opts TermOpts) Value() (driver.Value, error) { return quickValueJson(opts) } +type ShellStatePtr struct { + BaseHash string + DiffHashArr []string +} + +func (ssptr *ShellStatePtr) IsEmpty() bool { + if ssptr == nil || ssptr.BaseHash == "" { + return true + } + return false +} + type RemoteInstance struct { RIId string `json:"riid"` Name string `json:"name"` @@ -625,19 +637,27 @@ func (r *RemoteType) GetName() string { return r.RemoteCanonicalName } +type CmdDoneInfo struct { + Ts int64 `json:"ts"` + ExitCode int64 `json:"exitcode"` + DurationMs int64 `json:"durationms"` +} + type CmdType struct { SessionId string `json:"sessionid"` CmdId string `json:"cmdid"` Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` - RemoteState packet.ShellState `json:"remotestate"` + FeState FeStateType `json:"festate"` + StatePtr ShellStatePtr `json:"state"` TermOpts TermOpts `json:"termopts"` OrigTermOpts TermOpts `json:"origtermopts"` Status string `json:"status"` StartPk *packet.CmdStartPacketType `json:"startpk,omitempty"` - DonePk *packet.CmdDonePacketType `json:"donepk,omitempty"` + DoneInfo *CmdDoneInfo `json:"doneinfo,omitempty"` RunOut []packet.PacketType `json:"runout,omitempty"` RtnState bool `json:"rtnstate,omitempty"` + RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"` Remove bool `json:"remove,omitempty"` } @@ -694,14 +714,18 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["remoteid"] = cmd.Remote.RemoteId rtn["remotename"] = cmd.Remote.Name rtn["cmdstr"] = cmd.CmdStr - rtn["remotestate"] = quickJson(cmd.RemoteState) + rtn["festate"] = quickJson(cmd.FeState) + rtn["statebasehash"] = cmd.StatePtr.BaseHash + rtn["statediffhasharr"] = quickJsonArr(cmd.StatePtr.DiffHashArr) rtn["termopts"] = quickJson(cmd.TermOpts) rtn["origtermopts"] = quickJson(cmd.OrigTermOpts) rtn["status"] = cmd.Status rtn["startpk"] = quickJson(cmd.StartPk) - rtn["donepk"] = quickJson(cmd.DonePk) + rtn["doneinfo"] = quickJson(cmd.DoneInfo) rtn["runout"] = quickJson(cmd.RunOut) rtn["rtnstate"] = cmd.RtnState + rtn["rtnbasehash"] = cmd.RtnStatePtr.BaseHash + rtn["rtndiffhasharr"] = quickJsonArr(cmd.RtnStatePtr.DiffHashArr) return rtn } @@ -716,14 +740,18 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") quickSetStr(&cmd.Remote.Name, m, "remotename") quickSetStr(&cmd.CmdStr, m, "cmdstr") - quickSetJson(&cmd.RemoteState, m, "remotestate") + quickSetJson(&cmd.FeState, m, "festate") + quickSetStr(&cmd.StatePtr.BaseHash, m, "statebasehash") + quickSetJsonArr(&cmd.StatePtr.DiffHashArr, m, "statediffhasharr") quickSetJson(&cmd.TermOpts, m, "termopts") quickSetJson(&cmd.OrigTermOpts, m, "origtermopts") quickSetStr(&cmd.Status, m, "status") quickSetJson(&cmd.StartPk, m, "startpk") - quickSetJson(&cmd.DonePk, m, "donepk") + quickSetJson(&cmd.DoneInfo, m, "doneinfo") quickSetJson(&cmd.RunOut, m, "runout") quickSetBool(&cmd.RtnState, m, "rtnstate") + quickSetStr(&cmd.RtnStatePtr.BaseHash, m, "rtnbasehash") + quickSetJsonArr(&cmd.RtnStatePtr.DiffHashArr, m, "rtndiffhasharr") return &cmd } diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index d74da80b2..0decc6eb7 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -84,6 +84,16 @@ func ShellQuote(val string, forceQuote bool, maxLen int) string { } } +func EllipsisStr(s string, maxLen int) string { + if maxLen < 4 { + maxLen = 4 + } + if len(s) > maxLen { + return s[0:maxLen-3] + "..." + } + return s +} + func LongestPrefix(root string, strs []string) string { if len(strs) == 0 { return root From 81554e8fd2370bdd989061150e17c0496659328a Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 29 Nov 2022 12:45:19 -0800 Subject: [PATCH 204/397] fix concurrent write on websocket (only happened under intense write stress) --- pkg/wsshell/wsshell.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/wsshell/wsshell.go b/pkg/wsshell/wsshell.go index 3bffac1fe..d6fe69e7c 100644 --- a/pkg/wsshell/wsshell.go +++ b/pkg/wsshell/wsshell.go @@ -78,15 +78,12 @@ func (ws *WSShell) WriteJson(val interface{}) error { } func (ws *WSShell) WritePump() { - ticker := time.NewTicker(pingPeriodTickTime) + ticker := time.NewTicker(initialPingTime) defer func() { ticker.Stop() ws.Conn.Close() }() - go func() { - time.Sleep(initialPingTime) - ws.WritePing() - }() + initialPing := true for { select { case <-ticker.C: @@ -95,6 +92,10 @@ func (ws *WSShell) WritePump() { log.Printf("WritePump %s err: %v\n", ws.RemoteAddr, err) return } + if initialPing { + initialPing = false + ticker.Reset(pingPeriodTickTime) + } case msgBytes, ok := <-ws.WriteChan: if !ok { From 8624e9c844860f6cdbbd22db3557b6fb7c80a42e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 5 Dec 2022 22:59:00 -0800 Subject: [PATCH 205/397] add line staring to schema and to cmdrunner --- db/migrations/000001_init.up.sql | 1 + db/schema.sql | 9 +++++-- pkg/cmdrunner/cmdrunner.go | 42 ++++++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 36 ++++++++++++++++++++++++--- pkg/sstore/sstore.go | 1 + 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index b214f60ba..d4d65ed45 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -97,6 +97,7 @@ CREATE TABLE line ( cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, contentheight int NOT NULL, + star int NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); diff --git a/db/schema.sql b/db/schema.sql index 34d8bbcc4..b35e347b1 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -91,6 +91,7 @@ CREATE TABLE line ( cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, contentheight int NOT NULL, + star int NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -118,14 +119,18 @@ CREATE TABLE cmd ( remoteid varchar(36) NOT NULL, remotename varchar(50) NOT NULL, cmdstr text NOT NULL, - remotestate json NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, termopts json NOT NULL, origtermopts json NOT NULL, status varchar(10) NOT NULL, startpk json NOT NULL, - donepk json NOT NULL, + doneinfo json NOT NULL, runout json NOT NULL, rtnstate bool NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e516bd369..00db35cb0 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -141,6 +141,7 @@ func init() { registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) + registerCmdFn("line:star", LineStarCommand) registerCmdFn("history", HistoryCommand) @@ -1520,6 +1521,47 @@ func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } +func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/line:star requires an argument (line number or id)") + } + if len(pk.Args) > 2 { + return nil, fmt.Errorf("/line:star only takes up to 2 arguments (line-number and star-value)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + starVal, err := resolveNonNegInt(pk.Args[1], 1) + if err != nil { + return nil, fmt.Errorf("/line:star invalid star-value (not integer): %v", err) + } + if starVal > 5 { + return nil, fmt.Errorf("/line:star invalid star-value must be in the range of 0-5") + } + err = sstore.UpdateLineStar(ctx, lineId, starVal) + if err != nil { + return nil, fmt.Errorf("/line:star error updating star value: %v", err) + } + lineObj, err := sstore.GetLineById(ctx, lineId) + if err != nil { + return nil, fmt.Errorf("/line:star error getting line: %v", err) + } + if lineObj == nil { + // no line (which is strange given we checked for it above). just return a nop. + return nil, nil + } + return sstore.ModelUpdate{Line: lineObj}, nil +} + func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 039887555..d8af3c302 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -569,7 +569,7 @@ func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lin lineId = tx.GetString(query, sessionId, windowId, lineArg) } else { // id match - query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` + query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` lineId = tx.GetString(query, sessionId, windowId, lineArg) } return nil @@ -652,8 +652,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star)` tx.NamedExecWrap(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) @@ -1445,3 +1445,33 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, } return state, nil } + +func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET star = ? WHERE lineid = ?` + tx.ExecWrap(query, starVal, lineId) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +// can return nil, nil if line is not found +func GetLineById(ctx context.Context, lineId string) (*LineType, error) { + var rtn *LineType + txErr := WithTx(ctx, func(tx *TxWrap) error { + var line LineType + query := `SELECT * FROM line WHERE lineid = ?` + found := tx.GetWrap(&line, query, lineId) + if found { + rtn = &line + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 688b1a932..3cf6a7f05 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -579,6 +579,7 @@ type LineType struct { Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` + Star bool `json:"star,omitempty"` Remove bool `json:"remove,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` } From 35006dfc65faceea63d5fcfff84c0818f4f5132e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 19 Dec 2022 17:36:19 -0800 Subject: [PATCH 206/397] rename sh2/scripthaus to prompt. maxcommandlen set. --- cmd/main-server.go | 4 ++-- pkg/cmdrunner/cmdrunner.go | 18 +++++++++++------- pkg/remote/remote.go | 16 ++++++++-------- pkg/scbase/scbase.go | 26 +++++++++++++------------- pkg/sstore/sstore.go | 4 ++-- scripthaus.md | 6 +++--- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index aa0191067..43a6d20cb 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -400,8 +400,8 @@ func main() { return } - scHomeDir := scbase.GetScHomeDir() - log.Printf("[scripthaus] homedir = %q\n", scHomeDir) + scHomeDir := scbase.GetPromptHomeDir() + log.Printf("[prompt] homedir = %q\n", scHomeDir) scLock, err := scbase.AcquireSCLock() if err != nil || scLock == nil { diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 00db35cb0..210994357 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -42,14 +42,15 @@ const MaxNameLen = 50 const MaxRemoteAliasLen = 50 const PasswordUnchangedSentinel = "--unchanged--" const DefaultPTERM = "MxM" +const MaxCommandLen = 4096 var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "alias", "unalias", "function", "reset"} -var NoHistCmds = []string{"_compgen", "line", "history"} -var GlobalCmds = []string{"session", "screen", "remote", "killserver", "set"} +var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset"} +var NoHistCmds = []string{"_compgen", "line", "_history", "_killserver"} +var GlobalCmds = []string{"session", "screen", "remote", "set"} var SetVarNameMap map[string]string = map[string]string{ "tabcolor": "screen.tabcolor", @@ -108,7 +109,7 @@ func init() { registerCmdFn("cr", CrCommand) registerCmdFn("_compgen", CompGenCommand) registerCmdFn("clear", ClearCommand) - registerCmdFn("reset", ResetCommand) + registerCmdFn("reset", RemoteResetCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -143,9 +144,9 @@ func init() { registerCmdFn("line:show", LineShowCommand) registerCmdFn("line:star", LineStarCommand) - registerCmdFn("history", HistoryCommand) + registerCmdFn("_history", HistoryCommand) - registerCmdFn("killserver", KillServerCommand) + registerCmdFn("_killserver", KillServerCommand) registerCmdFn("set", SetCommand) } @@ -332,6 +333,9 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") } + if len(pk.Args[0]) > MaxCommandLen { + return nil, fmt.Errorf("command length too long len:%d, max:%d", len(pk.Args[0]), MaxCommandLen) + } var historyContext historyContextType ctxWithHistory := context.WithValue(ctx, historyContextKey, &historyContext) var update sstore.UpdatePacket @@ -1359,7 +1363,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return update, nil } -func ResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, err diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c5e8fd930..c2eee2fdb 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -746,9 +746,9 @@ func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{ realStr = realStr + "\r\n" } if strings.HasPrefix(realStr, "*") { - realStr = "\033[0m\033[31mscripthaus>\033[0m " + realStr[1:] + realStr = "\033[0m\033[31mprompt>\033[0m " + realStr[1:] } else { - realStr = "\033[0m\033[32mscripthaus>\033[0m " + realStr + realStr = "\033[0m\033[32mprompt>\033[0m " + realStr } barr := msh.PtyBuffer.Bytes() if len(barr) > 0 && barr[len(barr)-1] != '\n' { @@ -955,8 +955,8 @@ func addScVarsToState(state *packet.ShellState) *packet.ShellState { } rtn := *state envMap := shexec.DeclMapFromState(&rtn) - envMap["SCRIPTHAUS"] = &shexec.DeclareDeclType{Name: "SCRIPTHAUS", Value: "1"} - envMap["SCRIPTHAUS_VERSION"] = &shexec.DeclareDeclType{Name: "SCRIPTHAUS_VERSION", Value: scbase.ScriptHausVersion} + envMap["PROMPT"] = &shexec.DeclareDeclType{Name: "PROMPT", Value: "1"} + envMap["PROMPT_VERSION"] = &shexec.DeclareDeclType{Name: "PROMPT_VERSION", Value: scbase.PromptVersion} rtn.ShellVars = shexec.SerializeDeclMap(envMap) return &rtn } @@ -968,8 +968,8 @@ func stripScVarsFromState(state *packet.ShellState) *packet.ShellState { rtn := *state rtn.HashVal = "" envMap := shexec.DeclMapFromState(&rtn) - delete(envMap, "SCRIPTHAUS") - delete(envMap, "SCRIPTHAUS_VERSION") + delete(envMap, "PROMPT") + delete(envMap, "PROMPT_VERSION") rtn.ShellVars = shexec.SerializeDeclMap(envMap) return &rtn } @@ -985,8 +985,8 @@ func stripScVarsFromStateDiff(stateDiff *packet.ShellStateDiff) *packet.ShellSta if err != nil { return stateDiff } - delete(mapDiff.ToAdd, "SCRIPTHAUS") - delete(mapDiff.ToAdd, "SCRIPTHAUS_VERSION") + delete(mapDiff.ToAdd, "PROMPT") + delete(mapDiff.ToAdd, "PROMPT_VERSION") rtn.VarsDiff = mapDiff.Encode() return &rtn } diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 13004c9b3..a673655fb 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -18,31 +18,31 @@ import ( ) const HomeVarName = "HOME" -const ScHomeVarName = "SCRIPTHAUS_HOME" +const PromptHomeVarName = "PROMPT_HOME" const SessionsDirBaseName = "sessions" -const SCLockFile = "sh2.lock" -const ScriptHausDirName = "scripthaus" -const ScriptHausAppPathVarName = "SCRIPTHAUS_APP_PATH" -const ScriptHausVersion = "v0.1.0" +const SCLockFile = "prompt.lock" +const PromptDirName = "prompt" +const PromptAppPathVarName = "PROMPT_APP_PATH" +const PromptVersion = "v0.1.0" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} // must match js -func GetScHomeDir() string { - scHome := os.Getenv(ScHomeVarName) +func GetPromptHomeDir() string { + scHome := os.Getenv(PromptHomeVarName) if scHome == "" { homeVar := os.Getenv(HomeVarName) if homeVar == "" { homeVar = "/" } - scHome = path.Join(homeVar, ScriptHausDirName) + scHome = path.Join(homeVar, PromptDirName) } return scHome } func MShellBinaryFromPackage(version string, goos string, goarch string) (io.ReadCloser, error) { - appPath := os.Getenv(ScriptHausAppPathVarName) + appPath := os.Getenv(PromptAppPathVarName) if appPath == "" { return base.MShellBinaryFromOptDir(version, goos, goarch) } @@ -64,10 +64,10 @@ func MShellBinaryFromPackage(version string, goos string, goarch string) (io.Rea } func AcquireSCLock() (*os.File, error) { - homeDir := GetScHomeDir() + homeDir := GetPromptHomeDir() err := ensureDir(homeDir) if err != nil { - return nil, fmt.Errorf("cannot find/create SCRIPTHAUS_HOME directory %q", homeDir) + return nil, fmt.Errorf("cannot find/create PROMPT_HOME directory %q", homeDir) } lockFileName := path.Join(homeDir, SCLockFile) fd, err := os.Create(lockFileName) @@ -92,7 +92,7 @@ func EnsureSessionDir(sessionId string) (string, error) { if ok { return sdir, nil } - scHome := GetScHomeDir() + scHome := GetPromptHomeDir() sdir = path.Join(scHome, SessionsDirBaseName, sessionId) err := ensureDir(sdir) if err != nil { @@ -111,7 +111,7 @@ func ensureDir(dirName string) error { if err != nil { return err } - log.Printf("[scripthaus] created directory %q\n", dirName) + log.Printf("[prompt] created directory %q\n", dirName) info, err = os.Stat(dirName) } if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 3cf6a7f05..bc76fb985 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -29,7 +29,7 @@ import ( const LineTypeCmd = "cmd" const LineTypeText = "text" const LineNoHeight = -1 -const DBFileName = "sh2.db" +const DBFileName = "prompt.db" const DefaultSessionName = "default" const DefaultWindowName = "default" @@ -75,7 +75,7 @@ var globalDB *sqlx.DB var globalDBErr error func GetSessionDBName() string { - scHome := scbase.GetScHomeDir() + scHome := scbase.GetPromptHomeDir() return path.Join(scHome, DBFileName) } diff --git a/scripthaus.md b/scripthaus.md index a7f1964d3..563af253d 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -2,15 +2,15 @@ ```bash # @scripthaus command dump-schema -sqlite3 /Users/mike/scripthaus/sh2.db .schema > db/schema.sql +sqlite3 /Users/mike/prompt/prompt.db .schema > db/schema.sql ``` ```bash # @scripthaus command opendb -sqlite3 /Users/mike/scripthaus/sh2.db +sqlite3 /Users/mike/prompt/prompt.db ``` ```bash # @scripthaus command build -go build -o ~/scripthaus/local-server cmd/main-server.go +go build -o ~/prompt/local-server cmd/main-server.go ``` From 2684aaef8543656eb87e410e395c4d5a8d123b50 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 19 Dec 2022 17:39:19 -0800 Subject: [PATCH 207/397] change fn prefix that mshell uses to mshell --- pkg/cmdrunner/shparse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 8ffe08f2f..f6dfbabe5 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -346,7 +346,7 @@ func ParseFuncs(funcs string) (map[string]string, error) { // TODO where to put parse errors continue } - if strings.HasPrefix(funcName, "_scripthaus_") { + if strings.HasPrefix(funcName, "_mshell_") { continue } if funcName != "" { From 21bbab88c890baacc56e6c9b22847da7a74303d0 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 19 Dec 2022 18:52:08 -0800 Subject: [PATCH 208/397] authkey get/generate. new schema for incognito/closed screens/sessions. rename scbase functions to use Prompt name --- cmd/main-server.go | 2 +- db/migrations/000001_init.up.sql | 8 +++-- db/schema.sql | 8 +++-- pkg/cmdrunner/cmdrunner.go | 13 ++++--- pkg/scbase/scbase.go | 62 ++++++++++++++++++++++++++------ pkg/sstore/dbops.go | 29 ++++++++++----- pkg/sstore/sstore.go | 12 +++++-- 7 files changed, 103 insertions(+), 31 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 43a6d20cb..79309fc54 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -403,7 +403,7 @@ func main() { scHomeDir := scbase.GetPromptHomeDir() log.Printf("[prompt] homedir = %q\n", scHomeDir) - scLock, err := scbase.AcquireSCLock() + scLock, err := scbase.AcquirePromptLock() if err != nil || scLock == nil { log.Printf("[error] cannot acquire sh2 lock: %v\n", err) return diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index d4d65ed45..aa0033375 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -13,6 +13,7 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, + closed boolean NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -41,6 +42,8 @@ CREATE TABLE screen ( screenopts json NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, + incognito boolean NOT NULL, + closed boolean NOT NULL, PRIMARY KEY (sessionid, screenid) ); @@ -136,7 +139,7 @@ CREATE TABLE cmd ( startpk json NOT NULL, doneinfo json NOT NULL, runout json NOT NULL, - rtnstate bool NOT NULL, + rtnstate boolean NOT NULL, rtnbasehash varchar(36) NOT NULL, rtndiffhasharr json NOT NULL, PRIMARY KEY (sessionid, cmdid) @@ -156,5 +159,6 @@ CREATE TABLE history ( haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, cmdstr text NOT NULL, - ismetacmd boolean + ismetacmd boolean, + incognito boolean ); diff --git a/db/schema.sql b/db/schema.sql index b35e347b1..4f2f532c6 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -14,6 +14,7 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, + closed boolean NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -40,6 +41,8 @@ CREATE TABLE screen ( screenopts json NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, + incognito boolean NOT NULL, + closed boolean NOT NULL, PRIMARY KEY (sessionid, screenid) ); CREATE TABLE screen_window ( @@ -128,7 +131,7 @@ CREATE TABLE cmd ( startpk json NOT NULL, doneinfo json NOT NULL, runout json NOT NULL, - rtnstate bool NOT NULL, + rtnstate boolean NOT NULL, rtnbasehash varchar(36) NOT NULL, rtndiffhasharr json NOT NULL, PRIMARY KEY (sessionid, cmdid) @@ -147,5 +150,6 @@ CREATE TABLE history ( haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, cmdstr text NOT NULL, - ismetacmd boolean + ismetacmd boolean, + incognito boolean ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 210994357..a9d4d750c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -275,7 +275,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U // runPacket.State is set in remote.RunCommand() runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() - runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenSCUUID()) + runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenPromptUUID()) runPacket.UsePty = true ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) @@ -306,8 +306,12 @@ 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.GenSCUUID(), + HistoryId: scbase.GenPromptUUID(), Ts: time.Now().UnixMilli(), UserId: DefaultUserId, SessionId: ids.SessionId, @@ -318,6 +322,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history CmdId: historyContext.CmdId, CmdStr: cmdStr, IsMetaCmd: isMetaCmd, + Incognito: isIncognito, } if !isMetaCmd && historyContext.RemotePtr != nil { hitem.Remote = *historyContext.RemotePtr @@ -785,7 +790,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("/remote:new %v", err)) } r := &sstore.RemoteType{ - RemoteId: scbase.GenSCUUID(), + RemoteId: scbase.GenPromptUUID(), PhysicalId: "", RemoteType: sstore.RemoteTypeSsh, RemoteAlias: editArgs.Alias, @@ -941,7 +946,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ SessionId: ids.SessionId, - CmdId: scbase.GenSCUUID(), + CmdId: scbase.GenPromptUUID(), CmdStr: cmdStr, Remote: ids.Remote.RemotePtr, TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}, diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index a673655fb..8199654b9 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -20,10 +20,11 @@ import ( const HomeVarName = "HOME" const PromptHomeVarName = "PROMPT_HOME" const SessionsDirBaseName = "sessions" -const SCLockFile = "prompt.lock" +const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptAppPathVarName = "PROMPT_APP_PATH" const PromptVersion = "v0.1.0" +const PromptAuthKeyFileName = "prompt.authkey" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} @@ -63,13 +64,54 @@ func MShellBinaryFromPackage(version string, goos string, goarch string) (io.Rea return fd, nil } -func AcquireSCLock() (*os.File, error) { +func createPromptAuthKeyFile(fileName string) (string, error) { + fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return "", err + } + defer fd.Close() + keyStr := GenPromptUUID() + _, err = fd.Write([]byte(keyStr)) + if err != nil { + return "", err + } + return keyStr, nil +} + +func ReadPromptAuthKey() (string, error) { + homeDir := GetPromptHomeDir() + err := ensureDir(homeDir) + if err != nil { + return "", fmt.Errorf("cannot find/create PROMPT_HOME directory %q", homeDir) + } + fileName := path.Join(homeDir, PromptAuthKeyFileName) + fd, err := os.Open(fileName) + if err != nil && errors.Is(err, fs.ErrNotExist) { + return createPromptAuthKeyFile(fileName) + } + if err != nil { + return "", fmt.Errorf("error opening prompt authkey:%s: %v", fileName, err) + } + defer fd.Close() + buf, err := io.ReadAll(fd) + if err != nil { + return "", fmt.Errorf("error reading prompt authkey:%s: %v", fileName, err) + } + keyStr := string(buf) + _, err = uuid.Parse(keyStr) + if err != nil { + return "", fmt.Errorf("invalid authkey:%s format: %v", fileName, err) + } + return keyStr, nil +} + +func AcquirePromptLock() (*os.File, error) { homeDir := GetPromptHomeDir() err := ensureDir(homeDir) if err != nil { return nil, fmt.Errorf("cannot find/create PROMPT_HOME directory %q", homeDir) } - lockFileName := path.Join(homeDir, SCLockFile) + lockFileName := path.Join(homeDir, PromptLockFile) fd, err := os.Create(lockFileName) if err != nil { return nil, err @@ -151,19 +193,19 @@ func RunOutFile(sessionId string, cmdId string) (string, error) { return fmt.Sprintf("%s/%s.runout", sdir, cmdId), nil } -type ScFileNameGenerator struct { - ScHome string +type PromptFileNameGenerator struct { + PromptHome string } -func (g ScFileNameGenerator) PtyOutFile(ck base.CommandKey) string { - return path.Join(g.ScHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".ptyout") +func (g PromptFileNameGenerator) PtyOutFile(ck base.CommandKey) string { + return path.Join(g.PromptHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".ptyout") } -func (g ScFileNameGenerator) RunOutFile(ck base.CommandKey) string { - return path.Join(g.ScHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".runout") +func (g PromptFileNameGenerator) RunOutFile(ck base.CommandKey) string { + return path.Join(g.PromptHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".runout") } -func GenSCUUID() string { +func GenPromptUUID() string { for { rtn := uuid.New().String() _, err := strconv.Atoi(rtn[0:8]) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d8af3c302..471eafa61 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -14,7 +14,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd" +const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito" const DefaultMaxHistoryItems = 1000 func NumSessions(ctx context.Context) (int, error) { @@ -157,14 +157,24 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { } txErr := WithTx(ctx, func(tx *TxWrap) error { query := `INSERT INTO history - ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd)` + ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito)` tx.NamedExecWrap(query, hitem.ToMap()) return nil }) return txErr } +func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) (bool, error) { + var rtn bool + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT incognito FROM screen WHERE sessionid = ? AND screenid = ?` + tx.GetWrap(&rtn, query, sessionId, screenId) + return nil + }) + return rtn, txErr +} + func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { // check sessionid/windowid format because we are directly inserting them into the SQL if sessionId != "" { @@ -390,12 +400,13 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { // also creates default window, returns sessionId // if sessionName == "", it will be generated func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (UpdatePacket, error) { - newSessionId := scbase.GenSCUUID() + newSessionId := scbase.GenPromptUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) - query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, ownerid, sharemode, accesskey) VALUES (?, ?, '', ?, ?, '', 'local', '')` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, closed, ownerid, sharemode, accesskey) + VALUES (?, ?, '', ?, ?, 0, '', 'local', '')` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { @@ -493,8 +504,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) - newScreenId = scbase.GenSCUUID() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode) VALUES (?, ?, ?, ?, ?, ?, '', 'local')` + newScreenId = scbase.GenPromptUUID() + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, closed) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0)` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` @@ -541,7 +552,7 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) string { w := &WindowType{ SessionId: sessionId, - WindowId: scbase.GenSCUUID(), + WindowId: scbase.GenPromptUUID(), CurRemote: curRemote, NextLineNum: 1, WinOpts: WindowOptsType{}, @@ -949,7 +960,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r ri = RIFromMap(m) if ri == nil { ri = &RemoteInstance{ - RIId: scbase.GenSCUUID(), + RIId: scbase.GenPromptUUID(), Name: remotePtr.Name, SessionId: sessionId, WindowId: windowId, diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index bc76fb985..efa2636f8 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -152,6 +152,7 @@ type SessionType struct { ShareMode string `json:"sharemode"` AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` + Closed bool `json:"closed,omitempty"` Screens []*ScreenType `json:"screens"` Remotes []*RemoteInstance `json:"remotes"` @@ -305,6 +306,7 @@ 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 return rtn } @@ -328,6 +330,7 @@ func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { quickSetStr(&h.Remote.Name, m, "remotename") quickSetBool(&h.IsMetaCmd, m, "ismetacmd") quickSetStr(&h.HistoryNum, m, "historynum") + quickSetBool(&h.Incognito, m, "incognito") return &h } @@ -352,6 +355,8 @@ type ScreenType struct { ScreenOpts *ScreenOptsType `json:"screenopts"` OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` + Incognito bool `json:"incognito,omitempty"` + Closed bool `json:"closed,omitempty"` Windows []*ScreenWindowType `json:"windows"` // only for updates @@ -430,6 +435,7 @@ type HistoryItemType struct { CmdStr string `json:"cmdstr"` Remote RemotePtrType `json:"remote"` IsMetaCmd bool `json:"ismetacmd"` + Incognito bool `json:"incognito,omitempty"` // only for updates Remove bool `json:"remove"` @@ -761,7 +767,7 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn.SessionId = sessionId rtn.WindowId = windowId rtn.UserId = userId - rtn.LineId = scbase.GenSCUUID() + rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeCmd @@ -775,7 +781,7 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri rtn.SessionId = sessionId rtn.WindowId = windowId rtn.UserId = userId - rtn.LineId = scbase.GenSCUUID() + rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeText @@ -824,7 +830,7 @@ func EnsureLocalRemote(ctx context.Context) error { } // create the local remote localRemote := &RemoteType{ - RemoteId: scbase.GenSCUUID(), + RemoteId: scbase.GenPromptUUID(), PhysicalId: physicalId, RemoteType: RemoteTypeSsh, RemoteAlias: LocalRemoteAlias, From 1697010d55daf0b953c65f46d852bfdde01254d7 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Dec 2022 16:16:46 -0800 Subject: [PATCH 209/397] handle simple authkey authentication for local-server --- cmd/main-server.go | 79 ++++++++++++++++++---------------------- pkg/scpacket/scpacket.go | 1 + pkg/scws/scws.go | 61 +++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 59 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 79309fc54..b6c27b0e1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -28,6 +28,8 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/wsshell" ) +type WebFnType = func(http.ResponseWriter, *http.Request) + const HttpReadTimeout = 5 * time.Second const HttpWriteTimeout = 21 * time.Second const HttpMaxHeaderBytes = 60000 @@ -40,6 +42,7 @@ const WSStatePacketChSize = 20 var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState +var GlobalAuthKey string func setWSState(state *scws.WSState) { GlobalLock.Lock() @@ -81,7 +84,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) { } state := getWSState(clientId) if state == nil { - state = scws.MakeWSState(clientId) + state = scws.MakeWSState(clientId, GlobalAuthKey) state.ReplaceShell(shell) setWSState(state) } else { @@ -119,10 +122,6 @@ func writeToFifo(fifoName string, data []byte) error { } func HandleGetClientData(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") cdata, err := sstore.EnsureClientData(r.Context()) if err != nil { WriteJsonError(w, err) @@ -133,15 +132,6 @@ func HandleGetClientData(w http.ResponseWriter, r *http.Request) { } func HandleSetWinSize(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") - if r.Method == "GET" || r.Method == "OPTIONS" { - w.WriteHeader(200) - return - } decoder := json.NewDecoder(r.Body) var winSize sstore.ClientWinSizeType err := decoder.Decode(&winSize) @@ -160,10 +150,6 @@ func HandleSetWinSize(w http.ResponseWriter, r *http.Request) { // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") qvals := r.URL.Query() sessionId := qvals.Get("sessionid") windowId := qvals.Get("windowid") @@ -226,10 +212,6 @@ func HandleRtnState(w http.ResponseWriter, r *http.Request) { } func HandleRemotePty(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") qvals := r.URL.Query() remoteId := qvals.Get("remoteid") if remoteId == "" { @@ -255,10 +237,6 @@ func HandleRemotePty(w http.ResponseWriter, r *http.Request) { } func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Vary", "Origin") - w.Header().Set("Cache-Control", "no-cache") qvals := r.URL.Query() sessionId := qvals.Get("sessionid") cmdId := qvals.Get("cmdid") @@ -330,15 +308,7 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("panic: %v", r)) return }() - w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") - w.Header().Set("Vary", "Origin") w.Header().Set("Cache-Control", "no-cache") - if r.Method == "GET" || r.Method == "OPTIONS" { - w.WriteHeader(200) - return - } decoder := json.NewDecoder(r.Body) var commandPk scpacket.FeCommandPacketType err := decoder.Decode(&commandPk) @@ -355,6 +325,24 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { return } +func AuthKeyWrap(fn WebFnType) WebFnType { + return func(w http.ResponseWriter, r *http.Request) { + reqAuthKey := r.Header.Get("X-AuthKey") + if reqAuthKey == "" { + w.WriteHeader(500) + w.Write([]byte("no x-authkey header")) + return + } + if reqAuthKey != GlobalAuthKey { + w.WriteHeader(500) + w.Write([]byte("x-authkey header is invalid")) + return + } + w.Header().Set("Cache-Control", "no-cache") + fn(w, r) + } +} + func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) @@ -405,10 +393,9 @@ func main() { scLock, err := scbase.AcquirePromptLock() if err != nil || scLock == nil { - log.Printf("[error] cannot acquire sh2 lock: %v\n", err) + log.Printf("[error] cannot acquire prompt lock: %v\n", err) return } - if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "--migrate") { err := sstore.MigrateCommandOpts(os.Args[1:]) if err != nil { @@ -416,6 +403,12 @@ func main() { } return } + authKey, err := scbase.ReadPromptAuthKey() + if err != nil { + log.Printf("[error] %v\n", err) + return + } + GlobalAuthKey = authKey err = sstore.TryMigrateUp() if err != nil { log.Printf("[error] migrate up: %v\n", err) @@ -451,13 +444,13 @@ func main() { go stdinReadWatch() go runWebSocketServer() gr := mux.NewRouter() - gr.HandleFunc("/api/ptyout", HandleGetPtyOut) - gr.HandleFunc("/api/remote-pty", HandleRemotePty) - gr.HandleFunc("/api/rtnstate", HandleRtnState) - gr.HandleFunc("/api/get-window", HandleGetWindow) - gr.HandleFunc("/api/run-command", HandleRunCommand).Methods("GET", "POST", "OPTIONS") - gr.HandleFunc("/api/get-client-data", HandleGetClientData) - gr.HandleFunc("/api/set-winsize", HandleSetWinSize) + gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) + gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty)) + gr.HandleFunc("/api/rtnstate", AuthKeyWrap(HandleRtnState)) + gr.HandleFunc("/api/get-window", AuthKeyWrap(HandleGetWindow)) + gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") + gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) + gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) server := &http.Server{ Addr: MainServerAddr, ReadTimeout: HttpReadTimeout, diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 7ea1bb396..bb88338d1 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -77,6 +77,7 @@ type WatchScreenPacketType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` Connect bool `json:"connect"` + AuthKey string `json:"authkey"` } func init() { diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 045e39b2e..34a1cd5a8 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -19,25 +19,40 @@ const WSStatePacketChSize = 20 const MaxInputDataSize = 1000 type WSState struct { - Lock *sync.Mutex - ClientId string - ConnectTime time.Time - Shell *wsshell.WSShell - UpdateCh chan interface{} - UpdateQueue []interface{} + Lock *sync.Mutex + ClientId string + ConnectTime time.Time + Shell *wsshell.WSShell + UpdateCh chan interface{} + UpdateQueue []interface{} + Authenticated bool + AuthKey string SessionId string ScreenId string } -func MakeWSState(clientId string) *WSState { +func MakeWSState(clientId string, authKey string) *WSState { rtn := &WSState{} rtn.Lock = &sync.Mutex{} rtn.ClientId = clientId rtn.ConnectTime = time.Now() + rtn.AuthKey = authKey return rtn } +func (ws *WSState) SetAuthenticated(authVal bool) { + ws.Lock.Lock() + defer ws.Lock.Unlock() + ws.Authenticated = authVal +} + +func (ws *WSState) IsAuthenticated() bool { + ws.Lock.Lock() + defer ws.Lock.Unlock() + return ws.Authenticated +} + func (ws *WSState) GetShell() *wsshell.WSShell { ws.Lock.Lock() defer ws.Lock.Unlock() @@ -151,6 +166,15 @@ func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error return fmt.Errorf("invalid watchscreen screenid: %w", err) } } + if wsPk.AuthKey == "" { + ws.SetAuthenticated(false) + return fmt.Errorf("invalid watchscreen, no authkey") + } + if wsPk.AuthKey != ws.AuthKey { + ws.SetAuthenticated(false) + return fmt.Errorf("invalid watchscreen, invalid authkey") + } + ws.SetAuthenticated(true) if wsPk.SessionId == "" || wsPk.ScreenId == "" { ws.UnWatchScreen() } else { @@ -179,6 +203,20 @@ func (ws *WSState) RunWSRead() { log.Printf("error unmarshalling ws message: %v\n", err) continue } + if pk.GetType() == scpacket.WatchScreenPacketStr { + wsPk := pk.(*scpacket.WatchScreenPacketType) + err := ws.handleWatchScreen(wsPk) + if err != nil { + // TODO send errors back to client, likely unrecoverable + log.Printf("[ws %s] error %v\n", err) + } + continue + } + isAuth := ws.IsAuthenticated() + if !isAuth { + log.Printf("[error] cannot process ws-packet[%s], not authenticated\n", pk.GetType()) + continue + } if pk.GetType() == scpacket.FeInputPacketStr { feInputPk := pk.(*scpacket.FeInputPacketType) if feInputPk.Remote.OwnerId != "" { @@ -198,15 +236,6 @@ func (ws *WSState) RunWSRead() { }() continue } - if pk.GetType() == scpacket.WatchScreenPacketStr { - wsPk := pk.(*scpacket.WatchScreenPacketType) - err := ws.handleWatchScreen(wsPk) - if err != nil { - // TODO send errors back to client, likely unrecoverable - log.Printf("[ws %s] error %v\n", err) - } - continue - } if pk.GetType() == scpacket.RemoteInputPacketStr { inputPk := pk.(*scpacket.RemoteInputPacketType) if inputPk.RemoteId == "" { From b0a40ee629b5a500b65d2e706e78242a5c7e1a2f Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 20 Dec 2022 21:58:58 -0800 Subject: [PATCH 210/397] signal command --- pkg/cmdrunner/cmdrunner.go | 86 +++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a9d4d750c..3ebb907da 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -43,12 +43,14 @@ const MaxRemoteAliasLen = 50 const PasswordUnchangedSentinel = "--unchanged--" const DefaultPTERM = "MxM" const MaxCommandLen = 4096 +const MaxSignalLen = 12 +const MaxSignalNum = 64 var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset"} +var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} var NoHistCmds = []string{"_compgen", "line", "_history", "_killserver"} var GlobalCmds = []string{"session", "screen", "remote", "set"} @@ -78,6 +80,7 @@ var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$") var wsRe = regexp.MustCompile("\\s+") +var sigNameRe = regexp.MustCompile("^((SIG[A-Z0-9]+)|(\\d+))$") type contextType string @@ -110,6 +113,7 @@ func init() { registerCmdFn("_compgen", CompGenCommand) registerCmdFn("clear", ClearCommand) registerCmdFn("reset", RemoteResetCommand) + registerCmdFn("signal", SignalCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -251,6 +255,19 @@ func resolvePosInt(arg string, def int) (int, error) { return ival, nil } +func isAllDigits(arg string) bool { + if len(arg) == 0 { + return false + } + for i := 0; i < len(arg); i++ { + if arg[i] >= '0' && arg[i] <= '9' { + continue + } + return false + } + return true +} + func resolveNonNegInt(arg string, def int) (int, error) { if arg == "" { return def, nil @@ -1658,6 +1675,73 @@ func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return nil, nil } +func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/signal requires a first argument (line number or id)") + } + if len(pk.Args) == 1 { + return nil, fmt.Errorf("/signal requires a second argument (signal name)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) + if err != nil { + return nil, fmt.Errorf("error getting line: %v", err) + } + if line == nil { + return nil, fmt.Errorf("line %q not found", lineArg) + } + if cmd == nil { + return nil, fmt.Errorf("line %q does not have a command", lineArg) + } + if cmd.Status != sstore.CmdStatusRunning { + return nil, fmt.Errorf("line %q command is not running, cannot send signal", lineArg) + } + sigArg := pk.Args[1] + if isAllDigits(sigArg) { + val, _ := strconv.Atoi(sigArg) + if val <= 0 || val > MaxSignalNum { + return nil, fmt.Errorf("signal number is out of bounds: %q", sigArg) + } + } else if !strings.HasPrefix(sigArg, "SIG") { + sigArg = "SIG" + sigArg + } + sigArg = strings.ToUpper(sigArg) + if len(sigArg) > 12 { + return nil, fmt.Errorf("invalid signal (too long): %q", sigArg) + } + if !sigNameRe.MatchString(sigArg) { + return nil, fmt.Errorf("invalid signal name/number: %q", sigArg) + } + msh := remote.GetRemoteById(cmd.Remote.RemoteId) + if msh == nil { + return nil, fmt.Errorf("cannot send signal, no remote found for command") + } + if !msh.IsConnected() { + return nil, fmt.Errorf("cannot send signal, remote is not connected") + } + siPk := packet.MakeSpecialInputPacket() + siPk.CK = base.MakeCommandKey(cmd.SessionId, cmd.CmdId) + siPk.SigName = sigArg + err = msh.SendSpecialInput(siPk) + if err != nil { + return nil, fmt.Errorf("cannot send signal: %v", err) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("sent line %s signal %s", lineArg, sigArg), + }, + } + return update, nil +} + func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { go func() { log.Printf("received /killserver, shutting down\n") From d2530339e4f0bbc09d79d487cebda626f3698269 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 21 Dec 2022 17:45:40 -0800 Subject: [PATCH 211/397] line hidden attribute. add line:hide and line:purge commands --- db/migrations/000001_init.up.sql | 1 + pkg/cmdrunner/cmdrunner.go | 68 +++++++++++++++++++++++++++++++- pkg/sstore/dbops.go | 43 +++++++++++++++++++- pkg/sstore/fileops.go | 8 ++++ pkg/sstore/sstore.go | 5 ++- 5 files changed, 120 insertions(+), 5 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index aa0033375..bdaa2dd2e 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -101,6 +101,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, + hidden boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3ebb907da..bc78f4eb0 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -147,6 +147,8 @@ func init() { registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) registerCmdFn("line:star", LineStarCommand) + registerCmdFn("line:hide", LineHideCommand) + registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("_history", HistoryCommand) @@ -1544,7 +1546,7 @@ func SwResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst } func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) + return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "purge"}, "or", false)) } func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -1588,6 +1590,70 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return sstore.ModelUpdate{Line: lineObj}, nil } +func LineHideCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/line:hide requires an argument (line number or id)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + shouldHide := true + if len(pk.Args) >= 2 { + shouldHide = resolveBool(pk.Args[1], true) + } + err = sstore.SetLineHiddenById(ctx, lineId, shouldHide) + if err != nil { + return nil, fmt.Errorf("/line:hide error updating hidden status: %v", err) + } + lineObj, err := sstore.GetLineById(ctx, lineId) + if err != nil { + return nil, fmt.Errorf("/line:star error getting line: %v", err) + } + if lineObj == nil { + // no line (which is strange given we checked for it above). just return a nop. + return nil, nil + } + return sstore.ModelUpdate{Line: lineObj}, nil +} + +func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/line:purge requires an argument (line number or id)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + err = sstore.PurgeLineById(ctx, ids.SessionId, lineId) + if err != nil { + return nil, fmt.Errorf("/line:purge error purging line: %v", err) + } + lineObj := &sstore.LineType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + LineId: lineId, + Remove: true, + } + return sstore.ModelUpdate{Line: lineObj}, nil +} + func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 471eafa61..9fbd0ae08 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -663,8 +663,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star, hidden) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star,:hidden)` tx.NamedExecWrap(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) @@ -1486,3 +1486,42 @@ func GetLineById(ctx context.Context, lineId string) (*LineType, error) { } return rtn, nil } + +func SetLineHiddenById(ctx context.Context, lineId string, hidden bool) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET hidden = ? WHERE lineid = ?` + tx.ExecWrap(query, hidden, lineId) + return nil + }) + return txErr +} + +func purgeCmdById(ctx context.Context, sessionId string, cmdId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `DELETE FROM cmd WHERE sessionid = ? AND cmdid = ?` + tx.ExecWrap(query, sessionId, cmdId) + return DeletePtyOutFile(tx.Context(), sessionId, cmdId) + }) + return txErr +} + +func PurgeLineById(ctx context.Context, sessionId string, lineId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT cmdid FROM line WHERE sessionid = ? AND lineid = ?` + cmdId := tx.GetString(query, sessionId, lineId) + query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` + tx.ExecWrap(query, sessionId, lineId) + if cmdId != "" { + query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` + cmdRefCount := tx.GetInt(query, sessionId, cmdId) + if cmdRefCount == 0 { + err := purgeCmdById(tx.Context(), sessionId, cmdId) + if err != nil { + return err + } + } + } + return nil + }) + return txErr +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 1fdad2cd8..5fc689bf5 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -127,3 +127,11 @@ func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) { } return rtn, nil } + +func DeletePtyOutFile(ctx context.Context, sessionId string, cmdId string) error { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return err + } + return os.Remove(ptyOutFileName) +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index efa2636f8..ef40f2a31 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -585,9 +585,10 @@ type LineType struct { Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` - Star bool `json:"star,omitempty"` - Remove bool `json:"remove,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` + Star bool `json:"star,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Remove bool `json:"remove,omitempty"` } type ResolveItem struct { From 962261ec35df43a3010cf8a31ef4cff2f5253863 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 23 Dec 2022 15:56:29 -0800 Subject: [PATCH 212/397] updates for /screen:close --- pkg/cmdrunner/cmdrunner.go | 86 +++++++++++++++++++++++++++++++-- pkg/cmdrunner/resolver.go | 19 +++++--- pkg/sstore/dbops.go | 98 ++++++++++++++++++++++++++++++++++---- pkg/sstore/sstore.go | 20 ++++---- pkg/sstore/txwrap.go | 6 +++ 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index bc78f4eb0..e76d92ff4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -123,9 +123,11 @@ func init() { registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:close", ScreenCloseCommand) + registerCmdFn("screen:purge", ScreenPurgeCommand) registerCmdFn("screen:open", ScreenOpenCommand) registerCmdAlias("screen:new", ScreenOpenCommand) registerCmdFn("screen:set", ScreenSetCommand) + registerCmdFn("screen:showall", ScreenShowAllCommand) registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) @@ -378,10 +380,53 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) + ids, err := resolveUiIds(ctx, pk, R_Session) // don't force R_Screen if err != nil { return nil, fmt.Errorf("/screen:close cannot close 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:close cannot resolve screen arg: %v", err) + } + screenId = ri.Id + } + if screenId == "" { + return nil, fmt.Errorf("/screen:close no active screen or screen arg passed") + } + closeVal := true + if len(pk.Args) > 1 { + closeVal = resolveBool(pk.Args[1], true) + } + var update sstore.UpdatePacket + if closeVal { + update, err = sstore.CloseScreen(ctx, ids.SessionId, screenId) + if err != nil { + return nil, err + } + return update, nil + } else { + fmt.Printf("unclose screen %s\n", screenId) + err = sstore.UnCloseScreen(ctx, ids.SessionId, screenId) + if err != nil { + return nil, fmt.Errorf("/screen:close cannot re-open screen: %v", err) + } + screen, err := sstore.GetScreenById(ctx, ids.SessionId, screenId) + if err != nil { + return nil, fmt.Errorf("/screen:close cannot get updated screen obj: %v", err) + } + update, session := sstore.MakeSingleSessionUpdate(ids.SessionId) + session.Screens = append(session.Screens, screen) + return update, nil + } +} + +func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, fmt.Errorf("/screen:purge cannot close screen: %w", err) + } update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { return nil, err @@ -900,6 +945,33 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) }, nil } +func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session) + screenArr, err := sstore.GetAllSessionScreens(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("/screen:showall error getting screen list: %v", err) + } + var buf bytes.Buffer + for _, screen := range screenArr { + var closedStr string + if screen.Closed { + closedStr = " (closed)" + } + screenIdxStr := "-" + if screen.ScreenIdx != 0 { + screenIdxStr = strconv.Itoa(int(screen.ScreenIdx)) + } + outStr := fmt.Sprintf("%s %-30s %s\n", screen.ScreenId, screen.Name+closedStr, screenIdxStr) + buf.WriteString(outStr) + } + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("all screens for session"), + InfoLines: splitLinesForInfo(buf.String()), + }, + }, nil +} + func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -1309,13 +1381,19 @@ func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("cannot delete session: %v", err) } - sessionIds, _ := sstore.GetAllSessionIds(ctx) // ignore error, session is already deleted so that's the main return value delSession := &sstore.SessionType{SessionId: ids.SessionId, Remove: true} update := sstore.ModelUpdate{ Sessions: []*sstore.SessionType{delSession}, } - if len(sessionIds) > 0 { - update.ActiveSessionId = sessionIds[0] + activeSessionId, _ := sstore.GetActiveSessionId(ctx) // ignore error + if activeSessionId == "" { + sessionIds, _ := sstore.GetAllSessionIds(ctx) // ignore error, session is already deleted so that's the main return value + if len(sessionIds) > 0 { + err = sstore.SetActiveSessionId(ctx, sessionIds[0]) + if err != nil { + update.ActiveSessionId = sessionIds[0] + } + } } return update, nil } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index b29337345..7a5edaf0e 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -58,7 +58,7 @@ func sessionsToResolveItems(sessions []*sstore.SessionType) []ResolveItem { } rtn := make([]ResolveItem, len(sessions)) for idx, session := range sessions { - rtn[idx] = ResolveItem{Name: session.Name, Id: session.SessionId} + rtn[idx] = ResolveItem{Name: session.Name, Id: session.SessionId, Hidden: session.Closed} } return rtn } @@ -69,7 +69,7 @@ func screensToResolveItems(screens []*sstore.ScreenType) []ResolveItem { } rtn := make([]ResolveItem, len(screens)) for idx, screen := range screens { - rtn[idx] = ResolveItem{Name: screen.Name, Id: screen.ScreenId} + rtn[idx] = ResolveItem{Name: screen.Name, Id: screen.ScreenId, Hidden: screen.Closed} } return rtn } @@ -133,7 +133,13 @@ func parsePosArg(posStr string) *posArgType { return &posArgType{Pos: pos} } -func resolveByPosition(isNumeric bool, items []ResolveItem, curId string, posStr string) *ResolveItem { +func resolveByPosition(isNumeric bool, allItems []ResolveItem, curId string, posStr string) *ResolveItem { + items := make([]ResolveItem, 0, len(allItems)) + for _, item := range allItems { + if !item.Hidden { + items = append(items, item) + } + } if len(items) == 0 { return nil } @@ -160,7 +166,8 @@ func resolveByPosition(isNumeric bool, items []ResolveItem, curId string, posStr finalPos = curIdx + posArg.Pos } else if isNumeric { // these resolve items have a "Num" set that should be used to look up non-relative positions - for _, item := range items { + // use allItems for numeric resolve + for _, item := range allItems { if item.Num == posArg.Pos { return &item } @@ -342,7 +349,7 @@ func genericResolve(arg string, curArg string, items []ResolveItem, isNumeric bo if (isUuid && item.Id == arg) || (tryPuid && strings.HasPrefix(item.Id, arg)) { return &item, nil } - if item.Name != "" { + if !item.Hidden && item.Name != "" { if item.Name == arg { return &item, nil } @@ -396,7 +403,7 @@ func resolveScreenArg(sessionId string, screenArg string) (string, error) { return "", nil } if _, err := uuid.Parse(screenArg); err != nil { - return "", fmt.Errorf("invalid screen arg specified (must be sessionid) '%s'", screenArg) + return "", fmt.Errorf("invalid screen arg specified (must be screenid) '%s'", screenArg) } return screenArg, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9fbd0ae08..fa855d614 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -233,6 +233,7 @@ func GetHistoryItems(ctx context.Context, sessionId string, windowId string, opt return rtn, nil } +// includes closed sessions func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { @@ -249,7 +250,7 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { func GetAllSessionIds(ctx context.Context) ([]string, error) { var rtn []string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid from session ORDER by sessionidx` + query := `SELECT sessionid from session WHERE NOT closed ORDER by sessionidx` rtn = tx.SelectStrings(query) return nil }) @@ -287,7 +288,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { session.Full = true } var screens []*ScreenType - query = `SELECT * FROM screen ORDER BY screenidx` + query = `SELECT * FROM screen WHERE NOT closed ORDER BY screenidx` tx.SelectWrap(&screens, query) screenMap := make(map[string][]*ScreenType) for _, screen := range screens { @@ -352,6 +353,7 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win return rtnWindow, err } +// includes closed screens func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -362,6 +364,16 @@ func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, er return rtn, txErr } +func GetAllSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { + var rtn []*ScreenType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY closed, screenidx` + tx.SelectWrap(&rtn, query, sessionId) + return nil + }) + return rtn, txErr +} + func GetSessionById(ctx context.Context, id string) (*SessionType, error) { allSessionsUpdate, err := GetAllSessions(ctx) if err != nil { @@ -436,7 +448,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo func SetActiveSessionId(ctx context.Context, sessionId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session WHERE sessionid = ?` + query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT closed` if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot switch to session, not found") } @@ -447,6 +459,16 @@ func SetActiveSessionId(ctx context.Context, sessionId string) error { return txErr } +func GetActiveSessionId(ctx context.Context) (string, error) { + var rtnId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT activesessionid FROM client` + rtnId = tx.GetString(query) + return nil + }) + return rtnId, txErr +} + func SetWinSize(ctx context.Context, winSize ClientWinSizeType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET winsize = ?` @@ -492,7 +514,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (UpdatePacket, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session WHERE sessionid = ?` + query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT closed` if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot create screen, no session found") } @@ -501,8 +523,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, return fmt.Errorf("cannot create screen, no local remote found") } newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) - maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ?`, sessionId) - screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ?`, sessionId) + maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = scbase.GenPromptUUID() query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, closed) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0)` @@ -810,7 +832,7 @@ func getNextId(ids []string, delId string) string { func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND NOT closed` if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("cannot switch to screen, screen=%s does not exist in session=%s", screenId, sessionId) } @@ -826,12 +848,66 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U func CleanWindows() { } +func CloseScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { + var newActiveScreenId string + txErr := WithTx(ctx, func(tx *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)") + } + query = `SELECT closed 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 closed` + numScreens := tx.GetInt(query, sessionId) + if numScreens <= 1 { + return fmt.Errorf("cannot close the last screen in a session") + } + query = `UPDATE screen SET closed = 1, screenidx = 0 WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, 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 closed ORDER BY screenidx`, sessionId) + nextId := getNextId(screenIds, screenId) + tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + newActiveScreenId = nextId + } + return nil + }) + if txErr != nil { + return nil, txErr + } + update, session := MakeSingleSessionUpdate(sessionId) + session.ActiveScreenId = newActiveScreenId + session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) + return update, nil +} + +func UnCloseScreen(ctx context.Context, sessionId string, screenId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND closed` + if !tx.Exists(query, sessionId, screenId) { + return fmt.Errorf("cannot re-open screen (not found or not closed)") + } + origScreenName := tx.GetString(`SELECT name FROM screen WHERE sessionid = ? AND screenid = ?`, sessionId, screenId) + maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) + newScreenName := fmtUniqueName(origScreenName, "s-%d", 2, screenNames) + query = `UPDATE screen SET closed = 0, screenidx = ?, name = ? WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, maxScreenIdx+1, newScreenName, sessionId, screenId) + return nil + }) + return txErr +} + func DeleteScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId) if isActive { - screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? ORDER BY screenidx`, sessionId) + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT closed ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) newActiveScreenId = nextId @@ -1068,7 +1144,7 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("screen does not exist") } - query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ?` + query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ? AND NOT closed` dupScreenId := tx.GetString(query, sessionId, name) if dupScreenId == screenId { return nil @@ -1169,8 +1245,10 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, if !tx.Exists(query, sessionId) { return fmt.Errorf("not found") } - query = `SELECT count(*) FROM screen WHERE sessionid = ?` + query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT closed` rtn.NumScreens = tx.GetInt(query, sessionId) + query = `SELECT count(*) FROM screen WHERE sessionid = ? AND closed` + rtn.NumClosedScreens = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM window WHERE sessionid = ?` rtn.NumWindows = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM line WHERE sessionid = ?` diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index ef40f2a31..eb6e33578 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -162,12 +162,13 @@ type SessionType struct { } type SessionStatsType struct { - SessionId string `json:"sessionid"` - NumScreens int `json:"numscreens"` - NumWindows int `json:"numwindows"` - NumLines int `json:"numlines"` - NumCmds int `json:"numcmds"` - DiskStats SessionDiskSizeType `json:"diskstats"` + SessionId string `json:"sessionid"` + NumScreens int `json:"numscreens"` + NumClosedScreens int `json:"numclosedscreens"` + NumWindows int `json:"numwindows"` + NumLines int `json:"numlines"` + NumCmds int `json:"numcmds"` + DiskStats SessionDiskSizeType `json:"diskstats"` } type WindowOptsType struct { @@ -592,9 +593,10 @@ type LineType struct { } type ResolveItem struct { - Name string - Num int - Id string + Name string + Num int + Id string + Hidden bool } type SSHOpts struct { diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go index 02e5698a9..67761950b 100644 --- a/pkg/sstore/txwrap.go +++ b/pkg/sstore/txwrap.go @@ -105,6 +105,12 @@ func (tx *TxWrap) GetString(query string, args ...interface{}) string { return rtnStr } +func (tx *TxWrap) GetBool(query string, args ...interface{}) bool { + var rtnBool bool + tx.GetWrap(&rtnBool, query, args...) + return rtnBool +} + func (tx *TxWrap) SelectStrings(query string, args ...interface{}) []string { var rtnArr []string tx.SelectWrap(&rtnArr, query, args...) From 5645a0148258a9d9dfdce4c1a65f590bfe62efa8 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 25 Dec 2022 13:03:11 -0800 Subject: [PATCH 213/397] archived not closed --- db/migrations/000001_init.up.sql | 4 +- db/schema.sql | 5 +- pkg/cmdrunner/cmdrunner.go | 14 ++++- pkg/cmdrunner/resolver.go | 4 +- pkg/sstore/dbops.go | 94 ++++++++++++++++++++++---------- pkg/sstore/sstore.go | 18 +++--- 6 files changed, 92 insertions(+), 47 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index bdaa2dd2e..25bc0f07d 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -13,7 +13,7 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, - closed boolean NOT NULL, + archived boolean NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -43,7 +43,7 @@ CREATE TABLE screen ( ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, incognito boolean NOT NULL, - closed boolean NOT NULL, + archived boolean NOT NULL, PRIMARY KEY (sessionid, screenid) ); diff --git a/db/schema.sql b/db/schema.sql index 4f2f532c6..bb477b14e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -14,7 +14,7 @@ CREATE TABLE session ( sessionidx int NOT NULL, activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, - closed boolean NOT NULL, + archived boolean NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -42,7 +42,7 @@ CREATE TABLE screen ( ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, incognito boolean NOT NULL, - closed boolean NOT NULL, + archived boolean NOT NULL, PRIMARY KEY (sessionid, screenid) ); CREATE TABLE screen_window ( @@ -95,6 +95,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, + hidden boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e76d92ff4..591619073 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -120,6 +120,8 @@ func init() { registerCmdAlias("session:new", SessionOpenCommand) registerCmdFn("session:set", SessionSetCommand) registerCmdFn("session:delete", SessionDeleteCommand) + registerCmdFn("session:archive", SessionArchiveCommand) + registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:close", ScreenCloseCommand) @@ -954,14 +956,14 @@ func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) var buf bytes.Buffer for _, screen := range screenArr { var closedStr string - if screen.Closed { + if screen.Archived { closedStr = " (closed)" } screenIdxStr := "-" if screen.ScreenIdx != 0 { screenIdxStr = strconv.Itoa(int(screen.ScreenIdx)) } - outStr := fmt.Sprintf("%s %-30s %s\n", screen.ScreenId, screen.Name+closedStr, screenIdxStr) + outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+closedStr, screen.ScreenId, screenIdxStr) buf.WriteString(outStr) } return sstore.ModelUpdate{ @@ -2067,3 +2069,11 @@ func resolveSetArg(argName string) (bool, string, string) { } return true, scopeName, varName } + +func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + +func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 7a5edaf0e..c2f1c3752 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -58,7 +58,7 @@ func sessionsToResolveItems(sessions []*sstore.SessionType) []ResolveItem { } rtn := make([]ResolveItem, len(sessions)) for idx, session := range sessions { - rtn[idx] = ResolveItem{Name: session.Name, Id: session.SessionId, Hidden: session.Closed} + rtn[idx] = ResolveItem{Name: session.Name, Id: session.SessionId, Hidden: session.Archived} } return rtn } @@ -69,7 +69,7 @@ func screensToResolveItems(screens []*sstore.ScreenType) []ResolveItem { } rtn := make([]ResolveItem, len(screens)) for idx, screen := range screens { - rtn[idx] = ResolveItem{Name: screen.Name, Id: screen.ScreenId, Hidden: screen.Closed} + rtn[idx] = ResolveItem{Name: screen.Name, Id: screen.ScreenId, Hidden: screen.Archived} } return rtn } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index fa855d614..1ce6b6593 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -233,7 +233,7 @@ func GetHistoryItems(ctx context.Context, sessionId string, windowId string, opt return rtn, nil } -// includes closed sessions +// includes archived sessions func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { @@ -247,10 +247,11 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { return rtn, nil } +// does not include archived func GetAllSessionIds(ctx context.Context) ([]string, error) { var rtn []string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid from session WHERE NOT closed ORDER by sessionidx` + query := `SELECT sessionid from session WHERE NOT archived ORDER by sessionidx` rtn = tx.SelectStrings(query) return nil }) @@ -288,7 +289,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { session.Full = true } var screens []*ScreenType - query = `SELECT * FROM screen WHERE NOT closed ORDER BY screenidx` + query = `SELECT * FROM screen WHERE NOT archived ORDER BY screenidx` tx.SelectWrap(&screens, query) screenMap := make(map[string][]*ScreenType) for _, screen := range screens { @@ -353,7 +354,7 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win return rtnWindow, err } -// includes closed screens +// includes archived screens func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -367,7 +368,7 @@ func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, er func GetAllSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY closed, screenidx` + query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx` tx.SelectWrap(&rtn, query, sessionId) return nil }) @@ -417,8 +418,8 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) - query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, closed, ownerid, sharemode, accesskey) - VALUES (?, ?, '', ?, ?, 0, '', 'local', '')` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, ownerid, sharemode, accesskey) + VALUES (?, ?, '', ?, ?, 0, '', 'local', '')` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { @@ -448,7 +449,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo func SetActiveSessionId(ctx context.Context, sessionId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT closed` + query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot switch to session, not found") } @@ -514,7 +515,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (UpdatePacket, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT closed` + query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot create screen, no session found") } @@ -523,11 +524,11 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, return fmt.Errorf("cannot create screen, no local remote found") } newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) - maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) - screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) + maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = scbase.GenPromptUUID() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, closed) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0)` + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0)` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` @@ -832,7 +833,7 @@ func getNextId(ids []string, delId string) string { func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND NOT closed` + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND NOT archived` if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("cannot switch to screen, screen=%s does not exist in session=%s", screenId, sessionId) } @@ -845,7 +846,31 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U return update, txErr } -func CleanWindows() { +func CleanWindows(sessionId string) { + txErr := WithTx(context.Background(), func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid NOT IN (SELECT windowid FROM screen_window WHERE sessionid = ?)` + removedWindowIds := tx.SelectStrings(query, sessionId, sessionId) + if len(removedWindowIds) == 0 { + return nil + } + for _, windowId := range removedWindowIds { + query = `DELETE FROM window WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + query = `DELETE FROM line WHERE sessinid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + } + query = `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` + removedCmds := tx.SelectStrings(query, sessionId, sessionId) + query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` + tx.ExecWrap(query, sessionId, sessionId) + fmt.Printf("removed cmds: %v\n", removedCmds) + return nil + }) + if txErr != nil { + fmt.Printf("ERROR cleaning windows sessionid:%s: %v\n", sessionId, txErr) + } } func CloseScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { @@ -855,21 +880,21 @@ func CloseScreen(ctx context.Context, sessionId string, screenId string) (Update if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("cannot close screen (not found)") } - query = `SELECT closed FROM screen WHERE sessionid = ? AND screenid = ?` + 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 closed` + query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived` numScreens := tx.GetInt(query, sessionId) if numScreens <= 1 { return fmt.Errorf("cannot close the last screen in a session") } - query = `UPDATE screen SET closed = 1, screenidx = 0 WHERE sessionid = ? AND screenid = ?` + query = `UPDATE screen SET archived = 1, screenidx = 0 WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, 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 closed ORDER BY screenidx`, sessionId) + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) newActiveScreenId = nextId @@ -887,15 +912,15 @@ func CloseScreen(ctx context.Context, sessionId string, screenId string) (Update func UnCloseScreen(ctx context.Context, sessionId string, screenId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND closed` + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND archived` if !tx.Exists(query, sessionId, screenId) { - return fmt.Errorf("cannot re-open screen (not found or not closed)") + return fmt.Errorf("cannot re-open screen (not found or not archived)") } origScreenName := tx.GetString(`SELECT name FROM screen WHERE sessionid = ? AND screenid = ?`, sessionId, screenId) - maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) - screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT closed`, sessionId) + maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) newScreenName := fmtUniqueName(origScreenName, "s-%d", 2, screenNames) - query = `UPDATE screen SET closed = 0, screenidx = ?, name = ? WHERE sessionid = ? AND screenid = ?` + query = `UPDATE screen SET archived = 0, screenidx = ?, name = ? WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, maxScreenIdx+1, newScreenName, sessionId, screenId) return nil }) @@ -905,14 +930,23 @@ func UnCloseScreen(ctx context.Context, sessionId string, screenId string) error func DeleteScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + if !tx.Exists(query, sessionId, screenId) { + return fmt.Errorf("cannot purge screen (not found)") + } + 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") + } 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 closed ORDER BY screenidx`, sessionId) + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) newActiveScreenId = nextId } - query := `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` + query = `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, sessionId, screenId) query = `DELETE FROM screen WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, sessionId, screenId) @@ -921,7 +955,7 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat if txErr != nil { return nil, txErr } - go CleanWindows() + go CleanWindows(sessionId) update, session := MakeSingleSessionUpdate(sessionId) session.ActiveScreenId = newActiveScreenId session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) @@ -1144,7 +1178,7 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("screen does not exist") } - query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ? AND NOT closed` + query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ? AND NOT archived` dupScreenId := tx.GetString(query, sessionId, name) if dupScreenId == screenId { return nil @@ -1245,10 +1279,10 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, if !tx.Exists(query, sessionId) { return fmt.Errorf("not found") } - query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT closed` + query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived` rtn.NumScreens = tx.GetInt(query, sessionId) - query = `SELECT count(*) FROM screen WHERE sessionid = ? AND closed` - rtn.NumClosedScreens = tx.GetInt(query, sessionId) + query = `SELECT count(*) FROM screen WHERE sessionid = ? AND archived` + rtn.NumArchivedScreens = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM window WHERE sessionid = ?` rtn.NumWindows = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM line WHERE sessionid = ?` diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index eb6e33578..889626834 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -152,7 +152,7 @@ type SessionType struct { ShareMode string `json:"sharemode"` AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` - Closed bool `json:"closed,omitempty"` + Archived bool `json:"archived,omitempty"` Screens []*ScreenType `json:"screens"` Remotes []*RemoteInstance `json:"remotes"` @@ -162,13 +162,13 @@ type SessionType struct { } type SessionStatsType struct { - SessionId string `json:"sessionid"` - NumScreens int `json:"numscreens"` - NumClosedScreens int `json:"numclosedscreens"` - NumWindows int `json:"numwindows"` - NumLines int `json:"numlines"` - NumCmds int `json:"numcmds"` - DiskStats SessionDiskSizeType `json:"diskstats"` + SessionId string `json:"sessionid"` + NumScreens int `json:"numscreens"` + NumArchivedScreens int `json:"numarchivedscreens"` + NumWindows int `json:"numwindows"` + NumLines int `json:"numlines"` + NumCmds int `json:"numcmds"` + DiskStats SessionDiskSizeType `json:"diskstats"` } type WindowOptsType struct { @@ -357,7 +357,7 @@ type ScreenType struct { OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` Incognito bool `json:"incognito,omitempty"` - Closed bool `json:"closed,omitempty"` + Archived bool `json:"archived,omitempty"` Windows []*ScreenWindowType `json:"windows"` // only for updates From f1a46dea82621e4a4a546694d5eb0bd1caadfa69 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 25 Dec 2022 13:21:48 -0800 Subject: [PATCH 214/397] screen archiving --- db/migrations/000001_init.up.sql | 2 ++ db/schema.sql | 2 ++ pkg/cmdrunner/cmdrunner.go | 36 ++++++++++++++++---------------- pkg/sstore/dbops.go | 36 ++++++++++++++------------------ pkg/sstore/sstore.go | 2 ++ 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 25bc0f07d..4c1e02721 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -14,6 +14,7 @@ CREATE TABLE session ( activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, archived boolean NOT NULL, + archivedts bigint NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -44,6 +45,7 @@ CREATE TABLE screen ( sharemode varchar(12) NOT NULL, incognito boolean NOT NULL, archived boolean NOT NULL, + archivedts bigint NOT NULL, PRIMARY KEY (sessionid, screenid) ); diff --git a/db/schema.sql b/db/schema.sql index bb477b14e..3e3edf927 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -15,6 +15,7 @@ CREATE TABLE session ( activescreenid varchar(36) NOT NULL, notifynum int NOT NULL, archived boolean NOT NULL, + archivedts bigint NOT NULL, ownerid varchar(36) NOT NULL, sharemode varchar(12) NOT NULL, accesskey varchar(36) NOT NULL @@ -43,6 +44,7 @@ CREATE TABLE screen ( sharemode varchar(12) NOT NULL, incognito boolean NOT NULL, archived boolean NOT NULL, + archivedts bigint NOT NULL, PRIMARY KEY (sessionid, screenid) ); CREATE TABLE screen_window ( diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 591619073..d9710ae96 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -124,7 +124,7 @@ func init() { registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("screen", ScreenCommand) - registerCmdFn("screen:close", ScreenCloseCommand) + registerCmdFn("screen:archive", ScreenArchiveCommand) registerCmdFn("screen:purge", ScreenPurgeCommand) registerCmdFn("screen:open", ScreenOpenCommand) registerCmdAlias("screen:new", ScreenOpenCommand) @@ -381,42 +381,42 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return update, rtnErr } -func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func ScreenArchiveCommand(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:close cannot close screen: %w", err) + return nil, fmt.Errorf("/screen:archive cannot archive 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:close cannot resolve screen arg: %v", err) + return nil, fmt.Errorf("/screen:archive cannot resolve screen arg: %v", err) } screenId = ri.Id } if screenId == "" { - return nil, fmt.Errorf("/screen:close no active screen or screen arg passed") + return nil, fmt.Errorf("/screen:archive no active screen or screen arg passed") } - closeVal := true + archiveVal := true if len(pk.Args) > 1 { - closeVal = resolveBool(pk.Args[1], true) + archiveVal = resolveBool(pk.Args[1], true) } var update sstore.UpdatePacket - if closeVal { - update, err = sstore.CloseScreen(ctx, ids.SessionId, screenId) + if archiveVal { + update, err = sstore.ArchiveScreen(ctx, ids.SessionId, screenId) if err != nil { return nil, err } return update, nil } else { - fmt.Printf("unclose screen %s\n", screenId) - err = sstore.UnCloseScreen(ctx, ids.SessionId, screenId) + fmt.Printf("unarchive screen %s\n", screenId) + err = sstore.UnArchiveScreen(ctx, ids.SessionId, screenId) if err != nil { - return nil, fmt.Errorf("/screen:close cannot re-open screen: %v", err) + return nil, fmt.Errorf("/screen:archive cannot re-open screen: %v", err) } screen, err := sstore.GetScreenById(ctx, ids.SessionId, screenId) if err != nil { - return nil, fmt.Errorf("/screen:close cannot get updated screen obj: %v", err) + return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err) } update, session := sstore.MakeSingleSessionUpdate(ids.SessionId) session.Screens = append(session.Screens, screen) @@ -427,7 +427,7 @@ func ScreenCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { - return nil, fmt.Errorf("/screen:purge cannot close screen: %w", err) + return nil, fmt.Errorf("/screen:purge cannot purge screen: %w", err) } update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { @@ -949,21 +949,21 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) - screenArr, err := sstore.GetAllSessionScreens(ctx, ids.SessionId) + screenArr, err := sstore.GetSessionScreens(ctx, ids.SessionId) if err != nil { return nil, fmt.Errorf("/screen:showall error getting screen list: %v", err) } var buf bytes.Buffer for _, screen := range screenArr { - var closedStr string + var archivedStr string if screen.Archived { - closedStr = " (closed)" + archivedStr = " (archived)" } screenIdxStr := "-" if screen.ScreenIdx != 0 { screenIdxStr = strconv.Itoa(int(screen.ScreenIdx)) } - outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+closedStr, screen.ScreenId, screenIdxStr) + outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+archivedStr, screen.ScreenId, screenIdxStr) buf.WriteString(outStr) } return sstore.ModelUpdate{ diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1ce6b6593..32e86a050 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -289,7 +289,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { session.Full = true } var screens []*ScreenType - query = `SELECT * FROM screen WHERE NOT archived ORDER BY screenidx` + query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` tx.SelectWrap(&screens, query) screenMap := make(map[string][]*ScreenType) for _, screen := range screens { @@ -358,17 +358,7 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY screenidx` - tx.SelectWrap(&rtn, query, sessionId) - return nil - }) - return rtn, txErr -} - -func GetAllSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { - var rtn []*ScreenType - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx` + query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` tx.SelectWrap(&rtn, query, sessionId) return nil }) @@ -418,8 +408,8 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%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, ownerid, sharemode, accesskey) - VALUES (?, ?, '', ?, ?, 0, '', 'local', '')` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, archivedts, ownerid, sharemode, accesskey) + VALUES (?, ?, '', ?, ?, 0, 0, '', 'local', '')` tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { @@ -528,7 +518,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) newScreenId = scbase.GenPromptUUID() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0)` + query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived, archivedts) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0, 0)` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` @@ -858,14 +848,20 @@ func CleanWindows(sessionId string) { tx.ExecWrap(query, sessionId, windowId) query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) - query = `DELETE FROM line WHERE sessinid = ? AND windowid = ?` + query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) } query = `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` removedCmds := tx.SelectStrings(query, sessionId, sessionId) query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` tx.ExecWrap(query, sessionId, sessionId) + if tx.Err != nil { + return nil + } fmt.Printf("removed cmds: %v\n", removedCmds) + for _, cmdId := range removedCmds { + DeletePtyOutFile(tx.Context(), sessionId, cmdId) + } return nil }) if txErr != nil { @@ -873,7 +869,7 @@ func CleanWindows(sessionId string) { } } -func CloseScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { +func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` @@ -890,8 +886,8 @@ func CloseScreen(ctx context.Context, sessionId string, screenId string) (Update if numScreens <= 1 { return fmt.Errorf("cannot close the last screen in a session") } - query = `UPDATE screen SET archived = 1, screenidx = 0 WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, sessionId, screenId) + query = `UPDATE screen SET archived = 1, archivedts = ?, screenidx = 0 WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(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) @@ -910,7 +906,7 @@ func CloseScreen(ctx context.Context, sessionId string, screenId string) (Update return update, nil } -func UnCloseScreen(ctx context.Context, sessionId string, screenId string) error { +func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND archived` if !tx.Exists(query, sessionId, screenId) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 889626834..a34bb1665 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -153,6 +153,7 @@ type SessionType struct { AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` Archived bool `json:"archived,omitempty"` + ArchivedTs int64 `json:"archivedts,omitempty"` Screens []*ScreenType `json:"screens"` Remotes []*RemoteInstance `json:"remotes"` @@ -358,6 +359,7 @@ type ScreenType struct { ShareMode string `json:"sharemode"` Incognito bool `json:"incognito,omitempty"` Archived bool `json:"archived,omitempty"` + ArchivedTs int64 `json:"archivedts,omitempty"` Windows []*ScreenWindowType `json:"windows"` // only for updates From 6bc948af5d5414a92008ea7442c03a830f01ed05 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 12:18:13 -0800 Subject: [PATCH 215/397] woring on screen archiving --- pkg/sstore/dbops.go | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 32e86a050..5e91ae990 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -515,8 +515,13 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, } newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) - screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) - screenName := fmtUniqueName(origScreenName, "s%d", maxScreenIdx+1, screenNames) + var screenName string + if origScreenName == "" { + screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) + screenName = fmtUniqueName("", "s%d", maxScreenIdx+1, screenNames) + } else { + screenName = origScreenName + } newScreenId = scbase.GenPromptUUID() query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived, archivedts) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0, 0)` tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) @@ -823,7 +828,7 @@ func getNextId(ids []string, delId string) string { func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND NOT archived` + 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) } @@ -884,7 +889,7 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived` numScreens := tx.GetInt(query, sessionId) if numScreens <= 1 { - return fmt.Errorf("cannot close the last screen in a session") + 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.ExecWrap(query, time.Now().UnixMilli(), sessionId, screenId) @@ -912,12 +917,9 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("cannot re-open screen (not found or not archived)") } - origScreenName := tx.GetString(`SELECT name FROM screen WHERE sessionid = ? AND screenid = ?`, sessionId, screenId) maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) - screenNames := tx.SelectStrings(`SELECT name FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) - newScreenName := fmtUniqueName(origScreenName, "s-%d", 2, screenNames) - query = `UPDATE screen SET archived = 0, screenidx = ?, name = ? WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, maxScreenIdx+1, newScreenName, sessionId, screenId) + query = `UPDATE screen SET archived = 0, screenidx = ? WHERE sessionid = ? AND screenid = ?` + tx.ExecWrap(query, maxScreenIdx+1, sessionId, screenId) return nil }) return txErr @@ -1174,14 +1176,6 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("screen does not exist") } - query = `SELECT screenid FROM screen WHERE sessionid = ? AND name = ? AND NOT archived` - dupScreenId := tx.GetString(query, sessionId, name) - if dupScreenId == screenId { - return nil - } - if dupScreenId != "" { - return fmt.Errorf("invalid duplicate screen name '%s'", name) - } query = `UPDATE screen SET name = ? WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, name, sessionId, screenId) return nil From a485294f0bf88640de218545d724ebfd4395901c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 12:38:47 -0800 Subject: [PATCH 216/397] bugfixes for screen archives --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/cmdrunner/resolver.go | 6 +++--- pkg/sstore/dbops.go | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d9710ae96..9209467d0 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -949,7 +949,7 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) - screenArr, err := sstore.GetSessionScreens(ctx, ids.SessionId) + screenArr, err := sstore.GetBareSessionScreens(ctx, ids.SessionId) if err != nil { return nil, fmt.Errorf("/screen:showall error getting screen list: %v", err) } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index c2f1c3752..c719ac317 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -280,7 +280,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i } func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string, curScreenArg string) (*ResolveItem, error) { - screens, err := sstore.GetSessionScreens(ctx, sessionId) + screens, err := sstore.GetBareSessionScreens(ctx, sessionId) if err != nil { return nil, fmt.Errorf("could not retreive screens for session=%s: %v", sessionId, err) } @@ -349,11 +349,11 @@ func genericResolve(arg string, curArg string, items []ResolveItem, isNumeric bo if (isUuid && item.Id == arg) || (tryPuid && strings.HasPrefix(item.Id, arg)) { return &item, nil } - if !item.Hidden && item.Name != "" { + if item.Name != "" { if item.Name == arg { return &item, nil } - if strings.HasPrefix(item.Name, arg) { + if !item.Hidden && strings.HasPrefix(item.Name, arg) { prefixMatches = append(prefixMatches, item) } } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5e91ae990..a6931e2e1 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -354,8 +354,8 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win return rtnWindow, err } -// includes archived screens -func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { +// includes archived screens (does not include screen windows) +func GetBareSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` @@ -907,7 +907,10 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda } update, session := MakeSingleSessionUpdate(sessionId) session.ActiveScreenId = newActiveScreenId - session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) + newScreen, _ := GetScreenById(ctx, sessionId, screenId) + if newScreen != nil { + session.Screens = append(session.Screens, newScreen) + } return update, nil } From af16ab1aed83bb3979c15d560643cf8cc3e0a007 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 16:09:21 -0800 Subject: [PATCH 217/397] working on session archive/delete --- pkg/cmdrunner/cmdrunner.go | 125 +++++++++++++++++-------- pkg/cmdrunner/resolver.go | 13 +++ pkg/sstore/dbops.go | 183 ++++++++++++++++++++++++++++++------- pkg/sstore/fileops.go | 8 ++ pkg/sstore/updatebus.go | 11 --- 5 files changed, 259 insertions(+), 81 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 9209467d0..8d218b209 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -418,8 +418,14 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err) } - update, session := sstore.MakeSingleSessionUpdate(ids.SessionId) - session.Screens = append(session.Screens, screen) + bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("/screen:archive cannot retrieve updated session obj: %v", err) + } + bareSession.Screens = append(bareSession.Screens, screen) + update := sstore.ModelUpdate{ + Sessions: []*sstore.SessionType{bareSession}, + } return update, nil } } @@ -502,11 +508,17 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if err != nil { return nil, err } - update, session := sstore.MakeSingleSessionUpdate(ids.SessionId) - session.Screens = append(session.Screens, screenObj) - update.Info = &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)), - TimeoutMs: 2000, + bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("/screen:set cannot retrieve session: %v", err) + } + bareSession.Screens = append(bareSession.Screens, screenObj) + update := sstore.ModelUpdate{ + Sessions: []*sstore.SessionType{bareSession}, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, } return update, nil } @@ -963,7 +975,7 @@ func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if screen.ScreenIdx != 0 { screenIdxStr = strconv.Itoa(int(screen.ScreenIdx)) } - outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+archivedStr, screen.ScreenId, screenIdxStr) + outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+archivedStr, screen.ScreenId, screenIdxStr) buf.WriteString(outStr) } return sstore.ModelUpdate{ @@ -1379,27 +1391,79 @@ func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } - err = sstore.DeleteSession(ctx, ids.SessionId) + update, err := sstore.DeleteSession(ctx, ids.SessionId) if err != nil { return nil, fmt.Errorf("cannot delete session: %v", err) } - delSession := &sstore.SessionType{SessionId: ids.SessionId, Remove: true} - update := sstore.ModelUpdate{ - Sessions: []*sstore.SessionType{delSession}, - } - activeSessionId, _ := sstore.GetActiveSessionId(ctx) // ignore error - if activeSessionId == "" { - sessionIds, _ := sstore.GetAllSessionIds(ctx) // ignore error, session is already deleted so that's the main return value - if len(sessionIds) > 0 { - err = sstore.SetActiveSessionId(ctx, sessionIds[0]) - if err != nil { - update.ActiveSessionId = sessionIds[0] - } - } - } return update, nil } +func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, 0) // don't force R_Session + if err != nil { + return nil, err + } + sessionId := "" + if len(pk.Args) >= 1 { + ritem, err := resolveSession(ctx, pk.Args[0], ids.SessionId) + if err != nil { + return nil, fmt.Errorf("/session:archive error resolving session %q: %w", pk.Args[0], err) + } + if ritem == nil { + return nil, fmt.Errorf("/session:archive session %q not found", pk.Args[0]) + } + sessionId = ritem.Id + } else { + sessionId = ids.SessionId + } + if sessionId == "" { + return nil, fmt.Errorf("/session:archive no sessionid found") + } + archiveVal := true + if len(pk.Args) >= 2 { + archiveVal = resolveBool(pk.Args[1], true) + } + if archiveVal { + update, err := sstore.ArchiveSession(ctx, sessionId) + if err != nil { + return nil, fmt.Errorf("cannot archive session: %v", err) + } + return update, nil + } else { + update, err := sstore.UnArchiveSession(ctx, sessionId) + if err != nil { + return nil, fmt.Errorf("cannot un-archive session: %v", err) + } + return update, nil + } +} + +func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + sessions, err := sstore.GetBareSessions(ctx) + if err != nil { + return nil, fmt.Errorf("error retrieving sessions: %v", err) + } + var buf bytes.Buffer + for _, session := range sessions { + var archivedStr string + if session.Archived { + archivedStr = " (archived)" + } + sessionIdxStr := "-" + if session.SessionIdx != 0 { + sessionIdxStr = strconv.Itoa(int(session.SessionIdx)) + } + outStr := fmt.Sprintf("%-30s %s %s\n", session.Name+archivedStr, session.SessionId, sessionIdxStr) + buf.WriteString(outStr) + } + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: "all sessions", + InfoLines: splitLinesForInfo(buf.String()), + }, + }, nil +} + func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { @@ -1444,12 +1508,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if firstArg == "" { return nil, fmt.Errorf("usage /session [name|id|pos], no param specified") } - bareSessions, err := sstore.GetBareSessions(ctx) - if err != nil { - return nil, err - } - ritems := sessionsToResolveItems(bareSessions) - ritem, err := genericResolve(firstArg, ids.SessionId, ritems, false, "session") + ritem, err := resolveSession(ctx, firstArg, ids.SessionId) if err != nil { return nil, err } @@ -2069,11 +2128,3 @@ func resolveSetArg(argName string) (bool, string, string) { } return true, scopeName, varName } - -func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil -} - -func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, nil -} diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index c719ac317..1fc63b1f8 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -288,6 +288,19 @@ func resolveSessionScreen(ctx context.Context, sessionId string, screenArg strin return genericResolve(screenArg, curScreenArg, ritems, false, "screen") } +func resolveSession(ctx context.Context, sessionArg string, curSessionArg string) (*ResolveItem, error) { + bareSessions, err := sstore.GetBareSessions(ctx) + if err != nil { + return nil, err + } + ritems := sessionsToResolveItems(bareSessions) + ritem, err := genericResolve(sessionArg, curSessionArg, ritems, false, "session") + if err != nil { + return nil, err + } + return ritem, nil +} + func resolveLine(ctx context.Context, sessionId string, windowId string, lineArg string, curLineArg string) (*ResolveItem, error) { lines, err := sstore.GetLineResolveItems(ctx, sessionId, windowId) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a6931e2e1..543606477 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -237,7 +237,7 @@ func GetHistoryItems(ctx context.Context, sessionId string, windowId string, opt func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM session ORDER BY sessionidx` + query := `SELECT * FROM session ORDER BY archived, sessionidx, archivedts` tx.SelectWrap(&rtn, query) return nil }) @@ -247,8 +247,8 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { return rtn, nil } -// does not include archived -func GetAllSessionIds(ctx context.Context) ([]string, error) { +// does not include archived, finds lowest sessionidx (for resetting active session) +func GetFirstSessionId(ctx context.Context) (string, error) { var rtn []string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid from session WHERE NOT archived ORDER by sessionidx` @@ -256,9 +256,12 @@ func GetAllSessionIds(ctx context.Context) ([]string, error) { return nil }) if txErr != nil { - return nil, txErr + return "", txErr } - return rtn, nil + if len(rtn) == 0 { + return "", nil + } + return rtn[0], nil } func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, error) { @@ -281,7 +284,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { var rtn []*SessionType var activeSessionId string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM session` + query := `SELECT * FROM session ORDER BY archived, sessionidx, archivedts` tx.SelectWrap(&rtn, query) sessionMap := make(map[string]*SessionType) for _, session := range rtn { @@ -439,7 +442,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo func SetActiveSessionId(ctx context.Context, sessionId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` + query := `SELECT sessionid FROM session WHERE sessionid = ?` if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot switch to session, not found") } @@ -507,7 +510,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, txErr := WithTx(ctx, func(tx *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") + return fmt.Errorf("cannot create screen, no session found (or session archived)") } remoteId := tx.GetString(`SELECT remoteid FROM remote WHERE remotealias = ?`, LocalRemoteAlias) if remoteId == "" { @@ -534,16 +537,19 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, } return nil }) + if txErr != nil { + return nil, txErr + } newScreen, err := GetScreenById(ctx, sessionId, newScreenId) if err != nil { return nil, err } - update, session := MakeSingleSessionUpdate(sessionId) - if activate { - session.ActiveScreenId = newScreenId + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, err } - session.Screens = append(session.Screens, newScreen) - return update, txErr + bareSession.Screens = append(bareSession.Screens, newScreen) + return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil } func GetScreenById(ctx context.Context, sessionId string, screenId string) (*ScreenType, error) { @@ -836,9 +842,14 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U tx.ExecWrap(query, screenId, sessionId) return nil }) - update, session := MakeSingleSessionUpdate(sessionId) - session.ActiveScreenId = screenId - return update, txErr + if txErr != nil { + return nil, txErr + } + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, err + } + return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil } func CleanWindows(sessionId string) { @@ -905,11 +916,14 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda if txErr != nil { return nil, txErr } - update, session := MakeSingleSessionUpdate(sessionId) - session.ActiveScreenId = newActiveScreenId + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, txErr + } + update := ModelUpdate{Sessions: []*SessionType{bareSession}} newScreen, _ := GetScreenById(ctx, sessionId, screenId) if newScreen != nil { - session.Screens = append(session.Screens, newScreen) + bareSession.Screens = append(bareSession.Screens, newScreen) } return update, nil } @@ -957,10 +971,12 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat return nil, txErr } go CleanWindows(sessionId) - update, session := MakeSingleSessionUpdate(sessionId) - session.ActiveScreenId = newActiveScreenId - session.Screens = append(session.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) - return update, nil + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, err + } + bareSession.Screens = append(bareSession.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) + return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil } func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, *ShellStatePtr, error) { @@ -1138,7 +1154,7 @@ func reorderStrings(strs []string, toMove string, newIndex int) []string { func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM session ORDER BY sessionidx, name, sessionid` + query := `SELECT sessionid FROM session WHERE NOT archived ORDER BY sessionidx, name, sessionid` ids := tx.SelectStrings(query) if sessionId != "" { ids = reorderStrings(ids, sessionId, newIndex) @@ -1158,13 +1174,17 @@ func SetSessionName(ctx context.Context, sessionId string, name string) error { if !tx.Exists(query, sessionId) { return fmt.Errorf("session does not exist") } - query = `SELECT sessionid FROM session WHERE name = ?` - dupSessionId := tx.GetString(query, name) - if dupSessionId == sessionId { - return nil - } - if dupSessionId != "" { - return fmt.Errorf("invalid duplicate session name '%s'", name) + query = `SELECT archived FROM session WHERE sessionid = ?` + isArchived := tx.GetBool(query, sessionId) + if !isArchived { + query = `SELECT sessionid FROM session WHERE name = ? AND NOT archived` + dupSessionId := tx.GetString(query, name) + if dupSessionId == sessionId { + return nil + } + if dupSessionId != "" { + return fmt.Errorf("invalid duplicate session name '%s'", name) + } } query = `UPDATE session SET name = ? WHERE sessionid = ?` tx.ExecWrap(query, name, sessionId) @@ -1261,8 +1281,105 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term return txErr } -func DeleteSession(ctx context.Context, sessionId string) error { - return nil +func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) { + var newActiveSessionId string + 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") + } + query = `DELETE FROM session WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM screen WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM screen_window WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM window WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM history WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM line WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + query = `DELETE FROM cmd WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + newActiveSessionId, _ = fixActiveSessionId(tx.Context()) + return nil + }) + if txErr != nil { + return nil, txErr + } + delErr := DeleteSessionDir(ctx, sessionId) + update := ModelUpdate{} + if newActiveSessionId != "" { + update.ActiveSessionId = newActiveSessionId + } + if delErr != nil { + update.Info = &InfoMsgType{ + InfoMsg: fmt.Sprintf("error removing session files: %v", delErr), + } + } + update.Sessions = append(update.Sessions, &SessionType{SessionId: sessionId, Remove: true}) + return update, nil +} + +func fixActiveSessionId(ctx context.Context) (string, error) { + var newActiveSessionId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + curActiveSessionId := tx.GetString("SELECT activesessionid FROM client") + query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` + if tx.Exists(query, curActiveSessionId) { + return nil + } + var err error + newActiveSessionId, err = GetFirstSessionId(tx.Context()) + if err != nil { + return err + } + tx.ExecWrap("UPDATE client SET activesessionid = ?", newActiveSessionId) + return nil + }) + if txErr != nil { + return "", txErr + } + return newActiveSessionId, nil +} + +func ArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) { + if sessionId == "" { + return nil, fmt.Errorf("invalid blank sessionid") + } + var newActiveSessionId string + 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") + } + query = `SELECT archived FROM session WHERE sessionid = ?` + isArchived := tx.GetBool(query, sessionId) + if isArchived { + return nil + } + query = `UPDATE session SET archived = 1, archivedts = ? WHERE sessionid = ?` + tx.ExecWrap(query, time.Now().UnixMilli(), sessionId) + newActiveSessionId, _ = fixActiveSessionId(tx.Context()) + return nil + }) + if txErr != nil { + return nil, txErr + } + bareSession, _ := GetBareSessionById(ctx, sessionId) + update := ModelUpdate{} + if bareSession != nil { + update.Sessions = append(update.Sessions, bareSession) + } + if newActiveSessionId != "" { + update.ActiveSessionId = newActiveSessionId + } + return update, nil +} + +func UnArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) { + return nil, nil } func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, error) { diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 5fc689bf5..164e96239 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -135,3 +135,11 @@ func DeletePtyOutFile(ctx context.Context, sessionId string, cmdId string) error } return os.Remove(ptyOutFileName) } + +func DeleteSessionDir(ctx context.Context, sessionId string) error { + sessionDir, err := base.EnsureSessionDir(sessionId) + if err != nil { + return fmt.Errorf("error getting sessiondir: %w", err) + } + return os.RemoveAll(sessionDir) +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 302064517..4c5ba2aab 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -49,17 +49,6 @@ func (ModelUpdate) UpdateType() string { return ModelUpdateStr } -func MakeSingleSessionUpdate(sessionId string) (ModelUpdate, *SessionType) { - session := &SessionType{ - SessionId: sessionId, - NotifyNum: -1, - } - update := ModelUpdate{ - Sessions: []*SessionType{session}, - } - return update, session -} - func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { modelUpdate, ok := update.(ModelUpdate) if !ok { From 5933287e0db82c9b413c71fd2c2103a345eda801 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 18:42:55 -0800 Subject: [PATCH 218/397] unarchive session --- pkg/cmdrunner/cmdrunner.go | 9 ++++++++- pkg/sstore/dbops.go | 40 ++++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8d218b209..340a0f1ea 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1428,12 +1428,19 @@ func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot archive session: %v", err) } + update.Info = &sstore.InfoMsgType{ + InfoMsg: "session archived", + } return update, nil } else { - update, err := sstore.UnArchiveSession(ctx, sessionId) + activate := resolveBool(pk.Kwargs["activate"], false) + update, err := sstore.UnArchiveSession(ctx, sessionId, activate) if err != nil { return nil, fmt.Errorf("cannot un-archive session: %v", err) } + update.Info = &sstore.InfoMsgType{ + InfoMsg: "session un-archived", + } return update, nil } } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 543606477..c293462b4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1344,7 +1344,7 @@ func fixActiveSessionId(ctx context.Context) (string, error) { return newActiveSessionId, nil } -func ArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) { +func ArchiveSession(ctx context.Context, sessionId string) (*ModelUpdate, error) { if sessionId == "" { return nil, fmt.Errorf("invalid blank sessionid") } @@ -1368,7 +1368,7 @@ func ArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) return nil, txErr } bareSession, _ := GetBareSessionById(ctx, sessionId) - update := ModelUpdate{} + update := &ModelUpdate{} if bareSession != nil { update.Sessions = append(update.Sessions, bareSession) } @@ -1378,8 +1378,40 @@ func ArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) return update, nil } -func UnArchiveSession(ctx context.Context, sessionId string) (UpdatePacket, error) { - return nil, nil +func UnArchiveSession(ctx context.Context, sessionId string, activate bool) (*ModelUpdate, error) { + if sessionId == "" { + return nil, fmt.Errorf("invalid blank sessionid") + } + 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") + } + query = `SELECT archived FROM session WHERE sessionid = ?` + isArchived := tx.GetBool(query, sessionId) + if !isArchived { + return nil + } + query = `UPDATE session SET archived = 0, archivedts = 0 WHERE sessionid = ?` + tx.ExecWrap(query, sessionId) + if activate { + query = `UPDATE client SET activesessionid = ?` + tx.ExecWrap(query, sessionId) + } + return nil + }) + if txErr != nil { + return nil, txErr + } + bareSession, _ := GetBareSessionById(ctx, sessionId) + update := &ModelUpdate{} + if bareSession != nil { + update.Sessions = append(update.Sessions, bareSession) + } + if activate { + update.ActiveSessionId = sessionId + } + return update, nil } func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, error) { From 4974e9405f6bbecf2b348ee23ffbd479b1bf91ee Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 18:49:45 -0800 Subject: [PATCH 219/397] use scbase not base for fileops --- pkg/scbase/scbase.go | 6 ++++++ pkg/sstore/fileops.go | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 8199654b9..3bcd8bde4 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -146,6 +146,12 @@ func EnsureSessionDir(sessionId string) (string, error) { return sdir, nil } +func GetSessionsDir() string { + promptHome := GetPromptHomeDir() + sdir := path.Join(promptHome, SessionsDirBaseName) + return sdir +} + func ensureDir(dirName string) error { info, err := os.Stat(dirName) if errors.Is(err, fs.ErrNotExist) { diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 164e96239..016c84137 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -8,7 +8,6 @@ import ( "path" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/cirfile" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) @@ -96,7 +95,7 @@ func directorySize(dirName string) (SessionDiskSizeType, error) { } func SessionDiskSize(sessionId string) (SessionDiskSizeType, error) { - sessionDir, err := base.EnsureSessionDir(sessionId) + sessionDir, err := scbase.EnsureSessionDir(sessionId) if err != nil { return SessionDiskSizeType{}, err } @@ -104,7 +103,7 @@ func SessionDiskSize(sessionId string) (SessionDiskSizeType, error) { } func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) { - sdir := base.GetSessionsDir() + sdir := scbase.GetSessionsDir() entries, err := os.ReadDir(sdir) if err != nil { return nil, err @@ -137,9 +136,10 @@ func DeletePtyOutFile(ctx context.Context, sessionId string, cmdId string) error } func DeleteSessionDir(ctx context.Context, sessionId string) error { - sessionDir, err := base.EnsureSessionDir(sessionId) + sessionDir, err := scbase.EnsureSessionDir(sessionId) if err != nil { return fmt.Errorf("error getting sessiondir: %w", err) } + fmt.Printf("remove-all %s\n", sessionDir) return os.RemoveAll(sessionDir) } From 73bc162824f50afd4763ba8cb8914cf8a264b722 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 26 Dec 2022 19:06:46 -0800 Subject: [PATCH 220/397] session:show command --- pkg/cmdrunner/cmdrunner.go | 45 ++++++++++++++++++++++++++++++++++++++ pkg/sstore/fileops.go | 2 ++ 2 files changed, 47 insertions(+) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 340a0f1ea..e22ef7d56 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -122,6 +122,7 @@ func init() { registerCmdFn("session:delete", SessionDeleteCommand) registerCmdFn("session:archive", SessionArchiveCommand) registerCmdFn("session:showall", SessionShowAllCommand) + registerCmdFn("session:show", SessionShowCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:archive", ScreenArchiveCommand) @@ -1445,6 +1446,50 @@ func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } } +func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session) + if err != nil { + return nil, err + } + session, err := sstore.GetSessionById(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("cannot get session: %w", err) + } + if session == nil { + return nil, fmt.Errorf("session not found") + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "sessionid", session.SessionId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "name", session.Name)) + if session.SessionIdx != 0 { + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "index", session.SessionIdx)) + } + if session.Archived { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archived", "true")) + ts := time.UnixMilli(session.ArchivedTs) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archivedts", ts.Format("2006-01-02 15:04:05"))) + } + stats, err := sstore.GetSessionStats(ctx, ids.SessionId) + if err != nil { + return nil, fmt.Errorf("error getting session stats: %w", err) + } + var screenArchiveStr string + if stats.NumArchivedScreens > 0 { + screenArchiveStr = fmt.Sprintf(" (%d archived)", stats.NumArchivedScreens) + } + buf.WriteString(fmt.Sprintf(" %-15s %d%s\n", "screens", stats.NumScreens, screenArchiveStr)) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "lines", stats.NumLines)) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "cmds", stats.NumCmds)) + buf.WriteString(fmt.Sprintf(" %-15s %0.2fM\n", "disksize", float64(stats.DiskStats.TotalSize)/1000000)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "disk-location", stats.DiskStats.Location)) + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: "session info", + InfoLines: splitLinesForInfo(buf.String()), + }, + }, nil +} + func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { sessions, err := sstore.GetBareSessions(ctx) if err != nil { diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 016c84137..87b91e628 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -70,10 +70,12 @@ type SessionDiskSizeType struct { NumFiles int TotalSize int64 ErrorCount int + Location string } func directorySize(dirName string) (SessionDiskSizeType, error) { var rtn SessionDiskSizeType + rtn.Location = dirName entries, err := os.ReadDir(dirName) if err != nil { return rtn, err From 43689ac3f8b5879d1b81fc23813c9d23ac048a90 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 27 Dec 2022 23:12:27 -0800 Subject: [PATCH 221/397] line archived (not line:hidden). remove some debugging code --- db/migrations/000001_init.up.sql | 2 +- pkg/cmdrunner/cmdrunner.go | 16 ++++++++-------- pkg/cmdrunner/termopts.go | 4 ++-- pkg/comp/comp.go | 1 - pkg/shparse/comp.go | 2 -- pkg/sstore/dbops.go | 10 +++++----- pkg/sstore/sstore.go | 2 +- 7 files changed, 17 insertions(+), 20 deletions(-) diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 4c1e02721..e13caed8f 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -103,7 +103,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, - hidden boolean NOT NULL, + archived boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e22ef7d56..6729db5b1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -152,7 +152,7 @@ func init() { registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) registerCmdFn("line:star", LineStarCommand) - registerCmdFn("line:hide", LineHideCommand) + registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("_history", HistoryCommand) @@ -1781,13 +1781,13 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return sstore.ModelUpdate{Line: lineObj}, nil } -func LineHideCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, err } if len(pk.Args) == 0 { - return nil, fmt.Errorf("/line:hide requires an argument (line number or id)") + return nil, fmt.Errorf("/line:archive requires an argument (line number or id)") } lineArg := pk.Args[0] lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) @@ -1797,17 +1797,17 @@ func LineHideCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - shouldHide := true + shouldArchive := true if len(pk.Args) >= 2 { - shouldHide = resolveBool(pk.Args[1], true) + shouldArchive = resolveBool(pk.Args[1], true) } - err = sstore.SetLineHiddenById(ctx, lineId, shouldHide) + err = sstore.SetLineArchivedById(ctx, lineId, shouldArchive) if err != nil { - return nil, fmt.Errorf("/line:hide error updating hidden status: %v", err) + return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err) } lineObj, err := sstore.GetLineById(ctx, lineId) if err != nil { - return nil, fmt.Errorf("/line:star error getting line: %v", err) + return nil, fmt.Errorf("/line:archive error getting line: %v", err) } if lineObj == nil { // no line (which is strange given we checked for it above). just return a nop. diff --git a/pkg/cmdrunner/termopts.go b/pkg/cmdrunner/termopts.go index 34c35f59e..e5c87a855 100644 --- a/pkg/cmdrunner/termopts.go +++ b/pkg/cmdrunner/termopts.go @@ -11,8 +11,8 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/remote" ) -// PTERM=WxH,Wx25 -// PTERM="Wx25!" +// PTERM=MxM,Mx25 +// PTERM="Mx25!" // PTERM=80x25,80x35 type PTermOptsType struct { diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 245254799..8dc8f457e 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -525,7 +525,6 @@ func DoCompGen(ctx context.Context, cmdStr utilfn.StrWithPos, compCtx CompContex words := shparse.Tokenize(cmdStr.Str) cmds := shparse.ParseCommands(words) compPos := shparse.FindCompletionPos(cmds, cmdStr.Pos) - fmt.Printf("comppos: %v\n", compPos) if compPos.CompType == shparse.CompTypeInvalid { return nil, nil, nil } diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index fbc6065c1..f33e1ad56 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -1,7 +1,6 @@ package shparse import ( - "fmt" "strings" "github.com/scripthaus-dev/sh2-server/pkg/utilfn" @@ -278,7 +277,6 @@ func (cpos CompletionPos) Extend(origStr utilfn.StrWithPos, extensionStr string, compWord = MakeEmptyWord(WordTypeLit, nil, cpos.RawPos, true) } realOffset := compWord.Offset + cpos.SuperOffset - fmt.Printf("cpos-extend: %d[%s] ext[%s] cword[%v] off:%d super:%d real:%d\n", len([]rune(origStr.Str)), origStr, extensionStr, compWord, compWord.Offset, cpos.SuperOffset, realOffset) if strings.HasSuffix(extensionStr, "/") { extensionComplete = false } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c293462b4..1f30fb347 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -687,8 +687,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star, hidden) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star,:hidden)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star, archived) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star,:archived)` tx.NamedExecWrap(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) @@ -1741,10 +1741,10 @@ func GetLineById(ctx context.Context, lineId string) (*LineType, error) { return rtn, nil } -func SetLineHiddenById(ctx context.Context, lineId string, hidden bool) error { +func SetLineArchivedById(ctx context.Context, lineId string, archived bool) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET hidden = ? WHERE lineid = ?` - tx.ExecWrap(query, hidden, lineId) + query := `UPDATE line SET archived = ? WHERE lineid = ?` + tx.ExecWrap(query, archived, lineId) return nil }) return txErr diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index a34bb1665..eadecb222 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -590,7 +590,7 @@ type LineType struct { Ephemeral bool `json:"ephemeral,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` - Hidden bool `json:"hidden,omitempty"` + Archived bool `json:"archived,omitempty"` Remove bool `json:"remove,omitempty"` } From 16b808bb21edddab72a492b8ed062979b3cd7fad Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 28 Dec 2022 13:56:19 -0800 Subject: [PATCH 222/397] working on autoconnect --- cmd/main-server.go | 4 +++- pkg/cmdrunner/resolver.go | 10 ++++++++-- pkg/remote/remote.go | 28 ++++++++++++++++++++++++++-- pkg/scbase/scbase.go | 10 +++++++++- pkg/scws/scws.go | 4 ++-- pkg/sstore/dbops.go | 7 ++----- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index b6c27b0e1..38b622c2e 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -35,8 +35,10 @@ const HttpWriteTimeout = 21 * time.Second const HttpMaxHeaderBytes = 60000 const HttpTimeoutDuration = 21 * time.Second -const WebSocketServerAddr = "localhost:8081" const MainServerAddr = "localhost:8080" +const WebSocketServerAddr = "localhost:8081" +const MainServerDevAddr = "localhost:8090" +const WebSocketServerDevAddr = "localhost:8091" const WSStateReconnectTime = 30 * time.Second const WSStatePacketChSize = 20 diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 1fc63b1f8..e70135b28 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -270,10 +270,16 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i } if rtype&R_RemoteConnected > 0 { if !rtn.Remote.RState.IsConnected() { - return rtn, fmt.Errorf("remote '%s' is not connected", rtn.Remote.DisplayName) + err = rtn.Remote.MShell.TryAutoConnect() + if err != nil { + return rtn, fmt.Errorf("error trying to auto-connect remote %q", rtn.Remote.DisplayName) + } + } + if !rtn.Remote.RState.IsConnected() { + return rtn, fmt.Errorf("remote %q is not connected", rtn.Remote.DisplayName) } if rtn.Remote.StatePtr == nil || rtn.Remote.FeState == nil { - return rtn, fmt.Errorf("remote '%s' state is not available", rtn.Remote.DisplayName) + return rtn, fmt.Errorf("remote %q state is not available", rtn.Remote.DisplayName) } } return rtn, nil diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c2eee2fdb..8b30aea7f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -93,6 +93,7 @@ type MShellProc struct { MakeClientCancelFn context.CancelFunc StateMap map[string]*packet.ShellState // sha1->state CurrentState string // sha1 + NumTryConnect int // install InstallStatus string @@ -247,7 +248,7 @@ func LoadRemoteById(ctx context.Context, remoteId string) error { defer GlobalStore.Lock.Unlock() existingRemote := GlobalStore.Map[remoteId] if existingRemote != nil { - return fmt.Errorf("cannot add remote %d, already in global map", remoteId) + return fmt.Errorf("cannot add remote %s, already in global map", remoteId) } GlobalStore.Map[r.RemoteId] = msh if r.ConnectMode == sstore.ConnectModeStartup { @@ -1234,7 +1235,7 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) } if line == nil { - return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC, windowId) + return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC) } return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum) } @@ -1821,3 +1822,26 @@ func (msh *MShellProc) getFeStateFromDiff(stateDiff *packet.ShellStateDiff) (*ss return sstore.FeStateFromShellState(&newState), nil } } + +func (msh *MShellProc) TryAutoConnect() error { + if msh.IsConnected() { + return nil + } + rcopy := msh.GetRemoteCopy() + if !rcopy.AutoInstall { + return nil + } + var err error + msh.WithLock(func() { + if msh.NumTryConnect > 5 { + err = fmt.Errorf("cannot auto-connect remote %q (too many unsuccessful tries)", msh.Remote.GetName()) + return + } + msh.NumTryConnect++ + }) + if err != nil { + return err + } + msh.Launch() + return nil +} diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 3bcd8bde4..b70289077 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -19,9 +19,11 @@ import ( const HomeVarName = "HOME" const PromptHomeVarName = "PROMPT_HOME" +const PromptDevVarName = "PROMPT_DEV" const SessionsDirBaseName = "sessions" const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" +const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" const PromptVersion = "v0.1.0" const PromptAuthKeyFileName = "prompt.authkey" @@ -37,7 +39,13 @@ func GetPromptHomeDir() string { if homeVar == "" { homeVar = "/" } - scHome = path.Join(homeVar, PromptDirName) + pdev := os.Getenv(PromptDevVarName) + if pdev != "" { + scHome = path.Join(homeVar, PromptDevDirName) + } else { + scHome = path.Join(homeVar, PromptDirName) + } + } return scHome } diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 34a1cd5a8..b84fb4ba6 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -208,7 +208,7 @@ func (ws *WSState) RunWSRead() { err := ws.handleWatchScreen(wsPk) if err != nil { // TODO send errors back to client, likely unrecoverable - log.Printf("[ws %s] error %v\n", err) + log.Printf("[ws %s] error %v\n", ws.ClientId, err) } continue } @@ -264,7 +264,7 @@ func sendCmdInput(pk *scpacket.FeInputPacketType) error { } msh := remote.GetRemoteById(pk.Remote.RemoteId) if msh == nil { - return fmt.Errorf("remote %d not found", pk.Remote.RemoteId) + return fmt.Errorf("remote %s not found", pk.Remote.RemoteId) } if len(pk.InputData64) > 0 { inputLen := packet.B64DecodedLen(pk.InputData64) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1f30fb347..5e81c9794 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -886,7 +886,6 @@ func CleanWindows(sessionId string) { } func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { - var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, sessionId, screenId) { @@ -909,7 +908,6 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) - newActiveScreenId = nextId } return nil }) @@ -943,7 +941,6 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err } func DeleteScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { - var newActiveScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, sessionId, screenId) { @@ -959,7 +956,6 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) - newActiveScreenId = nextId } query = `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` tx.ExecWrap(query, sessionId, screenId) @@ -976,7 +972,8 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat return nil, err } bareSession.Screens = append(bareSession.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) - return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil + update := ModelUpdate{Sessions: []*SessionType{bareSession}} + return update, nil } func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, *ShellStatePtr, error) { From e1e1a52e060b98fb23bed4f2bf3c4d3abec3b05c Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 28 Dec 2022 16:59:54 -0800 Subject: [PATCH 223/397] autoconnect remotes --- pkg/cmdrunner/resolver.go | 7 ++++++- pkg/remote/remote.go | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index e70135b28..264d5be8f 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -272,8 +272,13 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if !rtn.Remote.RState.IsConnected() { err = rtn.Remote.MShell.TryAutoConnect() if err != nil { - return rtn, fmt.Errorf("error trying to auto-connect remote %q", rtn.Remote.DisplayName) + return rtn, fmt.Errorf("error trying to auto-connect remote %q: %w", rtn.Remote.DisplayName, err) } + rrNew, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.WindowId) + if err != nil { + return rtn, err + } + rtn.Remote = rrNew } if !rtn.Remote.RState.IsConnected() { return rtn, fmt.Errorf("remote %q is not connected", rtn.Remote.DisplayName) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 8b30aea7f..f23316227 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1828,13 +1828,13 @@ func (msh *MShellProc) TryAutoConnect() error { return nil } rcopy := msh.GetRemoteCopy() - if !rcopy.AutoInstall { + if rcopy.ConnectMode == sstore.ConnectModeManual { return nil } var err error msh.WithLock(func() { if msh.NumTryConnect > 5 { - err = fmt.Errorf("cannot auto-connect remote %q (too many unsuccessful tries)", msh.Remote.GetName()) + err = fmt.Errorf("too many unsuccessful tries") return } msh.NumTryConnect++ @@ -1843,5 +1843,8 @@ func (msh *MShellProc) TryAutoConnect() error { return err } msh.Launch() + if !msh.IsConnected() { + return fmt.Errorf("error connecting") + } return nil } From 9ab5d1e5298f5423b2024e1afe6c282c4e2d9f9c Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 28 Dec 2022 17:47:12 -0800 Subject: [PATCH 224/397] implement devmode with alternate directory/ports so main/dev can be run simultaneously --- cmd/main-server.go | 16 ++++++++++++---- pkg/scbase/scbase.go | 5 +++++ pkg/sstore/sstore.go | 3 +++ scripthaus.md | 10 +++++----- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 38b622c2e..4e8dfc7eb 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -348,15 +348,19 @@ func AuthKeyWrap(fn WebFnType) WebFnType { func runWebSocketServer() { gr := mux.NewRouter() gr.HandleFunc("/ws", HandleWs) + serverAddr := WebSocketServerAddr + if scbase.IsDevMode() { + serverAddr = WebSocketServerDevAddr + } server := &http.Server{ - Addr: WebSocketServerAddr, + Addr: serverAddr, ReadTimeout: HttpReadTimeout, WriteTimeout: HttpWriteTimeout, MaxHeaderBytes: HttpMaxHeaderBytes, Handler: gr, } server.SetKeepAlivesEnabled(false) - log.Printf("Running websocket server on %s\n", WebSocketServerAddr) + log.Printf("Running websocket server on %s\n", serverAddr) err := server.ListenAndServe() if err != nil { log.Printf("[error] trying to run websocket server: %v\n", err) @@ -453,15 +457,19 @@ func main() { gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) + serverAddr := MainServerAddr + if scbase.IsDevMode() { + serverAddr = MainServerDevAddr + } server := &http.Server{ - Addr: MainServerAddr, + Addr: serverAddr, ReadTimeout: HttpReadTimeout, WriteTimeout: HttpWriteTimeout, MaxHeaderBytes: HttpMaxHeaderBytes, Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"), } server.SetKeepAlivesEnabled(false) - log.Printf("Running main server on %s\n", MainServerAddr) + log.Printf("Running main server on %s\n", serverAddr) err = server.ListenAndServe() if err != nil { log.Printf("ERROR: %v\n", err) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index b70289077..edc2827c1 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -31,6 +31,11 @@ const PromptAuthKeyFileName = "prompt.authkey" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} +func IsDevMode() bool { + pdev := os.Getenv(PromptDevVarName) + return pdev != "" +} + // must match js func GetPromptHomeDir() string { scHome := os.Getenv(PromptHomeVarName) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index eadecb222..4277c6cfb 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -94,6 +94,9 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) if globalDBErr != nil { globalDBErr = fmt.Errorf("opening db[%s]: %w", dbName, globalDBErr) + log.Printf("[db] error: %v\n", globalDBErr) + } else { + log.Printf("[db] successfully opened db %s\n", dbName) } } return globalDB, globalDBErr diff --git a/scripthaus.md b/scripthaus.md index 563af253d..88dc22bd3 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -1,16 +1,16 @@ # SH2 Server Commands ```bash -# @scripthaus command dump-schema -sqlite3 /Users/mike/prompt/prompt.db .schema > db/schema.sql +# @scripthaus command dump-schema-dev +sqlite3 /Users/mike/prompt-dev/prompt.db .schema > db/schema.sql ``` ```bash -# @scripthaus command opendb -sqlite3 /Users/mike/prompt/prompt.db +# @scripthaus command opendb-dev +sqlite3 /Users/mike/prompt-dev/prompt.db ``` ```bash # @scripthaus command build -go build -o ~/prompt/local-server cmd/main-server.go +go build -o ~/prompt-dev/local-server cmd/main-server.go ``` From f5e0801978b86c53c169c81f9e0e1a0d2f360aa7 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 28 Dec 2022 23:09:37 -0800 Subject: [PATCH 225/397] update local mshell binary path -- use the mshell packaged with Prompt app --- pkg/remote/remote.go | 42 +++++++++++++++++++++++++++++------------- pkg/scbase/scbase.go | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index f23316227..15566815f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -39,9 +39,6 @@ const RemoteTermRows = 8 const RemoteTermCols = 80 const PtyReadBufSize = 100 -const MShellVersion = "v0.2.0" -const MShellVersionConstraint = "^0.2" - const MShellServerCommandFmt = ` PATH=$PATH:~/.mshell; which mshell-[%VERSION%] > /dev/null; @@ -53,8 +50,16 @@ else fi ` +func MakeLocalMShellCommandStr() (string, error) { + mshellPath, err := scbase.LocalMShellBinaryPath() + if err != nil { + return "", err + } + return fmt.Sprintf("%s --server", mshellPath), nil +} + func MakeServerCommandStr() string { - return strings.ReplaceAll(MShellServerCommandFmt, "[%VERSION%]", semver.MajorMinor(base.MShellVersion)) + return strings.ReplaceAll(MShellServerCommandFmt, "[%VERSION%]", semver.MajorMinor(scbase.MShellVersion)) } const ( @@ -65,8 +70,8 @@ const ( ) func init() { - if MShellVersion != base.MShellVersion { - panic(fmt.Sprintf("sh2-server mshell version must match '%s' vs '%s'", MShellVersion, base.MShellVersion)) + if scbase.MShellVersion != base.MShellVersion { + panic(fmt.Sprintf("sh2-server mshell version must match '%s' vs '%s'", scbase.MShellVersion, base.MShellVersion)) } } @@ -871,7 +876,7 @@ func (msh *MShellProc) RunInstall() { msh.WriteToPtyBuffer("*error: cannot install on remote that is already trying to install, cancel current install to try again\n") return } - msh.WriteToPtyBuffer("installing mshell %s to %s...\n", MShellVersion, remoteCopy.RemoteCanonicalName) + msh.WriteToPtyBuffer("installing mshell %s to %s...\n", scbase.MShellVersion, remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true cmdStr := shexec.MakeInstallCommandStr() @@ -900,7 +905,7 @@ func (msh *MShellProc) RunInstall() { msgFn := func(msg string) { msh.WriteToPtyBuffer("%s", msg) } - err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, nil, scbase.MShellBinaryFromPackage, msgFn) + err = shexec.RunInstallFromCmd(clientCtx, ecmd, true, nil, scbase.MShellBinaryReader, msgFn) if err == context.Canceled { msh.WriteToPtyBuffer("*install canceled\n") msh.WithLock(func() { @@ -919,7 +924,7 @@ func (msh *MShellProc) RunInstall() { msh.InstallCancelFn = nil msh.NeedsMShellUpgrade = false }) - msh.WriteToPtyBuffer("successfully installed mshell %s\n", MShellVersion) + msh.WriteToPtyBuffer("successfully installed mshell %s\n", scbase.MShellVersion) go msh.NotifyRemoteUpdate() return } @@ -1018,7 +1023,18 @@ func (msh *MShellProc) Launch() { if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" { sshOpts.BatchMode = true } - cmdStr := MakeServerCommandStr() + var cmdStr string + if sshOpts.SSHHost == "" { + var err error + cmdStr, err = MakeLocalMShellCommandStr() + if err != nil { + msh.WriteToPtyBuffer("*error, cannot find local mshell binary: %v\n", err) + return + } + log.Printf("local mshell binary: %s\n", cmdStr) + } else { + cmdStr = MakeServerCommandStr() + } ecmd := sshOpts.MakeSSHExecCmd(cmdStr) cmdPty, err := msh.addControllingTty(ecmd) if err != nil { @@ -1053,7 +1069,7 @@ func (msh *MShellProc) Launch() { if initPk != nil { msh.UName = initPk.UName mshellVersion = initPk.Version - if semver.Compare(mshellVersion, MShellVersion) < 0 { + if semver.Compare(mshellVersion, scbase.MShellVersion) < 0 { // only set NeedsMShellUpgrade if we got an InitPk msh.NeedsMShellUpgrade = true } @@ -1077,8 +1093,8 @@ func (msh *MShellProc) Launch() { }) return } - if err == nil && semver.MajorMinor(mshellVersion) != semver.MajorMinor(MShellVersion) { - err = fmt.Errorf("mshell version is not compatible current=%s remote=%s", MShellVersion, mshellVersion) + if err == nil && semver.MajorMinor(mshellVersion) != semver.MajorMinor(scbase.MShellVersion) { + err = fmt.Errorf("mshell version is not compatible current=%s remote=%s", scbase.MShellVersion, mshellVersion) } if err != nil { msh.setErrorStatus(err) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index edc2827c1..cc2789164 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -8,6 +8,7 @@ import ( "log" "os" "path" + "runtime" "strconv" "sync" @@ -27,6 +28,7 @@ const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" const PromptVersion = "v0.1.0" const PromptAuthKeyFileName = "prompt.authkey" +const MShellVersion = "v0.2.0" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} @@ -55,24 +57,43 @@ func GetPromptHomeDir() string { return scHome } -func MShellBinaryFromPackage(version string, goos string, goarch string) (io.ReadCloser, error) { +func MShellBinaryDir() string { appPath := os.Getenv(PromptAppPathVarName) if appPath == "" { - return base.MShellBinaryFromOptDir(version, goos, goarch) + appPath = "." } + if IsDevMode() { + return path.Join(appPath, "dev-bin") + } + return path.Join(appPath, "bin", "mshell") +} + +func MShellBinaryPath(version string, goos string, goarch string) (string, error) { if !base.ValidGoArch(goos, goarch) { - return nil, fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch) + return "", fmt.Errorf("invalid goos/goarch combination: %s/%s", goos, goarch) } + binaryDir := MShellBinaryDir() versionStr := semver.MajorMinor(version) if versionStr == "" { - return nil, fmt.Errorf("invalid mshell version: %q", version) + return "", fmt.Errorf("invalid mshell version: %q", version) } fileName := fmt.Sprintf("mshell-%s-%s.%s", versionStr, goos, goarch) - fullFileName := path.Join(appPath, "bin", "mshell", fileName) - log.Printf("mshell-binary %q\n", fullFileName) - fd, err := os.Open(fullFileName) + fullFileName := path.Join(binaryDir, fileName) + return fullFileName, nil +} + +func LocalMShellBinaryPath() (string, error) { + return MShellBinaryPath(MShellVersion, runtime.GOOS, runtime.GOARCH) +} + +func MShellBinaryReader(version string, goos string, goarch string) (io.ReadCloser, error) { + mshellPath, err := MShellBinaryPath(version, goos, goarch) if err != nil { - return nil, fmt.Errorf("cannot open mshell binary %q: %v", fullFileName, err) + return nil, err + } + fd, err := os.Open(mshellPath) + if err != nil { + return nil, fmt.Errorf("cannot open mshell binary %q: %v", mshellPath, err) } return fd, nil } From 2f7aa944050b22c1ba94eeccd466712c6e095bc2 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 29 Dec 2022 00:07:16 -0800 Subject: [PATCH 226/397] sudo remote and colors --- pkg/remote/remote.go | 75 +++++++++++++++++++++++++++++--------------- pkg/sstore/sstore.go | 20 ++++++++++++ 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 15566815f..1267ede85 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -50,12 +50,16 @@ else fi ` -func MakeLocalMShellCommandStr() (string, error) { +func MakeLocalMShellCommandStr(isSudo bool) (string, error) { mshellPath, err := scbase.LocalMShellBinaryPath() if err != nil { return "", err } - return fmt.Sprintf("%s --server", mshellPath), nil + if isSudo { + return fmt.Sprintf("sudo %s --server", mshellPath), nil + } else { + return fmt.Sprintf("%s --server", mshellPath), nil + } } func MakeServerCommandStr() string { @@ -119,26 +123,27 @@ type RunCmdType struct { } type RemoteRuntimeState struct { - RemoteType string `json:"remotetype"` - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalremoteid"` - RemoteAlias string `json:"remotealias,omitempty"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteVars map[string]string `json:"remotevars"` - DefaultFeState *sstore.FeStateType `json:"defaultfestate"` - Status string `json:"status"` - ErrorStr string `json:"errorstr,omitempty"` - InstallStatus string `json:"installstatus"` - InstallErrorStr string `json:"installerrorstr,omitempty"` - NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - Archived bool `json:"archived,omitempty"` - RemoteIdx int64 `json:"remoteidx"` - UName string `json:"uname"` - MShellVersion string `json:"mshellversion"` - WaitingForPassword bool `json:"waitingforpassword,omitempty"` - Local bool `json:"local,omitempty"` + RemoteType string `json:"remotetype"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalremoteid"` + RemoteAlias string `json:"remotealias,omitempty"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteVars map[string]string `json:"remotevars"` + DefaultFeState *sstore.FeStateType `json:"defaultfestate"` + Status string `json:"status"` + ErrorStr string `json:"errorstr,omitempty"` + InstallStatus string `json:"installstatus"` + InstallErrorStr string `json:"installerrorstr,omitempty"` + NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + Archived bool `json:"archived,omitempty"` + RemoteIdx int64 `json:"remoteidx"` + UName string `json:"uname"` + MShellVersion string `json:"mshellversion"` + WaitingForPassword bool `json:"waitingforpassword,omitempty"` + Local bool `json:"local,omitempty"` + RemoteOpts *sstore.RemoteOptsType `json:"remoteopts,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { @@ -221,6 +226,7 @@ func LoadRemotes(ctx context.Context) error { return err } var numLocal int + var numSudoLocal int for _, remote := range allRemotes { msh := MakeMShell(remote) GlobalStore.Map[remote.RemoteId] = msh @@ -228,7 +234,11 @@ func LoadRemotes(ctx context.Context) error { go msh.Launch() } if remote.Local { - numLocal++ + if remote.RemoteSudo { + numSudoLocal++ + } else { + numLocal++ + } } } if numLocal == 0 { @@ -237,6 +247,9 @@ func LoadRemotes(ctx context.Context) error { if numLocal > 1 { return fmt.Errorf("multiple local remotes found") } + if numSudoLocal > 1 { + return fmt.Errorf("multiple local sudo remotes found") + } return nil } @@ -378,7 +391,7 @@ func GetLocalRemote() *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() for _, msh := range GlobalStore.Map { - if msh.IsLocal() { + if msh.IsLocal() && !msh.IsSudo() { return msh } } @@ -457,6 +470,12 @@ func (msh *MShellProc) IsLocal() bool { return msh.Remote.Local } +func (msh *MShellProc) IsSudo() bool { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.Remote.RemoteSudo +} + func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -476,6 +495,10 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { NeedsMShellUpgrade: msh.NeedsMShellUpgrade, Local: msh.Remote.Local, } + if msh.Remote.RemoteOpts != nil { + optsCopy := *msh.Remote.RemoteOpts + state.RemoteOpts = &optsCopy + } if msh.Err != nil { state.ErrorStr = msh.Err.Error() } @@ -1024,9 +1047,9 @@ func (msh *MShellProc) Launch() { sshOpts.BatchMode = true } var cmdStr string - if sshOpts.SSHHost == "" { + if sshOpts.SSHHost == "" && remoteCopy.Local { var err error - cmdStr, err = MakeLocalMShellCommandStr() + cmdStr, err = MakeLocalMShellCommandStr(remoteCopy.RemoteSudo) if err != nil { msh.WriteToPtyBuffer("*error, cannot find local mshell binary: %v\n", err) return diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 4277c6cfb..fa82889d3 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -856,6 +856,26 @@ func EnsureLocalRemote(ctx context.Context) error { return err } log.Printf("[db] added local remote '%s', id=%s\n", localRemote.RemoteCanonicalName, localRemote.RemoteId) + sudoRemote := &RemoteType{ + RemoteId: scbase.GenPromptUUID(), + PhysicalId: "", + RemoteType: RemoteTypeSsh, + RemoteAlias: "sudo", + RemoteCanonicalName: fmt.Sprintf("sudo@%s@%s", user.Username, hostName), + RemoteSudo: true, + RemoteUser: "root", + RemoteHost: hostName, + ConnectMode: ConnectModeManual, + AutoInstall: true, + SSHOpts: &SSHOpts{Local: true}, + RemoteOpts: &RemoteOptsType{Color: "red"}, + Local: true, + } + err = UpsertRemote(ctx, sudoRemote) + if err != nil { + return err + } + log.Printf("[db] added sudo remote '%s', id=%s\n", sudoRemote.RemoteCanonicalName, sudoRemote.RemoteId) return nil } From a20da453f43b59453d17e3f215f30394162ceb3a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 30 Dec 2022 17:01:17 -0800 Subject: [PATCH 227/397] cr command (bare) shows all remotes --- pkg/cmdrunner/cmdrunner.go | 51 +++++++++++++++++++++++++++++++++++++- pkg/remote/remote.go | 27 +++++++++++++------- pkg/sstore/dbops.go | 19 ++++++++++++++ pkg/sstore/sstore.go | 14 +++++++++++ 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 6729db5b1..f462326f9 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1012,6 +1012,55 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/remote requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } +func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids resolvedIds) (sstore.UpdatePacket, error) { + var buf bytes.Buffer + riArr, err := sstore.GetRIsForWindow(ctx, ids.SessionId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("cannot get remote instances: %w", err) + } + rmap := remote.GetRemoteMap() + for _, ri := range riArr { + rptr := sstore.RemotePtrType{RemoteId: ri.RemoteId, Name: ri.Name} + msh := rmap[ri.RemoteId] + if msh == nil { + continue + } + baseDisplayName := msh.GetDisplayName() + displayName := rptr.GetDisplayName(baseDisplayName) + cwdStr := "-" + if ri.FeState.Cwd != "" { + cwdStr = ri.FeState.Cwd + } + buf.WriteString(fmt.Sprintf("%-30s %-50s\n", displayName, cwdStr)) + } + riBaseMap := make(map[string]bool) + for _, ri := range riArr { + if ri.Name == "" { + riBaseMap[ri.RemoteId] = true + } + } + for remoteId, msh := range rmap { + if riBaseMap[remoteId] { + continue + } + feState := msh.GetDefaultFeState() + if feState == nil { + continue + } + cwdStr := "-" + if feState.Cwd != "" { + cwdStr = feState.Cwd + } + buf.WriteString(fmt.Sprintf("%-30s %-50s (default)\n", msh.GetDisplayName(), cwdStr)) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil +} + func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) if err != nil { @@ -1019,7 +1068,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up } newRemote := firstArg(pk) if newRemote == "" { - return nil, nil + return crShowCommand(ctx, pk, ids) } remoteName, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 1267ede85..662f14a68 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -202,17 +202,11 @@ func (state RemoteRuntimeState) GetBaseDisplayName() string { } func (state RemoteRuntimeState) GetDisplayName(rptr *sstore.RemotePtrType) string { - name := state.GetBaseDisplayName() + baseDisplayName := state.GetBaseDisplayName() if rptr == nil { - return name + return baseDisplayName } - if rptr.Name != "" { - name = name + ":" + rptr.Name - } - if rptr.OwnerId != "" { - name = "@" + rptr.OwnerId + ":" + name - } - return name + return rptr.GetDisplayName(baseDisplayName) } func LoadRemotes(ctx context.Context) error { @@ -387,6 +381,16 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } +func GetRemoteMap() map[string]*MShellProc { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + rtn := make(map[string]*MShellProc) + for remoteId, msh := range GlobalStore.Map { + rtn[remoteId] = msh + } + return rtn +} + func GetLocalRemote() *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -1887,3 +1891,8 @@ func (msh *MShellProc) TryAutoConnect() error { } return nil } + +func (msh *MShellProc) GetDisplayName() string { + rcopy := msh.GetRemoteCopy() + return rcopy.GetName() +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5e81c9794..d73d35ce4 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1776,3 +1776,22 @@ func PurgeLineById(ctx context.Context, sessionId string, lineId string) error { }) return txErr } + +func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([]*RemoteInstance, error) { + var rtn []*RemoteInstance + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND (windowid = '' OR windowid = ?)` + riMaps := tx.SelectMaps(query, sessionId, windowId) + for _, m := range riMaps { + ri := RIFromMap(m) + if ri != nil { + rtn = append(rtn, ri) + } + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index fa82889d3..c6493a6c1 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -210,6 +210,20 @@ func (r RemotePtrType) IsSessionScope() bool { return strings.HasPrefix(r.Name, "*") } +func (rptr *RemotePtrType) GetDisplayName(baseDisplayName string) string { + name := baseDisplayName + if rptr == nil { + return name + } + if rptr.Name != "" { + name = name + ":" + rptr.Name + } + if rptr.OwnerId != "" { + name = "@" + rptr.OwnerId + ":" + name + } + return name +} + func (r RemotePtrType) Validate() error { if r.OwnerId != "" { if _, err := uuid.Parse(r.OwnerId); err != nil { From 96b25900fcdbd73bd254a992ebf1fe96f40207ec Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 2 Jan 2023 12:09:01 -0800 Subject: [PATCH 228/397] archive/purge lines for clear --- pkg/sstore/dbops.go | 63 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d73d35ce4..9c4b5a29a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -852,6 +852,26 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil } +func cleanSessionCmds(ctx context.Context, sessionId string) error { + txErr := WithTx(context.Background(), func(tx *TxWrap) error { + query := `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` + removedCmds := tx.SelectStrings(query, sessionId, sessionId) + query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` + tx.ExecWrap(query, sessionId, sessionId) + if tx.Err != nil { + return nil + } + for _, cmdId := range removedCmds { + DeletePtyOutFile(tx.Context(), sessionId, cmdId) + } + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + func CleanWindows(sessionId string) { txErr := WithTx(context.Background(), func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid NOT IN (SELECT windowid FROM screen_window WHERE sessionid = ?)` @@ -867,18 +887,7 @@ func CleanWindows(sessionId string) { query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) } - query = `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - removedCmds := tx.SelectStrings(query, sessionId, sessionId) - query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - tx.ExecWrap(query, sessionId, sessionId) - if tx.Err != nil { - return nil - } - fmt.Printf("removed cmds: %v\n", removedCmds) - for _, cmdId := range removedCmds { - DeletePtyOutFile(tx.Context(), sessionId, cmdId) - } - return nil + return cleanSessionCmds(tx.Context(), sessionId) }) if txErr != nil { fmt.Printf("ERROR cleaning windows sessionid:%s: %v\n", sessionId, txErr) @@ -1219,7 +1228,30 @@ func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts return txErr } -func ClearWindow(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { +func ArchiveWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { + var lineIds []string + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("window does not exist") + } + query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` + lineIds = tx.SelectStrings(query, sessionId, windowId) + query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + return nil + }) + if txErr != nil { + return nil, txErr + } + win, err := GetWindowById(ctx, sessionId, windowId) + if err != nil { + return nil, err + } + return &ModelUpdate{Window: win}, nil +} + +func PurgeWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { var lineIds []string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` @@ -1230,6 +1262,8 @@ func ClearWindow(ctx context.Context, sessionId string, windowId string) (*Model lineIds = tx.SelectStrings(query, sessionId, windowId) query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) + query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) query = `UPDATE window SET nextlinenum = 1 WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) return nil @@ -1237,6 +1271,7 @@ func ClearWindow(ctx context.Context, sessionId string, windowId string) (*Model if txErr != nil { return nil, txErr } + go cleanSessionCmds(context.Background(), sessionId) win, err := GetWindowById(ctx, sessionId, windowId) if err != nil { return nil, err @@ -1762,6 +1797,8 @@ func PurgeLineById(ctx context.Context, sessionId string, lineId string) error { cmdId := tx.GetString(query, sessionId, lineId) query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` tx.ExecWrap(query, sessionId, lineId) + query = `DELETE FROM history WHERE sessionid = ? AND lineid = ?` + tx.ExecWrap(query, sessionId, lineId) if cmdId != "" { query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` cmdRefCount := tx.GetInt(query, sessionId, cmdId) From 0f599207cf8716340e9f90c4327635ddabe528a1 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 2 Jan 2023 12:09:19 -0800 Subject: [PATCH 229/397] clear can either archive or purge lines --- pkg/cmdrunner/cmdrunner.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index f462326f9..7bd39717b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1665,15 +1665,28 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if err != nil { return nil, err } - update, err := sstore.ClearWindow(ctx, ids.SessionId, ids.WindowId) - if err != nil { - return nil, fmt.Errorf("clearing window: %v", err) + if resolveBool(pk.Kwargs["purge"], false) { + update, err := sstore.PurgeWindowLines(ctx, ids.SessionId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("clearing window: %v", err) + } + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("window cleared (all lines purged)"), + TimeoutMs: 2000, + } + return update, nil + } else { + update, err := sstore.ArchiveWindowLines(ctx, ids.SessionId, ids.WindowId) + if err != nil { + return nil, fmt.Errorf("clearing window: %v", err) + } + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("window cleared"), + TimeoutMs: 2000, + } + return update, nil } - update.Info = &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("window cleared"), - TimeoutMs: 2000, - } - return update, nil + } const DefaultMaxHistoryItems = 10000 From 7f1509255058ef3a153cd48dcc0c690d0bf304ad Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 2 Jan 2023 12:13:55 -0800 Subject: [PATCH 230/397] change back to /history --- pkg/cmdrunner/cmdrunner.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 7bd39717b..3e40f8219 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -51,7 +51,7 @@ var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cy var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} -var NoHistCmds = []string{"_compgen", "line", "_history", "_killserver"} +var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var GlobalCmds = []string{"session", "screen", "remote", "set"} var SetVarNameMap map[string]string = map[string]string{ @@ -155,7 +155,7 @@ func init() { registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) - registerCmdFn("_history", HistoryCommand) + registerCmdFn("history", HistoryCommand) registerCmdFn("_killserver", KillServerCommand) From aa56db0bc46a96c975c8d189c9777641611405d3 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Jan 2023 17:16:20 -0800 Subject: [PATCH 231/397] fix userhostre to accept IP addresses --- pkg/cmdrunner/cmdrunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3e40f8219..319794e67 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -75,7 +75,7 @@ var SetVarScopes = []SetVarScope{ } var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") -var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z][a-z0-9.-]*)(?::([0-9]+))?$") +var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$") From be978a25b272a43a32eb41e048f77941e58589b9 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Jan 2023 20:53:46 -0800 Subject: [PATCH 232/397] reinit focus --- cmd/main-server.go | 6 +++++- pkg/sstore/dbops.go | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 4e8dfc7eb..c1ab5f008 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -444,7 +444,11 @@ func main() { err = sstore.HangupAllRunningCmds(context.Background()) if err != nil { - log.Printf("[error] calling HUP on all running commands\n") + log.Printf("[error] calling HUP on all running commands: %v\n", err) + } + err = sstore.ReInitFocus(context.Background()) + if err != nil { + log.Printf("[error] resetting window focus: %v\n", err) } go stdinReadWatch() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9c4b5a29a..747aa4a7c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -784,6 +784,14 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err }) } +func ReInitFocus(ctx context.Context) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE screen_window SET focustype = 'input'` + tx.ExecWrap(query) + return nil + }) +} + func HangupAllRunningCmds(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE status = ?` From db727b232f4c08acb53d54c7b963268c6f722e67 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 11 Jan 2023 22:26:27 -0800 Subject: [PATCH 233/397] fix unused variable --- pkg/sstore/dbops.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 747aa4a7c..a665741a3 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1237,14 +1237,11 @@ func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts } func ArchiveWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { - var lineIds []string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, sessionId, windowId) { return fmt.Errorf("window does not exist") } - query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` - lineIds = tx.SelectStrings(query, sessionId, windowId) query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, sessionId, windowId) return nil From 64ec186fa085d60e28b81126e3a4c4cb328d94a9 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 16 Jan 2023 23:36:52 -0800 Subject: [PATCH 234/397] new API for client to log activity --- cmd/main-server.go | 35 +++++++++++++++++++ db/migrations/000002_activity.down.sql | 1 + db/migrations/000002_activity.up.sql | 12 +++++++ pkg/scbase/scbase.go | 4 +++ pkg/sstore/dbops.go | 48 ++++++++++++++++++++++++++ pkg/sstore/sstore.go | 22 ++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 db/migrations/000002_activity.down.sql create mode 100644 db/migrations/000002_activity.up.sql diff --git a/cmd/main-server.go b/cmd/main-server.go index c1ab5f008..5f9878a4f 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -46,6 +46,12 @@ var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string +type ClientActiveState struct { + Fg bool `json:"fg"` + Active bool `json:"active"` + Open bool `json:"open"` +} + func setWSState(state *scws.WSState) { GlobalLock.Lock() defer GlobalLock.Unlock() @@ -150,6 +156,34 @@ func HandleSetWinSize(w http.ResponseWriter, r *http.Request) { return } +// params: fg, active, open +func HandleLogActiveState(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var activeState ClientActiveState + err := decoder.Decode(&activeState) + if err != nil { + WriteJsonError(w, fmt.Errorf("error decoding json: %w", err)) + return + } + activity := sstore.ActivityUpdate{} + if activeState.Fg { + activity.FgMinutes = 1 + } + if activeState.Active { + activity.ActiveMinutes = 1 + } + if activeState.Open { + activity.OpenMinutes = 1 + } + err = sstore.UpdateCurrentActivity(r.Context(), activity) + if err != nil { + WriteJsonError(w, fmt.Errorf("error updating activity: %w", err)) + return + } + WriteJsonSuccess(w, true) + return +} + // params: sessionid, windowid func HandleGetWindow(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() @@ -461,6 +495,7 @@ func main() { gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) + gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState)) serverAddr := MainServerAddr if scbase.IsDevMode() { serverAddr = MainServerDevAddr diff --git a/db/migrations/000002_activity.down.sql b/db/migrations/000002_activity.down.sql new file mode 100644 index 000000000..6a508d33f --- /dev/null +++ b/db/migrations/000002_activity.down.sql @@ -0,0 +1 @@ +DROP TABLE activity; diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql new file mode 100644 index 000000000..c2ccd32ee --- /dev/null +++ b/db/migrations/000002_activity.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE activity ( + day varchar(20) PRIMARY KEY, + uploaded boolean NOT NULL, + numlines int NOT NULL, + activeminutes int NOT NULL, + fgminutes int NOT NULL, + openminutes int NOT NULL, + tzname varchar(50) NOT NULL, + tzoffset int NOT NULL, + clientversion varchar(20) NOT NULL, + clientarch varchar(20) NOT NULL +); diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index cc2789164..5a25af57b 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -318,3 +318,7 @@ func NumFormatB2(num int64) string { return signStr + strconv.FormatFloat(gVal, 'f', 2, 64) + "G" } } + +func ClientArch() string { + return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a665741a3..d02a53013 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1837,3 +1837,51 @@ func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([] } return rtn, nil } + +func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { + now := time.Now() + dayStr := now.Format("2006-01-02") + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT day FROM activity WHERE day = ?` + if !tx.Exists(query, dayStr) { + query = `INSERT INTO activity (day, uploaded, numlines, fgminutes, activeminutes, openminutes, tzname, tzoffset, clientversion, clientarch) + VALUES (?, 0, 0, 0, 0, 0, ?, ?, ?, ?)` + tzName, tzOffset := now.Zone() + if len(tzName) > MaxTzNameLen { + tzName = tzName[0:MaxTzNameLen] + } + tx.ExecWrap(query, dayStr, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) + } + query = `UPDATE activity SET numlines = numlines + ?, fgminutes = fgminutes + ?, activeminutes = activeminutes + ?, openminutes = openminutes + ? WHERE day = ?` + tx.ExecWrap(query, update.NumLines, update.FgMinutes, update.ActiveMinutes, update.OpenMinutes, dayStr) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) { + var rtn []*ActivityType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM activity WHERE uploaded = 0 ORDER BY day DESC LIMIT 30` + tx.SelectWrap(&rtn, query) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + +func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE activity SET uploaded = 1 WHERE day = ?` + for _, activity := range activityArr { + tx.ExecWrap(query, activity.Day) + } + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c6493a6c1..ecea27bb2 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -70,6 +70,8 @@ const ( SWFocusCmdFg = "cmd-fg" ) +const MaxTzNameLen = 50 + var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error @@ -110,6 +112,26 @@ type ClientWinSizeType struct { FullScreen bool `json:"fullscreen,omitempty"` } +type ActivityUpdate struct { + FgMinutes int + ActiveMinutes int + OpenMinutes int + NumLines int +} + +type ActivityType struct { + Day string `json:"day"` + Uploaded bool `json:"-"` + NumLines int `json:"numlines"` + ActiveMinutes int `json:"activeminutes"` + FgMinutes int `json:"fgminutes"` + OpenMinutes int `json:"openminutes"` + TzName string `json:"tzname"` + TzOffset int `json:"tzoffset"` + ClientVersion string `json:"clientversion"` + ClientArch string `json:"clientarch"` +} + type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"` From 85c943f65a35e8dc40835bc54e44bc67f1f00e37 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 16 Jan 2023 23:39:58 -0800 Subject: [PATCH 235/397] bump version to v0.1.1 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 5a25af57b..7b73f26ad 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -26,7 +26,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.0" +const PromptVersion = "v0.1.1" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 353bfad4341ea1b602080c067204e3b67d5ad934 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 17 Jan 2023 16:02:44 -0800 Subject: [PATCH 236/397] change numlines to numcommands. log all interactive eval commands to activity table. stub pcloud telemetry call --- cmd/main-server.go | 6 ++++++ db/migrations/000002_activity.up.sql | 2 +- pkg/cmdrunner/cmdrunner.go | 9 ++++++++- pkg/pcloud/pcloud.go | 5 +++++ pkg/sstore/dbops.go | 14 ++++++++++---- pkg/sstore/sstore.go | 4 ++-- 6 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 pkg/pcloud/pcloud.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 5f9878a4f..25e291051 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -20,6 +20,7 @@ import ( "github.com/gorilla/mux" "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/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" @@ -405,6 +406,10 @@ func test() error { return nil } +func doBeforeClose() { + pcloud.SendTelemetry() +} + // watch stdin, kill server if stdin is closed func stdinReadWatch() { buf := make([]byte, 1024) @@ -412,6 +417,7 @@ func stdinReadWatch() { _, err := os.Stdin.Read(buf) if err != nil { log.Printf("stdin closed/error, shutting down: %v\n", err) + doBeforeClose() time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) } diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql index c2ccd32ee..a143305a1 100644 --- a/db/migrations/000002_activity.up.sql +++ b/db/migrations/000002_activity.up.sql @@ -1,7 +1,7 @@ CREATE TABLE activity ( day varchar(20) PRIMARY KEY, uploaded boolean NOT NULL, - numlines int NOT NULL, + numcommands int NOT NULL, activeminutes int NOT NULL, fgminutes int NOT NULL, openminutes int NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 319794e67..13e120c3c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -365,6 +365,13 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args[0]) > MaxCommandLen { return nil, fmt.Errorf("command length too long len:%d, max:%d", len(pk.Args[0]), MaxCommandLen) } + if pk.Interactive { + err := sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{NumCommands: 1}) + if err != nil { + log.Printf("[error] incrementing activity numcommands: %v\n", err) + // fall through (non-fatal error) + } + } var historyContext historyContextType ctxWithHistory := context.WithValue(ctx, historyContextKey, &historyContext) var update sstore.UpdatePacket @@ -376,7 +383,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil)) if err != nil { log.Printf("[error] adding to history: %v\n", err) - // continue... + // fall through (non-fatal error) } } return update, rtnErr diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go new file mode 100644 index 000000000..33df9a2b7 --- /dev/null +++ b/pkg/pcloud/pcloud.go @@ -0,0 +1,5 @@ +package pcloud + +func SendTelemetry() error { + return nil +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d02a53013..2a5019047 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1844,16 +1844,22 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT day FROM activity WHERE day = ?` if !tx.Exists(query, dayStr) { - query = `INSERT INTO activity (day, uploaded, numlines, fgminutes, activeminutes, openminutes, tzname, tzoffset, clientversion, clientarch) - VALUES (?, 0, 0, 0, 0, 0, ?, ?, ?, ?)` + query = `INSERT INTO activity (day, uploaded, numcommands, fgminutes, activeminutes, openminutes, tzname, tzoffset, clientversion, clientarch) + VALUES (?, 0, 0, 0, 0, 0, ?, ?, ?, ?)` tzName, tzOffset := now.Zone() if len(tzName) > MaxTzNameLen { tzName = tzName[0:MaxTzNameLen] } tx.ExecWrap(query, dayStr, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) } - query = `UPDATE activity SET numlines = numlines + ?, fgminutes = fgminutes + ?, activeminutes = activeminutes + ?, openminutes = openminutes + ? WHERE day = ?` - tx.ExecWrap(query, update.NumLines, update.FgMinutes, update.ActiveMinutes, update.OpenMinutes, dayStr) + query = `UPDATE activity + SET numcommands = numcommands + ?, + fgminutes = fgminutes + ?, + activeminutes = activeminutes + ?, + openminutes = openminutes + ?, + clientversion = ? + WHERE day = ?` + tx.ExecWrap(query, update.NumCommands, update.FgMinutes, update.ActiveMinutes, update.OpenMinutes, scbase.PromptVersion, dayStr) return nil }) if txErr != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index ecea27bb2..be68bdd27 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -116,13 +116,13 @@ type ActivityUpdate struct { FgMinutes int ActiveMinutes int OpenMinutes int - NumLines int + NumCommands int } type ActivityType struct { Day string `json:"day"` Uploaded bool `json:"-"` - NumLines int `json:"numlines"` + NumCommands int `json:"numcommands"` ActiveMinutes int `json:"activeminutes"` FgMinutes int `json:"fgminutes"` OpenMinutes int `json:"openminutes"` From dc051beeb885c952060f422ec10abd26c4256ec3 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Jan 2023 11:10:12 -0800 Subject: [PATCH 237/397] add client:set command --- pkg/cmdrunner/cmdrunner.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 13e120c3c..58d193b4d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -52,18 +52,20 @@ var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoins var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} -var GlobalCmds = []string{"session", "screen", "remote", "set"} +var GlobalCmds = []string{"session", "screen", "remote", "set", "client"} var SetVarNameMap map[string]string = map[string]string{ - "tabcolor": "screen.tabcolor", - "pterm": "window.pterm", - "anchor": "sw.anchor", - "focus": "sw.focus", - "line": "sw.line", + "tabcolor": "screen.tabcolor", + "pterm": "window.pterm", + "anchor": "sw.anchor", + "focus": "sw.focus", + "line": "sw.line", + "telemetry": "client.telemetry", } var SetVarScopes = []SetVarScope{ SetVarScope{ScopeName: "global", VarNames: []string{}}, + SetVarScope{ScopeName: "client", VarNames: []string{"telemetry"}}, SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}}, SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos"}}, SetVarScope{ScopeName: "window", VarNames: []string{"pterm"}}, @@ -155,6 +157,9 @@ func init() { registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) + registerCmdFn("client", ClientCommand) + registerCmdFn("client:set", ClientSetCommand) + registerCmdFn("history", HistoryCommand) registerCmdFn("_killserver", KillServerCommand) @@ -2077,6 +2082,14 @@ func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return nil, nil } +func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set"}, "or", false)) +} + +func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, fmt.Errorf("not implemented") +} + func formatTermOpts(termOpts sstore.TermOpts) string { if termOpts.Cols == 0 { return "???" From ea897bf53c1d72c4df427aa83b32b3f18afeacdf Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 22 Jan 2023 23:10:18 -0800 Subject: [PATCH 238/397] send telemetry data to pcloud. pcloud dev settings (with PCLOUD_ENDPOINT). /client:show and /client:set commands (for no-telemetry) --- cmd/main-server.go | 20 +++- db/migrations/000002_activity.down.sql | 2 + db/migrations/000002_activity.up.sql | 2 + pkg/cmdrunner/cmdrunner.go | 66 ++++++++++++- pkg/pcloud/pcloud.go | 129 ++++++++++++++++++++++++- pkg/sstore/migrate.go | 7 ++ pkg/sstore/sstore.go | 16 +++ 7 files changed, 236 insertions(+), 6 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 25e291051..bf66c9d8b 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -406,8 +406,13 @@ func test() error { return nil } -func doBeforeClose() { - pcloud.SendTelemetry() +func sendTelemetryWrapper() { + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + err := pcloud.SendTelemetry(ctx) + if err != nil { + log.Printf("[error] sending telemetry: %v\n", err) + } } // watch stdin, kill server if stdin is closed @@ -417,7 +422,7 @@ func stdinReadWatch() { _, err := os.Stdin.Read(buf) if err != nil { log.Printf("stdin closed/error, shutting down: %v\n", err) - doBeforeClose() + sendTelemetryWrapper() time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) } @@ -491,6 +496,15 @@ func main() { log.Printf("[error] resetting window focus: %v\n", err) } + go func() { + log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) + time.Sleep(1 * time.Minute) + for { + sendTelemetryWrapper() + // send new telemetry every 8-hours + time.Sleep(8 * time.Hour) + } + }() go stdinReadWatch() go runWebSocketServer() gr := mux.NewRouter() diff --git a/db/migrations/000002_activity.down.sql b/db/migrations/000002_activity.down.sql index 6a508d33f..75ccae383 100644 --- a/db/migrations/000002_activity.down.sql +++ b/db/migrations/000002_activity.down.sql @@ -1 +1,3 @@ DROP TABLE activity; + +ALTER TABLE client DROP COLUMN clientopts; diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql index a143305a1..dbd82104d 100644 --- a/db/migrations/000002_activity.up.sql +++ b/db/migrations/000002_activity.up.sql @@ -10,3 +10,5 @@ CREATE TABLE activity ( clientversion varchar(20) NOT NULL, clientarch varchar(20) NOT NULL ); + +ALTER TABLE client ADD COLUMN clientopts json NOT NULL DEFAULT ''; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 58d193b4d..456aa29c8 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -19,6 +19,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/comp" + "github.com/scripthaus-dev/sh2-server/pkg/pcloud" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" @@ -159,6 +160,7 @@ func init() { registerCmdFn("client", ClientCommand) registerCmdFn("client:set", ClientSetCommand) + registerCmdFn("client:show", ClientShowCommand) registerCmdFn("history", HistoryCommand) @@ -2083,11 +2085,71 @@ func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set"}, "or", false)) + return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set", "show"}, "or", false)) +} + +func boolToStr(v bool, trueStr string, falseStr string) string { + if v { + return trueStr + } + return falseStr +} + +func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("client info"), + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil } func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("not implemented") + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + var varsUpdated []string + if pk.Kwargs["telemetry"] != "" { + noTelemetry := !resolveBool(pk.Kwargs["telemetry"], true) + if clientData.ClientOpts.NoTelemetry != noTelemetry { + clientOpts := clientData.ClientOpts + clientOpts.NoTelemetry = noTelemetry + err = sstore.SetClientOpts(ctx, clientOpts) + if err != nil { + return nil, fmt.Errorf("error trying to update client telemetry: %v", err) + } + log.Printf("client telemetry setting updated to %v\n", !noTelemetry) + err = pcloud.SendNoTelemetryUpdate(ctx, noTelemetry) + if err != nil { + // ignore error, just log + log.Printf("[error] sending no-telemetry update: %v\n", err) + log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") + } + } else { + log.Printf("client telemetry setting unchanged, is %v\n", !noTelemetry) + } + varsUpdated = append(varsUpdated, "telemetry") + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/client:set no updates, can set %s", formatStrs([]string{"telemetry"}, "or", false)) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, + } + return update, nil } func formatTermOpts(termOpts sstore.TermOpts) string { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 33df9a2b7..ffb39d2cd 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -1,5 +1,132 @@ package pcloud -func SendTelemetry() error { +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + + "github.com/scripthaus-dev/sh2-server/pkg/scbase" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +const PCloudEndpoint = "https://api.getprompt.dev/central" +const PCloudEndpointVarName = "PCLOUD_ENDPOINT" +const APIVersion = 1 + +type NoTelemetryInputType struct { + ClientId string `json:"clientid"` + Value bool `json:"value"` +} + +type TelemetryInputType struct { + UserId string `json:"userid"` + ClientId string `json:"clientid"` + Activity []*sstore.ActivityType `json:"activity"` +} + +func GetEndpoint() string { + if !scbase.IsDevMode() { + return PCloudEndpoint + } + endpoint := os.Getenv(PCloudEndpointVarName) + if endpoint == "" || !strings.HasPrefix(endpoint, "https://") { + panic("Invalid PCloud dev endpoint, PCLOUD_ENDPOINT not set or invalid") + } + return endpoint +} + +func makePostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { + var dataReader io.Reader + if data != nil { + byteArr, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err) + } + dataReader = bytes.NewReader(byteArr) + } + fullUrl := GetEndpoint() + apiUrl + req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader) + if err != nil { + return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion)) + req.Header.Set("X-PromptAPIUrl", apiUrl) + req.Close = true + return req, nil +} + +func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) { + apiUrl := req.Header.Get("X-PromptAPIUrl") + log.Printf("sending request %v\n", req.URL) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error contacting pcloud %q service: %v", apiUrl, err) + } + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return resp, fmt.Errorf("error reading %q response body: %v", apiUrl, err) + } + if resp.StatusCode != http.StatusOK { + return resp, fmt.Errorf("error contacting pcloud %q service: %s", apiUrl, resp.Status) + } + if outputObj != nil && resp.Header.Get("Content-Type") == "application/json" { + err = json.Unmarshal(bodyBytes, outputObj) + if err != nil { + return resp, fmt.Errorf("error decoding json: %v", err) + } + } + return resp, nil +} + +func SendTelemetry(ctx context.Context) error { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return fmt.Errorf("cannot retrieve client data: %v", err) + } + if clientData.ClientOpts.NoTelemetry { + return nil + } + activity, err := sstore.GetNonUploadedActivity(ctx) + if err != nil { + return fmt.Errorf("cannot get activity: %v", err) + } + if len(activity) == 0 { + return nil + } + log.Printf("sending telemetry data\n") + input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, Activity: activity} + req, err := makePostReq(ctx, "/telemetry", input) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } + return nil +} + +func SendNoTelemetryUpdate(ctx context.Context, noTelemetryVal bool) error { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return fmt.Errorf("cannot retrieve client data: %v", err) + } + req, err := makePostReq(ctx, "/no-telemetry", NoTelemetryInputType{ClientId: clientData.ClientId, Value: noTelemetryVal}) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } return nil } diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index db43816de..5dcff1c1c 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "strconv" + "time" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/file" @@ -99,12 +100,18 @@ func MigratePrintVersion() error { func MigrateCommandOpts(opts []string) error { var err error if opts[0] == "--migrate-up" { + fmt.Printf("migrate-up %v\n", GetSessionDBName()) + time.Sleep(3 * time.Second) err = MigrateUp() } else if opts[0] == "--migrate-down" { + fmt.Printf("migrate-down %v\n", GetSessionDBName()) + time.Sleep(3 * time.Second) err = MigrateDown() } else if opts[0] == "--migrate-goto" { n, err := strconv.Atoi(opts[1]) if err == nil { + fmt.Printf("migrate-goto %v => %d\n", GetSessionDBName(), n) + time.Sleep(3 * time.Second) err = MigrateGoto(uint(n)) } } else { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index be68bdd27..cff8f2083 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -132,6 +132,10 @@ type ActivityType struct { ClientArch string `json:"clientarch"` } +type ClientOptsType struct { + NoTelemetry bool `json:"notelemetry,omitempty"` +} + type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"` @@ -141,6 +145,7 @@ type ClientData struct { UserPublicKey *ecdsa.PublicKey `json:"-"` ActiveSessionId string `json:"activesessionid"` WinSize ClientWinSizeType `json:"winsize"` + ClientOpts ClientOptsType `json:"clientopts"` } func (c *ClientData) ToMap() map[string]interface{} { @@ -151,6 +156,7 @@ func (c *ClientData) ToMap() map[string]interface{} { rtn["userpublickeybytes"] = c.UserPublicKeyBytes rtn["activesessionid"] = c.ActiveSessionId rtn["winsize"] = quickJson(c.WinSize) + rtn["clientopts"] = quickJson(c.ClientOpts) return rtn } @@ -165,6 +171,7 @@ func ClientDataFromMap(m map[string]interface{}) *ClientData { quickSetBytes(&c.UserPublicKeyBytes, m, "userpublickeybytes") quickSetStr(&c.ActiveSessionId, m, "activesessionid") quickSetJson(&c.WinSize, m, "winsize") + quickSetJson(&c.ClientOpts, m, "clientopts") return &c } @@ -1005,3 +1012,12 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { } return &rtn, nil } + +func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET clientopts = ?` + tx.ExecWrap(query, quickJson(clientOpts)) + return nil + }) + return txErr +} From f8c675c3e76e8224fd77fda7672f15a4ad0ce5ed Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 23 Jan 2023 12:54:32 -0800 Subject: [PATCH 239/397] updates to telemetry, separate telemetry commands, use json data --- cmd/main-server.go | 13 ++- db/migrations/000002_activity.up.sql | 5 +- pkg/cmdrunner/cmdrunner.go | 123 +++++++++++++++++++-------- pkg/pcloud/pcloud.go | 12 ++- pkg/sstore/dbops.go | 36 +++++--- pkg/sstore/sstore.go | 32 ++++--- 6 files changed, 156 insertions(+), 65 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index bf66c9d8b..d3fef53da 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -407,9 +407,18 @@ func test() error { } func sendTelemetryWrapper() { + defer func() { + r := recover() + if r == nil { + return + } + log.Printf("[error] in sendTelemetryWrapper: %v\n", r) + debug.PrintStack() + return + }() ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - err := pcloud.SendTelemetry(ctx) + err := pcloud.SendTelemetry(ctx, false) if err != nil { log.Printf("[error] sending telemetry: %v\n", err) } @@ -498,7 +507,7 @@ func main() { go func() { log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) - time.Sleep(1 * time.Minute) + time.Sleep(30 * time.Second) for { sendTelemetryWrapper() // send new telemetry every 8-hours diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql index dbd82104d..d6a84aa3b 100644 --- a/db/migrations/000002_activity.up.sql +++ b/db/migrations/000002_activity.up.sql @@ -1,10 +1,7 @@ CREATE TABLE activity ( day varchar(20) PRIMARY KEY, uploaded boolean NOT NULL, - numcommands int NOT NULL, - activeminutes int NOT NULL, - fgminutes int NOT NULL, - openminutes int NOT NULL, + tdata json NOT NULL, tzname varchar(50) NOT NULL, tzoffset int NOT NULL, clientversion varchar(20) NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 456aa29c8..40ca70385 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -53,15 +53,14 @@ var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoins var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} -var GlobalCmds = []string{"session", "screen", "remote", "set", "client"} +var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry"} var SetVarNameMap map[string]string = map[string]string{ - "tabcolor": "screen.tabcolor", - "pterm": "window.pterm", - "anchor": "sw.anchor", - "focus": "sw.focus", - "line": "sw.line", - "telemetry": "client.telemetry", + "tabcolor": "screen.tabcolor", + "pterm": "window.pterm", + "anchor": "sw.anchor", + "focus": "sw.focus", + "line": "sw.line", } var SetVarScopes = []SetVarScope{ @@ -159,9 +158,14 @@ func init() { registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("client", ClientCommand) - registerCmdFn("client:set", ClientSetCommand) registerCmdFn("client:show", ClientShowCommand) + registerCmdFn("telemetry", TelemetryCommand) + registerCmdFn("telemetry:on", TelemetryOnCommand) + registerCmdFn("telemetry:off", TelemetryOffCommand) + registerCmdFn("telemetry:send", TelemetrySendCommand) + registerCmdFn("telemetry:show", TelemetryShowCommand) + registerCmdFn("history", HistoryCommand) registerCmdFn("_killserver", KillServerCommand) @@ -2085,7 +2089,7 @@ func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set", "show"}, "or", false)) + return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) } func boolToStr(v bool, trueStr string, falseStr string) string { @@ -2113,45 +2117,94 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return update, nil } -func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func TelemetryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, fmt.Errorf("/telemetry requires a subcommand: %s", formatStrs([]string{"show", "on", "off", "send"}, "or", false)) +} + +func setNoTelemetry(ctx context.Context, clientData *sstore.ClientData, noTelemetryVal bool) error { + clientOpts := clientData.ClientOpts + clientOpts.NoTelemetry = noTelemetryVal + err := sstore.SetClientOpts(ctx, clientOpts) + if err != nil { + return fmt.Errorf("error trying to update client telemetry: %v", err) + } + log.Printf("client no-telemetry setting updated to %v\n", noTelemetryVal) + err = pcloud.SendNoTelemetryUpdate(ctx, clientOpts.NoTelemetry) + if err != nil { + // ignore error, just log + log.Printf("[error] sending no-telemetry update: %v\n", err) + log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") + } + return nil +} + +func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) } - var varsUpdated []string - if pk.Kwargs["telemetry"] != "" { - noTelemetry := !resolveBool(pk.Kwargs["telemetry"], true) - if clientData.ClientOpts.NoTelemetry != noTelemetry { - clientOpts := clientData.ClientOpts - clientOpts.NoTelemetry = noTelemetry - err = sstore.SetClientOpts(ctx, clientOpts) - if err != nil { - return nil, fmt.Errorf("error trying to update client telemetry: %v", err) - } - log.Printf("client telemetry setting updated to %v\n", !noTelemetry) - err = pcloud.SendNoTelemetryUpdate(ctx, noTelemetry) - if err != nil { - // ignore error, just log - log.Printf("[error] sending no-telemetry update: %v\n", err) - log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") - } - } else { - log.Printf("client telemetry setting unchanged, is %v\n", !noTelemetry) - } - varsUpdated = append(varsUpdated, "telemetry") + if !clientData.ClientOpts.NoTelemetry { + return sstore.InfoMsgUpdate("telemetry is already on"), nil } - if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/client:set no updates, can set %s", formatStrs([]string{"telemetry"}, "or", false)) + err = setNoTelemetry(ctx, clientData, false) + if err != nil { + return nil, err } + err = pcloud.SendTelemetry(ctx, false) + if err != nil { + // ignore error, but log + log.Printf("[error] sending telemetry update (in /telemetry:on): %v\n", err) + } + return sstore.InfoMsgUpdate("telemetry is now on"), nil +} + +func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + if clientData.ClientOpts.NoTelemetry { + return sstore.InfoMsgUpdate("telemetry is already off"), nil + } + err = setNoTelemetry(ctx, clientData, true) + if err != nil { + return nil, err + } + return sstore.InfoMsgUpdate("telemetry is now off"), nil +} + +func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)), - TimeoutMs: 2000, + InfoTitle: fmt.Sprintf("telemetry info"), + InfoLines: splitLinesForInfo(buf.String()), }, } return update, nil } +func TelemetrySendCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + force := resolveBool(pk.Kwargs["force"], false) + if clientData.ClientOpts.NoTelemetry && !force { + return nil, fmt.Errorf("cannot send telemetry, telemetry is off. pass force=1 to force the send, or turn on telemetry with /telemetry:on") + } + err = pcloud.SendTelemetry(ctx, force) + if err != nil { + return nil, fmt.Errorf("failed to send telemetry: %v", err) + } + return sstore.InfoMsgUpdate("telemetry sent"), nil +} + func formatTermOpts(termOpts sstore.TermOpts) string { if termOpts.Cols == 0 { return "???" diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index ffb39d2cd..e55d42307 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -28,6 +28,7 @@ type NoTelemetryInputType struct { type TelemetryInputType struct { UserId string `json:"userid"` ClientId string `json:"clientid"` + CurDay string `json:"curday"` Activity []*sstore.ActivityType `json:"activity"` } @@ -87,12 +88,12 @@ func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) return resp, nil } -func SendTelemetry(ctx context.Context) error { +func SendTelemetry(ctx context.Context, force bool) error { clientData, err := sstore.EnsureClientData(ctx) if err != nil { return fmt.Errorf("cannot retrieve client data: %v", err) } - if clientData.ClientOpts.NoTelemetry { + if !force && clientData.ClientOpts.NoTelemetry { return nil } activity, err := sstore.GetNonUploadedActivity(ctx) @@ -103,7 +104,8 @@ func SendTelemetry(ctx context.Context) error { return nil } log.Printf("sending telemetry data\n") - input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, Activity: activity} + dayStr := sstore.GetCurDayStr() + input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, CurDay: dayStr, Activity: activity} req, err := makePostReq(ctx, "/telemetry", input) if err != nil { return err @@ -112,6 +114,10 @@ func SendTelemetry(ctx context.Context) error { if err != nil { return err } + err = sstore.MarkActivityAsUploaded(ctx, activity) + if err != nil { + return fmt.Errorf("error marking activity as uploaded: %v", err) + } return nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 2a5019047..c1feacfe7 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1838,28 +1838,37 @@ func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([] return rtn, nil } -func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { +func GetCurDayStr() string { now := time.Now() dayStr := now.Format("2006-01-02") + return dayStr +} + +func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { + now := time.Now() + dayStr := GetCurDayStr() txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT day FROM activity WHERE day = ?` - if !tx.Exists(query, dayStr) { - query = `INSERT INTO activity (day, uploaded, numcommands, fgminutes, activeminutes, openminutes, tzname, tzoffset, clientversion, clientarch) - VALUES (?, 0, 0, 0, 0, 0, ?, ?, ?, ?)` + var tdata TelemetryData + query := `SELECT tdata FROM activity WHERE day = ?` + found := tx.GetWrap(&tdata, query, dayStr) + if !found { + query = `INSERT INTO activity (day, uploaded, tdata, tzname, tzoffset, clientversion, clientarch) + VALUES (?, 0, ?, ?, ?, ?, ?)` tzName, tzOffset := now.Zone() if len(tzName) > MaxTzNameLen { tzName = tzName[0:MaxTzNameLen] } - tx.ExecWrap(query, dayStr, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) + tx.ExecWrap(query, dayStr, tdata, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) } + tdata.NumCommands += update.NumCommands + tdata.FgMinutes += update.FgMinutes + tdata.ActiveMinutes += update.ActiveMinutes + tdata.OpenMinutes += update.OpenMinutes query = `UPDATE activity - SET numcommands = numcommands + ?, - fgminutes = fgminutes + ?, - activeminutes = activeminutes + ?, - openminutes = openminutes + ?, + SET tdata = ?, clientversion = ? WHERE day = ?` - tx.ExecWrap(query, update.NumCommands, update.FgMinutes, update.ActiveMinutes, update.OpenMinutes, scbase.PromptVersion, dayStr) + tx.ExecWrap(query, tdata, scbase.PromptVersion, dayStr) return nil }) if txErr != nil { @@ -1881,10 +1890,15 @@ func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) { return rtn, nil } +// note, will not mark the current day as uploaded func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) error { + dayStr := GetCurDayStr() txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE activity SET uploaded = 1 WHERE day = ?` for _, activity := range activityArr { + if activity.Day == dayStr { + continue + } tx.ExecWrap(query, activity.Day) } return nil diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index cff8f2083..d75db565a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -120,16 +120,28 @@ type ActivityUpdate struct { } type ActivityType struct { - Day string `json:"day"` - Uploaded bool `json:"-"` - NumCommands int `json:"numcommands"` - ActiveMinutes int `json:"activeminutes"` - FgMinutes int `json:"fgminutes"` - OpenMinutes int `json:"openminutes"` - TzName string `json:"tzname"` - TzOffset int `json:"tzoffset"` - ClientVersion string `json:"clientversion"` - ClientArch string `json:"clientarch"` + Day string `json:"day"` + Uploaded bool `json:"-"` + TData TelemetryData `json:"tdata"` + TzName string `json:"tzname"` + TzOffset int `json:"tzoffset"` + ClientVersion string `json:"clientversion"` + ClientArch string `json:"clientarch"` +} + +type TelemetryData struct { + NumCommands int `json:"numcommands"` + ActiveMinutes int `json:"activeminutes"` + FgMinutes int `json:"fgminutes"` + OpenMinutes int `json:"openminutes"` +} + +func (tdata TelemetryData) Value() (driver.Value, error) { + return quickValueJson(tdata) +} + +func (tdata *TelemetryData) Scan(val interface{}) error { + return quickScanJson(tdata, val) } type ClientOptsType struct { From 5914e57afca52412be0b084b78fc34826983d9cd Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 23 Jan 2023 13:28:00 -0800 Subject: [PATCH 240/397] export PROMPT and PROMPT_VERSION, always start remote --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/remote/remote.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 40ca70385..39d7f1627 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -902,7 +902,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if editArgs.Color != "" { r.RemoteOpts = &sstore.RemoteOptsType{Color: editArgs.Color} } - err = remote.AddRemote(ctx, r) + err = remote.AddRemote(ctx, r, true) if err != nil { return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 662f14a68..c7cdd9dc9 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -283,7 +283,7 @@ func ReadRemotePty(ctx context.Context, remoteId string) (int64, []byte, error) return offset, barr, nil } -func AddRemote(ctx context.Context, r *sstore.RemoteType) error { +func AddRemote(ctx context.Context, r *sstore.RemoteType, shouldStart bool) error { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -306,7 +306,7 @@ func AddRemote(ctx context.Context, r *sstore.RemoteType) error { newMsh := MakeMShell(r) GlobalStore.Map[r.RemoteId] = newMsh go newMsh.NotifyRemoteUpdate() - if r.ConnectMode == sstore.ConnectModeStartup { + if shouldStart { go newMsh.Launch() } return nil @@ -988,8 +988,8 @@ func addScVarsToState(state *packet.ShellState) *packet.ShellState { } rtn := *state envMap := shexec.DeclMapFromState(&rtn) - envMap["PROMPT"] = &shexec.DeclareDeclType{Name: "PROMPT", Value: "1"} - envMap["PROMPT_VERSION"] = &shexec.DeclareDeclType{Name: "PROMPT_VERSION", Value: scbase.PromptVersion} + envMap["PROMPT"] = &shexec.DeclareDeclType{Name: "PROMPT", Value: "1", Args: "x"} + envMap["PROMPT_VERSION"] = &shexec.DeclareDeclType{Name: "PROMPT_VERSION", Value: scbase.PromptVersion, Args: "x"} rtn.ShellVars = shexec.SerializeDeclMap(envMap) return &rtn } From ae15fbdf91b1692b016634ad6bf818a95793116a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 23 Jan 2023 13:47:36 -0800 Subject: [PATCH 241/397] create remote should show remote page, and force an interactive connect --- pkg/cmdrunner/cmdrunner.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 39d7f1627..4890e9aae 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -656,7 +656,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } - go ids.Remote.MShell.Launch() + go ids.Remote.MShell.Launch(true) return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, @@ -907,13 +907,11 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } // SUCCESS - update := sstore.ModelUpdate{ + return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("remote %q created", r.RemoteCanonicalName), - TimeoutMs: 2000, + PtyRemoteId: r.RemoteId, }, - } - return update, nil + }, nil } func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { From bf36d4f9424040a6ed95c1a3d15712312a5f54a8 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 23 Jan 2023 13:47:57 -0800 Subject: [PATCH 242/397] interactive connect -- does not force ssh batch mode --- pkg/remote/remote.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c7cdd9dc9..4090132fb 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -225,7 +225,7 @@ func LoadRemotes(ctx context.Context) error { msh := MakeMShell(remote) GlobalStore.Map[remote.RemoteId] = msh if remote.ConnectMode == sstore.ConnectModeStartup { - go msh.Launch() + go msh.Launch(false) } if remote.Local { if remote.RemoteSudo { @@ -264,7 +264,7 @@ func LoadRemoteById(ctx context.Context, remoteId string) error { } GlobalStore.Map[r.RemoteId] = msh if r.ConnectMode == sstore.ConnectModeStartup { - go msh.Launch() + go msh.Launch(false) } return nil } @@ -307,7 +307,7 @@ func AddRemote(ctx context.Context, r *sstore.RemoteType, shouldStart bool) erro GlobalStore.Map[r.RemoteId] = newMsh go newMsh.NotifyRemoteUpdate() if shouldStart { - go newMsh.Launch() + go newMsh.Launch(true) } return nil } @@ -951,7 +951,7 @@ func (msh *MShellProc) RunInstall() { msh.InstallCancelFn = nil msh.NeedsMShellUpgrade = false }) - msh.WriteToPtyBuffer("successfully installed mshell %s\n", scbase.MShellVersion) + msh.WriteToPtyBuffer("successfully installed mshell %s to ~/.mshell\n", scbase.MShellVersion) go msh.NotifyRemoteUpdate() return } @@ -1024,7 +1024,7 @@ func stripScVarsFromStateDiff(stateDiff *packet.ShellStateDiff) *packet.ShellSta return &rtn } -func (msh *MShellProc) Launch() { +func (msh *MShellProc) Launch(interactive bool) { remoteCopy := msh.GetRemoteCopy() if remoteCopy.Archived { msh.WriteToPtyBuffer("cannot launch archived remote\n") @@ -1047,7 +1047,7 @@ func (msh *MShellProc) Launch() { msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true - if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" { + if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" && !interactive { sshOpts.BatchMode = true } var cmdStr string @@ -1885,7 +1885,7 @@ func (msh *MShellProc) TryAutoConnect() error { if err != nil { return err } - msh.Launch() + msh.Launch(false) if !msh.IsConnected() { return fmt.Errorf("error connecting") } From 2bb098e94064fc349b7f3d46e8fe6e216216971c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 23 Jan 2023 15:48:54 -0800 Subject: [PATCH 243/397] exit stdin loop on error, force a sigkill after 10s --- cmd/main-server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/main-server.go b/cmd/main-server.go index d3fef53da..ed7db32a5 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -434,8 +434,11 @@ func stdinReadWatch() { sendTelemetryWrapper() time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) + break } } + time.Sleep(10 * time.Second) + syscall.Kill(syscall.Getpid(), syscall.SIGKILL) } func main() { From 67ad7d017e7a87ec884ae9842e648bef0a49f9c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 25 Jan 2023 13:57:36 -0800 Subject: [PATCH 244/397] add session:purge (to be compatible with screen) --- pkg/cmdrunner/cmdrunner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 4890e9aae..a38633c3f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -121,7 +121,8 @@ func init() { registerCmdFn("session:open", SessionOpenCommand) registerCmdAlias("session:new", SessionOpenCommand) registerCmdFn("session:set", SessionSetCommand) - registerCmdFn("session:delete", SessionDeleteCommand) + registerCmdAlias("session:delete", SessionDeleteCommand) + registerCmdFn("session:purge", SessionDeleteCommand) registerCmdFn("session:archive", SessionArchiveCommand) registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("session:show", SessionShowCommand) From 18048a675acfe5af508358aea5a6663eba246a78 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 25 Jan 2023 14:29:12 -0800 Subject: [PATCH 245/397] fix /scren:purge hanging issue --- pkg/sstore/dbops.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c1feacfe7..1a1b52e52 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -861,26 +861,25 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U } func cleanSessionCmds(ctx context.Context, sessionId string) error { - txErr := WithTx(context.Background(), func(tx *TxWrap) error { + var removedCmds []string + txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - removedCmds := tx.SelectStrings(query, sessionId, sessionId) + removedCmds = tx.SelectStrings(query, sessionId, sessionId) query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` tx.ExecWrap(query, sessionId, sessionId) - if tx.Err != nil { - return nil - } - for _, cmdId := range removedCmds { - DeletePtyOutFile(tx.Context(), sessionId, cmdId) - } return nil }) if txErr != nil { return txErr } + for _, cmdId := range removedCmds { + DeletePtyOutFile(ctx, sessionId, cmdId) + } return nil } func CleanWindows(sessionId string) { + // NOTE: context.Background() here! (this could take a long time, and is async) txErr := WithTx(context.Background(), func(tx *TxWrap) error { query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid NOT IN (SELECT windowid FROM screen_window WHERE sessionid = ?)` removedWindowIds := tx.SelectStrings(query, sessionId, sessionId) From 518e900403b4d6fd6cbfbaced4573ac744c5e338 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 25 Jan 2023 22:30:25 -0800 Subject: [PATCH 246/397] bump to v0.1.2 --- cmd/main-server.go | 1 + pkg/scbase/scbase.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index ed7db32a5..45ac67059 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -452,6 +452,7 @@ func main() { } scHomeDir := scbase.GetPromptHomeDir() + log.Printf("[prompt] local server version %s\n", scbase.PromptVersion) log.Printf("[prompt] homedir = %q\n", scHomeDir) scLock, err := scbase.AcquirePromptLock() diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 7b73f26ad..705b62649 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -26,7 +26,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.1" +const PromptVersion = "v0.1.2" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 848f321ae7a06873f5178eefe8f33601f79b5d44 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 25 Jan 2023 22:49:50 -0800 Subject: [PATCH 247/397] backend version, check telemetry send more often (but send on the same interval) -- works better with computer suspend. --- cmd/main-server.go | 28 +++++++++++++++++++--------- pkg/cmdrunner/cmdrunner.go | 1 + 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 45ac67059..54ab16ffc 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -43,6 +43,10 @@ const WebSocketServerDevAddr = "localhost:8091" const WSStateReconnectTime = 30 * time.Second const WSStatePacketChSize = 20 +const InitialTelemetryWait = 30 * time.Second +const TelemetryTick = 30 * time.Minute +const TelemetryInterval = 8 * time.Hour + var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string @@ -424,6 +428,19 @@ func sendTelemetryWrapper() { } } +func telemetryLoop() { + var lastSent time.Time + time.Sleep(InitialTelemetryWait) + for { + dur := time.Now().Sub(lastSent) + if lastSent.IsZero() || dur >= TelemetryInterval { + lastSent = time.Now() + sendTelemetryWrapper() + } + time.Sleep(TelemetryTick) + } +} + // watch stdin, kill server if stdin is closed func stdinReadWatch() { buf := make([]byte, 1024) @@ -509,15 +526,8 @@ func main() { log.Printf("[error] resetting window focus: %v\n", err) } - go func() { - log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) - time.Sleep(30 * time.Second) - for { - sendTelemetryWrapper() - // send new telemetry every 8-hours - time.Sleep(8 * time.Hour) - } - }() + log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) + go telemetryLoop() go stdinReadWatch() go runWebSocketServer() gr := mux.NewRouter() diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a38633c3f..8412aa8ef 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2106,6 +2106,7 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "backend", scbase.PromptVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ From cd41be5d04324183ce26425a8f93cf77899ed147 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 26 Jan 2023 14:41:26 -0800 Subject: [PATCH 248/397] use relative path for mshell --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c928ff767..9efde4020 100644 --- a/go.mod +++ b/go.mod @@ -26,4 +26,4 @@ require ( mvdan.cc/sh/v3 v3.5.1 // indirect ) -replace github.com/scripthaus-dev/mshell v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ +replace github.com/scripthaus-dev/mshell v0.0.0 => ../mshell/ From 0696669c538912ffe144b2c1f9f4d85b63d2f1f6 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 28 Jan 2023 21:15:05 -0800 Subject: [PATCH 249/397] update schema --- db/schema.sql | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 3e3edf927..ae1baed9c 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE client ( userpublickeybytes blob NOT NULL, userprivatekeybytes blob NOT NULL, winsize json NOT NULL -); +, clientopts json NOT NULL DEFAULT ''); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, @@ -97,7 +97,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, - hidden boolean NOT NULL, + archived boolean NOT NULL, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -156,3 +156,12 @@ CREATE TABLE history ( ismetacmd boolean, incognito boolean ); +CREATE TABLE activity ( + day varchar(20) PRIMARY KEY, + uploaded boolean NOT NULL, + tdata json NOT NULL, + tzname varchar(50) NOT NULL, + tzoffset int NOT NULL, + clientversion varchar(20) NOT NULL, + clientarch varchar(20) NOT NULL +); From 6ea1dd65bbdf9baaf98cb713b4d9948e8662946d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 30 Jan 2023 11:36:02 -0800 Subject: [PATCH 250/397] update ports --- cmd/main-server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 54ab16ffc..345e46dfd 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -36,8 +36,8 @@ const HttpWriteTimeout = 21 * time.Second const HttpMaxHeaderBytes = 60000 const HttpTimeoutDuration = 21 * time.Second -const MainServerAddr = "localhost:8080" -const WebSocketServerAddr = "localhost:8081" +const MainServerAddr = "localhost:1619" // PromptServer, P=16, S=19, PS=1619 +const WebSocketServerAddr = "localhost:1623" // PromptWebsock, P=16, W=23, PW=1623 const MainServerDevAddr = "localhost:8090" const WebSocketServerDevAddr = "localhost:8091" const WSStateReconnectTime = 30 * time.Second From 108517bd086edc56b2a33134cb57436c2eaba948 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 31 Jan 2023 17:56:56 -0800 Subject: [PATCH 251/397] add /screen:reset to remove all remote instances for screen --- pkg/cmdrunner/cmdrunner.go | 56 +++++++++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 25 +++++++++++++++-- pkg/sstore/updatebus.go | 2 +- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8412aa8ef..d40c84edc 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -134,6 +134,7 @@ func init() { registerCmdAlias("screen:new", ScreenOpenCommand) registerCmdFn("screen:set", ScreenSetCommand) registerCmdFn("screen:showall", ScreenShowAllCommand) + registerCmdFn("screen:reset", ScreenResetCommand) registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) @@ -145,6 +146,7 @@ func init() { registerCmdFn("remote:connect", RemoteConnectCommand) registerCmdFn("remote:install", RemoteInstallCommand) registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) + registerCmdFn("remote:reset", RemoteResetCommand) registerCmdFn("sw:set", SwSetCommand) registerCmdFn("sw:resize", SwResizeCommand) @@ -1004,6 +1006,52 @@ func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) }, nil } +func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, err + } + screen, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("error retrieving screen: %v", err) + } + localRemote := remote.GetLocalRemote() + if localRemote == nil { + return nil, fmt.Errorf("error getting local remote (not found)") + } + rptr := sstore.RemotePtrType{RemoteId: localRemote.RemoteId} + var windows []*sstore.WindowType + sessionUpdate := &sstore.SessionType{SessionId: ids.SessionId} + for _, sw := range screen.Windows { + ris, err := sstore.WindowReset(ctx, ids.SessionId, sw.WindowId) + if err != nil { + return nil, fmt.Errorf("error resetting screen window: %v", err) + } + sessionUpdate.Remotes = append(sessionUpdate.Remotes, ris...) + err = sstore.UpdateCurRemote(ctx, ids.SessionId, sw.WindowId, rptr) + if err != nil { + return nil, fmt.Errorf("cannot reset window remote back to local: %w", err) + } + winUpdate := &sstore.WindowType{SessionId: ids.SessionId, WindowId: sw.WindowId, CurRemote: rptr} + windows = append(windows, winUpdate) + } + outputStr := "reset screen state (all remote state reset)" + cmd, err := makeStaticCmd(ctx, "screen:reset", ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Interactive = pk.Interactive + update.Windows = windows + update.Sessions = []*sstore.SessionType{sessionUpdate} + return update, nil +} + func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -1016,11 +1064,11 @@ func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName) localRemote := remote.GetLocalRemote() if localRemote != nil { - update.Window = &sstore.WindowType{ + update.Windows = []*sstore.WindowType{&sstore.WindowType{ SessionId: ids.SessionId, WindowId: ids.WindowId, CurRemote: sstore.RemotePtrType{RemoteId: localRemote.GetRemoteId()}, - } + }} } return update, nil } @@ -1102,11 +1150,11 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) } update := sstore.ModelUpdate{ - Window: &sstore.WindowType{ + Windows: []*sstore.WindowType{&sstore.WindowType{ SessionId: ids.SessionId, WindowId: ids.WindowId, CurRemote: *rptr, - }, + }}, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("current remote = %q", remoteName), TimeoutMs: 2000, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1a1b52e52..bd09ff2c2 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1252,7 +1252,7 @@ func ArchiveWindowLines(ctx context.Context, sessionId string, windowId string) if err != nil { return nil, err } - return &ModelUpdate{Window: win}, nil + return &ModelUpdate{Windows: []*WindowType{win}}, nil } func PurgeWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { @@ -1289,7 +1289,7 @@ func PurgeWindowLines(ctx context.Context, sessionId string, windowId string) (* } win.Lines = append(win.Lines, line) } - return &ModelUpdate{Window: win}, nil + return &ModelUpdate{Windows: []*WindowType{win}}, nil } func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string) ([]*CmdType, error) { @@ -1317,6 +1317,27 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term return txErr } +// returns riids of deleted RIs +func WindowReset(ctx context.Context, sessionId string, windowId string) ([]*RemoteInstance, error) { + var delRis []*RemoteInstance + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + if !tx.Exists(query, sessionId, windowId) { + return fmt.Errorf("window does not exist") + } + query = `SELECT riid FROM remote_instance WHERE sessionid = ? AND windowid = ?` + riids := tx.SelectStrings(query, sessionId, windowId) + for _, riid := range riids { + ri := &RemoteInstance{SessionId: sessionId, WindowId: windowId, RIId: riid, Remove: true} + delRis = append(delRis, ri) + } + query = `DELETE FROM remote_instance WHERE sessionid = ? AND windowid = ?` + tx.ExecWrap(query, sessionId, windowId) + return nil + }) + return delRis, txErr +} + func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) { var newActiveSessionId string txErr := WithTx(ctx, func(tx *TxWrap) error { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 4c5ba2aab..84cbb3021 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -32,7 +32,7 @@ func (PtyDataUpdate) UpdateType() string { type ModelUpdate struct { Sessions []*SessionType `json:"sessions,omitempty"` ActiveSessionId string `json:"activesessionid,omitempty"` - Window *WindowType `json:"window,omitempty"` + Windows []*WindowType `json:"windows,omitempty"` ScreenWindows []*ScreenWindowType `json:"screenwindows,omitempty"` Line *LineType `json:"line,omitempty"` Cmd *CmdType `json:"cmd,omitempty"` From c0876a6ab657d3fd7829790395b6e6f8d7c2af85 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 31 Jan 2023 22:21:19 -0800 Subject: [PATCH 252/397] line:setheight command --- pkg/cmdrunner/cmdrunner.go | 31 ++++++++++++++++++++++++++++++- pkg/sstore/dbops.go | 12 ++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d40c84edc..25c4e073f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -159,6 +159,7 @@ func init() { registerCmdFn("line:star", LineStarCommand) registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) + registerCmdFn("line:setheight", LineSetHeightCommand) registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) @@ -1864,7 +1865,35 @@ func SwResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst } 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"}, "or", false)) + return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "purge", "setheight"}, "or", false)) +} + +func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) != 2 { + return nil, fmt.Errorf("/line:setheight requires 2 arguments (linearg and height)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + heightVal, err := resolveNonNegInt(pk.Args[1], 0) + if heightVal == 0 { + return nil, fmt.Errorf("/line:setheight invalid height val") + } + if heightVal > 10000 { + return nil, fmt.Errorf("/line:setheight invalid height val (too large): %d", heightVal) + } + err = sstore.UpdateLineHeight(ctx, lineId, heightVal) + if err != nil { + return nil, fmt.Errorf("/line:setheight error updating height: %v", err) + } + // we don't need to pass the updated line height (it is "write only") + return nil, nil } func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index bd09ff2c2..7af987a0f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1780,6 +1780,18 @@ func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { return nil } +func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET contentheight = ? WHERE lineid = ?` + tx.ExecWrap(query, heightVal, lineId) + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + // can return nil, nil if line is not found func GetLineById(ctx context.Context, lineId string) (*LineType, error) { var rtn *LineType From 9914bd080bf8809eff53e44c2a1b3fafc61eec12 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 1 Feb 2023 00:48:00 -0800 Subject: [PATCH 253/397] bump version to v0.1.3 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 705b62649..059c77345 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -26,7 +26,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.2" +const PromptVersion = "v0.1.3" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 171fa9128dbe04a7ca0011e00cf87b42368865cd Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 1 Feb 2023 09:45:33 -0800 Subject: [PATCH 254/397] fix invalid height error --- pkg/cmdrunner/cmdrunner.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 25c4e073f..13f610bd2 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1882,10 +1882,10 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("error looking up lineid: %v", err) } heightVal, err := resolveNonNegInt(pk.Args[1], 0) - if heightVal == 0 { - return nil, fmt.Errorf("/line:setheight invalid height val") + if err != nil { + return nil, fmt.Errorf("/line:setheight invalid height val: %v", err) } - if heightVal > 10000 { + if heightVal > 1000 { return nil, fmt.Errorf("/line:setheight invalid height val (too large): %d", heightVal) } err = sstore.UpdateLineHeight(ctx, lineId, heightVal) From 57a8e49d73ae17f1ffdf14982b5551ad984fa259 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Feb 2023 00:30:23 -0800 Subject: [PATCH 255/397] updates for line renderer --- db/migrations/000003_renderer.down.sql | 2 ++ db/migrations/000003_renderer.up.sql | 2 ++ pkg/cmdrunner/cmdrunner.go | 12 ++++++++++-- pkg/sstore/dbops.go | 4 ++-- pkg/sstore/fileops.go | 8 ++++++++ pkg/sstore/sstore.go | 8 +++++--- 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 db/migrations/000003_renderer.down.sql create mode 100644 db/migrations/000003_renderer.up.sql diff --git a/db/migrations/000003_renderer.down.sql b/db/migrations/000003_renderer.down.sql new file mode 100644 index 000000000..4ef6ef5a3 --- /dev/null +++ b/db/migrations/000003_renderer.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE line DROP COLUMN renderer; + diff --git a/db/migrations/000003_renderer.up.sql b/db/migrations/000003_renderer.up.sql new file mode 100644 index 000000000..43f9de435 --- /dev/null +++ b/db/migrations/000003_renderer.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE line ADD COLUMN renderer varchar(50) NOT NULL DEFAULT ''; + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 13f610bd2..c75a34c9a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1197,7 +1197,7 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr } func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType) (*sstore.ModelUpdate, error) { - rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd) + rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd, "") if err != nil { return nil, err } @@ -1885,7 +1885,7 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/line:setheight invalid height val: %v", err) } - if heightVal > 1000 { + if heightVal > 10000 { return nil, fmt.Errorf("/line:setheight invalid height val (too large): %d", heightVal) } err = sstore.UpdateLineHeight(ctx, lineId, heightVal) @@ -2051,6 +2051,14 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if cmd.RtnState { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "rtnstate", "true")) } + stat, _ := sstore.StatCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId) + if stat == nil { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", "-")) + } else { + fileDataStr := fmt.Sprintf("v%d data=%d offset=%d max=%s", stat.Version, stat.DataSize, stat.FileOffset, scbase.NumFormatB2(stat.MaxSize)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr)) + } } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 7af987a0f..5bf2bee78 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -687,8 +687,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, ephemeral, contentheight, star, archived) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:ephemeral,:contentheight,:star,:archived)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived)` tx.NamedExecWrap(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 87b91e628..bd654ee62 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -24,6 +24,14 @@ func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSi return f.Close() } +func StatCmdPtyFile(ctx context.Context, sessionId string, cmdId string) (*cirfile.Stat, error) { + ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + if err != nil { + return nil, err + } + return cirfile.StatCirFile(ctx, ptyOutFileName) +} + func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { if pos < 0 { return nil, fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d75db565a..95ccafbba 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -643,6 +643,7 @@ type LineType struct { LineNumTemp bool `json:"linenumtemp,omitempty"` LineLocal bool `json:"linelocal"` LineType string `json:"linetype"` + Renderer string `json:"renderer,omitempty"` Text string `json:"text,omitempty"` CmdId string `json:"cmdid,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` @@ -825,7 +826,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { return &cmd } -func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId string) *LineType { +func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId string, renderer string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId rtn.WindowId = windowId @@ -836,6 +837,7 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri rtn.LineType = LineTypeCmd rtn.CmdId = cmdId rtn.ContentHeight = LineNoHeight + rtn.Renderer = renderer return rtn } @@ -862,8 +864,8 @@ func AddCommentLine(ctx context.Context, sessionId string, windowId string, user return rtnLine, nil } -func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string, cmd *CmdType) (*LineType, error) { - rtnLine := makeNewLineCmd(sessionId, windowId, userId, cmd.CmdId) +func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { + rtnLine := makeNewLineCmd(sessionId, windowId, userId, cmd.CmdId, renderer) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err From bc0abacfd244fce5dbaa833955d665fd9ebee3dd Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Feb 2023 16:17:54 -0800 Subject: [PATCH 256/397] use new external module github.com/sawka/txwrap --- go.mod | 15 ++- go.sum | 19 +++- pkg/sstore/dbops.go | 226 ++++++++++++++++++++++++------------------- pkg/sstore/sstore.go | 7 +- pkg/sstore/txwrap.go | 189 ------------------------------------ 5 files changed, 157 insertions(+), 299 deletions(-) delete mode 100644 pkg/sstore/txwrap.go diff --git a/go.mod b/go.mod index 9efde4020..c4ed9eb89 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,26 @@ module github.com/scripthaus-dev/sh2-server go 1.17 require ( + github.com/alessio/shellescape v1.4.1 + github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 + github.com/creack/pty v1.1.18 github.com/golang-migrate/migrate/v4 v4.15.2 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.14 + github.com/sawka/txwrap v0.1.0 github.com/scripthaus-dev/mshell v0.0.0 + golang.org/x/mod v0.5.1 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + mvdan.cc/sh/v3 v3.5.1 ) require ( - github.com/alessio/shellescape v1.4.1 // indirect - github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect - github.com/creack/pty v1.1.18 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/mattn/go-shellwords v1.0.12 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/mod v0.5.1 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - mvdan.cc/sh/v3 v3.5.1 // indirect ) replace github.com/scripthaus-dev/mshell v0.0.0 => ../mshell/ diff --git a/go.sum b/go.sum index 97dd6f71f..e47f8fcbf 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -332,6 +333,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= @@ -395,9 +397,10 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= @@ -551,6 +554,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= @@ -578,6 +582,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -748,10 +753,13 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -789,7 +797,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -906,6 +913,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -955,6 +963,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -964,6 +975,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sawka/txwrap v0.1.0 h1:uWGplmEJUEd9WGYZy9fU+hoC2Z6Yal4NMH5DbKsUTdo= +github.com/sawka/txwrap v0.1.0/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -1518,6 +1531,7 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= @@ -1802,6 +1816,7 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5bf2bee78..77d9d7792 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -5,9 +5,12 @@ import ( "fmt" "strconv" "strings" + "sync" "time" "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/sawka/txwrap" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" @@ -17,6 +20,35 @@ import ( const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito" const DefaultMaxHistoryItems = 1000 +type SingleConnDBGetter struct { + SingleConnLock *sync.Mutex +} + +type TxWrap = txwrap.TxWrap + +var dbWrap *SingleConnDBGetter + +func init() { + dbWrap = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}} +} + +func (dbg *SingleConnDBGetter) GetDB(ctx context.Context) (*sqlx.DB, error) { + db, err := GetDB(ctx) + if err != nil { + return nil, err + } + dbg.SingleConnLock.Lock() + return db, nil +} + +func (dbg *SingleConnDBGetter) ReleaseDB(db *sqlx.DB) { + dbg.SingleConnLock.Unlock() +} + +func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { + return txwrap.DBGWithTx(ctx, dbWrap, fn) +} + func NumSessions(ctx context.Context) (int, error) { var numSessions int txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -129,7 +161,7 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT remoteid FROM remote WHERE remoteid = ?` if tx.Exists(query, r.RemoteId) { - tx.ExecWrap(`DELETE FROM remote WHERE remoteid = ?`, r.RemoteId) + tx.Exec(`DELETE FROM remote WHERE remoteid = ?`, r.RemoteId) } query = `SELECT remoteid FROM remote WHERE remotecanonicalname = ?` if tx.Exists(query, r.RemoteCanonicalName) { @@ -145,7 +177,7 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { query = `INSERT INTO remote ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local) VALUES (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local)` - tx.NamedExecWrap(query, r.ToMap()) + tx.NamedExec(query, r.ToMap()) return nil }) return txErr @@ -159,7 +191,7 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito) VALUES (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito)` - tx.NamedExecWrap(query, hitem.ToMap()) + tx.NamedExec(query, hitem.ToMap()) return nil }) return txErr @@ -169,7 +201,7 @@ func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) ( var rtn bool txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT incognito FROM screen WHERE sessionid = ? AND screenid = ?` - tx.GetWrap(&rtn, query, sessionId, screenId) + tx.Get(&rtn, query, sessionId, screenId) return nil }) return rtn, txErr @@ -238,7 +270,7 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session ORDER BY archived, sessionidx, archivedts` - tx.SelectWrap(&rtn, query) + tx.Select(&rtn, query) return nil }) if err != nil { @@ -268,7 +300,7 @@ func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, er var rtn SessionType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session WHERE sessionid = ?` - tx.GetWrap(&rtn, query, sessionId) + tx.Get(&rtn, query, sessionId) return nil }) if txErr != nil { @@ -285,7 +317,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { var activeSessionId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session ORDER BY archived, sessionidx, archivedts` - tx.SelectWrap(&rtn, query) + tx.Select(&rtn, query) sessionMap := make(map[string]*SessionType) for _, session := range rtn { sessionMap[session.SessionId] = session @@ -293,7 +325,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { } var screens []*ScreenType query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` - tx.SelectWrap(&screens, query) + tx.Select(&screens, query) screenMap := make(map[string][]*ScreenType) for _, screen := range screens { screenArr := screenMap[screen.SessionId] @@ -305,7 +337,7 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { } var sws []*ScreenWindowType query = `SELECT * FROM screen_window` - tx.SelectWrap(&sws, query) + tx.Select(&sws, query) screenIdMap := make(map[string]*ScreenType) for _, screen := range screens { screenIdMap[screen.SessionId+screen.ScreenId] = screen @@ -346,7 +378,7 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win } rtnWindow = WindowFromMap(m) query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` - tx.SelectWrap(&rtnWindow.Lines, query, sessionId, windowId) + tx.Select(&rtnWindow.Lines, query, sessionId, windowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` cmdMaps := tx.SelectMaps(query, sessionId, windowId) for _, m := range cmdMaps { @@ -362,7 +394,7 @@ func GetBareSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` - tx.SelectWrap(&rtn, query, sessionId) + tx.Select(&rtn, query, sessionId) return nil }) return rtn, txErr @@ -413,14 +445,14 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo maxSessionIdx := tx.GetInt(`SELECT COALESCE(max(sessionidx), 0) FROM session`) query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, archivedts, ownerid, sharemode, accesskey) VALUES (?, ?, '', ?, ?, 0, 0, '', 'local', '')` - tx.ExecWrap(query, newSessionId, sessionName, maxSessionIdx+1, 0) + tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { return err } if activate { query = `UPDATE client SET activesessionid = ?` - tx.ExecWrap(query, newSessionId) + tx.Exec(query, newSessionId) } return nil }) @@ -447,7 +479,7 @@ func SetActiveSessionId(ctx context.Context, sessionId string) error { return fmt.Errorf("cannot switch to session, not found") } query = `UPDATE client SET activesessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) return nil }) return txErr @@ -466,7 +498,7 @@ func GetActiveSessionId(ctx context.Context) (string, error) { func SetWinSize(ctx context.Context, winSize ClientWinSizeType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET winsize = ?` - tx.ExecWrap(query, quickJson(winSize)) + tx.Exec(query, quickJson(winSize)) return nil }) return txErr @@ -527,13 +559,13 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, } newScreenId = scbase.GenPromptUUID() query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived, archivedts) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0, 0)` - tx.ExecWrap(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) + tx.Exec(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) layout := LayoutType{Type: LayoutFull} query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - tx.ExecWrap(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, SWAnchorType{}, "input") + tx.Exec(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, SWAnchorType{}, "input") if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` - tx.ExecWrap(query, newScreenId, sessionId) + tx.Exec(query, newScreenId, sessionId) } return nil }) @@ -557,13 +589,13 @@ func GetScreenById(ctx context.Context, sessionId string, screenId string) (*Scr txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM screen WHERE sessionid = ? AND screenid = ?` var screen ScreenType - found := tx.GetWrap(&screen, query, sessionId, screenId) + found := tx.Get(&screen, query, sessionId, screenId) if !found { return nil } rtnScreen = &screen query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ?` - tx.SelectWrap(&screen.Windows, query, sessionId, screenId) + tx.Select(&screen.Windows, query, sessionId, screenId) screen.Full = true return nil }) @@ -586,7 +618,7 @@ func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) strin wmap := w.ToMap() query := `INSERT INTO window ( sessionid, windowid, curremoteownerid, curremoteid, curremotename, nextlinenum, winopts, ownerid, sharemode, shareopts) VALUES (:sessionid,:windowid,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:winopts,:ownerid,:sharemode,:shareopts)` - tx.NamedExecWrap(query, wmap) + tx.NamedExec(query, wmap) return w.WindowId } @@ -625,7 +657,7 @@ func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, } var lineVal LineType query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` - found := tx.GetWrap(&lineVal, query, sessionId, windowId, lineId) + found := tx.Get(&lineVal, query, sessionId, windowId, lineId) if !found { return nil } @@ -653,7 +685,7 @@ func GetLineCmdByCmdId(ctx context.Context, sessionId string, windowId string, c } var lineVal LineType query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND cmdid = ?` - found := tx.GetWrap(&lineVal, query, sessionId, windowId, cmdId) + found := tx.Get(&lineVal, query, sessionId, windowId, cmdId) if !found { return nil } @@ -689,9 +721,9 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { line.LineNum = int64(nextLineNum) query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived) VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived)` - tx.NamedExecWrap(query, line) + tx.NamedExec(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, nextLineNum+1, line.SessionId, line.WindowId) + tx.Exec(query, nextLineNum+1, line.SessionId, line.WindowId) if cmd != nil { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() @@ -699,7 +731,7 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` - tx.NamedExecWrap(query, cmdMap) + tx.NamedExec(query, cmdMap) } return nil }) @@ -741,7 +773,7 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, CmdStatusDone, quickJson(doneInfo), ck.GetSessionId(), ck.GetCmdId()) + tx.Exec(query, CmdStatusDone, quickJson(doneInfo), ck.GetSessionId(), ck.GetCmdId()) var err error rtnCmd, err = GetCmdById(tx.Context(), ck.GetSessionId(), ck.GetCmdId()) if err != nil { @@ -764,7 +796,7 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt } txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetSessionId(), ck.GetCmdId()) + tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetSessionId(), ck.GetCmdId()) return nil }) if txErr != nil { @@ -779,7 +811,7 @@ 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 sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, quickJson(errPk), errPk.CK.GetSessionId(), errPk.CK.GetCmdId()) + tx.Exec(query, quickJson(errPk), errPk.CK.GetSessionId(), errPk.CK.GetCmdId()) return nil }) } @@ -787,7 +819,7 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err func ReInitFocus(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE screen_window SET focustype = 'input'` - tx.ExecWrap(query) + tx.Exec(query) return nil }) } @@ -795,7 +827,7 @@ 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 = ?` - tx.ExecWrap(query, CmdStatusHangup, CmdStatusRunning) + tx.Exec(query, CmdStatusHangup, CmdStatusRunning) return nil }) } @@ -803,7 +835,7 @@ func HangupAllRunningCmds(ctx context.Context) error { func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE status = ? AND remoteid = ?` - tx.ExecWrap(query, CmdStatusHangup, CmdStatusRunning, remoteId) + tx.Exec(query, CmdStatusHangup, CmdStatusRunning, remoteId) return nil }) } @@ -811,7 +843,7 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { func HangupCmd(ctx context.Context, ck base.CommandKey) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ? WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, CmdStatusHangup, ck.GetSessionId(), ck.GetCmdId()) + tx.Exec(query, CmdStatusHangup, ck.GetSessionId(), ck.GetCmdId()) return nil }) } @@ -847,7 +879,7 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U 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.ExecWrap(query, screenId, sessionId) + tx.Exec(query, screenId, sessionId) return nil }) if txErr != nil { @@ -866,7 +898,7 @@ func cleanSessionCmds(ctx context.Context, sessionId string) error { query := `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` removedCmds = tx.SelectStrings(query, sessionId, sessionId) query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - tx.ExecWrap(query, sessionId, sessionId) + tx.Exec(query, sessionId, sessionId) return nil }) if txErr != nil { @@ -888,11 +920,11 @@ func CleanWindows(sessionId string) { } for _, windowId := range removedWindowIds { query = `DELETE FROM window WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) } return cleanSessionCmds(tx.Context(), sessionId) }) @@ -918,12 +950,12 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda 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.ExecWrap(query, time.Now().UnixMilli(), sessionId, 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 := getNextId(screenIds, screenId) - tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) } return nil }) @@ -950,7 +982,7 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err } maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) query = `UPDATE screen SET archived = 0, screenidx = ? WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, maxScreenIdx+1, sessionId, screenId) + tx.Exec(query, maxScreenIdx+1, sessionId, screenId) return nil }) return txErr @@ -971,12 +1003,12 @@ func DeleteScreen(ctx context.Context, sessionId string, screenId string) (Updat if isActive { screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) - tx.ExecWrap(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) } query = `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, sessionId, screenId) + tx.Exec(query, sessionId, screenId) query = `DELETE FROM screen WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, sessionId, screenId) + tx.Exec(query, sessionId, screenId) return nil }) if txErr != nil { @@ -1114,7 +1146,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r } query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, festate, statebasehash, statediffhasharr) VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:festate,:statebasehash,:statediffhasharr)` - tx.NamedExecWrap(query, ri.ToMap()) + tx.NamedExec(query, ri.ToMap()) return nil } else { query = `UPDATE remote_instance SET festate = ?, statebasehash = ?, statediffhasharr = ? WHERE riid = ?` @@ -1123,7 +1155,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r if err != nil { return err } - tx.ExecWrap(query, quickJson(ri.FeState), ri.StateBaseHash, quickJsonArr(ri.StateDiffHashArr), ri.RIId) + tx.Exec(query, quickJson(ri.FeState), ri.StateBaseHash, quickJsonArr(ri.StateDiffHashArr), ri.RIId) return nil } }) @@ -1137,7 +1169,7 @@ func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, rem return fmt.Errorf("cannot update curremote: no window found") } query = `UPDATE window SET curremoteownerid = ?, curremoteid = ?, curremotename = ? WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) + tx.Exec(query, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) return nil }) return txErr @@ -1174,7 +1206,7 @@ func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error } query = `UPDATE session SET sessionid = ? WHERE sessionid = ?` for idx, id := range ids { - tx.ExecWrap(query, id, idx+1) + tx.Exec(query, id, idx+1) } return nil }) @@ -1200,7 +1232,7 @@ func SetSessionName(ctx context.Context, sessionId string, name string) error { } } query = `UPDATE session SET name = ? WHERE sessionid = ?` - tx.ExecWrap(query, name, sessionId) + tx.Exec(query, name, sessionId) return nil }) return txErr @@ -1213,7 +1245,7 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name return fmt.Errorf("screen does not exist") } query = `UPDATE screen SET name = ? WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, name, sessionId, screenId) + tx.Exec(query, name, sessionId, screenId) return nil }) return txErr @@ -1229,7 +1261,7 @@ func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts return fmt.Errorf("screen does not exist") } query = `UPDATE screen SET screenopts = ? WHERE sessionid = ? AND screenid = ?` - tx.ExecWrap(query, opts, sessionId, screenId) + tx.Exec(query, opts, sessionId, screenId) return nil }) return txErr @@ -1242,7 +1274,7 @@ func ArchiveWindowLines(ctx context.Context, sessionId string, windowId string) return fmt.Errorf("window does not exist") } query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) return nil }) if txErr != nil { @@ -1265,11 +1297,11 @@ func PurgeWindowLines(ctx context.Context, sessionId string, windowId string) (* query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` lineIds = tx.SelectStrings(query, sessionId, windowId) query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) query = `UPDATE window SET nextlinenum = 1 WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) return nil }) if txErr != nil { @@ -1311,7 +1343,7 @@ func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, termOpts TermOpts) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET termopts = ? WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, termOpts, sessionId, cmdId) + tx.Exec(query, termOpts, sessionId, cmdId) return nil }) return txErr @@ -1332,7 +1364,7 @@ func WindowReset(ctx context.Context, sessionId string, windowId string) ([]*Rem delRis = append(delRis, ri) } query = `DELETE FROM remote_instance WHERE sessionid = ? AND windowid = ?` - tx.ExecWrap(query, sessionId, windowId) + tx.Exec(query, sessionId, windowId) return nil }) return delRis, txErr @@ -1346,19 +1378,19 @@ func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) return fmt.Errorf("session does not exist") } query = `DELETE FROM session WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM screen WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM screen_window WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM window WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM history WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM line WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) query = `DELETE FROM cmd WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) newActiveSessionId, _ = fixActiveSessionId(tx.Context()) return nil }) @@ -1392,7 +1424,7 @@ func fixActiveSessionId(ctx context.Context) (string, error) { if err != nil { return err } - tx.ExecWrap("UPDATE client SET activesessionid = ?", newActiveSessionId) + tx.Exec("UPDATE client SET activesessionid = ?", newActiveSessionId) return nil }) if txErr != nil { @@ -1417,7 +1449,7 @@ func ArchiveSession(ctx context.Context, sessionId string) (*ModelUpdate, error) return nil } query = `UPDATE session SET archived = 1, archivedts = ? WHERE sessionid = ?` - tx.ExecWrap(query, time.Now().UnixMilli(), sessionId) + tx.Exec(query, time.Now().UnixMilli(), sessionId) newActiveSessionId, _ = fixActiveSessionId(tx.Context()) return nil }) @@ -1450,10 +1482,10 @@ func UnArchiveSession(ctx context.Context, sessionId string, activate bool) (*Mo return nil } query = `UPDATE session SET archived = 0, archivedts = 0 WHERE sessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) if activate { query = `UPDATE client SET activesessionid = ?` - tx.ExecWrap(query, sessionId) + tx.Exec(query, sessionId) } return nil }) @@ -1524,27 +1556,27 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter return fmt.Errorf("remote has duplicate alias, cannot update") } query = `UPDATE remote SET remotealias = ? WHERE remoteid = ?` - tx.ExecWrap(query, alias, remoteId) + tx.Exec(query, alias, remoteId) } if mode, found := editMap[RemoteField_ConnectMode]; found { query = `UPDATE remote SET connectmode = ? WHERE remoteid = ?` - tx.ExecWrap(query, mode, remoteId) + tx.Exec(query, mode, remoteId) } if autoInstall, found := editMap[RemoteField_AutoInstall]; found { query = `UPDATE remote SET autoinstall = ? WHERE remoteid = ?` - tx.ExecWrap(query, autoInstall, remoteId) + tx.Exec(query, autoInstall, remoteId) } if sshKey, found := editMap[RemoteField_SSHKey]; found { query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshidentity', ?) WHERE remoteid = ?` - tx.ExecWrap(query, sshKey, remoteId) + tx.Exec(query, sshKey, remoteId) } if sshPassword, found := editMap[RemoteField_SSHPassword]; found { query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshpassword', ?) WHERE remoteid = ?` - tx.ExecWrap(query, sshPassword, remoteId) + tx.Exec(query, sshPassword, remoteId) } if color, found := editMap[RemoteField_Color]; found { query = `UPDATE remote SET remoteopts = json_set(remoteopts, '$.color', ?) WHERE remoteid = ?` - tx.ExecWrap(query, color, remoteId) + tx.Exec(query, color, remoteId) } var err error rtn, err = GetRemoteById(tx.Context(), remoteId) @@ -1575,23 +1607,23 @@ func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, } if anchorLine, found := editMap[SWField_AnchorLine]; found { query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchorline', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.ExecWrap(query, anchorLine, sessionId, screenId, windowId) + tx.Exec(query, anchorLine, sessionId, screenId, windowId) } if anchorOffset, found := editMap[SWField_AnchorOffset]; found { query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchoroffset', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.ExecWrap(query, anchorOffset, sessionId, screenId, windowId) + tx.Exec(query, anchorOffset, sessionId, screenId, windowId) } if sline, found := editMap[SWField_SelectedLine]; found { query = `UPDATE screen_window SET selectedline = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.ExecWrap(query, sline, sessionId, screenId, windowId) + tx.Exec(query, sline, sessionId, screenId, windowId) } if focusType, found := editMap[SWField_Focus]; found { query = `UPDATE screen_window SET focustype = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.ExecWrap(query, focusType, sessionId, screenId, windowId) + tx.Exec(query, focusType, sessionId, screenId, windowId) } var sw ScreenWindowType query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` - found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) + found := tx.Get(&sw, query, sessionId, screenId, windowId) if found { rtn = &sw } @@ -1608,7 +1640,7 @@ func GetScreenWindowByIds(ctx context.Context, sessionId string, screenId string txErr := WithTx(ctx, func(tx *TxWrap) error { var sw ScreenWindowType query := `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` - found := tx.GetWrap(&sw, query, sessionId, screenId, windowId) + found := tx.Get(&sw, query, sessionId, screenId, windowId) if found { rtn = &sw } @@ -1624,7 +1656,7 @@ func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT lineid as id, linenum as num FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` - tx.SelectWrap(&rtn, query, sessionId, windowId) + tx.Select(&rtn, query, sessionId, windowId) return nil }) if txErr != nil { @@ -1648,7 +1680,7 @@ func UpdateSWsWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([] AND l.cmdid = ? )` var swKeys []SWKey - tx.SelectWrap(&swKeys, query, sessionId, cmdId) + tx.Select(&swKeys, query, sessionId, cmdId) if len(swKeys) == 0 { return nil } @@ -1681,7 +1713,7 @@ func StoreStateBase(ctx context.Context, state *packet.ShellState) error { return nil } query = `INSERT INTO state_base (basehash, ts, version, data) VALUES (:basehash,:ts,:version,:data)` - tx.NamedExecWrap(query, stateBase) + tx.NamedExec(query, stateBase) return nil }) if txErr != nil { @@ -1712,7 +1744,7 @@ func StoreStateDiff(ctx context.Context, diff *packet.ShellStateDiff) error { return nil } query = `INSERT INTO state_diff (diffhash, ts, basehash, diffhasharr, data) VALUES (:diffhash,:ts,:basehash,:diffhasharr,:data)` - tx.NamedExecWrap(query, stateDiff.ToMap()) + tx.NamedExec(query, stateDiff.ToMap()) return nil }) if txErr != nil { @@ -1730,7 +1762,7 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, txErr := WithTx(ctx, func(tx *TxWrap) error { var stateBase StateBase query := `SELECT * FROM state_base WHERE basehash = ?` - found := tx.GetWrap(&stateBase, query, ssPtr.BaseHash) + found := tx.Get(&stateBase, query, ssPtr.BaseHash) if !found { return fmt.Errorf("ShellState %s not found", ssPtr.BaseHash) } @@ -1771,7 +1803,7 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET star = ? WHERE lineid = ?` - tx.ExecWrap(query, starVal, lineId) + tx.Exec(query, starVal, lineId) return nil }) if txErr != nil { @@ -1783,7 +1815,7 @@ func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET contentheight = ? WHERE lineid = ?` - tx.ExecWrap(query, heightVal, lineId) + tx.Exec(query, heightVal, lineId) return nil }) if txErr != nil { @@ -1798,7 +1830,7 @@ func GetLineById(ctx context.Context, lineId string) (*LineType, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { var line LineType query := `SELECT * FROM line WHERE lineid = ?` - found := tx.GetWrap(&line, query, lineId) + found := tx.Get(&line, query, lineId) if found { rtn = &line } @@ -1813,7 +1845,7 @@ func GetLineById(ctx context.Context, lineId string) (*LineType, error) { func SetLineArchivedById(ctx context.Context, lineId string, archived bool) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET archived = ? WHERE lineid = ?` - tx.ExecWrap(query, archived, lineId) + tx.Exec(query, archived, lineId) return nil }) return txErr @@ -1822,7 +1854,7 @@ func SetLineArchivedById(ctx context.Context, lineId string, archived bool) erro func purgeCmdById(ctx context.Context, sessionId string, cmdId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `DELETE FROM cmd WHERE sessionid = ? AND cmdid = ?` - tx.ExecWrap(query, sessionId, cmdId) + tx.Exec(query, sessionId, cmdId) return DeletePtyOutFile(tx.Context(), sessionId, cmdId) }) return txErr @@ -1833,9 +1865,9 @@ func PurgeLineById(ctx context.Context, sessionId string, lineId string) error { query := `SELECT cmdid FROM line WHERE sessionid = ? AND lineid = ?` cmdId := tx.GetString(query, sessionId, lineId) query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` - tx.ExecWrap(query, sessionId, lineId) + tx.Exec(query, sessionId, lineId) query = `DELETE FROM history WHERE sessionid = ? AND lineid = ?` - tx.ExecWrap(query, sessionId, lineId) + tx.Exec(query, sessionId, lineId) if cmdId != "" { query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` cmdRefCount := tx.GetInt(query, sessionId, cmdId) @@ -1882,7 +1914,7 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { txErr := WithTx(ctx, func(tx *TxWrap) error { var tdata TelemetryData query := `SELECT tdata FROM activity WHERE day = ?` - found := tx.GetWrap(&tdata, query, dayStr) + found := tx.Get(&tdata, query, dayStr) if !found { query = `INSERT INTO activity (day, uploaded, tdata, tzname, tzoffset, clientversion, clientarch) VALUES (?, 0, ?, ?, ?, ?, ?)` @@ -1890,7 +1922,7 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { if len(tzName) > MaxTzNameLen { tzName = tzName[0:MaxTzNameLen] } - tx.ExecWrap(query, dayStr, tdata, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) + tx.Exec(query, dayStr, tdata, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) } tdata.NumCommands += update.NumCommands tdata.FgMinutes += update.FgMinutes @@ -1900,7 +1932,7 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { SET tdata = ?, clientversion = ? WHERE day = ?` - tx.ExecWrap(query, tdata, scbase.PromptVersion, dayStr) + tx.Exec(query, tdata, scbase.PromptVersion, dayStr) return nil }) if txErr != nil { @@ -1913,7 +1945,7 @@ func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) { var rtn []*ActivityType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM activity WHERE uploaded = 0 ORDER BY day DESC LIMIT 30` - tx.SelectWrap(&rtn, query) + tx.Select(&rtn, query) return nil }) if txErr != nil { @@ -1931,7 +1963,7 @@ func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) er if activity.Day == dayStr { continue } - tx.ExecWrap(query, activity.Day) + tx.Exec(query, activity.Day) } return nil }) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 95ccafbba..5683b1683 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -19,6 +19,7 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/sawka/txwrap" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/sh2-server/pkg/scbase" @@ -86,7 +87,7 @@ func IsValidConnectMode(mode string) bool { } func GetDB(ctx context.Context) (*sqlx.DB, error) { - if IsTxWrapContext(ctx) { + if txwrap.IsTxWrapContext(ctx) { return nil, fmt.Errorf("cannot call GetDB from within a running transaction") } globalDBLock.Lock() @@ -975,7 +976,7 @@ func createClientData(tx *TxWrap) error { } query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` - tx.NamedExecWrap(query, c.ToMap()) + tx.NamedExec(query, c.ToMap()) log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } @@ -1030,7 +1031,7 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET clientopts = ?` - tx.ExecWrap(query, quickJson(clientOpts)) + tx.Exec(query, quickJson(clientOpts)) return nil }) return txErr diff --git a/pkg/sstore/txwrap.go b/pkg/sstore/txwrap.go deleted file mode 100644 index 67761950b..000000000 --- a/pkg/sstore/txwrap.go +++ /dev/null @@ -1,189 +0,0 @@ -package sstore - -import ( - "context" - "database/sql" - "sync" - - "github.com/jmoiron/sqlx" -) - -type TxWrap struct { - Txx *sqlx.Tx - Err error - Ctx context.Context -} - -type txWrapKey struct{} - -// single-threaded access to DB -var globalNestingLock = &sync.Mutex{} - -func IsTxWrapContext(ctx context.Context) bool { - ctxVal := ctx.Value(txWrapKey{}) - return ctxVal != nil -} - -func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { - var txWrap *TxWrap - ctxVal := ctx.Value(txWrapKey{}) - if ctxVal != nil { - txWrap = ctxVal.(*TxWrap) - if txWrap.Err != nil { - return txWrap.Err - } - } - if txWrap == nil { - globalNestingLock.Lock() - defer globalNestingLock.Unlock() - - db, err := GetDB(ctx) - if err != nil { - return err - } - tx, beginErr := db.BeginTxx(ctx, nil) - if beginErr != nil { - return beginErr - } - txWrap = &TxWrap{Txx: tx, Ctx: ctx} - defer func() { - if p := recover(); p != nil { - txWrap.Txx.Rollback() - panic(p) - } - if rtnErr != nil { - txWrap.Txx.Rollback() - } else { - rtnErr = txWrap.Txx.Commit() - } - }() - } - fnErr := fn(txWrap) - if txWrap.Err == nil && fnErr != nil { - txWrap.Err = fnErr - } - if txWrap.Err != nil { - return txWrap.Err - } - return nil -} - -func (tx *TxWrap) Context() context.Context { - return context.WithValue(tx.Ctx, txWrapKey{}, tx) -} - -func (tx *TxWrap) NamedExecWrap(query string, arg interface{}) sql.Result { - if tx.Err != nil { - return nil - } - result, err := tx.Txx.NamedExec(query, arg) - if err != nil { - tx.Err = err - } - return result -} - -func (tx *TxWrap) ExecWrap(query string, args ...interface{}) sql.Result { - if tx.Err != nil { - return nil - } - result, err := tx.Txx.Exec(query, args...) - if err != nil { - tx.Err = err - } - return result -} - -func (tx *TxWrap) Exists(query string, args ...interface{}) bool { - var dest interface{} - return tx.GetWrap(&dest, query, args...) -} - -func (tx *TxWrap) GetString(query string, args ...interface{}) string { - var rtnStr string - tx.GetWrap(&rtnStr, query, args...) - return rtnStr -} - -func (tx *TxWrap) GetBool(query string, args ...interface{}) bool { - var rtnBool bool - tx.GetWrap(&rtnBool, query, args...) - return rtnBool -} - -func (tx *TxWrap) SelectStrings(query string, args ...interface{}) []string { - var rtnArr []string - tx.SelectWrap(&rtnArr, query, args...) - return rtnArr -} - -func (tx *TxWrap) GetInt(query string, args ...interface{}) int { - var rtnInt int - tx.GetWrap(&rtnInt, query, args...) - return rtnInt -} - -func (tx *TxWrap) GetWrap(dest interface{}, query string, args ...interface{}) bool { - if tx.Err != nil { - return false - } - err := tx.Txx.Get(dest, query, args...) - if err != nil && err == sql.ErrNoRows { - return false - } - if err != nil { - tx.Err = err - return false - } - return true -} - -func (tx *TxWrap) SelectWrap(dest interface{}, query string, args ...interface{}) { - if tx.Err != nil { - return - } - err := tx.Txx.Select(dest, query, args...) - if err != nil { - tx.Err = err - } - return -} - -func (tx *TxWrap) SelectMaps(query string, args ...interface{}) []map[string]interface{} { - if tx.Err != nil { - return nil - } - rows, err := tx.Txx.Queryx(query, args...) - if err != nil { - tx.Err = err - return nil - } - var rtn []map[string]interface{} - for rows.Next() { - m := make(map[string]interface{}) - err = rows.MapScan(m) - if err != nil { - tx.Err = err - return nil - } - rtn = append(rtn, m) - } - return rtn -} - -func (tx *TxWrap) GetMap(query string, args ...interface{}) map[string]interface{} { - if tx.Err != nil { - return nil - } - row := tx.Txx.QueryRowx(query, args...) - m := make(map[string]interface{}) - err := row.MapScan(m) - if err != nil { - if err == sql.ErrNoRows { - return nil - } - tx.Err = err - return nil - } - return m -} From 5fa7bcc108d7e97f880e3d386d21ed6b146a2da2 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Feb 2023 15:49:22 -0800 Subject: [PATCH 257/397] bump to version 0.1.4 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 059c77345..c8b71eabf 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -26,7 +26,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.3" +const PromptVersion = "v0.1.4" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From d0b03c359a15f5d6a499ea2545e5c5640fb1066c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Feb 2023 15:41:39 -0800 Subject: [PATCH 258/397] setscreenidx and getdbversion --- pkg/cmdrunner/cmdrunner.go | 9 ++++++ pkg/sstore/dbops.go | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c75a34c9a..8ead68913 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -524,6 +524,10 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } varsUpdated = append(varsUpdated, "tabcolor") } + if pk.Kwargs["pos"] != "" { + + varsUpdated = append(varsUpdated, "pos") + } if len(varsUpdated) == 0 { return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor"}, "or", false)) } @@ -2188,11 +2192,16 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if err != nil { return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) } + dbVersion, err := sstore.GetDBVersion(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve db version: %v\n", err) + } var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "backend", scbase.PromptVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "db-version", dbVersion)) update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("client info"), diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 77d9d7792..13d137d32 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1969,3 +1969,67 @@ func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) er }) return txErr } + +func foundInStrArr(strs []string, s string) bool { + for _, sval := range strs { + if s == sval { + return true + } + } + return false +} + +// newPos is 0-indexed +func reorderStrs(strs []string, toMove string, newPos int) []string { + if !foundInStrArr(strs, toMove) { + return strs + } + var added bool + rtn := make([]string, 0, len(strs)) + for _, s := range strs { + if s == toMove { + continue + } + if len(rtn) == newPos { + added = true + rtn = append(rtn, toMove) + } + rtn = append(rtn, s) + } + if !added { + rtn = append(rtn, toMove) + } + return rtn +} + +// newScreenIdx is 1-indexed +func SetScreenIdx(ctx context.Context, sessionId string, screenId string, newScreenIdx int) error { + if newScreenIdx <= 0 { + return fmt.Errorf("invalid screenidx/pos, must be greater than 0") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ? AND NOT archived` + if !tx.Exists(query, sessionId, screenId) { + return fmt.Errorf("invalid screen, not found (or archived)") + } + query = `SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx` + screens := tx.SelectStrings(query, sessionId) + newScreens := reorderStrs(screens, screenId, newScreenIdx-1) + query = `UPDATE screen SET screenidx = ? WHERE sessionid = ? AND screenid = ?` + for idx, sid := range newScreens { + tx.Exec(query, idx+1, sessionId, sid) + } + return nil + }) + return txErr +} + +func GetDBVersion(ctx context.Context) (int, error) { + var version int + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT version FROM schema_migrations` + version = tx.GetInt(query) + return nil + }) + return version, txErr +} From 37ffcd03c8965c0c1bf8d38ef08f0e02261bc480 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Feb 2023 15:42:27 -0800 Subject: [PATCH 259/397] new bookmark tables --- db/migrations/000004_bookmarks.down.sql | 3 +++ db/migrations/000004_bookmarks.up.sql | 21 +++++++++++++++++++++ db/schema.sql | 21 ++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 db/migrations/000004_bookmarks.down.sql create mode 100644 db/migrations/000004_bookmarks.up.sql diff --git a/db/migrations/000004_bookmarks.down.sql b/db/migrations/000004_bookmarks.down.sql new file mode 100644 index 000000000..5e6d5c796 --- /dev/null +++ b/db/migrations/000004_bookmarks.down.sql @@ -0,0 +1,3 @@ +DROP TABLE bookmark; +DROP TABLE bookmark_order; +DROP TABLE bookmark_cmd; diff --git a/db/migrations/000004_bookmarks.up.sql b/db/migrations/000004_bookmarks.up.sql new file mode 100644 index 000000000..100caf013 --- /dev/null +++ b/db/migrations/000004_bookmarks.up.sql @@ -0,0 +1,21 @@ +CREATE TABLE bookmark ( + bookmarkid varchar(36) PRIMARY KEY, + createdts bigint NOT NULL, + cmdstr text NOT NULL, + alias varchar(50) NOT NULL, + tags json NOT NULL, + description text NOT NULL +); + +CREATE TABLE bookmark_order ( + tag varchar(50) NOT NULL, + bookmarkid varchar(36) NOT NULL, + orderidx int NOT NULL, + PRIMARY KEY (tag, bookmarkid) +); + +CREATE TABLE bookmark_cmd ( + bookmarkid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (bookmarkid, cmdid) +); diff --git a/db/schema.sql b/db/schema.sql index ae1baed9c..1a03c50ad 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -97,7 +97,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, - archived boolean NOT NULL, + archived boolean NOT NULL, renderer varchar(50) NOT NULL DEFAULT '', PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -165,3 +165,22 @@ CREATE TABLE activity ( clientversion varchar(20) NOT NULL, clientarch varchar(20) NOT NULL ); +CREATE TABLE bookmark ( + bookmarkid varchar(36) PRIMARY KEY, + createdts bigint NOT NULL, + cmdstr text NOT NULL, + alias varchar(50) NOT NULL, + tags json NOT NULL, + description text NOT NULL +); +CREATE TABLE bookmark_order ( + tag varchar(50) NOT NULL, + bookmarkid varchar(36) NOT NULL, + orderidx int NOT NULL, + PRIMARY KEY (tag, bookmarkid) +); +CREATE TABLE bookmark_cmd ( + bookmarkid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (bookmarkid, cmdid) +); From 3845429fd2e10abfb506233f38347e6750e87ffb Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Feb 2023 21:39:29 -0800 Subject: [PATCH 260/397] insert bookmark --- db/migrations/000004_bookmarks.down.sql | 3 + db/migrations/000004_bookmarks.up.sql | 7 ++- db/schema.sql | 5 +- pkg/cmdrunner/cmdrunner.go | 58 +++++++++++++++++- pkg/sstore/dbops.go | 78 +++++++++++++++++++++++-- pkg/sstore/sstore.go | 37 ++++++++++++ 6 files changed, 178 insertions(+), 10 deletions(-) diff --git a/db/migrations/000004_bookmarks.down.sql b/db/migrations/000004_bookmarks.down.sql index 5e6d5c796..13248684c 100644 --- a/db/migrations/000004_bookmarks.down.sql +++ b/db/migrations/000004_bookmarks.down.sql @@ -1,3 +1,6 @@ DROP TABLE bookmark; DROP TABLE bookmark_order; DROP TABLE bookmark_cmd; + +ALTER TABLE line DROP COLUMN bookmarked; +ALTER TABLE line DROP COLUMN pinned; diff --git a/db/migrations/000004_bookmarks.up.sql b/db/migrations/000004_bookmarks.up.sql index 100caf013..35d3589ef 100644 --- a/db/migrations/000004_bookmarks.up.sql +++ b/db/migrations/000004_bookmarks.up.sql @@ -16,6 +16,11 @@ CREATE TABLE bookmark_order ( CREATE TABLE bookmark_cmd ( bookmarkid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, - PRIMARY KEY (bookmarkid, cmdid) + PRIMARY KEY (bookmarkid, sessionid, cmdid) ); + +ALTER TABLE line ADD COLUMN bookmarked boolean NOT NULL DEFAULT 0; +ALTER TABLE line ADD COLUMN pinned boolean NOT NULL DEFAULT 0; + diff --git a/db/schema.sql b/db/schema.sql index 1a03c50ad..8236fc204 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -97,7 +97,7 @@ CREATE TABLE line ( ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, - archived boolean NOT NULL, renderer varchar(50) NOT NULL DEFAULT '', + archived boolean NOT NULL, renderer varchar(50) NOT NULL DEFAULT '', bookmarked boolean NOT NULL DEFAULT 0, pinned boolean NOT NULL DEFAULT 0, PRIMARY KEY (sessionid, windowid, lineid) ); CREATE TABLE remote ( @@ -181,6 +181,7 @@ CREATE TABLE bookmark_order ( ); CREATE TABLE bookmark_cmd ( bookmarkid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, - PRIMARY KEY (bookmarkid, cmdid) + PRIMARY KEY (bookmarkid, sessionid, cmdid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8ead68913..8fb53381c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -157,6 +157,8 @@ func init() { registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) registerCmdFn("line:star", LineStarCommand) + registerCmdFn("line:bookmark", LineBookmarkCommand) + registerCmdFn("line:pin", LinePinCommand) registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("line:setheight", LineSetHeightCommand) @@ -1900,6 +1902,58 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, nil } +func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + if err != nil { + return nil, err + } + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/line:bookmark requires an argument (line number or id)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) + if err != nil { + return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) + } + if cmdObj == nil { + return nil, fmt.Errorf("cannot bookmark non-cmd line") + } + ck := base.MakeCommandKey(lineObj.SessionId, cmdObj.CmdId) + bm := &sstore.BookmarkType{ + BookmarkId: uuid.New().String(), + CreatedTs: time.Now().UnixMilli(), + CmdStr: cmdObj.CmdStr, + Alias: "", + Tags: nil, + Description: "", + CmdIds: []base.CommandKey{ck}, + } + err = sstore.InsertBookmark(ctx, bm) + if err != nil { + return nil, fmt.Errorf("cannot insert bookmark: %v", err) + } + newLineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) + if err != nil { + return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) + } + if newLineObj == nil { + // no line (which is strange given we checked for it above). just return a nop. + return nil, nil + } + return sstore.ModelUpdate{Line: newLineObj}, nil +} + +func LinePinCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + return nil, nil +} + func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { @@ -1930,7 +1984,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:star error updating star value: %v", err) } - lineObj, err := sstore.GetLineById(ctx, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) if err != nil { return nil, fmt.Errorf("/line:star error getting line: %v", err) } @@ -1965,7 +2019,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err) } - lineObj, err := sstore.GetLineById(ctx, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) if err != nil { return nil, fmt.Errorf("/line:archive error getting line: %v", err) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 13d137d32..af6443384 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -719,8 +719,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived)` + query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked, pinned) + VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked,:pinned)` tx.NamedExec(query, line) query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.Exec(query, nextLineNum+1, line.SessionId, line.WindowId) @@ -899,6 +899,8 @@ func cleanSessionCmds(ctx context.Context, sessionId string) error { removedCmds = tx.SelectStrings(query, sessionId, sessionId) query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` tx.Exec(query, sessionId, sessionId) + query = `DELETE FROM bookmark_cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM cmd WHERE sessionid = ?)` + tx.Exec(query, sessionId, sessionId) return nil }) if txErr != nil { @@ -1391,6 +1393,8 @@ func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) tx.Exec(query, sessionId) query = `DELETE FROM cmd WHERE sessionid = ?` tx.Exec(query, sessionId) + query = `DELETE FROM bookmark_cmd WHERE sessionid = ?` + tx.Exec(query, sessionId) newActiveSessionId, _ = fixActiveSessionId(tx.Context()) return nil }) @@ -1825,12 +1829,12 @@ func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { } // can return nil, nil if line is not found -func GetLineById(ctx context.Context, lineId string) (*LineType, error) { +func GetLineById(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, error) { var rtn *LineType txErr := WithTx(ctx, func(tx *TxWrap) error { var line LineType - query := `SELECT * FROM line WHERE lineid = ?` - found := tx.Get(&line, query, lineId) + query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` + found := tx.Get(&line, query, sessionId, windowId, lineId) if found { rtn = &line } @@ -2033,3 +2037,67 @@ func GetDBVersion(ctx context.Context) (int, error) { }) return version, txErr } + +func InsertBookmark(ctx context.Context, bm *BookmarkType) error { + if bm == nil || bm.BookmarkId == "" { + return fmt.Errorf("invalid empty bookmark id") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?` + if tx.Exists(query, bm.BookmarkId) { + return fmt.Errorf("bookmarkid already exists") + } + query = `INSERT INTO bookmark ( bookmarkid, createdts, cmdstr, alias, tags, description) + VALUES (:bookmarkid,:createdts,:cmdstr,:alias,:tags,:description)` + tx.NamedExec(query, bm.ToMap()) + for _, tag := range append(bm.Tags, "") { + query = `SELECT COALESCE(max(orderidx), 0) FROM bookmark_order WHERE tag = ?` + maxOrder := tx.GetInt(query, tag) + query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)` + tx.Exec(query, tag, bm.BookmarkId, maxOrder+1) + } + query = `INSERT INTO bookmark_cmd (bookmarkid, sessionid, cmdid) VALUES (?, ?, ?)` + for _, ck := range bm.CmdIds { + tx.Exec(query, bm.BookmarkId, ck.GetSessionId(), ck.GetCmdId()) + } + query = `UPDATE line SET bookmarked = 1 WHERE sessionid = ? AND cmdid = ?` + for _, ck := range bm.CmdIds { + tx.Exec(query, ck.GetSessionId(), ck.GetCmdId()) + } + return nil + }) + return txErr +} + +func fixupBookmarkOrder(tx *TxWrap) { + query := ` +WITH new_order AS ( + SELECT tag, bookmarkid, row_number() OVER (PARTITION BY tag ORDER BY orderidx) AS newidx FROM bookmark_order +) +UPDATE bookmark_order +SET orderidx = new_order.newidx +FROM new_order +WHERE bookmark_order.tag = new_order.tag AND bookmark_order.bookmarkid = new_order.bookmarkid +` + tx.Exec(query) +} + +func DeleteBookmark(ctx context.Context, bookmarkId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?` + if !tx.Exists(query, bookmarkId) { + return fmt.Errorf("bookmark not found") + } + query = `DELETE FROM bookmark WHERE bookmarkid = ?` + tx.Exec(query, bookmarkId) + query = `DELETE FROM bookmark_order WHERE bookmarkid = ?` + tx.Exec(query, bookmarkId) + query = `UPDATE line SET bookmarked = 0 WHERE bookmarked AND cmdid <> '' AND (sessionid||cmdid) IN (SELECT sessionid||cmdid FROM bookmark_cmd WHERE bookmarkid = ?) ` + tx.Exec(query, bookmarkId) + query = `DELETE FROM bookmark_cmd WHERE bookmarkid = ?` + tx.Exec(query, bookmarkId) + fixupBookmarkOrder(tx) + return nil + }) + return txErr +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 5683b1683..4099dbda0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -650,10 +650,47 @@ type LineType struct { Ephemeral bool `json:"ephemeral,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` + Bookmarked bool `json:"bookmarked,omitempty"` + Pinned bool `json:"pinned,omitempty"` Archived bool `json:"archived,omitempty"` Remove bool `json:"remove,omitempty"` } +type BookmarkType struct { + BookmarkId string `json:"bookmarkid"` + CreatedTs int64 `json:"createdts"` + CmdStr string `json:"cmdstr"` + Alias string `json:"alias,omitempty"` + Tags []string `json:"tags"` + Description string `json:"description"` + CmdIds []base.CommandKey `json:"cmdids"` +} + +func (bm *BookmarkType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["bookmarkid"] = bm.BookmarkId + rtn["createdts"] = bm.CreatedTs + rtn["cmdstr"] = bm.CmdStr + rtn["alias"] = bm.Alias + rtn["description"] = bm.Description + rtn["tags"] = quickJsonArr(bm.Tags) + return rtn +} + +func BookmarkFromMap(m map[string]interface{}) *BookmarkType { + if len(m) == 0 { + return nil + } + var bm BookmarkType + quickSetStr(&bm.BookmarkId, m, "bookmarkid") + quickSetInt64(&bm.CreatedTs, m, "createdts") + quickSetStr(&bm.Alias, m, "alias") + quickSetStr(&bm.CmdStr, m, "cmdstr") + quickSetStr(&bm.Description, m, "description") + quickSetJsonArr(&bm.Tags, m, "tags") + return &bm +} + type ResolveItem struct { Name string Num int From 63c620aea1f9aa02aab274ad46c232870ffc517a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Feb 2023 22:00:07 -0800 Subject: [PATCH 261/397] get bookmarks --- pkg/cmdrunner/cmdrunner.go | 20 ++++++++++++- pkg/sstore/dbops.go | 60 ++++++++++++++++++++++++++++++++++++-- pkg/sstore/sstore.go | 3 +- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8fb53381c..e4de7d96d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -174,6 +174,8 @@ func init() { registerCmdFn("history", HistoryCommand) + registerCmdFn("bookmarks:show", BookmarksShowCommand) + registerCmdFn("_killserver", KillServerCommand) registerCmdFn("set", SetCommand) @@ -1902,6 +1904,22 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, nil } +func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + // no resolve ui ids! + var tagName string // defaults to '' + if len(pk.Args) > 0 { + tagName = pk.Args[0] + } + bms, err := sstore.GetBookmarks(ctx, tagName) + if err != nil { + return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err) + } + for _, bm := range bms { + fmt.Printf("%v\n", bm) + } + return nil, nil +} + func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { @@ -1933,7 +1951,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) Alias: "", Tags: nil, Description: "", - CmdIds: []base.CommandKey{ck}, + Cmds: []base.CommandKey{ck}, } err = sstore.InsertBookmark(ctx, bm) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index af6443384..634108f9c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2038,6 +2038,62 @@ func GetDBVersion(ctx context.Context) (int, error) { return version, txErr } +type bookmarkOrderType struct { + BookmarkId string + OrderIdx int64 +} + +type bookmarkCmdType struct { + BookmarkId string + SessionId string + CmdId string +} + +func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { + var bms []*BookmarkType + txErr := WithTx(ctx, func(tx *TxWrap) error { + var query string + var marr []map[string]interface{} + if tag == "" { + query = `SELECT * FROM bookmark` + marr = tx.SelectMaps(query) + } else { + query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)` + marr = tx.SelectMaps(query, tag) + } + bmMap := make(map[string]*BookmarkType) + for _, m := range marr { + bm := BookmarkFromMap(m) + bms = append(bms, bm) + bmMap[bm.BookmarkId] = bm + } + var orders []bookmarkOrderType + query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?` + tx.Select(&orders, query, tag) + for _, bmOrder := range orders { + bm := bmMap[bmOrder.BookmarkId] + if bm != nil { + bm.OrderIdx = bmOrder.OrderIdx + } + } + var cmds []bookmarkCmdType + query = `SELECT bookmarkid, sessionid, cmdid FROM bookmark_cmd` + tx.Select(&cmds, query) + for _, cmd := range cmds { + bm := bmMap[cmd.BookmarkId] + if bm != nil { + bm.Cmds = append(bm.Cmds, base.MakeCommandKey(cmd.SessionId, cmd.CmdId)) + } + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return bms, nil +} + +// ignores OrderIdx field func InsertBookmark(ctx context.Context, bm *BookmarkType) error { if bm == nil || bm.BookmarkId == "" { return fmt.Errorf("invalid empty bookmark id") @@ -2057,11 +2113,11 @@ func InsertBookmark(ctx context.Context, bm *BookmarkType) error { tx.Exec(query, tag, bm.BookmarkId, maxOrder+1) } query = `INSERT INTO bookmark_cmd (bookmarkid, sessionid, cmdid) VALUES (?, ?, ?)` - for _, ck := range bm.CmdIds { + for _, ck := range bm.Cmds { tx.Exec(query, bm.BookmarkId, ck.GetSessionId(), ck.GetCmdId()) } query = `UPDATE line SET bookmarked = 1 WHERE sessionid = ? AND cmdid = ?` - for _, ck := range bm.CmdIds { + for _, ck := range bm.Cmds { tx.Exec(query, ck.GetSessionId(), ck.GetCmdId()) } return nil diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 4099dbda0..0a07b24e3 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -663,7 +663,8 @@ type BookmarkType struct { Alias string `json:"alias,omitempty"` Tags []string `json:"tags"` Description string `json:"description"` - CmdIds []base.CommandKey `json:"cmdids"` + Cmds []base.CommandKey `json:"cmds"` + OrderIdx int64 `json:"orderidx"` } func (bm *BookmarkType) ToMap() map[string]interface{} { From 40526a6fdfbe2850c0d839cc27919d7879093fdb Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Feb 2023 22:08:23 -0800 Subject: [PATCH 262/397] bookmarks view --- pkg/cmdrunner/cmdrunner.go | 8 +++++--- pkg/sstore/updatebus.go | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e4de7d96d..1e76825fb 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1914,10 +1914,12 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err) } - for _, bm := range bms { - fmt.Printf("%v\n", bm) + update := sstore.ModelUpdate{ + BookmarksView: &sstore.BookmarksViewType{ + Bookmarks: bms, + }, } - return nil, nil + return update, nil } func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 84cbb3021..19fd668d1 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -43,6 +43,7 @@ type ModelUpdate struct { History *HistoryInfoType `json:"history,omitempty"` Interactive bool `json:"interactive"` Connect bool `json:"connect,omitempty"` + BookmarksView *BookmarksViewType `json:"bookmarksview,omitempty"` } func (ModelUpdate) UpdateType() string { @@ -184,3 +185,7 @@ func MakeSessionsUpdateForRemote(sessionId string, ri *RemoteInstance) []*Sessio }, } } + +type BookmarksViewType struct { + Bookmarks []*BookmarkType `json:"bookmarks"` +} From e4113b5f51114fae9c1c5dff2834212152465c15 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Feb 2023 18:03:13 -0800 Subject: [PATCH 263/397] bookmark edit, delete, getbyid --- pkg/cmdrunner/cmdrunner.go | 73 +++++++++++++++++++++++++++++++++++--- pkg/sstore/dbops.go | 68 +++++++++++++++++++++++++++++++++++ pkg/sstore/sstore.go | 1 + pkg/sstore/updatebus.go | 3 +- 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1e76825fb..ff09bfc94 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -53,7 +53,7 @@ var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoins var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} -var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry"} +var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry", "bookmark", "bookmarks"} var SetVarNameMap map[string]string = map[string]string{ "tabcolor": "screen.tabcolor", @@ -176,6 +176,9 @@ func init() { registerCmdFn("bookmarks:show", BookmarksShowCommand) + registerCmdFn("bookmark:edit", BookmarkEditCommand) + registerCmdFn("bookmark:delete", BookmarkDeleteCommand) + registerCmdFn("_killserver", KillServerCommand) registerCmdFn("set", SetCommand) @@ -1915,13 +1918,75 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err) } update := sstore.ModelUpdate{ - BookmarksView: &sstore.BookmarksViewType{ - Bookmarks: bms, - }, + BookmarksView: true, + Bookmarks: bms, } return update, nil } +func BookmarkEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)") + } + bookmarkArg := pk.Args[0] + bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg) + if err != nil { + return nil, fmt.Errorf("error trying to resolve bookmark: %v", err) + } + if bookmarkId == "" { + return nil, fmt.Errorf("bookmark not found") + } + editMap := make(map[string]interface{}) + if descStr, found := pk.Kwargs["desc"]; found { + editMap[sstore.BookmarkField_Desc] = descStr + } + if cmdStr, found := pk.Kwargs["cmdstr"]; found { + editMap[sstore.BookmarkField_CmdStr] = cmdStr + } + if len(editMap) == 0 { + return nil, fmt.Errorf("no fields set, can set %s", formatStrs([]string{"desc", "cmdstr"}, "or", false)) + } + err = sstore.EditBookmark(ctx, bookmarkId, editMap) + if err != nil { + return nil, fmt.Errorf("error trying to edit bookmark: %v", err) + } + bm, err := sstore.GetBookmarkById(ctx, bookmarkId, "") + if err != nil { + return nil, fmt.Errorf("error retrieving edited bookmark: %v", err) + } + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: "bookmark edited", + }, + Bookmarks: []*sstore.BookmarkType{bm}, + }, nil +} + +func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)") + } + bookmarkArg := pk.Args[0] + bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg) + if err != nil { + return nil, fmt.Errorf("error trying to resolve bookmark: %v", err) + } + if bookmarkId == "" { + return nil, fmt.Errorf("bookmark not found") + } + err = sstore.DeleteBookmark(ctx, bookmarkId) + if err != nil { + return nil, fmt.Errorf("error deleting bookmark: %v", err) + } + bm := &sstore.BookmarkType{BookmarkId: bookmarkId, Remove: true} + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: "bookmark deleted", + }, + Bookmarks: []*sstore.BookmarkType{bm}, + }, nil +} + func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 634108f9c..06cce739e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2093,6 +2093,50 @@ func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { return bms, nil } +func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*BookmarkType, error) { + var rtn *BookmarkType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM bookmark WHERE bookmarkid = ?` + m := tx.GetMap(query, bookmarkId) + rtn = BookmarkFromMap(m) + if rtn == nil { + return nil + } + query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?` + orderIdx := tx.GetInt(query, bookmarkId, tag) + rtn.OrderIdx = int64(orderIdx) + query = `SELECT bookmarkid, sessionid, cmdid FROM bookmark_cmd WHERE bookmarkid = ?` + var cmds []bookmarkCmdType + tx.Select(&cmds, query, bookmarkId) + for _, cmd := range cmds { + rtn.Cmds = append(rtn.Cmds, base.MakeCommandKey(cmd.SessionId, cmd.CmdId)) + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + +func GetBookmarkIdByArg(ctx context.Context, bookmarkArg string) (string, error) { + var rtnId string + txErr := WithTx(ctx, func(tx *TxWrap) error { + if len(bookmarkArg) == 8 { + query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid LIKE (? || '%')` + rtnId = tx.GetString(query, bookmarkArg) + return nil + } + query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?` + rtnId = tx.GetString(query, bookmarkArg) + return nil + }) + if txErr != nil { + return "", txErr + } + return rtnId, nil +} + // ignores OrderIdx field func InsertBookmark(ctx context.Context, bm *BookmarkType) error { if bm == nil || bm.BookmarkId == "" { @@ -2125,6 +2169,30 @@ func InsertBookmark(ctx context.Context, bm *BookmarkType) error { return txErr } +const ( + BookmarkField_Desc = "desc" + BookmarkField_CmdStr = "cmdstr" +) + +func EditBookmark(ctx context.Context, bookmarkId string, editMap map[string]interface{}) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?` + if !tx.Exists(query, bookmarkId) { + return fmt.Errorf("bookmark not found") + } + if desc, found := editMap[BookmarkField_Desc]; found { + query = `UPDATE bookmark SET description = ? WHERE bookmarkid = ?` + tx.Exec(query, desc, bookmarkId) + } + if cmdStr, found := editMap[BookmarkField_CmdStr]; found { + query = `UPDATE bookmark SET cmdstr = ? WHERE bookmarkid = ?` + tx.Exec(query, cmdStr, bookmarkId) + } + return nil + }) + return txErr +} + func fixupBookmarkOrder(tx *TxWrap) { query := ` WITH new_order AS ( diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 0a07b24e3..808f49eaa 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -665,6 +665,7 @@ type BookmarkType struct { Description string `json:"description"` Cmds []base.CommandKey `json:"cmds"` OrderIdx int64 `json:"orderidx"` + Remove bool `json:"remove,omitempty"` } func (bm *BookmarkType) ToMap() map[string]interface{} { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 19fd668d1..115eb9ee3 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -43,7 +43,8 @@ type ModelUpdate struct { History *HistoryInfoType `json:"history,omitempty"` Interactive bool `json:"interactive"` Connect bool `json:"connect,omitempty"` - BookmarksView *BookmarksViewType `json:"bookmarksview,omitempty"` + BookmarksView bool `json:"bookmarksview,omitempty"` + Bookmarks []*BookmarkType `json:"bookmarks,omitempty"` } func (ModelUpdate) UpdateType() string { From 33af77b88c05b0fda5a8be3fd75da16acecf09f7 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Feb 2023 22:11:06 -0800 Subject: [PATCH 264/397] minor, finish bookmark functionality --- pkg/cmdrunner/cmdrunner.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ff09bfc94..346d31cc4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -176,7 +176,7 @@ func init() { registerCmdFn("bookmarks:show", BookmarksShowCommand) - registerCmdFn("bookmark:edit", BookmarkEditCommand) + registerCmdFn("bookmark:set", BookmarkSetCommand) registerCmdFn("bookmark:delete", BookmarkDeleteCommand) registerCmdFn("_killserver", KillServerCommand) @@ -1924,9 +1924,9 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return update, nil } -func BookmarkEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { - return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)") + return nil, fmt.Errorf("/bookmark:set requires one argument (bookmark id)") } bookmarkArg := pk.Args[0] bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg) From 8e1f34b93b53e518f2e6cc4ca6c39ca27aeb3ae2 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Feb 2023 22:41:56 -0800 Subject: [PATCH 265/397] telemetry updates --- cmd/main-server.go | 1 + pkg/cmdrunner/cmdrunner.go | 20 ++++++++++++++++++++ pkg/remote/remote.go | 6 ++++++ pkg/sstore/dbops.go | 6 ++++++ pkg/sstore/sstore.go | 8 ++++++++ 5 files changed, 41 insertions(+) diff --git a/cmd/main-server.go b/cmd/main-server.go index 345e46dfd..e983920f5 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -180,6 +180,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) { if activeState.Open { activity.OpenMinutes = 1 } + activity.NumConns = remote.NumRemotes() err = sstore.UpdateCurrentActivity(r.Context(), activity) if err != nil { WriteJsonError(w, fmt.Errorf("error updating activity: %w", err)) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 346d31cc4..444378c82 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -126,6 +126,7 @@ func init() { registerCmdFn("session:archive", SessionArchiveCommand) registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("session:show", SessionShowCommand) + registerCmdFn("session:openshared", SessionOpenSharedCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:archive", ScreenArchiveCommand) @@ -1497,6 +1498,15 @@ func validateRemoteColor(color string, typeStr string) error { return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(RemoteColorNames, "or", false)) } +func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + activity := sstore.ActivityUpdate{ClickShared: 1} + err := sstore.UpdateCurrentActivity(ctx, activity) + if err != nil { + log.Printf("error updating click-shared: %v\n", err) + } + return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)") +} + func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { activate := resolveBool(pk.Kwargs["activate"], true) newName := pk.Kwargs["name"] @@ -1803,6 +1813,12 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, err } show := !resolveBool(pk.Kwargs["noshow"], false) + if show { + err = sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{HistoryView: 1}) + if err != nil { + log.Printf("error updating current activity (history): %v\n", err) + } + } update := sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ HistoryType: htype, @@ -1917,6 +1933,10 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err) } + err = sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{BookmarksView: 1}) + if err != nil { + log.Printf("error updating current activity (bookmarks): %v\n", err) + } update := sstore.ModelUpdate{ BookmarksView: true, Bookmarks: bms, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 4090132fb..45477f8f0 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -349,6 +349,12 @@ func isPartialUUID(s string) bool { return partialUUIDRe.MatchString(s) } +func NumRemotes() int { + GlobalStore.Lock.Lock() + defer GlobalStore.Lock.Unlock() + return len(GlobalStore.Map) +} + func GetRemoteByArg(arg string) *MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 06cce739e..89f4f66dc 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1932,6 +1932,12 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { tdata.FgMinutes += update.FgMinutes tdata.ActiveMinutes += update.ActiveMinutes tdata.OpenMinutes += update.OpenMinutes + tdata.ClickShared += update.ClickShared + tdata.HistoryView += update.HistoryView + tdata.BookmarksView += update.BookmarksView + if update.NumConns > 0 { + tdata.NumConns = update.NumConns + } query = `UPDATE activity SET tdata = ?, clientversion = ? diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 808f49eaa..1ab2cb880 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -118,6 +118,10 @@ type ActivityUpdate struct { ActiveMinutes int OpenMinutes int NumCommands int + ClickShared int + HistoryView int + BookmarksView int + NumConns int } type ActivityType struct { @@ -135,6 +139,10 @@ type TelemetryData struct { ActiveMinutes int `json:"activeminutes"` FgMinutes int `json:"fgminutes"` OpenMinutes int `json:"openminutes"` + ClickShared int `json:"clickshared,omitempty"` + HistoryView int `json:"historyview,omitempty"` + BookmarksView int `json:"bookmarksview,omitempty"` + NumConns int `json:"numconns"` } func (tdata TelemetryData) Value() (driver.Value, error) { From fe3fb6d3778efa78d531a9de77f08b9d62533f11 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 23 Feb 2023 15:17:47 -0800 Subject: [PATCH 266/397] updates for activity, buildtime, etc. --- cmd/main-server.go | 9 ++++++++- go.mod | 2 +- pkg/cmdrunner/cmdrunner.go | 7 +++++++ pkg/scbase/scbase.go | 33 +++++++++++++++++++++++++++++++++ pkg/scpacket/scpacket.go | 1 + pkg/sstore/dbops.go | 11 ++++++----- pkg/sstore/sstore.go | 3 +++ scripthaus.md | 2 +- 8 files changed, 60 insertions(+), 8 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index e983920f5..b582908ec 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -50,6 +50,7 @@ const TelemetryInterval = 8 * time.Hour var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string +var BuildTime = "0" type ClientActiveState struct { Fg bool `json:"fg"` @@ -460,6 +461,8 @@ func stdinReadWatch() { } func main() { + scbase.BuildTime = BuildTime + if len(os.Args) >= 2 && os.Args[1] == "--test" { log.Printf("running test fn\n") err := test() @@ -470,7 +473,7 @@ func main() { } scHomeDir := scbase.GetPromptHomeDir() - log.Printf("[prompt] local server version %s\n", scbase.PromptVersion) + log.Printf("[prompt] local server version %s+%s\n", scbase.PromptVersion, scbase.BuildTime) log.Printf("[prompt] homedir = %q\n", scHomeDir) scLock, err := scbase.AcquirePromptLock() @@ -528,6 +531,10 @@ func main() { } log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) + err = sstore.UpdateCurrentActivity(context.Background(), sstore.ActivityUpdate{NumConns: remote.NumRemotes()}) // set at least one record into activity + if err != nil { + log.Printf("[error] updating activity: %v\n", err) + } go telemetryLoop() go stdinReadWatch() go runWebSocketServer() diff --git a/go.mod b/go.mod index c4ed9eb89..b2e96554b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/scripthaus-dev/sh2-server -go 1.17 +go 1.18 require ( github.com/alessio/shellescape v1.4.1 diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 444378c82..c9b3dafa1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2355,12 +2355,19 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if err != nil { return nil, fmt.Errorf("cannot retrieve db version: %v\n", err) } + clientVersion := "-" + if pk.UIContext != nil && pk.UIContext.Build != "" { + clientVersion = pk.UIContext.Build + } var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "backend", scbase.PromptVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) buf.WriteString(fmt.Sprintf(" %-15s %d\n", "db-version", dbVersion)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "client-version", clientVersion)) + buf.WriteString(fmt.Sprintf(" %-15s %s %s\n", "server-version", scbase.PromptVersion, scbase.BuildTime)) + buf.WriteString(fmt.Sprintf(" %-15s %s (%s)\n", "arch", scbase.ClientArch(), scbase.MacOSRelease())) update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("client info"), diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index c8b71eabf..12b68bd9c 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -1,16 +1,21 @@ package scbase import ( + "context" "errors" "fmt" "io" "io/fs" "log" "os" + "os/exec" "path" + "regexp" "runtime" "strconv" + "strings" "sync" + "time" "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/base" @@ -32,6 +37,7 @@ const MShellVersion = "v0.2.0" var SessionDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} +var BuildTime = "-" func IsDevMode() bool { pdev := os.Getenv(PromptDevVarName) @@ -322,3 +328,30 @@ func NumFormatB2(num int64) string { func ClientArch() string { return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) } + +var releaseRegex = regexp.MustCompile(`^\d+\.\d+\.\d+$`) +var osReleaseOnce = &sync.Once{} +var osRelease string + +func macOSRelease() string { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + out, err := exec.CommandContext(ctx, "uname", "-r").CombinedOutput() + if err != nil { + log.Printf("error executing uname -r: %v\n", err) + return "-" + } + releaseStr := strings.TrimSpace(string(out)) + if !releaseRegex.MatchString(releaseStr) { + log.Printf("invalid uname -r output: [%s]\n", releaseStr) + return "-" + } + return releaseStr +} + +func MacOSRelease() string { + osReleaseOnce.Do(func() { + osRelease = macOSRelease() + }) + return osRelease +} diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index bb88338d1..8c1b81843 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -55,6 +55,7 @@ type UIContextType struct { WindowId string `json:"windowid"` Remote *sstore.RemotePtrType `json:"remote,omitempty"` WinSize *packet.WinSize `json:"winsize,omitempty"` + Build string `json:"build,omitempty"` } type FeInputPacketType struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 89f4f66dc..612d2ed70 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1920,13 +1920,13 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { query := `SELECT tdata FROM activity WHERE day = ?` found := tx.Get(&tdata, query, dayStr) if !found { - query = `INSERT INTO activity (day, uploaded, tdata, tzname, tzoffset, clientversion, clientarch) - VALUES (?, 0, ?, ?, ?, ?, ?)` + query = `INSERT INTO activity (day, uploaded, tdata, tzname, tzoffset, clientversion, clientarch, buildtime, osrelease) + VALUES (?, 0, ?, ?, ?, ?, ? , ? , ?)` tzName, tzOffset := now.Zone() if len(tzName) > MaxTzNameLen { tzName = tzName[0:MaxTzNameLen] } - tx.Exec(query, dayStr, tdata, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch()) + tx.Exec(query, dayStr, tdata, tzName, tzOffset, scbase.PromptVersion, scbase.ClientArch(), scbase.BuildTime, scbase.MacOSRelease()) } tdata.NumCommands += update.NumCommands tdata.FgMinutes += update.FgMinutes @@ -1940,9 +1940,10 @@ func UpdateCurrentActivity(ctx context.Context, update ActivityUpdate) error { } query = `UPDATE activity SET tdata = ?, - clientversion = ? + clientversion = ?, + buildtime = ? WHERE day = ?` - tx.Exec(query, tdata, scbase.PromptVersion, dayStr) + tx.Exec(query, tdata, scbase.PromptVersion, scbase.BuildTime, dayStr) return nil }) if txErr != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1ab2cb880..1cd804140 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -122,6 +122,7 @@ type ActivityUpdate struct { HistoryView int BookmarksView int NumConns int + BuildTime string } type ActivityType struct { @@ -132,6 +133,8 @@ type ActivityType struct { TzOffset int `json:"tzoffset"` ClientVersion string `json:"clientversion"` ClientArch string `json:"clientarch"` + BuildTime string `json:"buildtime"` + OSRelease string `json:"osrelease"` } type TelemetryData struct { diff --git a/scripthaus.md b/scripthaus.md index 88dc22bd3..8768504e2 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -12,5 +12,5 @@ sqlite3 /Users/mike/prompt-dev/prompt.db ```bash # @scripthaus command build -go build -o ~/prompt-dev/local-server cmd/main-server.go +go build -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M')" -o ~/prompt-dev/local-server ./cmd ``` From ee4cfd2d4b20e7dd0ed280ba541778a94da01e90 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 23 Feb 2023 17:52:54 -0800 Subject: [PATCH 267/397] add db migrations --- db/migrations/000005_buildtime.down.sql | 2 ++ db/migrations/000005_buildtime.up.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 db/migrations/000005_buildtime.down.sql create mode 100644 db/migrations/000005_buildtime.up.sql diff --git a/db/migrations/000005_buildtime.down.sql b/db/migrations/000005_buildtime.down.sql new file mode 100644 index 000000000..2c83f8ef2 --- /dev/null +++ b/db/migrations/000005_buildtime.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE activity DROP COLUMN buildtime; +ALTER TABLE activity DROP COLUMN osrelease; diff --git a/db/migrations/000005_buildtime.up.sql b/db/migrations/000005_buildtime.up.sql new file mode 100644 index 000000000..1006efa17 --- /dev/null +++ b/db/migrations/000005_buildtime.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE activity ADD COLUMN buildtime varchar(20) NOT NULL DEFAULT '-'; +ALTER TABLE activity ADD COLUMN osrelease varchar(20) NOT NULL DEFAULT '-'; From 83314c3e48457320e4dada927d7b97fc43d32b2a Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Feb 2023 14:33:01 -0800 Subject: [PATCH 268/397] update termfontsize command /client:set --- db/migrations/000006_feopts.down.sql | 1 + db/migrations/000006_feopts.up.sql | 3 ++ pkg/cmdrunner/cmdrunner.go | 52 ++++++++++++++++++++++++---- pkg/sstore/dbops.go | 9 +++++ pkg/sstore/sstore.go | 7 ++++ pkg/sstore/updatebus.go | 1 + 6 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 db/migrations/000006_feopts.down.sql create mode 100644 db/migrations/000006_feopts.up.sql diff --git a/db/migrations/000006_feopts.down.sql b/db/migrations/000006_feopts.down.sql new file mode 100644 index 000000000..7f9b0092e --- /dev/null +++ b/db/migrations/000006_feopts.down.sql @@ -0,0 +1 @@ +ALTER TABLE client DROP COLUMN feopts; diff --git a/db/migrations/000006_feopts.up.sql b/db/migrations/000006_feopts.up.sql new file mode 100644 index 000000000..f822f5669 --- /dev/null +++ b/db/migrations/000006_feopts.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE client ADD COLUMN feopts json NOT NULL DEFAULT '{}'; + + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c9b3dafa1..90f137b43 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -166,6 +166,7 @@ func init() { registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) + registerCmdFn("client:set", ClientSetCommand) registerCmdFn("telemetry", TelemetryCommand) registerCmdFn("telemetry:on", TelemetryOnCommand) @@ -2336,7 +2337,7 @@ func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"show"}, "or", false)) + return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"show", "set"}, "or", false)) } func boolToStr(v bool, trueStr string, falseStr string) string { @@ -2346,10 +2347,49 @@ func boolToStr(v bool, trueStr string, falseStr string) string { return falseStr } +func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v", err) + } + var varsUpdated []string + if fontSizeStr, found := pk.Kwargs["termfontsize"]; found { + newFontSize, err := resolveNonNegInt(fontSizeStr, 0) + if err != nil { + return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15: %v", err) + } + if newFontSize < 8 || newFontSize > 15 { + return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15", err) + } + feOpts := clientData.FeOpts + feOpts.TermFontSize = newFontSize + err = sstore.UpdateClientFeOpts(ctx, feOpts) + if err != nil { + return nil, fmt.Errorf("error updating client feopts: %v", err) + } + varsUpdated = append(varsUpdated, "termfontsize") + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize"}, "or", false)) + } + clientData, err = sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, + ClientData: clientData, + } + return update, nil +} + func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { - return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + return nil, fmt.Errorf("cannot retrieve client data: %v", err) } dbVersion, err := sstore.GetDBVersion(ctx) if err != nil { @@ -2401,7 +2441,7 @@ func setNoTelemetry(ctx context.Context, clientData *sstore.ClientData, noTeleme func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { - return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + return nil, fmt.Errorf("cannot retrieve client data: %v", err) } if !clientData.ClientOpts.NoTelemetry { return sstore.InfoMsgUpdate("telemetry is already on"), nil @@ -2421,7 +2461,7 @@ func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { - return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + return nil, fmt.Errorf("cannot retrieve client data: %v", err) } if clientData.ClientOpts.NoTelemetry { return sstore.InfoMsgUpdate("telemetry is already off"), nil @@ -2436,7 +2476,7 @@ func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { - return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + return nil, fmt.Errorf("cannot retrieve client data: %v", err) } var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) @@ -2452,7 +2492,7 @@ func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) func TelemetrySendCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { - return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + return nil, fmt.Errorf("cannot retrieve client data: %v", err) } force := resolveBool(pk.Kwargs["force"], false) if clientData.ClientOpts.NoTelemetry && !force { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 612d2ed70..27365be6f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -504,6 +504,15 @@ func SetWinSize(ctx context.Context, winSize ClientWinSizeType) error { return txErr } +func UpdateClientFeOpts(ctx context.Context, feOpts FeOptsType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET feopts = ?` + tx.Exec(query, quickJson(feOpts)) + return nil + }) + return txErr +} + func containsStr(strs []string, testStr string) bool { for _, s := range strs { if s == testStr { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1cd804140..72e356f10 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -160,6 +160,10 @@ type ClientOptsType struct { NoTelemetry bool `json:"notelemetry,omitempty"` } +type FeOptsType struct { + TermFontSize int `json:"termfontsize,omitempty"` +} + type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"` @@ -170,6 +174,7 @@ type ClientData struct { ActiveSessionId string `json:"activesessionid"` WinSize ClientWinSizeType `json:"winsize"` ClientOpts ClientOptsType `json:"clientopts"` + FeOpts FeOptsType `json:"feopts"` } func (c *ClientData) ToMap() map[string]interface{} { @@ -181,6 +186,7 @@ func (c *ClientData) ToMap() map[string]interface{} { rtn["activesessionid"] = c.ActiveSessionId rtn["winsize"] = quickJson(c.WinSize) rtn["clientopts"] = quickJson(c.ClientOpts) + rtn["feopts"] = quickJson(c.FeOpts) return rtn } @@ -196,6 +202,7 @@ func ClientDataFromMap(m map[string]interface{}) *ClientData { quickSetStr(&c.ActiveSessionId, m, "activesessionid") quickSetJson(&c.WinSize, m, "winsize") quickSetJson(&c.ClientOpts, m, "clientopts") + quickSetJson(&c.FeOpts, m, "feopts") return &c } diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 115eb9ee3..9f6e95880 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -45,6 +45,7 @@ type ModelUpdate struct { Connect bool `json:"connect,omitempty"` BookmarksView bool `json:"bookmarksview,omitempty"` Bookmarks []*BookmarkType `json:"bookmarks,omitempty"` + ClientData *ClientData `json:"clientdata,omitempty"` } func (ModelUpdate) UpdateType() string { From 652930753c03c321c6cfcc80a1ef51320efaf38b Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Feb 2023 22:22:20 -0800 Subject: [PATCH 269/397] schema updates --- db/schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 8236fc204..aef3ecc2e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE client ( userpublickeybytes blob NOT NULL, userprivatekeybytes blob NOT NULL, winsize json NOT NULL -, clientopts json NOT NULL DEFAULT ''); +, clientopts json NOT NULL DEFAULT '', feopts json NOT NULL DEFAULT '{}'); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, @@ -164,7 +164,7 @@ CREATE TABLE activity ( tzoffset int NOT NULL, clientversion varchar(20) NOT NULL, clientarch varchar(20) NOT NULL -); +, buildtime varchar(20) NOT NULL DEFAULT '-', osrelease varchar(20) NOT NULL DEFAULT '-'); CREATE TABLE bookmark ( bookmarkid varchar(36) PRIMARY KEY, createdts bigint NOT NULL, From b557da86940083c54a5df74ac06c8a2ea86cfb84 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 Feb 2023 12:28:22 -0800 Subject: [PATCH 270/397] bump to v0.1.5 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 12b68bd9c..51c9a8a9f 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -31,7 +31,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.4" +const PromptVersion = "v0.1.5" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From fa393e2eec270c6134de698a05c9bafcfbb5c913 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Feb 2023 17:50:42 -0800 Subject: [PATCH 271/397] make a fake cmd for 'cr'. add 'connect' --- pkg/cmdrunner/cmdrunner.go | 62 +++++++++++++++++++++++++++++--------- pkg/cmdrunner/resolver.go | 6 ++-- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 90f137b43..5048b18a2 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -112,6 +112,7 @@ func init() { registerCmdFn("eval", EvalCommand) registerCmdFn("comment", CommentCommand) registerCmdFn("cr", CrCommand) + registerCmdFn("connect", CrCommand) registerCmdFn("_compgen", CompGenCommand) registerCmdFn("clear", ClearCommand) registerCmdFn("reset", RemoteResetCommand) @@ -205,6 +206,13 @@ func registerCmdAlias(cmdName string, fn MetaCmdFnType) { MetaCmdFnMap[cmdName] = MetaCmdEntryType{IsAlias: true, Fn: fn} } +func GetCmdStr(pk *scpacket.FeCommandPacketType) string { + if pk.MetaSubCmd == "" { + return pk.MetaCmd + } + return pk.MetaCmd + ":" + pk.MetaSubCmd +} + func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { metaCmd := SubMetaCmd(pk.MetaCmd) var cmdName string @@ -1140,40 +1148,64 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re return update, nil } +func GetFullRemoteDisplayName(rptr *sstore.RemotePtrType, rstate *remote.RemoteRuntimeState) string { + if rptr == nil { + return "(invalid)" + } + if rstate.RemoteAlias != "" { + fullName := rstate.RemoteAlias + if rptr.Name != "" { + fullName = fullName + ":" + rptr.Name + } + return fmt.Sprintf("[%s] (%s)", fullName, rstate.RemoteCanonicalName) + } else { + if rptr.Name != "" { + return fmt.Sprintf("[%s:%s]", rstate.RemoteCanonicalName, rptr.Name) + } + return fmt.Sprintf("[%s]", rstate.RemoteCanonicalName) + } +} + func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) if err != nil { - return nil, fmt.Errorf("/cr error: %w", err) + return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err) } newRemote := firstArg(pk) if newRemote == "" { return crShowCommand(ctx, pk, ids) } - remoteName, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + _, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) if err != nil { return nil, err } if rptr == nil { - return nil, fmt.Errorf("/cr error: remote %q not found", newRemote) + return nil, fmt.Errorf("/%s error: remote %q not found", GetCmdStr(pk), newRemote) } if rstate.Archived { - return nil, fmt.Errorf("/cr error: remote %q cannot switch to archived remote", newRemote) + return nil, fmt.Errorf("/%s error: remote %q cannot switch to archived remote", GetCmdStr(pk), newRemote) } err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, *rptr) if err != nil { - return nil, fmt.Errorf("/cr error: cannot update curremote: %w", err) + return nil, fmt.Errorf("/%s error: cannot update curremote: %w", GetCmdStr(pk), err) } - update := sstore.ModelUpdate{ - Windows: []*sstore.WindowType{&sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - CurRemote: *rptr, - }}, - Info: &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("current remote = %q", remoteName), - TimeoutMs: 2000, - }, + outputStr := fmt.Sprintf("connected to %s", GetFullRemoteDisplayName(rptr, rstate)) + cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err } + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Windows = []*sstore.WindowType{&sstore.WindowType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + CurRemote: *rptr, + }} + update.Interactive = pk.Interactive return update, nil } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 264d5be8f..8d15d0640 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -272,7 +272,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if !rtn.Remote.RState.IsConnected() { err = rtn.Remote.MShell.TryAutoConnect() if err != nil { - return rtn, fmt.Errorf("error trying to auto-connect remote %q: %w", rtn.Remote.DisplayName, err) + return rtn, fmt.Errorf("error trying to auto-connect remote [%s]: %w", rtn.Remote.DisplayName, err) } rrNew, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.WindowId) if err != nil { @@ -281,10 +281,10 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i rtn.Remote = rrNew } if !rtn.Remote.RState.IsConnected() { - return rtn, fmt.Errorf("remote %q is not connected", rtn.Remote.DisplayName) + return rtn, fmt.Errorf("remote [%s] is not connected", rtn.Remote.DisplayName) } if rtn.Remote.StatePtr == nil || rtn.Remote.FeState == nil { - return rtn, fmt.Errorf("remote %q state is not available", rtn.Remote.DisplayName) + return rtn, fmt.Errorf("remote [%s] state is not available", rtn.Remote.DisplayName) } } return rtn, nil From 014f1132a7337a0768d42bd6f3d7475a62bf1589 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 2 Mar 2023 00:31:19 -0800 Subject: [PATCH 272/397] working on playbooks and history view --- db/migrations/000007_playbooks.down.sql | 3 + db/migrations/000007_playbooks.up.sql | 16 ++++ pkg/cmdrunner/cmdrunner.go | 34 +++++++- pkg/sstore/dbops.go | 106 +++++++++++++++++++++++- pkg/sstore/sstore.go | 88 +++++++++++++++++++- pkg/sstore/updatebus.go | 9 +- 6 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 db/migrations/000007_playbooks.down.sql create mode 100644 db/migrations/000007_playbooks.up.sql diff --git a/db/migrations/000007_playbooks.down.sql b/db/migrations/000007_playbooks.down.sql new file mode 100644 index 000000000..4ed8c1169 --- /dev/null +++ b/db/migrations/000007_playbooks.down.sql @@ -0,0 +1,3 @@ +DROP TABLE playbook; + +DROP TABLE playbook_entry; diff --git a/db/migrations/000007_playbooks.up.sql b/db/migrations/000007_playbooks.up.sql new file mode 100644 index 000000000..506e4ea5d --- /dev/null +++ b/db/migrations/000007_playbooks.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE playbook ( + playbookid varchar(36) PRIMARY KEY, + playbookname varchar(100) NOT NULL, + description text NOT NULL, + entryids json NOT NULL +); + +CREATE TABLE playbook_entry ( + entryid varchar(36) PRIMARY KEY, + playbookid varchar(36) NOT NULL, + description text NOT NULL, + alias varchar(50) NOT NULL, + cmdstr text NOT NULL, + createdts bigint NOT NULL, + updatedts bigint NOT NULL +); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 5048b18a2..575d8a482 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -176,6 +176,7 @@ func init() { registerCmdFn("telemetry:show", TelemetryShowCommand) registerCmdFn("history", HistoryCommand) + registerCmdFn("history:viewall", HistoryViewAllCommand) registerCmdFn("bookmarks:show", BookmarksShowCommand) @@ -1809,6 +1810,35 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } +func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + _, err := resolveUiIds(ctx, pk, 0) + if err != nil { + return nil, err + } + offset, err := resolveNonNegInt(pk.Kwargs["offset"], 0) + if err != nil { + return nil, err + } + opts := sstore.HistoryQueryOpts{MaxItems: 51, Offset: offset} + if pk.Kwargs["text"] != "" { + opts.SearchText = pk.Kwargs["text"] + } + hitems, err := sstore.GetHistoryItems(ctx, "", "", opts) + if err != nil { + return nil, err + } + hvdata := &sstore.HistoryViewData{ + TotalCount: 0, + Offset: offset, + Items: hitems, + } + update := sstore.ModelUpdate{ + HistoryViewData: hvdata, + MainView: sstore.MainViewHistory, + } + return update, nil +} + const DefaultMaxHistoryItems = 10000 func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -1971,8 +2001,8 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) log.Printf("error updating current activity (bookmarks): %v\n", err) } update := sstore.ModelUpdate{ - BookmarksView: true, - Bookmarks: bms, + MainView: sstore.MainViewBookmarks, + Bookmarks: bms, } return update, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 27365be6f..f180d9864 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -223,6 +223,7 @@ func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts History } hnumStr := "" whereClause := "" + var queryArgs []interface{} if sessionId != "" && windowId != "" { whereClause = fmt.Sprintf("WHERE sessionid = '%s' AND windowid = '%s'", sessionId, windowId) hnumStr = "w" @@ -232,15 +233,26 @@ func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts History } else { hnumStr = "g" } + if opts.SearchText != "" { + if whereClause == "" { + whereClause = "WHERE cmdstr LIKE ? ESCAPE '\\'" + } else { + whereClause = whereClause + " AND cmdstr LIKE ? ESCAPE '\\'" + } + likeArg := opts.SearchText + likeArg = strings.ReplaceAll(likeArg, "%", "\\%") + likeArg = strings.ReplaceAll(likeArg, "_", "\\_") + queryArgs = append(queryArgs, "%"+likeArg+"%") + } maxItems := opts.MaxItems if maxItems == 0 { maxItems = DefaultMaxHistoryItems } - query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d", HistoryCols, hnumStr, whereClause, maxItems) + query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d OFFSET %d", HistoryCols, hnumStr, whereClause, maxItems, opts.Offset) if opts.FromTs > 0 { query = fmt.Sprintf("SELECT * FROM (%s) WHERE ts >= %d", query, opts.FromTs) } - marr := tx.SelectMaps(query) + marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { hitem := HistoryItemFromMap(m) @@ -2241,3 +2253,93 @@ func DeleteBookmark(ctx context.Context, bookmarkId string) error { }) return txErr } + +func CreatePlaybook(ctx context.Context, name string) (*PlaybookType, error) { + var rtn *PlaybookType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT playbookid FROM playbook WHERE name = ?` + if tx.Exists(query, name) { + return fmt.Errorf("playbook %q already exists", name) + } + rtn = &PlaybookType{} + rtn.PlaybookId = uuid.New().String() + rtn.PlaybookName = name + query = `INSERT INTO playbook ( playbookid, playbookname, description, entryids) + VALUES (:playbookid,:playbookname,:description,:entryids)` + tx.Exec(query, rtn.ToMap()) + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} + +func selectPlaybook(tx *TxWrap, playbookId string) *PlaybookType { + query := `SELECT * FROM playbook where playbookid = ?` + m := tx.GetMap(query, playbookId) + playbook := PlaybookFromMap(m) + return playbook +} + +func AddPlaybookEntry(ctx context.Context, entry *PlaybookEntry) error { + if entry.EntryId == "" { + return fmt.Errorf("invalid entryid") + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + playbook := selectPlaybook(tx, entry.PlaybookId) + if playbook == nil { + return fmt.Errorf("cannot add entry, playbook does not exist") + } + query := `SELECT entryid FROM playbook_entry WHERE entryid = ?` + if tx.Exists(query, entry.EntryId) { + return fmt.Errorf("cannot add entry, entryid already exists") + } + query = `INSERT INTO playbook_entry ( entryid, playbookid, description, alias, cmdstr, createdts, updatedts) + VALUES (:entryid,:playbookid,:description,:alias,:cmdstr,:createdts,:updatedts)` + tx.Exec(query, entry) + playbook.EntryIds = append(playbook.EntryIds, entry.EntryId) + query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?` + tx.Exec(query, quickJsonArr(playbook.EntryIds), entry.PlaybookId) + return nil + }) + return txErr +} + +func RemovePlaybookEntry(ctx context.Context, playbookId string, entryId string) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + playbook := selectPlaybook(tx, playbookId) + if playbook == nil { + return fmt.Errorf("cannot remove playbook entry, playbook does not exist") + } + query := `SELECT entryid FROM playbook_entry WHERE entryid = ?` + if !tx.Exists(query, entryId) { + return fmt.Errorf("cannot remove playbook entry, entry does not exist") + } + query = `DELETE FROM playbook_entry WHERE entryid = ?` + tx.Exec(query, entryId) + playbook.RemoveEntry(entryId) + query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?` + tx.Exec(query, quickJsonArr(playbook.EntryIds), playbookId) + return nil + }) + return txErr +} + +func GetPlaybookById(ctx context.Context, playbookId string) (*PlaybookType, error) { + var rtn *PlaybookType + txErr := WithTx(ctx, func(tx *TxWrap) error { + rtn = selectPlaybook(tx, playbookId) + if rtn == nil { + return nil + } + query := `SELECT * FROM playbook_entry WHERE playbookid = ?` + tx.Select(&rtn.Entries, query, playbookId) + rtn.OrderEntries() + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 72e356f10..c87af9c4a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -39,6 +39,12 @@ const DefaultScreenWindowName = "w1" const DefaultCwd = "~" +const ( + MainViewSession = "session" + MainViewBookmarks = "bookmarks" + MainViewHistory = "history" +) + const ( CmdStatusRunning = "running" CmdStatusDetached = "detached" @@ -525,8 +531,10 @@ type HistoryItemType struct { } type HistoryQueryOpts struct { - MaxItems int - FromTs int64 + Offset int + MaxItems int + FromTs int64 + SearchText string } type TermOpts struct { @@ -674,6 +682,82 @@ type LineType struct { Remove bool `json:"remove,omitempty"` } +type PlaybookType struct { + PlaybookId string `json:"playbookid"` + PlaybookName string `json:"playbookname"` + Description string `json:"description"` + EntryIds []string `json:"entryids"` + + // this is not persisted to DB, just for transport to FE + Entries []*PlaybookEntry `json:"entries"` +} + +func (p *PlaybookType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["playbookid"] = p.PlaybookId + rtn["playbookname"] = p.PlaybookName + rtn["description"] = p.Description + rtn["entryids"] = quickJsonArr(p.EntryIds) + return rtn +} + +func PlaybookFromMap(m map[string]interface{}) *PlaybookType { + if len(m) == 0 { + return nil + } + var p PlaybookType + quickSetStr(&p.PlaybookId, m, "playbookid") + quickSetStr(&p.PlaybookName, m, "playbookname") + quickSetStr(&p.Description, m, "description") + quickSetJsonArr(&p.Entries, m, "entries") + return &p +} + +// reorders p.Entries to match p.EntryIds +func (p *PlaybookType) OrderEntries() { + if len(p.Entries) == 0 { + return + } + m := make(map[string]*PlaybookEntry) + for _, entry := range p.Entries { + m[entry.EntryId] = entry + } + newList := make([]*PlaybookEntry, 0, len(p.EntryIds)) + for _, entryId := range p.EntryIds { + entry := m[entryId] + if entry != nil { + newList = append(newList, entry) + } + } + p.Entries = newList +} + +// removes from p.EntryIds (not from p.Entries) +func (p *PlaybookType) RemoveEntry(entryIdToRemove string) { + if len(p.EntryIds) == 0 { + return + } + newList := make([]string, 0, len(p.EntryIds)-1) + for _, entryId := range p.EntryIds { + if entryId == entryIdToRemove { + continue + } + newList = append(newList, entryId) + } + p.EntryIds = newList +} + +type PlaybookEntry struct { + PlaybookId string `json:"playbookid"` + EntryId string `json:"entryid"` + Alias string `json:"alias"` + CmdStr string `json:"cmdstr"` + UpdatedTs int64 `json:"updatedts"` + CreatedTs int64 `json:"createdts"` + Description string `json:"description"` + Remove bool `json:"remove,omitempty"` +} + type BookmarkType struct { BookmarkId string `json:"bookmarkid"` CreatedTs int64 `json:"createdts"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 9f6e95880..13a5eb54b 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -43,8 +43,9 @@ type ModelUpdate struct { History *HistoryInfoType `json:"history,omitempty"` Interactive bool `json:"interactive"` Connect bool `json:"connect,omitempty"` - BookmarksView bool `json:"bookmarksview,omitempty"` + MainView string `json:"mainview,omitempty"` Bookmarks []*BookmarkType `json:"bookmarks,omitempty"` + HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` ClientData *ClientData `json:"clientdata,omitempty"` } @@ -74,6 +75,12 @@ func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { } } +type HistoryViewData struct { + TotalCount int `json:"totalcount"` + Offset int `json:"offset"` + Items []*HistoryItemType `json:"items"` +} + type RemoteEditType struct { RemoteEdit bool `json:"remoteedit"` RemoteId string `json:"remoteid,omitempty"` From b842ef3215b14d50484d577fb15300205bb3ea0c Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 2 Mar 2023 22:26:15 -0800 Subject: [PATCH 273/397] add lines/cmds to historyview data. fix bug with lineid not getting added for comment commands --- pkg/cmdrunner/cmdrunner.go | 22 ++++++++++++++----- pkg/sstore/dbops.go | 45 ++++++++++++++++++++++++++++++++++++++ pkg/sstore/updatebus.go | 3 +++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 575d8a482..5c944413f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1454,6 +1454,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } + updateHistoryContext(ctx, rtnLine, nil) updateMap := make(map[string]interface{}) updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum updateMap[sstore.SWField_Focus] = sstore.SWFocusInput @@ -1810,6 +1811,8 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } +const HistoryViewPageSize = 50 + func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { _, err := resolveUiIds(ctx, pk, 0) if err != nil { @@ -1819,7 +1822,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, err } - opts := sstore.HistoryQueryOpts{MaxItems: 51, Offset: offset} + opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize + 1, Offset: offset} if pk.Kwargs["text"] != "" { opts.SearchText = pk.Kwargs["text"] } @@ -1827,11 +1830,20 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, err } - hvdata := &sstore.HistoryViewData{ - TotalCount: 0, - Offset: offset, - Items: hitems, + hvdata := &sstore.HistoryViewData{Offset: offset} + if len(hitems) > HistoryViewPageSize { + hvdata.HasMore = true + hvdata.Items = hitems[0:HistoryViewPageSize] + } else { + hvdata.HasMore = false + hvdata.Items = hitems } + lines, cmds, err := sstore.GetLineCmdsFromHistoryItems(ctx, hvdata.Items) + if err != nil { + return nil, err + } + hvdata.Lines = lines + hvdata.Cmds = cmds update := sstore.ModelUpdate{ HistoryViewData: hvdata, MainView: sstore.MainViewHistory, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f180d9864..9007a4556 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2343,3 +2343,48 @@ func GetPlaybookById(ctx context.Context, playbookId string) (*PlaybookType, err } return rtn, nil } + +func getLineIdsFromHistoryItems(historyItems []*HistoryItemType) []string { + var rtn []string + for _, hitem := range historyItems { + if hitem.LineId != "" { + rtn = append(rtn, hitem.LineId) + } + } + return rtn +} + +func getCmdIdsFromHistoryItems(historyItems []*HistoryItemType) []string { + var rtn []string + for _, hitem := range historyItems { + if hitem.CmdId != "" { + rtn = append(rtn, hitem.CmdId) + } + } + return rtn +} + +func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryItemType) ([]*LineType, []*CmdType, error) { + var lineArr []*LineType + var cmdArr []*CmdType + if len(historyItems) == 0 { + return nil, nil, nil + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` + tx.Select(&lineArr, query, quickJsonArr(getLineIdsFromHistoryItems(historyItems))) + query = `SELECT * FROM cmd WHERE cmdid IN (SELECT value FROM json_each(?))` + marr := tx.SelectMaps(query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) + for _, m := range marr { + cmd := CmdFromMap(m) + if cmd != nil { + cmdArr = append(cmdArr, cmd) + } + } + return nil + }) + if txErr != nil { + return nil, nil, txErr + } + return lineArr, cmdArr, nil +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 13a5eb54b..1e9e30dc9 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -79,6 +79,9 @@ type HistoryViewData struct { TotalCount int `json:"totalcount"` Offset int `json:"offset"` Items []*HistoryItemType `json:"items"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` + HasMore bool `json:"hasmore"` } type RemoteEditType struct { From 77d27272a5104cd48b0e11c3737b6fd09ed06778 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 2 Mar 2023 23:24:01 -0800 Subject: [PATCH 274/397] line:view command --- pkg/cmdrunner/cmdrunner.go | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 5c944413f..48ec85662 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -164,6 +164,7 @@ func init() { registerCmdFn("line:archive", LineArchiveCommand) registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("line:setheight", LineSetHeightCommand) + registerCmdFn("line:view", LineViewCommand) registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) @@ -607,6 +608,8 @@ func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore if m[2] != "" { anchorOffset, _ := strconv.Atoi(m[2]) updateMap[sstore.SWField_AnchorOffset] = anchorOffset + } else { + updateMap[sstore.SWField_AnchorOffset] = 0 } } if pk.Kwargs["focus"] != "" { @@ -1998,6 +2001,45 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, nil } +func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) != 3 { + return nil, fmt.Errorf("usage /line:view [session] [screen] [line]") + } + sessionArg := pk.Args[0] + screenArg := pk.Args[1] + lineArg := pk.Args[2] + sessionId, err := resolveSessionArg(sessionArg) + if err != nil { + return nil, fmt.Errorf("/line:view invalid session arg: %v", err) + } + screenRItem, err := resolveSessionScreen(ctx, sessionId, screenArg, "") + if err != nil { + return nil, fmt.Errorf("/line:view invalid screen arg: %v", err) + } + screen, err := sstore.GetScreenById(ctx, sessionId, screenRItem.Id) + if err != nil { + return nil, fmt.Errorf("/line:view could not get screen: %v", err) + } + lineRItem, err := resolveLine(ctx, sessionId, screen.ActiveWindowId, lineArg, "") + if err != nil { + return nil, fmt.Errorf("/line:view invalid line arg: %v", err) + } + update, err := sstore.SwitchScreenById(ctx, sessionId, screenRItem.Id) + if err != nil { + return nil, err + } + updateMap := make(map[string]interface{}) + updateMap[sstore.SWField_SelectedLine] = lineRItem.Num + updateMap[sstore.SWField_AnchorLine] = lineRItem.Num + updateMap[sstore.SWField_AnchorOffset] = 0 + sw, err := sstore.UpdateScreenWindow(ctx, sessionId, screenRItem.Id, screen.ActiveWindowId, updateMap) + if err != nil { + return nil, err + } + update.ScreenWindows = []*sstore.ScreenWindowType{sw} + return update, nil +} + func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { // no resolve ui ids! var tagName string // defaults to '' From 60e17f160f41a73d10ecf4005518fa56b34fc8f2 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 3 Mar 2023 10:15:57 -0800 Subject: [PATCH 275/397] add activesessionid to switch screen --- pkg/sstore/dbops.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9007a4556..8201c183e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -893,7 +893,7 @@ func getNextId(ids []string, delId string) string { return ids[0] } -func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { +func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*ModelUpdate, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, sessionId, screenId) { @@ -910,7 +910,7 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (U if err != nil { return nil, err } - return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil + return &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}, nil } func cleanSessionCmds(ctx context.Context, sessionId string) error { From 22d0c5687e6a2e3e49d532b0b3139a4b689b8da4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 3 Mar 2023 13:31:16 -0800 Subject: [PATCH 276/397] call to purge history items --- pkg/cmdrunner/cmdrunner.go | 74 +++++++++++++++++++++++++++++--------- pkg/sstore/dbops.go | 59 ++++++++++++++++++++++-------- pkg/sstore/updatebus.go | 1 + 3 files changed, 103 insertions(+), 31 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 48ec85662..3ceb7582b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -178,6 +178,7 @@ func init() { registerCmdFn("history", HistoryCommand) registerCmdFn("history:viewall", HistoryViewAllCommand) + registerCmdFn("history:purge", HistoryPurgeCommand) registerCmdFn("bookmarks:show", BookmarksShowCommand) @@ -1814,6 +1815,38 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore } +func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/history:purge requires at least one argument (history id)") + } + var historyIds []string + for _, historyArg := range pk.Args { + _, err := uuid.Parse(historyArg) + if err != nil { + return nil, fmt.Errorf("invalid historyid (must be uuid)") + } + historyIds = append(historyIds, historyArg) + } + historyItemsRemoved, 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{ + SessionId: historyItem.SessionId, + WindowId: historyItem.WindowId, + LineId: historyItem.LineId, + Remove: true, + } + update.Lines = append(update.Lines, lineObj) + } + return update, nil +} + const HistoryViewPageSize = 50 func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -2258,27 +2291,34 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return nil, err } if len(pk.Args) == 0 { - return nil, fmt.Errorf("/line:purge requires an argument (line number or id)") + return nil, fmt.Errorf("/line:purge requires at least one argument (line number or id)") } - lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + var lineIds []string + for _, lineArg := range pk.Args { + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + if lineId == "" { + return nil, fmt.Errorf("line %q not found", lineArg) + } + lineIds = append(lineIds, lineId) + } + err = sstore.PurgeLinesByIds(ctx, ids.SessionId, lineIds) if err != nil { - return nil, fmt.Errorf("error looking up lineid: %v", err) + return nil, fmt.Errorf("/line:purge error purging lines: %v", err) } - if lineId == "" { - return nil, fmt.Errorf("line %q not found", lineArg) + update := sstore.ModelUpdate{} + for _, lineId := range lineIds { + lineObj := &sstore.LineType{ + SessionId: ids.SessionId, + WindowId: ids.WindowId, + LineId: lineId, + Remove: true, + } + update.Lines = append(update.Lines, lineObj) } - err = sstore.PurgeLineById(ctx, ids.SessionId, lineId) - if err != nil { - return nil, fmt.Errorf("/line:purge error purging line: %v", err) - } - lineObj := &sstore.LineType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - LineId: lineId, - Remove: true, - } - return sstore.ModelUpdate{Line: lineObj}, nil + return update, nil } func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8201c183e..6886b7e72 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1885,21 +1885,23 @@ func purgeCmdById(ctx context.Context, sessionId string, cmdId string) error { return txErr } -func PurgeLineById(ctx context.Context, sessionId string, lineId string) error { +func PurgeLinesByIds(ctx context.Context, sessionId string, lineIds []string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT cmdid FROM line WHERE sessionid = ? AND lineid = ?` - cmdId := tx.GetString(query, sessionId, lineId) - query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` - tx.Exec(query, sessionId, lineId) - query = `DELETE FROM history WHERE sessionid = ? AND lineid = ?` - tx.Exec(query, sessionId, lineId) - if cmdId != "" { - query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` - cmdRefCount := tx.GetInt(query, sessionId, cmdId) - if cmdRefCount == 0 { - err := purgeCmdById(tx.Context(), sessionId, cmdId) - if err != nil { - return err + for _, lineId := range lineIds { + query := `SELECT cmdid FROM line WHERE sessionid = ? AND lineid = ?` + cmdId := tx.GetString(query, sessionId, lineId) + query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` + tx.Exec(query, sessionId, lineId) + query = `DELETE FROM history WHERE sessionid = ? AND lineid = ?` + tx.Exec(query, sessionId, lineId) + if cmdId != "" { + query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` + cmdRefCount := tx.GetInt(query, sessionId, cmdId) + if cmdRefCount == 0 { + err := purgeCmdById(tx.Context(), sessionId, cmdId) + if err != nil { + return err + } } } } @@ -2388,3 +2390,32 @@ func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryIte } return lineArr, cmdArr, nil } + +func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItemType, error) { + var rtn []*HistoryItemType + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT * FROM history WHERE historyid IN (SELECT value FROM json_each(?))` + marr := tx.SelectMaps(query, quickJsonArr(historyIds)) + for _, m := range marr { + hitem := HistoryItemFromMap(m) + if hitem != nil { + rtn = append(rtn, hitem) + } + } + 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.SessionId, []string{hitem.LineId}) + if err != nil { + return err + } + } + } + return nil + }) + if txErr != nil { + return nil, txErr + } + return rtn, nil +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 1e9e30dc9..cd7d7cf37 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -35,6 +35,7 @@ type ModelUpdate struct { Windows []*WindowType `json:"windows,omitempty"` ScreenWindows []*ScreenWindowType `json:"screenwindows,omitempty"` Line *LineType `json:"line,omitempty"` + Lines []*LineType `json:"lines,omitempty"` Cmd *CmdType `json:"cmd,omitempty"` CmdLine *CmdLineType `json:"cmdline,omitempty"` Info *InfoMsgType `json:"info,omitempty"` From 56b74f35f325f7ad5d8849a078f0f8720aaabbb2 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 3 Mar 2023 18:12:24 -0800 Subject: [PATCH 277/397] allow many more history query search options --- pkg/cmdrunner/cmdrunner.go | 34 +++++++++++++++++++++++++-- pkg/sstore/dbops.go | 48 ++++++++++++++++++++++---------------- pkg/sstore/sstore.go | 4 ++++ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3ceb7582b..35679625b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1862,7 +1862,36 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if pk.Kwargs["text"] != "" { opts.SearchText = pk.Kwargs["text"] } - hitems, err := sstore.GetHistoryItems(ctx, "", "", opts) + if pk.Kwargs["searchsession"] != "" { + sessionId, err := resolveSessionArg(pk.Kwargs["searchsession"]) + if err != nil { + return nil, fmt.Errorf("invalid searchsession: %v", err) + } + opts.SessionId = sessionId + } + if pk.Kwargs["searchremote"] != "" { + rptr, err := resolveRemoteArg(pk.Kwargs["searchremote"]) + if err != nil { + return nil, fmt.Errorf("invalid searchremote: %v", err) + } + if rptr != nil { + opts.RemoteId = rptr.RemoteId + } + } + if pk.Kwargs["fromts"] != "" { + fromTs, err := resolvePosInt(pk.Kwargs["fromts"], 0) + if err != nil { + return nil, fmt.Errorf("invalid fromts (must be unixtime (milliseconds): %v", err) + } + if fromTs > 0 { + opts.FromTs = int64(fromTs) + } + } + opts.NoMeta = resolveBool(pk.Kwargs["meta"], false) + if err != nil { + return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err) + } + hitems, err := sstore.GetHistoryItems(ctx, opts) if err != nil { return nil, err } @@ -1919,7 +1948,8 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } else if htype == HistoryTypeSession { hWindowId = "" } - hitems, err := sstore.GetHistoryItems(ctx, hSessionId, hWindowId, sstore.HistoryQueryOpts{MaxItems: maxItems}) + hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, WindowId: hWindowId} + hitems, err := sstore.GetHistoryItems(ctx, hopts) if err != nil { return nil, err } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 6886b7e72..224220d33 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -207,51 +207,59 @@ func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) ( return rtn, txErr } -func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { +func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts) ([]*HistoryItemType, error) { // check sessionid/windowid format because we are directly inserting them into the SQL - if sessionId != "" { - _, err := uuid.Parse(sessionId) + if opts.SessionId != "" { + _, err := uuid.Parse(opts.SessionId) if err != nil { return nil, fmt.Errorf("malformed sessionid") } } - if windowId != "" { - _, err := uuid.Parse(windowId) + if opts.WindowId != "" { + _, err := uuid.Parse(opts.WindowId) if err != nil { return nil, fmt.Errorf("malformed windowid") } } + if opts.RemoteId != "" { + _, err := uuid.Parse(opts.RemoteId) + if err != nil { + return nil, fmt.Errorf("malformed remoteid") + } + } hnumStr := "" - whereClause := "" + whereClause := "WHERE 1" var queryArgs []interface{} - if sessionId != "" && windowId != "" { - whereClause = fmt.Sprintf("WHERE sessionid = '%s' AND windowid = '%s'", sessionId, windowId) + if opts.SessionId != "" && opts.WindowId != "" { + whereClause += fmt.Sprintf(" AND sessionid = '%s' AND windowid = '%s'", opts.SessionId, opts.WindowId) hnumStr = "w" - } else if sessionId != "" { - whereClause = fmt.Sprintf("WHERE sessionid = '%s'", sessionId) + } else if opts.SessionId != "" { + whereClause += fmt.Sprintf(" AND sessionid = '%s'", opts.SessionId) hnumStr = "s" } else { hnumStr = "g" } if opts.SearchText != "" { - if whereClause == "" { - whereClause = "WHERE cmdstr LIKE ? ESCAPE '\\'" - } else { - whereClause = whereClause + " AND cmdstr LIKE ? ESCAPE '\\'" - } + whereClause += " AND cmdstr LIKE ? ESCAPE '\\'" likeArg := opts.SearchText likeArg = strings.ReplaceAll(likeArg, "%", "\\%") likeArg = strings.ReplaceAll(likeArg, "_", "\\_") queryArgs = append(queryArgs, "%"+likeArg+"%") } + if opts.FromTs > 0 { + whereClause += fmt.Sprintf(" AND ts > %d", opts.FromTs) + } + if opts.RemoteId != "" { + whereClause += fmt.Sprintf(" AND remoteid = '%s'", opts.RemoteId) + } + if opts.NoMeta { + whereClause += " AND NOT ismetacmd" + } maxItems := opts.MaxItems if maxItems == 0 { maxItems = DefaultMaxHistoryItems } query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d OFFSET %d", HistoryCols, hnumStr, whereClause, maxItems, opts.Offset) - if opts.FromTs > 0 { - query = fmt.Sprintf("SELECT * FROM (%s) WHERE ts >= %d", query, opts.FromTs) - } marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { @@ -261,11 +269,11 @@ func runHistoryQuery(tx *TxWrap, sessionId string, windowId string, opts History return rtn, nil } -func GetHistoryItems(ctx context.Context, sessionId string, windowId string, opts HistoryQueryOpts) ([]*HistoryItemType, error) { +func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) ([]*HistoryItemType, error) { var rtn []*HistoryItemType txErr := WithTx(ctx, func(tx *TxWrap) error { var err error - rtn, err = runHistoryQuery(tx, sessionId, windowId, opts) + rtn, err = runHistoryQuery(tx, opts) if err != nil { return err } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c87af9c4a..a9816511d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -535,6 +535,10 @@ type HistoryQueryOpts struct { MaxItems int FromTs int64 SearchText string + SessionId string + RemoteId string + WindowId string + NoMeta bool } type TermOpts struct { From 7516f880aec2838fa6f9fe678db2535cc973e182 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 5 Mar 2023 13:53:24 -0800 Subject: [PATCH 278/397] history search bugfixes --- pkg/cmdrunner/cmdrunner.go | 4 +++- pkg/sstore/dbops.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 35679625b..6e7ae1f12 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1887,7 +1887,9 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType opts.FromTs = int64(fromTs) } } - opts.NoMeta = resolveBool(pk.Kwargs["meta"], false) + if pk.Kwargs["meta"] != "" { + opts.NoMeta = !resolveBool(pk.Kwargs["meta"], true) + } if err != nil { return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 224220d33..0fa63c021 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -247,7 +247,7 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts) ([]*HistoryItemType, err queryArgs = append(queryArgs, "%"+likeArg+"%") } if opts.FromTs > 0 { - whereClause += fmt.Sprintf(" AND ts > %d", opts.FromTs) + whereClause += fmt.Sprintf(" AND ts <= %d", opts.FromTs) } if opts.RemoteId != "" { whereClause += fmt.Sprintf(" AND remoteid = '%s'", opts.RemoteId) From 501b067eadb501249fda5fbcdaf8e8513c7a8275 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Mar 2023 11:47:44 -0800 Subject: [PATCH 279/397] add a filtered search mode to history search --- pkg/cmdrunner/cmdrunner.go | 24 ++++++----- pkg/sstore/dbops.go | 86 ++++++++++++++++++++++++++++++++++---- pkg/sstore/sstore.go | 9 ++++ pkg/sstore/updatebus.go | 12 +++--- 4 files changed, 105 insertions(+), 26 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 6e7ae1f12..ae481b1c9 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1858,7 +1858,11 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, err } - opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize + 1, Offset: offset} + rawOffset, err := resolveNonNegInt(pk.Kwargs["rawoffset"], 0) + if err != nil { + return nil, err + } + opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset} if pk.Kwargs["text"] != "" { opts.SearchText = pk.Kwargs["text"] } @@ -1893,17 +1897,15 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err) } - hitems, err := sstore.GetHistoryItems(ctx, opts) + hresult, err := sstore.GetHistoryItems(ctx, opts) if err != nil { return nil, err } - hvdata := &sstore.HistoryViewData{Offset: offset} - if len(hitems) > HistoryViewPageSize { - hvdata.HasMore = true - hvdata.Items = hitems[0:HistoryViewPageSize] - } else { - hvdata.HasMore = false - hvdata.Items = hitems + hvdata := &sstore.HistoryViewData{ + Items: hresult.Items, + Offset: hresult.Offset, + NextRawOffset: hresult.NextRawOffset, + HasMore: hresult.HasMore, } lines, cmds, err := sstore.GetLineCmdsFromHistoryItems(ctx, hvdata.Items) if err != nil { @@ -1951,7 +1953,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto hWindowId = "" } hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, WindowId: hWindowId} - hitems, err := sstore.GetHistoryItems(ctx, hopts) + hresult, err := sstore.GetHistoryItems(ctx, hopts) if err != nil { return nil, err } @@ -1967,7 +1969,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto HistoryType: htype, SessionId: ids.SessionId, WindowId: ids.WindowId, - Items: hitems, + Items: hresult.Items, Show: show, } return update, nil diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 0fa63c021..91bd0ecdd 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -207,7 +207,79 @@ func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) ( return rtn, txErr } -func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts) ([]*HistoryItemType, error) { +const HistoryQueryChunkSize = 1000 + +func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*HistoryItemType) bool) (*HistoryItemType, int) { + for ; index < len(items); index++ { + item := items[index] + if filterFn(item) { + return item, index + } + } + return nil, index +} + +func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset int) bool { + if len(result.Items) == result.MaxItems { + result.HasMore = true + result.NextRawOffset = rawOffset + return false + } + result.Items = append(result.Items, item) + return true +} + +func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts, filterFn func(*HistoryItemType) bool) (*HistoryQueryResult, error) { + if opts.MaxItems == 0 { + return nil, fmt.Errorf("invalid query, maxitems is 0") + } + if opts.RawOffset < opts.Offset { + return nil, fmt.Errorf("invalid query, rawoffset[%d] is less than offset[%d]", opts.RawOffset, opts.Offset) + } + rtn := &HistoryQueryResult{Offset: opts.RawOffset, MaxItems: opts.MaxItems} + if filterFn == nil { + results, err := runHistoryQuery(tx, opts, opts.RawOffset, opts.MaxItems+1) + if err != nil { + return nil, err + } + if len(results) > opts.MaxItems { + rtn.Items = results[0:opts.MaxItems] + rtn.HasMore = true + rtn.NextRawOffset = opts.RawOffset + opts.MaxItems + } else { + rtn.Items = results + rtn.HasMore = false + rtn.NextRawOffset = 0 + } + return rtn, nil + } + rawOffset := opts.RawOffset + for { + resultItems, err := runHistoryQuery(tx, opts, rawOffset, HistoryQueryChunkSize) + if err != nil { + return nil, err + } + isDone := false + for resultIdx := 0; resultIdx < len(resultItems); resultIdx++ { + if !filterFn(resultItems[resultIdx]) { + continue + } + isDone = rtn.processItem(resultItems[resultIdx], rawOffset+resultIdx) + if isDone { + break + } + } + if isDone { + break + } + if len(resultItems) < HistoryQueryChunkSize { + break + } + } + return rtn, nil +} + +func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimit int) ([]*HistoryItemType, error) { // check sessionid/windowid format because we are directly inserting them into the SQL if opts.SessionId != "" { _, err := uuid.Parse(opts.SessionId) @@ -255,11 +327,7 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts) ([]*HistoryItemType, err if opts.NoMeta { whereClause += " AND NOT ismetacmd" } - maxItems := opts.MaxItems - if maxItems == 0 { - maxItems = DefaultMaxHistoryItems - } - query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d OFFSET %d", HistoryCols, hnumStr, whereClause, maxItems, opts.Offset) + query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d OFFSET %d", HistoryCols, hnumStr, whereClause, itemLimit, realOffset) marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { @@ -269,11 +337,11 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts) ([]*HistoryItemType, err return rtn, nil } -func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) ([]*HistoryItemType, error) { - var rtn []*HistoryItemType +func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryResult, error) { + var rtn *HistoryQueryResult txErr := WithTx(ctx, func(tx *TxWrap) error { var err error - rtn, err = runHistoryQuery(tx, opts) + rtn, err = runHistoryQueryWithFilter(tx, opts, nil) if err != nil { return err } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index a9816511d..93bb5d147 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -539,6 +539,15 @@ type HistoryQueryOpts struct { RemoteId string WindowId string NoMeta bool + RawOffset int +} + +type HistoryQueryResult struct { + MaxItems int + Items []*HistoryItemType + Offset int // the offset shown to user + HasMore bool + NextRawOffset int // internal offset used by pager for next query } type TermOpts struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index cd7d7cf37..d01ba3cac 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -77,12 +77,12 @@ func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { } type HistoryViewData struct { - TotalCount int `json:"totalcount"` - Offset int `json:"offset"` - Items []*HistoryItemType `json:"items"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` - HasMore bool `json:"hasmore"` + Items []*HistoryItemType `json:"items"` + Offset int `json:"offset"` + NextRawOffset int `json:"rawoffset"` + HasMore bool `json:"hasmore"` + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` } type RemoteEditType struct { From 454c8758cd5b60805d3b558df3466d3994895c7d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Mar 2023 13:54:38 -0800 Subject: [PATCH 280/397] bug fixes, allow cmd filtering --- pkg/cmdrunner/cmdrunner.go | 21 +++++++++++++++++++ pkg/sstore/dbops.go | 41 ++++++++++++++++---------------------- pkg/sstore/sstore.go | 4 ++++ pkg/sstore/updatebus.go | 3 ++- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ae481b1c9..a5f4835f1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1849,6 +1849,23 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) const HistoryViewPageSize = 50 +var cmdFilterLs = regexp.MustCompile(`^ls(\s|$)`) +var cmdFilterCd = regexp.MustCompile(`^cd(\s|$)`) + +func historyCmdFilter(hitem *sstore.HistoryItemType) bool { + cmdStr := hitem.CmdStr + if cmdStr == "" || strings.Index(cmdStr, ";") != -1 || strings.Index(cmdStr, "\n") != -1 { + return true + } + if cmdFilterLs.MatchString(cmdStr) { + return false + } + if cmdFilterCd.MatchString(cmdStr) { + return false + } + return true +} + func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { _, err := resolveUiIds(ctx, pk, 0) if err != nil { @@ -1894,6 +1911,9 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if pk.Kwargs["meta"] != "" { opts.NoMeta = !resolveBool(pk.Kwargs["meta"], true) } + if resolveBool(pk.Kwargs["filter"], false) { + opts.FilterFn = historyCmdFilter + } if err != nil { return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err) } @@ -1904,6 +1924,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType hvdata := &sstore.HistoryViewData{ Items: hresult.Items, Offset: hresult.Offset, + RawOffset: hresult.RawOffset, NextRawOffset: hresult.NextRawOffset, HasMore: hresult.HasMore, } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 91bd0ecdd..5740fb0fa 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -219,41 +219,33 @@ func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*His return nil, index } +// returns true if done, false if we still need to process more items func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset int) bool { + if result.Offset < result.prevItems { + result.prevItems++ + return false + } if len(result.Items) == result.MaxItems { result.HasMore = true result.NextRawOffset = rawOffset - return false + return true } result.Items = append(result.Items, item) - return true + return false } -func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts, filterFn func(*HistoryItemType) bool) (*HistoryQueryResult, error) { +func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts) (*HistoryQueryResult, error) { if opts.MaxItems == 0 { return nil, fmt.Errorf("invalid query, maxitems is 0") } - if opts.RawOffset < opts.Offset { - return nil, fmt.Errorf("invalid query, rawoffset[%d] is less than offset[%d]", opts.RawOffset, opts.Offset) - } rtn := &HistoryQueryResult{Offset: opts.RawOffset, MaxItems: opts.MaxItems} - if filterFn == nil { - results, err := runHistoryQuery(tx, opts, opts.RawOffset, opts.MaxItems+1) - if err != nil { - return nil, err - } - if len(results) > opts.MaxItems { - rtn.Items = results[0:opts.MaxItems] - rtn.HasMore = true - rtn.NextRawOffset = opts.RawOffset + opts.MaxItems - } else { - rtn.Items = results - rtn.HasMore = false - rtn.NextRawOffset = 0 - } - return rtn, nil + var rawOffset int + if opts.RawOffset >= opts.Offset { + rtn.prevItems = opts.Offset + rawOffset = opts.RawOffset + } else { + rawOffset = 0 } - rawOffset := opts.RawOffset for { resultItems, err := runHistoryQuery(tx, opts, rawOffset, HistoryQueryChunkSize) if err != nil { @@ -261,7 +253,7 @@ func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts, filterFn func( } isDone := false for resultIdx := 0; resultIdx < len(resultItems); resultIdx++ { - if !filterFn(resultItems[resultIdx]) { + if opts.FilterFn != nil && !opts.FilterFn(resultItems[resultIdx]) { continue } isDone = rtn.processItem(resultItems[resultIdx], rawOffset+resultIdx) @@ -275,6 +267,7 @@ func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts, filterFn func( if len(resultItems) < HistoryQueryChunkSize { break } + rawOffset += HistoryQueryChunkSize } return rtn, nil } @@ -341,7 +334,7 @@ func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryR var rtn *HistoryQueryResult txErr := WithTx(ctx, func(tx *TxWrap) error { var err error - rtn, err = runHistoryQueryWithFilter(tx, opts, nil) + rtn, err = runHistoryQueryWithFilter(tx, opts) if err != nil { return err } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 93bb5d147..9fe5c1ba9 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -540,14 +540,18 @@ type HistoryQueryOpts struct { WindowId string NoMeta bool RawOffset int + FilterFn func(*HistoryItemType) bool } type HistoryQueryResult struct { MaxItems int Items []*HistoryItemType Offset int // the offset shown to user + RawOffset int // internal offset HasMore bool NextRawOffset int // internal offset used by pager for next query + + prevItems int // holds number of items skipped by RawOffset } type TermOpts struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index d01ba3cac..36608d041 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -79,7 +79,8 @@ func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { type HistoryViewData struct { Items []*HistoryItemType `json:"items"` Offset int `json:"offset"` - NextRawOffset int `json:"rawoffset"` + RawOffset int `json:"rawoffset"` + NextRawOffset int `json:"nextrawoffset"` HasMore bool `json:"hasmore"` Lines []*LineType `json:"lines"` Cmds []*CmdType `json:"cmds"` From 66f55ad85ce7374680eefa7b04ad36a819547c20 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Mar 2023 15:42:50 -0800 Subject: [PATCH 281/397] fix bugs with history query paging --- pkg/sstore/dbops.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5740fb0fa..780406e86 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -221,7 +221,7 @@ func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*His // returns true if done, false if we still need to process more items func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset int) bool { - if result.Offset < result.prevItems { + if result.prevItems < result.Offset { result.prevItems++ return false } @@ -230,6 +230,9 @@ func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset i result.NextRawOffset = rawOffset return true } + if len(result.Items) == 0 { + result.RawOffset = rawOffset + } result.Items = append(result.Items, item) return false } @@ -238,7 +241,7 @@ func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts) (*HistoryQuery if opts.MaxItems == 0 { return nil, fmt.Errorf("invalid query, maxitems is 0") } - rtn := &HistoryQueryResult{Offset: opts.RawOffset, MaxItems: opts.MaxItems} + rtn := &HistoryQueryResult{Offset: opts.Offset, MaxItems: opts.MaxItems} var rawOffset int if opts.RawOffset >= opts.Offset { rtn.prevItems = opts.Offset From ca527e6643b44f3dab7a47c0b66a9731a6917df3 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Mar 2023 15:53:08 -0800 Subject: [PATCH 282/397] bump to v0.1.6 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 51c9a8a9f..6f97c6bd8 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -31,7 +31,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.5" +const PromptVersion = "v0.1.6" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From c9d0aabfe56cf34ab5c361e14c4d383ff2a7b1ac Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Mar 2023 09:37:52 -0800 Subject: [PATCH 283/397] add crypto libs --- go.mod | 1 + go.sum | 11 +---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index b2e96554b..b075557cb 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.14 github.com/sawka/txwrap v0.1.0 github.com/scripthaus-dev/mshell v0.0.0 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/mod v0.5.1 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad mvdan.cc/sh/v3 v3.5.1 diff --git a/go.sum b/go.sum index e47f8fcbf..cdbe4311c 100644 --- a/go.sum +++ b/go.sum @@ -74,7 +74,6 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -333,7 +332,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= @@ -398,10 +396,8 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= @@ -582,7 +578,6 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -754,7 +749,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -913,7 +907,6 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -963,9 +956,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -1157,6 +1148,7 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1816,7 +1808,6 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= -mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From b2347f0b452cda8ba6a64e50a230e8ef5c02b827 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Mar 2023 09:49:13 -0800 Subject: [PATCH 284/397] commit shparse test --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/cmdrunner/linux-decls.txt | 1986 +++++++++++++++++++++++++++++++++ pkg/cmdrunner/shparse_test.go | 57 + 3 files changed, 2044 insertions(+), 1 deletion(-) create mode 100644 pkg/cmdrunner/linux-decls.txt create mode 100644 pkg/cmdrunner/shparse_test.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a5f4835f1..a63f6757c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2570,7 +2570,7 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15: %v", err) } if newFontSize < 8 || newFontSize > 15 { - return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15", err) + return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15") } feOpts := clientData.FeOpts feOpts.TermFontSize = newFontSize diff --git a/pkg/cmdrunner/linux-decls.txt b/pkg/cmdrunner/linux-decls.txt new file mode 100644 index 000000000..acb5c807c --- /dev/null +++ b/pkg/cmdrunner/linux-decls.txt @@ -0,0 +1,1986 @@ +__expand_tilde_by_ref () +{ + if [[ ${!1} == \~* ]]; then + eval $1=$(printf ~%q "${!1#\~}"); + fi +} +__get_cword_at_cursor_by_ref () +{ + local cword words=(); + __reassemble_comp_words_by_ref "$1" words cword; + local i cur index=$COMP_POINT lead=${COMP_LINE:0:$COMP_POINT}; + if [[ $index -gt 0 && ( -n $lead && -n ${lead//[[:space:]]} ) ]]; then + cur=$COMP_LINE; + for ((i = 0; i <= cword; ++i )) + do + while [[ ${#cur} -ge ${#words[i]} && "${cur:0:${#words[i]}}" != "${words[i]}" ]]; do + cur="${cur:1}"; + [[ $index -gt 0 ]] && ((index--)); + done; + if [[ $i -lt $cword ]]; then + local old_size=${#cur}; + cur="${cur#"${words[i]}"}"; + local new_size=${#cur}; + (( index -= old_size - new_size )); + fi; + done; + [[ -n $cur && ! -n ${cur//[[:space:]]} ]] && cur=; + [[ $index -lt 0 ]] && index=0; + fi; + local "$2" "$3" "$4" && _upvars -a${#words[@]} $2 "${words[@]}" -v $3 "$cword" -v $4 "${cur:0:$index}" +} +__git_eread () +{ + test -r "$1" && IFS=' +' read "$2" < "$1" +} +__git_ps1 () +{ + local exit=$?; + local pcmode=no; + local detached=no; + local ps1pc_start='\u@\h:\w '; + local ps1pc_end='\$ '; + local printf_format=' (%s)'; + case "$#" in + 2 | 3) + pcmode=yes; + ps1pc_start="$1"; + ps1pc_end="$2"; + printf_format="${3:-$printf_format}"; + PS1="$ps1pc_start$ps1pc_end" + ;; + 0 | 1) + printf_format="${1:-$printf_format}" + ;; + *) + return $exit + ;; + esac; + local ps1_expanded=yes; + [ -z "${ZSH_VERSION-}" ] || [[ -o PROMPT_SUBST ]] || ps1_expanded=no; + [ -z "${BASH_VERSION-}" ] || shopt -q promptvars || ps1_expanded=no; + local repo_info rev_parse_exit_code; + repo_info="$(git rev-parse --git-dir --is-inside-git-dir --is-bare-repository --is-inside-work-tree --short HEAD 2>/dev/null)"; + rev_parse_exit_code="$?"; + if [ -z "$repo_info" ]; then + return $exit; + fi; + local short_sha=""; + if [ "$rev_parse_exit_code" = "0" ]; then + short_sha="${repo_info##* +}"; + repo_info="${repo_info% +*}"; + fi; + local inside_worktree="${repo_info##* +}"; + repo_info="${repo_info% +*}"; + local bare_repo="${repo_info##* +}"; + repo_info="${repo_info% +*}"; + local inside_gitdir="${repo_info##* +}"; + local g="${repo_info% +*}"; + if [ "true" = "$inside_worktree" ] && [ -n "${GIT_PS1_HIDE_IF_PWD_IGNORED-}" ] && [ "$(git config --bool bash.hideIfPwdIgnored)" != "false" ] && git check-ignore -q .; then + return $exit; + fi; + local r=""; + local b=""; + local step=""; + local total=""; + if [ -d "$g/rebase-merge" ]; then + __git_eread "$g/rebase-merge/head-name" b; + __git_eread "$g/rebase-merge/msgnum" step; + __git_eread "$g/rebase-merge/end" total; + if [ -f "$g/rebase-merge/interactive" ]; then + r="|REBASE-i"; + else + r="|REBASE-m"; + fi; + else + if [ -d "$g/rebase-apply" ]; then + __git_eread "$g/rebase-apply/next" step; + __git_eread "$g/rebase-apply/last" total; + if [ -f "$g/rebase-apply/rebasing" ]; then + __git_eread "$g/rebase-apply/head-name" b; + r="|REBASE"; + else + if [ -f "$g/rebase-apply/applying" ]; then + r="|AM"; + else + r="|AM/REBASE"; + fi; + fi; + else + if [ -f "$g/MERGE_HEAD" ]; then + r="|MERGING"; + else + if __git_sequencer_status; then + :; + else + if [ -f "$g/BISECT_LOG" ]; then + r="|BISECTING"; + fi; + fi; + fi; + fi; + if [ -n "$b" ]; then + :; + else + if [ -h "$g/HEAD" ]; then + b="$(git symbolic-ref HEAD 2>/dev/null)"; + else + local head=""; + if ! __git_eread "$g/HEAD" head; then + return $exit; + fi; + b="${head#ref: }"; + if [ "$head" = "$b" ]; then + detached=yes; + b="$( + case "${GIT_PS1_DESCRIBE_STYLE-}" in + (contains) + git describe --contains HEAD ;; + (branch) + git describe --contains --all HEAD ;; + (tag) + git describe --tags HEAD ;; + (describe) + git describe HEAD ;; + (* | default) + git describe --tags --exact-match HEAD ;; + esac 2>/dev/null)" || b="$short_sha..."; + b="($b)"; + fi; + fi; + fi; + fi; + if [ -n "$step" ] && [ -n "$total" ]; then + r="$r $step/$total"; + fi; + local w=""; + local i=""; + local s=""; + local u=""; + local c=""; + local p=""; + if [ "true" = "$inside_gitdir" ]; then + if [ "true" = "$bare_repo" ]; then + c="BARE:"; + else + b="GIT_DIR!"; + fi; + else + if [ "true" = "$inside_worktree" ]; then + if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] && [ "$(git config --bool bash.showDirtyState)" != "false" ]; then + git diff --no-ext-diff --quiet || w="*"; + git diff --no-ext-diff --cached --quiet || i="+"; + if [ -z "$short_sha" ] && [ -z "$i" ]; then + i="#"; + fi; + fi; + if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] && git rev-parse --verify --quiet refs/stash > /dev/null; then + s="$"; + fi; + if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] && [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] && git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*' > /dev/null 2> /dev/null; then + u="%${ZSH_VERSION+%}"; + fi; + if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then + __git_ps1_show_upstream; + fi; + fi; + fi; + local z="${GIT_PS1_STATESEPARATOR-" "}"; + if [ $pcmode = yes ] && [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then + __git_ps1_colorize_gitstring; + fi; + b=${b##refs/heads/}; + if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then + __git_ps1_branch_name=$b; + b="\${__git_ps1_branch_name}"; + fi; + local f="$w$i$s$u"; + local gitstring="$c$b${f:+$z$f}$r$p"; + if [ $pcmode = yes ]; then + if [ "${__git_printf_supports_v-}" != yes ]; then + gitstring=$(printf -- "$printf_format" "$gitstring"); + else + printf -v gitstring -- "$printf_format" "$gitstring"; + fi; + PS1="$ps1pc_start$gitstring$ps1pc_end"; + else + printf -- "$printf_format" "$gitstring"; + fi; + return $exit +} +__git_ps1_colorize_gitstring () +{ + if [[ -n ${ZSH_VERSION-} ]]; then + local c_red='%F{red}'; + local c_green='%F{green}'; + local c_lblue='%F{blue}'; + local c_clear='%f'; + else + local c_red='\[\e[31m\]'; + local c_green='\[\e[32m\]'; + local c_lblue='\[\e[1;34m\]'; + local c_clear='\[\e[0m\]'; + fi; + local bad_color=$c_red; + local ok_color=$c_green; + local flags_color="$c_lblue"; + local branch_color=""; + if [ $detached = no ]; then + branch_color="$ok_color"; + else + branch_color="$bad_color"; + fi; + c="$branch_color$c"; + z="$c_clear$z"; + if [ "$w" = "*" ]; then + w="$bad_color$w"; + fi; + if [ -n "$i" ]; then + i="$ok_color$i"; + fi; + if [ -n "$s" ]; then + s="$flags_color$s"; + fi; + if [ -n "$u" ]; then + u="$bad_color$u"; + fi; + r="$c_clear$r" +} +__git_ps1_show_upstream () +{ + local key value; + local svn_remote svn_url_pattern count n; + local upstream=git legacy="" verbose="" name=""; + svn_remote=(); + local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"; + while read -r key value; do + case "$key" in + bash.showupstream) + GIT_PS1_SHOWUPSTREAM="$value"; + if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then + p=""; + return; + fi + ;; + svn-remote.*.url) + svn_remote[$((${#svn_remote[@]} + 1))]="$value"; + svn_url_pattern="$svn_url_pattern\\|$value"; + upstream=svn+git + ;; + esac; + done <<< "$output"; + for option in ${GIT_PS1_SHOWUPSTREAM}; + do + case "$option" in + git | svn) + upstream="$option" + ;; + verbose) + verbose=1 + ;; + legacy) + legacy=1 + ;; + name) + name=1 + ;; + esac; + done; + case "$upstream" in + git) + upstream="@{upstream}" + ;; + svn*) + local -a svn_upstream; + svn_upstream=($(git log --first-parent -1 --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null)); + if [[ 0 -ne ${#svn_upstream[@]} ]]; then + svn_upstream=${svn_upstream[${#svn_upstream[@]} - 2]}; + svn_upstream=${svn_upstream%@*}; + local n_stop="${#svn_remote[@]}"; + for ((n=1; n <= n_stop; n++)) + do + svn_upstream=${svn_upstream#${svn_remote[$n]}}; + done; + if [[ -z "$svn_upstream" ]]; then + upstream=${GIT_SVN_ID:-git-svn}; + else + upstream=${svn_upstream#/}; + fi; + else + if [[ "svn+git" = "$upstream" ]]; then + upstream="@{upstream}"; + fi; + fi + ;; + esac; + if [[ -z "$legacy" ]]; then + count="$(git rev-list --count --left-right "$upstream"...HEAD 2>/dev/null)"; + else + local commits; + if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"; then + local commit behind=0 ahead=0; + for commit in $commits; + do + case "$commit" in + "<"*) + ((behind++)) + ;; + *) + ((ahead++)) + ;; + esac; + done; + count="$behind $ahead"; + else + count=""; + fi; + fi; + if [[ -z "$verbose" ]]; then + case "$count" in + "") + p="" + ;; + "0 0") + p="=" + ;; + "0 "*) + p=">" + ;; + *" 0") + p="<" + ;; + *) + p="<>" + ;; + esac; + else + case "$count" in + "") + p="" + ;; + "0 0") + p=" u=" + ;; + "0 "*) + p=" u+${count#0 }" + ;; + *" 0") + p=" u-${count% 0}" + ;; + *) + p=" u+${count#* }-${count% *}" + ;; + esac; + if [[ -n "$count" && -n "$name" ]]; then + __git_ps1_upstream_name=$(git rev-parse --abbrev-ref "$upstream" 2>/dev/null); + if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then + p="$p \${__git_ps1_upstream_name}"; + else + p="$p ${__git_ps1_upstream_name}"; + unset __git_ps1_upstream_name; + fi; + fi; + fi +} +__git_sequencer_status () +{ + local todo; + if test -f "$g/CHERRY_PICK_HEAD"; then + r="|CHERRY-PICKING"; + return 0; + else + if test -f "$g/REVERT_HEAD"; then + r="|REVERTING"; + return 0; + else + if __git_eread "$g/sequencer/todo" todo; then + case "$todo" in + p[\ \ ] | pick[\ \ ]*) + r="|CHERRY-PICKING"; + return 0 + ;; + revert[\ \ ]*) + r="|REVERTING"; + return 0 + ;; + esac; + fi; + fi; + fi; + return 1 +} +__load_completion () +{ + local -a dirs=(${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions); + local OIFS=$IFS IFS=: dir cmd="${1##*/}" compfile; + [[ -n $cmd ]] || return 1; + for dir in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; + do + dirs+=($dir/bash-completion/completions); + done; + IFS=$OIFS; + if [[ $BASH_SOURCE == */* ]]; then + dirs+=("${BASH_SOURCE%/*}/completions"); + else + dirs+=(./completions); + fi; + for dir in "${dirs[@]}"; + do + [[ -d "$dir" ]] || continue; + for compfile in "$cmd" "$cmd.bash" "_$cmd"; + do + compfile="$dir/$compfile"; + [[ -f "$compfile" ]] && . "$compfile" &> /dev/null && return 0; + done; + done; + [[ -n "${_xspecs[$cmd]}" ]] && complete -F _filedir_xspec "$cmd" && return 0; + return 1 +} +__ltrim_colon_completions () +{ + if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then + local colon_word=${1%"${1##*:}"}; + local i=${#COMPREPLY[*]}; + while [[ $((--i)) -ge 0 ]]; do + COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}; + done; + fi +} +__parse_options () +{ + local option option2 i IFS=' +,/|'; + option=; + local -a array=($1); + for i in "${array[@]}"; + do + case "$i" in + ---*) + break + ;; + --?*) + option=$i; + break + ;; + -?*) + [[ -n $option ]] || option=$i + ;; + *) + break + ;; + esac; + done; + [[ -n $option ]] || return 0; + IFS=' +'; + if [[ $option =~ (\[((no|dont)-?)\]). ]]; then + option2=${option/"${BASH_REMATCH[1]}"/}; + option2=${option2%%[<{().[]*}; + printf '%s\n' "${option2/=*/=}"; + option=${option/"${BASH_REMATCH[1]}"/"${BASH_REMATCH[2]}"}; + fi; + option=${option%%[<{().[]*}; + printf '%s\n' "${option/=*/=}" +} +__reassemble_comp_words_by_ref () +{ + local exclude i j line ref; + if [[ -n $1 ]]; then + exclude="${1//[^$COMP_WORDBREAKS]}"; + fi; + printf -v "$3" %s "$COMP_CWORD"; + if [[ -n $exclude ]]; then + line=$COMP_LINE; + for ((i=0, j=0; i < ${#COMP_WORDS[@]}; i++, j++)) + do + while [[ $i -gt 0 && ${COMP_WORDS[$i]} == +([$exclude]) ]]; do + [[ $line != [[:blank:]]* ]] && (( j >= 2 )) && ((j--)); + ref="$2[$j]"; + printf -v "$ref" %s "${!ref}${COMP_WORDS[i]}"; + [[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"; + line=${line#*"${COMP_WORDS[$i]}"}; + [[ $line == [[:blank:]]* ]] && ((j++)); + (( $i < ${#COMP_WORDS[@]} - 1)) && ((i++)) || break 2; + done; + ref="$2[$j]"; + printf -v "$ref" %s "${!ref}${COMP_WORDS[i]}"; + line=${line#*"${COMP_WORDS[i]}"}; + [[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"; + done; + [[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"; + else + for i in "${!COMP_WORDS[@]}"; + do + printf -v "$2[i]" %s "${COMP_WORDS[i]}"; + done; + fi +} +_allowed_groups () +{ + if _complete_as_root; then + local IFS=' +'; + COMPREPLY=($(compgen -g -- "$1")); + else + local IFS=' + '; + COMPREPLY=($(compgen -W "$(id -Gn 2>/dev/null || groups 2>/dev/null)" -- "$1")); + fi +} +_allowed_users () +{ + if _complete_as_root; then + local IFS=' +'; + COMPREPLY=($(compgen -u -- "${1:-$cur}")); + else + local IFS=' + '; + COMPREPLY=($(compgen -W "$(id -un 2>/dev/null || whoami 2>/dev/null)" -- "${1:-$cur}")); + fi +} +_apport-bug () +{ + local cur dashoptions prev param; + COMPREPLY=(); + cur=`_get_cword`; + prev=${COMP_WORDS[COMP_CWORD-1]}; + dashoptions='-h --help --save -v --version --tag -w --window'; + case "$prev" in + ubuntu-bug | apport-bug) + case "$cur" in + -*) + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + *) + _apport_parameterless + ;; + esac + ;; + --save) + COMPREPLY=($( compgen -o default -G "$cur*" )) + ;; + -w | --window) + dashoptions="--save --tag"; + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + -h | --help | -v | --version | --tag) + return 0 + ;; + *) + dashoptions="--tag"; + if ! [[ "${COMP_WORDS[*]}" =~ .*--save.* ]]; then + dashoptions="--save $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--window.* || "${COMP_WORDS[*]}" =~ .*\ -w\ .* ]]; then + dashoptions="-w --window $dashoptions"; + fi; + case "$cur" in + -*) + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + *) + _apport_parameterless + ;; + esac + ;; + esac +} +_apport-cli () +{ + local cur dashoptions prev param; + COMPREPLY=(); + cur=`_get_cword`; + prev=${COMP_WORDS[COMP_CWORD-1]}; + dashoptions='-h --help -f --file-bug -u --update-bug -s --symptom \ + -c --crash-file --save -v --version --tag -w --window'; + case "$prev" in + apport-cli) + case "$cur" in + -*) + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + *) + _apport_parameterless + ;; + esac + ;; + -f | --file-bug) + param="-P --pid -p --package -s --symptom"; + COMPREPLY=($( compgen -W "$param $(_apport_symptoms)" -- $cur)) + ;; + -s | --symptom) + COMPREPLY=($( compgen -W "$(_apport_symptoms)" -- $cur)) + ;; + --save) + COMPREPLY=($( compgen -o default -G "$cur*" )) + ;; + -c | --crash-file) + COMPREPLY=($( compgen -G "${cur}*.apport" + compgen -G "${cur}*.crash" )) + ;; + -w | --window) + dashoptions="--save --tag"; + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + -h | --help | -v | --version | --tag) + return 0 + ;; + *) + dashoptions='--tag'; + if ! [[ "${COMP_WORDS[*]}" =~ .*--save.* ]]; then + dashoptions="--save $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--window.* || "${COMP_WORDS[*]}" =~ .*\ -w\ .* ]]; then + dashoptions="-w --window $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--symptom.* || "${COMP_WORDS[*]}" =~ .*\ -s\ .* ]]; then + dashoptions="-s --symptom $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--update.* || "${COMP_WORDS[*]}" =~ .*\ -u\ .* ]]; then + dashoptions="-u --update $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--file-bug.* || "${COMP_WORDS[*]}" =~ .*\ -f\ .* ]]; then + dashoptions="-f --file-bug $dashoptions"; + fi; + if ! [[ "${COMP_WORDS[*]}" =~ .*--crash-file.* || "${COMP_WORDS[*]}" =~ .*\ -c\ .* ]]; then + dashoptions="-c --crash-file $dashoptions"; + fi; + case "$cur" in + -*) + COMPREPLY=($( compgen -W "$dashoptions" -- $cur )) + ;; + *) + _apport_parameterless + ;; + esac + ;; + esac +} +_apport-collect () +{ + local cur prev; + COMPREPLY=(); + cur=`_get_cword`; + prev=${COMP_WORDS[COMP_CWORD-1]}; + case "$prev" in + apport-collect) + COMPREPLY=($( compgen -W "-p --package --tag" -- $cur)) + ;; + -p | --package) + COMPREPLY=($( apt-cache pkgnames $cur 2> /dev/null )) + ;; + --tag) + return 0 + ;; + *) + if [[ "${COMP_WORDS[*]}" =~ .*\ -p.* || "${COMP_WORDS[*]}" =~ .*--package.* ]]; then + COMPREPLY=($( compgen -W "--tag" -- $cur)); + else + COMPREPLY=($( compgen -W "-p --package --tag" -- $cur)); + fi + ;; + esac +} +_apport-unpack () +{ + local cur prev; + COMPREPLY=(); + cur=`_get_cword`; + prev=${COMP_WORDS[COMP_CWORD-1]}; + case "$prev" in + apport-unpack) + COMPREPLY=($( compgen -G "${cur}*.apport" + compgen -G "${cur}*.crash" )) + ;; + esac +} +_apport_parameterless () +{ + local param; + param="$dashoptions $( apt-cache pkgnames $cur 2> /dev/null ) $( command ps axo pid | sed 1d ) $( _apport_symptoms ) $( compgen -G "${cur}*" )"; + COMPREPLY=($( compgen -W "$param" -- $cur)) +} +_apport_symptoms () +{ + local syms; + if [ -r /usr/share/apport/symptoms ]; then + for FILE in $(ls /usr/share/apport/symptoms); + do + if [[ ! "$FILE" =~ ^_.* && -n $(egrep "^def run\s*\(.*\):" /usr/share/apport/symptoms/$FILE) ]]; then + syms="$syms ${FILE%.py}"; + fi; + done; + fi; + echo $syms +} +_available_interfaces () +{ + local PATH=$PATH:/sbin; + COMPREPLY=($({ + if [[ ${1:-} == -w ]]; then + iwconfig + elif [[ ${1:-} == -a ]]; then + ifconfig || ip link show up + else + ifconfig -a || ip link show + fi + } 2>/dev/null | awk '/^[^ \t]/ { if ($1 ~ /^[0-9]+:/) { print $2 } else { print $1 } }')); + COMPREPLY=($(compgen -W '${COMPREPLY[@]/%[[:punct:]]/}' -- "$cur")) +} +_cd () +{ + local cur prev words cword; + _init_completion || return; + local IFS=' +' i j k; + compopt -o filenames; + if [[ -z "${CDPATH:-}" || "$cur" == ?(.)?(.)/* ]]; then + _filedir -d; + return; + fi; + local -r mark_dirs=$(_rl_enabled mark-directories && echo y); + local -r mark_symdirs=$(_rl_enabled mark-symlinked-directories && echo y); + for i in ${CDPATH//:/' +'}; + do + k="${#COMPREPLY[@]}"; + for j in $(compgen -d -- $i/$cur); + do + if [[ ( -n $mark_symdirs && -h $j || -n $mark_dirs && ! -h $j ) && ! -d ${j#$i/} ]]; then + j+="/"; + fi; + COMPREPLY[k++]=${j#$i/}; + done; + done; + _filedir -d; + if [[ ${#COMPREPLY[@]} -eq 1 ]]; then + i=${COMPREPLY[0]}; + if [[ "$i" == "$cur" && $i != "*/" ]]; then + COMPREPLY[0]="${i}/"; + fi; + fi; + return +} +_cd_devices () +{ + COMPREPLY+=($(compgen -f -d -X "!*/?([amrs])cd*" -- "${cur:-/dev/}")) +} +_command () +{ + local offset i; + offset=1; + for ((i=1; i <= COMP_CWORD; i++ )) + do + if [[ "${COMP_WORDS[i]}" != -* ]]; then + offset=$i; + break; + fi; + done; + _command_offset $offset +} +_command_offset () +{ + local word_offset=$1 i j; + for ((i=0; i < $word_offset; i++ )) + do + for ((j=0; j <= ${#COMP_LINE}; j++ )) + do + [[ "$COMP_LINE" == "${COMP_WORDS[i]}"* ]] && break; + COMP_LINE=${COMP_LINE:1}; + ((COMP_POINT--)); + done; + COMP_LINE=${COMP_LINE#"${COMP_WORDS[i]}"}; + ((COMP_POINT-=${#COMP_WORDS[i]})); + done; + for ((i=0; i <= COMP_CWORD - $word_offset; i++ )) + do + COMP_WORDS[i]=${COMP_WORDS[i+$word_offset]}; + done; + for ((i; i <= COMP_CWORD; i++ )) + do + unset 'COMP_WORDS[i]'; + done; + ((COMP_CWORD -= $word_offset)); + COMPREPLY=(); + local cur; + _get_comp_words_by_ref cur; + if [[ $COMP_CWORD -eq 0 ]]; then + local IFS=' +'; + compopt -o filenames; + COMPREPLY=($(compgen -d -c -- "$cur")); + else + local cmd=${COMP_WORDS[0]} compcmd=${COMP_WORDS[0]}; + local cspec=$(complete -p $cmd 2>/dev/null); + if [[ ! -n $cspec && $cmd == */* ]]; then + cspec=$(complete -p ${cmd##*/} 2>/dev/null); + [[ -n $cspec ]] && compcmd=${cmd##*/}; + fi; + if [[ ! -n $cspec ]]; then + compcmd=${cmd##*/}; + _completion_loader $compcmd; + cspec=$(complete -p $compcmd 2>/dev/null); + fi; + if [[ -n $cspec ]]; then + if [[ ${cspec#* -F } != $cspec ]]; then + local func=${cspec#*-F }; + func=${func%% *}; + if [[ ${#COMP_WORDS[@]} -ge 2 ]]; then + $func $cmd "${COMP_WORDS[${#COMP_WORDS[@]}-1]}" "${COMP_WORDS[${#COMP_WORDS[@]}-2]}"; + else + $func $cmd "${COMP_WORDS[${#COMP_WORDS[@]}-1]}"; + fi; + local opt; + while [[ $cspec == *" -o "* ]]; do + cspec=${cspec#*-o }; + opt=${cspec%% *}; + compopt -o $opt; + cspec=${cspec#$opt}; + done; + else + cspec=${cspec#complete}; + cspec=${cspec%%$compcmd}; + COMPREPLY=($(eval compgen "$cspec" -- '$cur')); + fi; + else + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + _minimal; + fi; + fi; + fi +} +_complete_as_root () +{ + [[ $EUID -eq 0 || -n ${root_command:-} ]] +} +_completion_loader () +{ + local cmd="${1:-_EmptycmD_}"; + __load_completion "$cmd" && return 124; + complete -F _minimal -- "$cmd" && return 124 +} +_configured_interfaces () +{ + if [[ -f /etc/debian_version ]]; then + COMPREPLY=($(compgen -W "$(command sed -ne 's|^iface \([^ ]\{1,\}\).*$|\1|p' /etc/network/interfaces /etc/network/interfaces.d/* 2>/dev/null)" -- "$cur")); + else + if [[ -f /etc/SuSE-release ]]; then + COMPREPLY=($(compgen -W "$(printf '%s\n' /etc/sysconfig/network/ifcfg-* | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")); + else + if [[ -f /etc/pld-release ]]; then + COMPREPLY=($(compgen -W "$(command ls -B /etc/sysconfig/interfaces | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")); + else + COMPREPLY=($(compgen -W "$(printf '%s\n' /etc/sysconfig/network-scripts/ifcfg-* | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")); + fi; + fi; + fi +} +_count_args () +{ + local i cword words; + __reassemble_comp_words_by_ref "$1" words cword; + args=1; + for ((i=1; i < cword; i++ )) + do + if [[ ${words[i]} != -* && ${words[i-1]} != $2 || ${words[i]} == $3 ]]; then + (( args++ )); + fi; + done +} +_dvd_devices () +{ + COMPREPLY+=($(compgen -f -d -X "!*/?(r)dvd*" -- "${cur:-/dev/}")) +} +_expand () +{ + if [[ "$cur" == \~*/* ]]; then + __expand_tilde_by_ref cur; + else + if [[ "$cur" == \~* ]]; then + _tilde "$cur" || eval COMPREPLY[0]=$(printf ~%q "${COMPREPLY[0]#\~}"); + return ${#COMPREPLY[@]}; + fi; + fi +} +_filedir () +{ + local IFS=' +'; + _tilde "$cur" || return; + local -a toks; + local reset; + if [[ "$1" == -d ]]; then + reset=$(shopt -po noglob); + set -o noglob; + toks=($(compgen -d -- "$cur")); + IFS=' '; + $reset; + IFS=' +'; + else + local quoted; + _quote_readline_by_ref "$cur" quoted; + local xspec=${1:+"!*.@($1|${1^^})"} plusdirs=(); + local opts=(-f -X "$xspec"); + [[ -n $xspec ]] && plusdirs=(-o plusdirs); + [[ -n ${COMP_FILEDIR_FALLBACK-} ]] || opts+=("${plusdirs[@]}"); + reset=$(shopt -po noglob); + set -o noglob; + toks+=($(compgen "${opts[@]}" -- $quoted)); + IFS=' '; + $reset; + IFS=' +'; + [[ -n ${COMP_FILEDIR_FALLBACK:-} && -n "$1" && ${#toks[@]} -lt 1 ]] && { + reset=$(shopt -po noglob); + set -o noglob; + toks+=($(compgen -f "${plusdirs[@]}" -- $quoted)); + IFS=' '; + $reset; + IFS=' +' + }; + fi; + if [[ ${#toks[@]} -ne 0 ]]; then + compopt -o filenames 2> /dev/null; + COMPREPLY+=("${toks[@]}"); + fi +} +_filedir_xspec () +{ + local cur prev words cword; + _init_completion || return; + _tilde "$cur" || return; + local IFS=' +' xspec=${_xspecs[${1##*/}]} tmp; + local -a toks; + toks=($( + compgen -d -- "$(quote_readline "$cur")" | { + while read -r tmp; do + printf '%s\n' $tmp + done + } + )); + eval xspec="${xspec}"; + local matchop=!; + if [[ $xspec == !* ]]; then + xspec=${xspec#!}; + matchop=@; + fi; + xspec="$matchop($xspec|${xspec^^})"; + toks+=($( + eval compgen -f -X "'!$xspec'" -- "\$(quote_readline "\$cur")" | { + while read -r tmp; do + [[ -n $tmp ]] && printf '%s\n' $tmp + done + } + )); + [[ -n ${COMP_FILEDIR_FALLBACK:-} && ${#toks[@]} -lt 1 ]] && { + local reset=$(shopt -po noglob); + set -o noglob; + toks+=($(compgen -f -- "$(quote_readline "$cur")")); + IFS=' '; + $reset; + IFS=' +' + }; + if [[ ${#toks[@]} -ne 0 ]]; then + compopt -o filenames; + COMPREPLY=("${toks[@]}"); + fi +} +_fstypes () +{ + local fss; + if [[ -e /proc/filesystems ]]; then + fss="$(cut -d' ' -f2 /proc/filesystems) + $(awk '! /\*/ { print $NF }' /etc/filesystems 2>/dev/null)"; + else + fss="$(awk '/^[ \t]*[^#]/ { print $3 }' /etc/fstab 2>/dev/null) + $(awk '/^[ \t]*[^#]/ { print $3 }' /etc/mnttab 2>/dev/null) + $(awk '/^[ \t]*[^#]/ { print $4 }' /etc/vfstab 2>/dev/null) + $(awk '{ print $1 }' /etc/dfs/fstypes 2>/dev/null) + $([[ -d /etc/fs ]] && command ls /etc/fs)"; + fi; + [[ -n $fss ]] && COMPREPLY+=($(compgen -W "$fss" -- "$cur")) +} +_get_comp_words_by_ref () +{ + local exclude flag i OPTIND=1; + local cur cword words=(); + local upargs=() upvars=() vcur vcword vprev vwords; + while getopts "c:i:n:p:w:" flag "$@"; do + case $flag in + c) + vcur=$OPTARG + ;; + i) + vcword=$OPTARG + ;; + n) + exclude=$OPTARG + ;; + p) + vprev=$OPTARG + ;; + w) + vwords=$OPTARG + ;; + esac; + done; + while [[ $# -ge $OPTIND ]]; do + case ${!OPTIND} in + cur) + vcur=cur + ;; + prev) + vprev=prev + ;; + cword) + vcword=cword + ;; + words) + vwords=words + ;; + *) + echo "bash_completion: $FUNCNAME: \`${!OPTIND}':" "unknown argument" 1>&2; + return 1 + ;; + esac; + (( OPTIND += 1 )); + done; + __get_cword_at_cursor_by_ref "$exclude" words cword cur; + [[ -n $vcur ]] && { + upvars+=("$vcur"); + upargs+=(-v $vcur "$cur") + }; + [[ -n $vcword ]] && { + upvars+=("$vcword"); + upargs+=(-v $vcword "$cword") + }; + [[ -n $vprev && $cword -ge 1 ]] && { + upvars+=("$vprev"); + upargs+=(-v $vprev "${words[cword - 1]}") + }; + [[ -n $vwords ]] && { + upvars+=("$vwords"); + upargs+=(-a${#words[@]} $vwords "${words[@]}") + }; + (( ${#upvars[@]} )) && local "${upvars[@]}" && _upvars "${upargs[@]}" +} +_get_cword () +{ + local LC_CTYPE=C; + local cword words; + __reassemble_comp_words_by_ref "$1" words cword; + if [[ -n ${2//[^0-9]/} ]]; then + printf "%s" "${words[cword-$2]}"; + else + if [[ "${#words[cword]}" -eq 0 || "$COMP_POINT" == "${#COMP_LINE}" ]]; then + printf "%s" "${words[cword]}"; + else + local i; + local cur="$COMP_LINE"; + local index="$COMP_POINT"; + for ((i = 0; i <= cword; ++i )) + do + while [[ "${#cur}" -ge ${#words[i]} && "${cur:0:${#words[i]}}" != "${words[i]}" ]]; do + cur="${cur:1}"; + [[ $index -gt 0 ]] && ((index--)); + done; + if [[ "$i" -lt "$cword" ]]; then + local old_size="${#cur}"; + cur="${cur#${words[i]}}"; + local new_size="${#cur}"; + (( index -= old_size - new_size )); + fi; + done; + if [[ "${words[cword]:0:${#cur}}" != "$cur" ]]; then + printf "%s" "${words[cword]}"; + else + printf "%s" "${cur:0:$index}"; + fi; + fi; + fi +} +_get_first_arg () +{ + local i; + arg=; + for ((i=1; i < COMP_CWORD; i++ )) + do + if [[ "${COMP_WORDS[i]}" != -* ]]; then + arg=${COMP_WORDS[i]}; + break; + fi; + done +} +_get_pword () +{ + if [[ $COMP_CWORD -ge 1 ]]; then + _get_cword "${@:-}" 1; + fi +} +_gids () +{ + if type getent &> /dev/null; then + COMPREPLY=($(compgen -W '$(getent group | cut -d: -f3)' -- "$cur")); + else + if type perl &> /dev/null; then + COMPREPLY=($(compgen -W '$(perl -e '"'"'while (($gid) = (getgrent)[2]) { print $gid . "\n" }'"'"')' -- "$cur")); + else + COMPREPLY=($(compgen -W '$(cut -d: -f3 /etc/group)' -- "$cur")); + fi; + fi +} +_have () +{ + PATH=$PATH:/usr/sbin:/sbin:/usr/local/sbin type $1 &> /dev/null +} +_included_ssh_config_files () +{ + [[ $# -lt 1 ]] && echo "bash_completion: $FUNCNAME: missing mandatory argument CONFIG" 1>&2; + local configfile i f; + configfile=$1; + local included=($(command sed -ne 's/^[[:blank:]]*[Ii][Nn][Cc][Ll][Uu][Dd][Ee][[:blank:]]\{1,\}\([^#%]*\)\(#.*\)\{0,1\}$/\1/p' "${configfile}")); + for i in "${included[@]}"; + do + if ! [[ "$i" =~ ^\~.*|^\/.* ]]; then + if [[ "$configfile" =~ ^\/etc\/ssh.* ]]; then + i="/etc/ssh/$i"; + else + i="$HOME/.ssh/$i"; + fi; + fi; + __expand_tilde_by_ref i; + for f in ${i}; + do + if [ -r $f ]; then + config+=("$f"); + _included_ssh_config_files $f; + fi; + done; + done +} +_init_completion () +{ + local exclude="" flag outx errx inx OPTIND=1; + while getopts "n:e:o:i:s" flag "$@"; do + case $flag in + n) + exclude+=$OPTARG + ;; + e) + errx=$OPTARG + ;; + o) + outx=$OPTARG + ;; + i) + inx=$OPTARG + ;; + s) + split=false; + exclude+== + ;; + esac; + done; + COMPREPLY=(); + local redir="@(?([0-9])<|?([0-9&])>?(>)|>&)"; + _get_comp_words_by_ref -n "$exclude<>&" cur prev words cword; + _variables && return 1; + if [[ $cur == $redir* || $prev == $redir ]]; then + local xspec; + case $cur in + 2'>'*) + xspec=$errx + ;; + *'>'*) + xspec=$outx + ;; + *'<'*) + xspec=$inx + ;; + *) + case $prev in + 2'>'*) + xspec=$errx + ;; + *'>'*) + xspec=$outx + ;; + *'<'*) + xspec=$inx + ;; + esac + ;; + esac; + cur="${cur##$redir}"; + _filedir $xspec; + return 1; + fi; + local i skip; + for ((i=1; i < ${#words[@]}; 1)) + do + if [[ ${words[i]} == $redir* ]]; then + [[ ${words[i]} == $redir ]] && skip=2 || skip=1; + words=("${words[@]:0:i}" "${words[@]:i+skip}"); + [[ $i -le $cword ]] && (( cword -= skip )); + else + (( i++ )); + fi; + done; + [[ $cword -le 0 ]] && return 1; + prev=${words[cword-1]}; + [[ -n ${split-} ]] && _split_longopt && split=true; + return 0 +} +_installed_modules () +{ + COMPREPLY=($(compgen -W "$(PATH="$PATH:/sbin" lsmod | awk '{if (NR != 1) print $1}')" -- "$1")) +} +_ip_addresses () +{ + local n; + case $1 in + -a) + n='6\?' + ;; + -6) + n='6' + ;; + esac; + local PATH=$PATH:/sbin; + local addrs=$({ LC_ALL=C ifconfig -a || ip addr show; } 2>/dev/null | + command sed -e 's/[[:space:]]addr:/ /' -ne "s|.*inet${n}[[:space:]]\{1,\}\([^[:space:]/]*\).*|\1|p"); + COMPREPLY+=($(compgen -W "$addrs" -- "$cur")) +} +_kernel_versions () +{ + COMPREPLY=($(compgen -W '$(command ls /lib/modules)' -- "$cur")) +} +_known_hosts () +{ + local cur prev words cword; + _init_completion -n : || return; + local options; + [[ "$1" == -a || "$2" == -a ]] && options=-a; + [[ "$1" == -c || "$2" == -c ]] && options+=" -c"; + _known_hosts_real $options -- "$cur" +} +_known_hosts_real () +{ + local configfile flag prefix OIFS=$IFS; + local cur user suffix aliases i host ipv4 ipv6; + local -a kh tmpkh khd config; + local OPTIND=1; + while getopts "ac46F:p:" flag "$@"; do + case $flag in + a) + aliases='yes' + ;; + c) + suffix=':' + ;; + F) + configfile=$OPTARG + ;; + p) + prefix=$OPTARG + ;; + 4) + ipv4=1 + ;; + 6) + ipv6=1 + ;; + esac; + done; + [[ $# -lt $OPTIND ]] && echo "bash_completion: $FUNCNAME: missing mandatory argument CWORD" 1>&2; + cur=${!OPTIND}; + (( OPTIND += 1 )); + [[ $# -ge $OPTIND ]] && echo "bash_completion: $FUNCNAME($*): unprocessed arguments:" $(while [[ $# -ge $OPTIND ]]; do printf '%s\n' ${!OPTIND}; shift; done) 1>&2; + [[ $cur == *@* ]] && user=${cur%@*}@ && cur=${cur#*@}; + kh=(); + if [[ -n $configfile ]]; then + [[ -r $configfile ]] && config+=("$configfile"); + else + for i in /etc/ssh/ssh_config ~/.ssh/config ~/.ssh2/config; + do + [[ -r $i ]] && config+=("$i"); + done; + fi; + for i in "${config[@]}"; + do + _included_ssh_config_files "$i"; + done; + if [[ ${#config[@]} -gt 0 ]]; then + local IFS=' +' j; + tmpkh=($(awk 'sub("^[ \t]*([Gg][Ll][Oo][Bb][Aa][Ll]|[Uu][Ss][Ee][Rr])[Kk][Nn][Oo][Ww][Nn][Hh][Oo][Ss][Tt][Ss][Ff][Ii][Ll][Ee][ \t]+", "") { print $0 }' "${config[@]}" | sort -u)); + IFS=$OIFS; + for i in "${tmpkh[@]}"; + do + while [[ $i =~ ^([^\"]*)\"([^\"]*)\"(.*)$ ]]; do + i=${BASH_REMATCH[1]}${BASH_REMATCH[3]}; + j=${BASH_REMATCH[2]}; + __expand_tilde_by_ref j; + [[ -r $j ]] && kh+=("$j"); + done; + for j in $i; + do + __expand_tilde_by_ref j; + [[ -r $j ]] && kh+=("$j"); + done; + done; + fi; + if [[ -z $configfile ]]; then + for i in /etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2 /etc/known_hosts /etc/known_hosts2 ~/.ssh/known_hosts ~/.ssh/known_hosts2; + do + [[ -r $i ]] && kh+=("$i"); + done; + for i in /etc/ssh2/knownhosts ~/.ssh2/hostkeys; + do + [[ -d $i ]] && khd+=("$i"/*pub); + done; + fi; + if [[ ${#kh[@]} -gt 0 || ${#khd[@]} -gt 0 ]]; then + if [[ ${#kh[@]} -gt 0 ]]; then + for i in "${kh[@]}"; + do + while read -ra tmpkh; do + set -- "${tmpkh[@]}"; + [[ $1 == [\|\#]* ]] && continue; + [[ $1 == @* ]] && shift; + local IFS=,; + for host in $1; + do + [[ $host == *[*?]* ]] && continue; + host="${host#[}"; + host="${host%]?(:+([0-9]))}"; + COMPREPLY+=($host); + done; + IFS=$OIFS; + done < "$i"; + done; + COMPREPLY=($(compgen -W '${COMPREPLY[@]}' -- "$cur")); + fi; + if [[ ${#khd[@]} -gt 0 ]]; then + for i in "${khd[@]}"; + do + if [[ "$i" == *key_22_$cur*.pub && -r "$i" ]]; then + host=${i/#*key_22_/}; + host=${host/%.pub/}; + COMPREPLY+=($host); + fi; + done; + fi; + for ((i=0; i < ${#COMPREPLY[@]}; i++ )) + do + COMPREPLY[i]=$prefix$user${COMPREPLY[i]}$suffix; + done; + fi; + if [[ ${#config[@]} -gt 0 && -n "$aliases" ]]; then + local hosts=$(command sed -ne 's/^[[:blank:]]*[Hh][Oo][Ss][Tt][[:blank:]]\{1,\}\([^#*?%]*\)\(#.*\)\{0,1\}$/\1/p' "${config[@]}"); + COMPREPLY+=($(compgen -P "$prefix$user" -S "$suffix" -W "$hosts" -- "$cur")); + fi; + if [[ -n ${COMP_KNOWN_HOSTS_WITH_AVAHI:-} ]] && type avahi-browse &> /dev/null; then + COMPREPLY+=($(compgen -P "$prefix$user" -S "$suffix" -W "$(avahi-browse -cpr _workstation._tcp 2>/dev/null | awk -F';' '/^=/ { print $7 }' | sort -u)" -- "$cur")); + fi; + COMPREPLY+=($(compgen -W "$(ruptime 2>/dev/null | awk '!/^ruptime:/ { print $1 }')" -- "$cur")); + if [[ -n ${COMP_KNOWN_HOSTS_WITH_HOSTFILE-1} ]]; then + COMPREPLY+=($(compgen -A hostname -P "$prefix$user" -S "$suffix" -- "$cur")); + fi; + if [[ -n $ipv4 ]]; then + COMPREPLY=("${COMPREPLY[@]/*:*$suffix/}"); + fi; + if [[ -n $ipv6 ]]; then + COMPREPLY=("${COMPREPLY[@]/+([0-9]).+([0-9]).+([0-9]).+([0-9])$suffix/}"); + fi; + if [[ -n $ipv4 || -n $ipv6 ]]; then + for i in "${!COMPREPLY[@]}"; + do + [[ -n ${COMPREPLY[i]} ]] || unset -v COMPREPLY[i]; + done; + fi; + __ltrim_colon_completions "$prefix$user$cur" +} +_longopt () +{ + local cur prev words cword split; + _init_completion -s || return; + case "${prev,,}" in + --help | --usage | --version) + return + ;; + --!(no-*)dir*) + _filedir -d; + return + ;; + --!(no-*)@(file|path)*) + _filedir; + return + ;; + --+([-a-z0-9_])) + local argtype=$(LC_ALL=C $1 --help 2>&1 | command sed -ne "s|.*$prev\[\{0,1\}=[<[]\{0,1\}\([-A-Za-z0-9_]\{1,\}\).*|\1|p"); + case ${argtype,,} in + *dir*) + _filedir -d; + return + ;; + *file* | *path*) + _filedir; + return + ;; + esac + ;; + esac; + $split && return; + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "$(LC_ALL=C $1 --help 2>&1 | while read -r line; do [[ $line =~ --[-A-Za-z0-9]+=? ]] && printf '%s\n' ${BASH_REMATCH[0]} + done)" -- "$cur")); + [[ $COMPREPLY == *= ]] && compopt -o nospace; + else + if [[ "$1" == *@(rmdir|chroot) ]]; then + _filedir -d; + else + [[ "$1" == *mkdir ]] && compopt -o nospace; + _filedir; + fi; + fi +} +_mac_addresses () +{ + local re='\([A-Fa-f0-9]\{2\}:\)\{5\}[A-Fa-f0-9]\{2\}'; + local PATH="$PATH:/sbin:/usr/sbin"; + COMPREPLY+=($( { LC_ALL=C ifconfig -a || ip link show; } 2>/dev/null | command sed -ne "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]].*/\1/p" -ne "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]]*$/\1/p" -ne "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]].*|\2|p" -ne "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]]*$|\2|p" + )); + COMPREPLY+=($({ arp -an || ip neigh show; } 2>/dev/null | command sed -ne "s/.*[[:space:]]\($re\)[[:space:]].*/\1/p" -ne "s/.*[[:space:]]\($re\)[[:space:]]*$/\1/p")); + COMPREPLY+=($(command sed -ne "s/^[[:space:]]*\($re\)[[:space:]].*/\1/p" /etc/ethers 2>/dev/null)); + COMPREPLY=($(compgen -W '${COMPREPLY[@]}' -- "$cur")); + __ltrim_colon_completions "$cur" +} +_minimal () +{ + local cur prev words cword split; + _init_completion -s || return; + $split && return; + _filedir +} +_modules () +{ + local modpath; + modpath=/lib/modules/$1; + COMPREPLY=($(compgen -W "$(command ls -RL $modpath 2>/dev/null | command sed -ne 's/^\(.*\)\.k\{0,1\}o\(\.[gx]z\)\{0,1\}$/\1/p')" -- "$cur")) +} +_ncpus () +{ + local var=NPROCESSORS_ONLN; + [[ $OSTYPE == *linux* ]] && var=_$var; + local n=$(getconf $var 2>/dev/null); + printf %s ${n:-1} +} +_parse_help () +{ + eval local cmd=$(quote "$1"); + local line; + { + case $cmd in + -) + cat + ;; + *) + LC_ALL=C "$(dequote "$cmd")" ${2:---help} 2>&1 + ;; + esac + } | while read -r line; do + [[ $line == *([[:blank:]])-* ]] || continue; + while [[ $line =~ ((^|[^-])-[A-Za-z0-9?][[:space:]]+)\[?[A-Z0-9]+([,_-]+[A-Z0-9]+)?(\.\.+)?\]? ]]; do + line=${line/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}"}; + done; + __parse_options "${line// or /, }"; + done +} +_parse_usage () +{ + eval local cmd=$(quote "$1"); + local line match option i char; + { + case $cmd in + -) + cat + ;; + *) + LC_ALL=C "$(dequote "$cmd")" ${2:---usage} 2>&1 + ;; + esac + } | while read -r line; do + while [[ $line =~ \[[[:space:]]*(-[^]]+)[[:space:]]*\] ]]; do + match=${BASH_REMATCH[0]}; + option=${BASH_REMATCH[1]}; + case $option in + -?(\[)+([a-zA-Z0-9?])) + for ((i=1; i < ${#option}; i++ )) + do + char=${option:i:1}; + [[ $char != '[' ]] && printf '%s\n' -$char; + done + ;; + *) + __parse_options "$option" + ;; + esac; + line=${line#*"$match"}; + done; + done +} +_pci_ids () +{ + COMPREPLY+=($(compgen -W "$(PATH="$PATH:/sbin" lspci -n | awk '{print $3}')" -- "$cur")) +} +_pgids () +{ + COMPREPLY=($(compgen -W '$(command ps axo pgid=)' -- "$cur")) +} +_pids () +{ + COMPREPLY=($(compgen -W '$(command ps axo pid=)' -- "$cur")) +} +_pnames () +{ + local -a procs; + if [[ "$1" == -s ]]; then + procs=($(command ps axo comm | command sed -e 1d)); + else + local line i=-1 OIFS=$IFS; + IFS=' +'; + local -a psout=($(command ps axo command=)); + IFS=$OIFS; + for line in "${psout[@]}"; + do + if [[ $i -eq -1 ]]; then + if [[ $line =~ ^(.*[[:space:]])COMMAND([[:space:]]|$) ]]; then + i=${#BASH_REMATCH[1]}; + else + break; + fi; + else + line=${line:$i}; + line=${line%% *}; + procs+=($line); + fi; + done; + if [[ $i -eq -1 ]]; then + for line in "${psout[@]}"; + do + if [[ $line =~ ^[[(](.+)[])]$ ]]; then + procs+=(${BASH_REMATCH[1]}); + else + line=${line%% *}; + line=${line##@(*/|-)}; + procs+=($line); + fi; + done; + fi; + fi; + COMPREPLY=($(compgen -X "" -W '${procs[@]}' -- "$cur" )) +} +_quote_readline_by_ref () +{ + if [ -z "$1" ]; then + printf -v $2 %s "$1"; + else + if [[ $1 == \'* ]]; then + printf -v $2 %s "${1:1}"; + else + if [[ $1 == \~* ]]; then + printf -v $2 \~%q "${1:1}"; + else + printf -v $2 %q "$1"; + fi; + fi; + fi; + [[ ${!2} == \$* ]] && eval $2=${!2} +} +_realcommand () +{ + type -P "$1" > /dev/null && { + if type -p realpath > /dev/null; then + realpath "$(type -P "$1")"; + else + if type -p greadlink > /dev/null; then + greadlink -f "$(type -P "$1")"; + else + if type -p readlink > /dev/null; then + readlink -f "$(type -P "$1")"; + else + type -P "$1"; + fi; + fi; + fi + } +} +_rl_enabled () +{ + [[ "$(bind -v)" == *$1+([[:space:]])on* ]] +} +_root_command () +{ + local PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin; + local root_command=$1; + _command +} +_service () +{ + local cur prev words cword; + _init_completion || return; + [[ $cword -gt 2 ]] && return; + if [[ $cword -eq 1 && $prev == ?(*/)service ]]; then + _services; + [[ -e /etc/mandrake-release ]] && _xinetd_services; + else + local sysvdirs; + _sysvdirs; + COMPREPLY=($(compgen -W '`command sed -e "y/|/ /" \ + -ne "s/^.*\(U\|msg_u\)sage.*{\(.*\)}.*$/\2/p" \ + ${sysvdirs[0]}/${prev##*/} 2>/dev/null` start stop' -- "$cur")); + fi +} +_services () +{ + local sysvdirs; + _sysvdirs; + local IFS=' +' reset=$(shopt -p nullglob); + shopt -s nullglob; + COMPREPLY=($(printf '%s\n' ${sysvdirs[0]}/!($_backup_glob|functions|README))); + $reset; + COMPREPLY+=($({ systemctl list-units --full --all || systemctl list-unit-files; } 2>/dev/null | awk '$1 ~ /\.service$/ { sub("\\.service$", "", $1); print $1 }')); + if [[ -x /sbin/upstart-udev-bridge ]]; then + COMPREPLY+=($(initctl list 2>/dev/null | cut -d' ' -f1)); + fi; + COMPREPLY=($(compgen -W '${COMPREPLY[@]#${sysvdirs[0]}/}' -- "$cur")) +} +_shells () +{ + local shell rest; + while read -r shell rest; do + [[ $shell == /* && $shell == "$cur"* ]] && COMPREPLY+=($shell); + done 2> /dev/null < /etc/shells +} +_signals () +{ + local -a sigs=($(compgen -P "$1" -A signal "SIG${cur#$1}")); + COMPREPLY+=("${sigs[@]/#${1}SIG/${1}}") +} +_split_longopt () +{ + if [[ "$cur" == --?*=* ]]; then + prev="${cur%%?(\\)=*}"; + cur="${cur#*=}"; + return 0; + fi; + return 1 +} +_sysvdirs () +{ + sysvdirs=(); + [[ -d /etc/rc.d/init.d ]] && sysvdirs+=(/etc/rc.d/init.d); + [[ -d /etc/init.d ]] && sysvdirs+=(/etc/init.d); + [[ -f /etc/slackware-version ]] && sysvdirs=(/etc/rc.d); + return 0 +} +_terms () +{ + COMPREPLY+=($(compgen -W "$({ command sed -ne 's/^\([^[:space:]#|]\{2,\}\)|.*/\1/p' /etc/termcap; + { toe -a || toe; } | awk '{ print $1 }'; + find /{etc,lib,usr/lib,usr/share}/terminfo/? -type f -maxdepth 1 | awk -F/ '{ print $NF }'; + } 2>/dev/null)" -- "$cur")) +} +_tilde () +{ + local result=0; + if [[ $1 == \~* && $1 != */* ]]; then + COMPREPLY=($(compgen -P '~' -u -- "${1#\~}")); + result=${#COMPREPLY[@]}; + [[ $result -gt 0 ]] && compopt -o filenames 2> /dev/null; + fi; + return $result +} +_uids () +{ + if type getent &> /dev/null; then + COMPREPLY=($(compgen -W '$(getent passwd | cut -d: -f3)' -- "$cur")); + else + if type perl &> /dev/null; then + COMPREPLY=($(compgen -W '$(perl -e '"'"'while (($uid) = (getpwent)[2]) { print $uid . "\n" }'"'"')' -- "$cur")); + else + COMPREPLY=($(compgen -W '$(cut -d: -f3 /etc/passwd)' -- "$cur")); + fi; + fi +} +_upvar () +{ + echo "bash_completion: $FUNCNAME: deprecated function," "use _upvars instead" 1>&2; + if unset -v "$1"; then + if (( $# == 2 )); then + eval $1=\"\$2\"; + else + eval $1=\(\"\${@:2}\"\); + fi; + fi +} +_upvars () +{ + if ! (( $# )); then + echo "bash_completion: $FUNCNAME: usage: $FUNCNAME" "[-v varname value] | [-aN varname [value ...]] ..." 1>&2; + return 2; + fi; + while (( $# )); do + case $1 in + -a*) + [[ -n ${1#-a} ]] || { + echo "bash_completion: $FUNCNAME:" "\`$1': missing number specifier" 1>&2; + return 1 + }; + printf %d "${1#-a}" &> /dev/null || { + echo bash_completion: "$FUNCNAME: \`$1': invalid number specifier" 1>&2; + return 1 + }; + [[ -n "$2" ]] && unset -v "$2" && eval $2=\(\"\${@:3:${1#-a}}\"\) && shift $((${1#-a} + 2)) || { + echo bash_completion: "$FUNCNAME: \`$1${2+ }$2': missing argument(s)" 1>&2; + return 1 + } + ;; + -v) + [[ -n "$2" ]] && unset -v "$2" && eval $2=\"\$3\" && shift 3 || { + echo "bash_completion: $FUNCNAME: $1:" "missing argument(s)" 1>&2; + return 1 + } + ;; + *) + echo "bash_completion: $FUNCNAME: $1: invalid option" 1>&2; + return 1 + ;; + esac; + done +} +_usb_ids () +{ + COMPREPLY+=($(compgen -W "$(PATH="$PATH:/sbin" lsusb | awk '{print $6}')" -- "$cur")) +} +_user_at_host () +{ + local cur prev words cword; + _init_completion -n : || return; + if [[ $cur == *@* ]]; then + _known_hosts_real "$cur"; + else + COMPREPLY=($(compgen -u -S @ -- "$cur")); + compopt -o nospace; + fi +} +_usergroup () +{ + if [[ $cur == *\\\\* || $cur == *:*:* ]]; then + return; + else + if [[ $cur == *\\:* ]]; then + local prefix; + prefix=${cur%%*([^:])}; + prefix=${prefix//\\}; + local mycur="${cur#*[:]}"; + if [[ $1 == -u ]]; then + _allowed_groups "$mycur"; + else + local IFS=' +'; + COMPREPLY=($(compgen -g -- "$mycur")); + fi; + COMPREPLY=($(compgen -P "$prefix" -W "${COMPREPLY[@]}")); + else + if [[ $cur == *:* ]]; then + local mycur="${cur#*:}"; + if [[ $1 == -u ]]; then + _allowed_groups "$mycur"; + else + local IFS=' +'; + COMPREPLY=($(compgen -g -- "$mycur")); + fi; + else + if [[ $1 == -u ]]; then + _allowed_users "$cur"; + else + local IFS=' +'; + COMPREPLY=($(compgen -u -- "$cur")); + fi; + fi; + fi; + fi +} +_userland () +{ + local userland=$(uname -s); + [[ $userland == @(Linux|GNU/*) ]] && userland=GNU; + [[ $userland == $1 ]] +} +_variables () +{ + if [[ $cur =~ ^(\$(\{[!#]?)?)([A-Za-z0-9_]*)$ ]]; then + if [[ $cur == \${* ]]; then + local arrs vars; + vars=($(compgen -A variable -P ${BASH_REMATCH[1]} -S '}' -- ${BASH_REMATCH[3]})) && arrs=($(compgen -A arrayvar -P ${BASH_REMATCH[1]} -S '[' -- ${BASH_REMATCH[3]})); + if [[ ${#vars[@]} -eq 1 && -n $arrs ]]; then + compopt -o nospace; + COMPREPLY+=(${arrs[*]}); + else + COMPREPLY+=(${vars[*]}); + fi; + else + COMPREPLY+=($(compgen -A variable -P '$' -- "${BASH_REMATCH[3]}")); + fi; + return 0; + else + if [[ $cur =~ ^(\$\{[#!]?)([A-Za-z0-9_]*)\[([^]]*)$ ]]; then + local IFS=' +'; + COMPREPLY+=($(compgen -W '$(printf %s\\n "${!'${BASH_REMATCH[2]}'[@]}")' -P "${BASH_REMATCH[1]}${BASH_REMATCH[2]}[" -S ']}' -- "${BASH_REMATCH[3]}")); + if [[ ${BASH_REMATCH[3]} == [@*] ]]; then + COMPREPLY+=("${BASH_REMATCH[1]}${BASH_REMATCH[2]}[${BASH_REMATCH[3]}]}"); + fi; + __ltrim_colon_completions "$cur"; + return 0; + else + if [[ $cur =~ ^\$\{[#!]?[A-Za-z0-9_]*\[.*\]$ ]]; then + COMPREPLY+=("$cur}"); + __ltrim_colon_completions "$cur"; + return 0; + else + case $prev in + TZ) + cur=/usr/share/zoneinfo/$cur; + _filedir; + for i in "${!COMPREPLY[@]}"; + do + if [[ ${COMPREPLY[i]} == *.tab ]]; then + unset 'COMPREPLY[i]'; + continue; + else + if [[ -d ${COMPREPLY[i]} ]]; then + COMPREPLY[i]+=/; + compopt -o nospace; + fi; + fi; + COMPREPLY[i]=${COMPREPLY[i]#/usr/share/zoneinfo/}; + done; + return 0 + ;; + TERM) + _terms; + return 0 + ;; + LANG | LC_*) + COMPREPLY=($(compgen -W '$(locale -a 2>/dev/null)' -- "$cur" )); + return 0 + ;; + esac; + fi; + fi; + fi; + return 1 +} +_xfunc () +{ + set -- "$@"; + local srcfile=$1; + shift; + declare -F $1 &> /dev/null || { + __load_completion "$srcfile" + }; + "$@" +} +_xinetd_services () +{ + local xinetddir=/etc/xinetd.d; + if [[ -d $xinetddir ]]; then + local IFS=' +' reset=$(shopt -p nullglob); + shopt -s nullglob; + local -a svcs=($(printf '%s\n' $xinetddir/!($_backup_glob))); + $reset; + COMPREPLY+=($(compgen -W '${svcs[@]#$xinetddir/}' -- "$cur")); + fi +} +command_not_found_handle () +{ + if [ -x /usr/lib/command-not-found ]; then + /usr/lib/command-not-found -- "$1"; + return $?; + else + if [ -x /usr/share/command-not-found/command-not-found ]; then + /usr/share/command-not-found/command-not-found -- "$1"; + return $?; + else + printf "%s: command not found\n" "$1" 1>&2; + return 127; + fi; + fi +} +dequote () +{ + eval printf %s "$1" 2> /dev/null +} +gawklibpath_append () +{ + [ -z "$AWKLIBPATH" ] && AWKLIBPATH=`gawk 'BEGIN {print ENVIRON["AWKLIBPATH"]}'`; + export AWKLIBPATH="$AWKLIBPATH:$*" +} +gawklibpath_default () +{ + unset AWKLIBPATH; + export AWKLIBPATH=`gawk 'BEGIN {print ENVIRON["AWKLIBPATH"]}'` +} +gawklibpath_prepend () +{ + [ -z "$AWKLIBPATH" ] && AWKLIBPATH=`gawk 'BEGIN {print ENVIRON["AWKLIBPATH"]}'`; + export AWKLIBPATH="$*:$AWKLIBPATH" +} +gawkpath_append () +{ + [ -z "$AWKPATH" ] && AWKPATH=`gawk 'BEGIN {print ENVIRON["AWKPATH"]}'`; + export AWKPATH="$AWKPATH:$*" +} +gawkpath_default () +{ + unset AWKPATH; + export AWKPATH=`gawk 'BEGIN {print ENVIRON["AWKPATH"]}'` +} +gawkpath_prepend () +{ + [ -z "$AWKPATH" ] && AWKPATH=`gawk 'BEGIN {print ENVIRON["AWKPATH"]}'`; + export AWKPATH="$*:$AWKPATH" +} +quote () +{ + local quoted=${1//\'/\'\\\'\'}; + printf "'%s'" "$quoted" +} +quote_readline () +{ + local quoted; + _quote_readline_by_ref "$1" ret; + printf %s "$ret" +} diff --git a/pkg/cmdrunner/shparse_test.go b/pkg/cmdrunner/shparse_test.go new file mode 100644 index 000000000..682d028e1 --- /dev/null +++ b/pkg/cmdrunner/shparse_test.go @@ -0,0 +1,57 @@ +package cmdrunner + +import ( + "fmt" + "os" + "testing" +) + +func xTestParseAliases(t *testing.T) { + m, err := ParseAliases(` +alias cdg='cd work/gopath/src/github.com/sawka' +alias s='scripthaus' +alias x='ls;ls"' +alias foo="bar \"hello\"" +alias x=y +`) + if err != nil { + fmt.Printf("err: %v\n", err) + return + } + fmt.Printf("m: %#v\n", m) +} + +func xTestParseFuncs(t *testing.T) { + file, err := os.ReadFile("./linux-decls.txt") + if err != nil { + t.Fatalf("error reading linux-decls: %v", err) + } + m, err := ParseFuncs(string(file)) + if err != nil { + t.Fatalf("error parsing funcs: %v", err) + } + for key, val := range m { + fmt.Printf("func: %s %d\n", key, len(val)) + } +} + +func testRSC(t *testing.T, cmd string, expected bool) { + rtn := IsReturnStateCommand(cmd) + if rtn != expected { + t.Errorf("cmd [%s], rtn=%v, expected=%v", cmd, rtn, expected) + } +} + +func TestIsReturnStateCommand(t *testing.T) { + testRSC(t, "FOO=1", true) + testRSC(t, "FOO=1 X=2", true) + testRSC(t, "ls", false) + testRSC(t, "export X", true) + testRSC(t, "export X=1", true) + testRSC(t, "declare -x FOO=1", true) + testRSC(t, "source ./test", true) + testRSC(t, "unset FOO BAR", true) + testRSC(t, "FOO=1; ls", true) + testRSC(t, ". ./test", true) + testRSC(t, "{ FOO=6; }", false) +} From b20f85f8a25339b5941dfed5d5e6d9cb5bb2db01 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Mar 2023 09:50:07 -0800 Subject: [PATCH 285/397] commit keygen package (unused) --- pkg/keygen/keygen.go | 112 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 pkg/keygen/keygen.go diff --git a/pkg/keygen/keygen.go b/pkg/keygen/keygen.go new file mode 100644 index 000000000..a8ae232ed --- /dev/null +++ b/pkg/keygen/keygen.go @@ -0,0 +1,112 @@ +// Utility functions for generating and reading public/private keypairs. +package keygen + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" + "os" + "time" +) + +const p384Params = "BgUrgQQAIg==" + +// Creates a keypair with CN=[id], private key at keyFileName, and +// public key certificate at certFileName. +func CreateKeyPair(keyFileName string, certFileName string, id string) error { + privateKey, err := CreatePrivateKey(keyFileName) + if err != nil { + return err + } + err = CreateCertificate(certFileName, privateKey, id) + if err != nil { + return err + } + return nil +} + +// Creates a private key at keyFileName (ECDSA, secp384r1 (P-384)), PEM format +func CreatePrivateKey() (*ecdsa.PrivateKey, error) { + curve := elliptic.P384() // secp384r1 + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, fmt.Errorf("Error generating P-384 key err:%w", err) + } + keyFile, err := os.Create(keyFileName) + if err != nil { + return nil, fmt.Errorf("error opening file:%s err:%w", keyFileName, err) + } + defer keyFile.Close() + pkBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, fmt.Errorf("Error MarshalPKCS8PrivateKey err:%w", err) + } + paramsBytes, err := base64.StdEncoding.DecodeString(p384Params) + if err != nil { + return nil, fmt.Errorf("Error decoding bytes for P-384 EC PARAMETERS err:%w", err) + } + var pemParamsBlock = &pem.Block{ + Type: "EC PARAMETERS", + Bytes: paramsBytes, + } + err = pem.Encode(keyFile, pemParamsBlock) + if err != nil { + return nil, fmt.Errorf("Error writing EC PARAMETERS pem block err:%w", err) + } + var pemPrivateBlock = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: pkBytes, + } + err = pem.Encode(keyFile, pemPrivateBlock) + if err != nil { + return nil, fmt.Errorf("Error writing EC PRIVATE KEY pem block err:%w", err) + } + return privateKey, nil +} + +// Creates a public key certificate at certFileName using privateKey with CN=[id]. +func CreateCertificate(certFileName string, privateKey *ecdsa.PrivateKey, id string) error { + serialNumber, err := rand.Int(rand.Reader, big.NewInt(1000000000000)) + if err != nil { + return fmt.Errorf("Cannot generate serial number err:%w", err) + } + notBefore, err := time.Parse("Jan 2 15:04:05 2006", "Jan 1 00:00:00 2020") + if err != nil { + return fmt.Errorf("Cannot Parse Date err:%w", err) + } + notAfter, err := time.Parse("Jan 2 15:04:05 2006", "Jan 1 00:00:00 2030") + if err != nil { + return fmt.Errorf("Cannot Parse Date err:%w", err) + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: id, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return fmt.Errorf("Error running x509.CreateCertificate err:%v\n", err) + } + certFile, err := os.Create(certFileName) + if err != nil { + return fmt.Errorf("Error opening file:%s err:%w", certFileName, err) + } + defer certFile.Close() + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + if err != nil { + return fmt.Errorf("Error writing CERTIFICATE pem block err:%w", err) + } + return nil +} From f5fd23120854e0a91d103fd706930d1d60421979 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Mar 2023 14:38:46 -0800 Subject: [PATCH 286/397] promptenc for encrypting/decrypting some plain fields in a struct to an encrypted field (json + aead) --- go.mod | 2 +- pkg/promptenc/promptenc.go | 180 +++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 pkg/promptenc/promptenc.go diff --git a/go.mod b/go.mod index b075557cb..937d43daf 100644 --- a/go.mod +++ b/go.mod @@ -26,4 +26,4 @@ require ( go.uber.org/atomic v1.7.0 // indirect ) -replace github.com/scripthaus-dev/mshell v0.0.0 => ../mshell/ + diff --git a/pkg/promptenc/promptenc.go b/pkg/promptenc/promptenc.go new file mode 100644 index 000000000..f7338e34e --- /dev/null +++ b/pkg/promptenc/promptenc.go @@ -0,0 +1,180 @@ +package promptenc + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/json" + "fmt" + "io" + "reflect" + + ccp "golang.org/x/crypto/chacha20poly1305" +) + +const EncTagName = "enc" +const EncFieldIndicator = "*" + +type Encryptor struct { + Key []byte + AEAD cipher.AEAD +} + +type HasOData interface { + GetOData() string +} + +func readRandBytes(n int) ([]byte, error) { + rtn := make([]byte, n) + _, err := io.ReadFull(rand.Reader, rtn) + return rtn, err +} + +func MakeRandomEncryptor() (*Encryptor, error) { + key, err := readRandBytes(ccp.KeySize) + if err != nil { + return nil, err + } + rtn := &Encryptor{Key: key} + rtn.AEAD, err = ccp.NewX(rtn.Key) + if err != nil { + return nil, err + } + return rtn, nil +} + +func (enc *Encryptor) EncryptData(plainText []byte, odata string) ([]byte, error) { + outputBuf := make([]byte, enc.AEAD.NonceSize()+enc.AEAD.Overhead()+len(plainText)) + nonce := outputBuf[0:enc.AEAD.NonceSize()] + _, err := io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + // we're going to append the cipherText to nonce. so the encrypted data is [nonce][ciphertext] + // note that outputbuf should be the correct size to hold the rtn value + rtn := enc.AEAD.Seal(nonce, nonce, plainText, []byte(odata)) + return rtn, nil +} + +func (enc *Encryptor) DecryptData(encData []byte, odata string) (map[string]interface{}, error) { + minLen := enc.AEAD.NonceSize() + enc.AEAD.Overhead() + if len(encData) < minLen { + return nil, fmt.Errorf("invalid encdata, len:%d is less than minimum len:%d", len(encData), minLen) + } + m := make(map[string]interface{}) + nonce := encData[0:enc.AEAD.NonceSize()] + cipherText := encData[enc.AEAD.NonceSize():] + plainText, err := enc.AEAD.Open(nil, nonce, cipherText, []byte(odata)) + if err != nil { + return nil, err + } + err = json.Unmarshal(plainText, &m) + if err != nil { + return nil, err + } + return m, nil +} + +type EncryptMeta struct { + EncField *reflect.StructField + PlainFields map[string]reflect.StructField +} + +func isByteArrayType(t reflect.Type) bool { + return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 +} + +func metaFromType(v interface{}) (*EncryptMeta, error) { + if v == nil { + return nil, fmt.Errorf("Encryptor cannot encrypt nil") + } + rt := reflect.TypeOf(v) + if rt.Kind() != reflect.Pointer { + return nil, fmt.Errorf("Encryptor invalid type %T, not a pointer type", v) + } + rtElem := rt.Elem() + if rtElem.Kind() != reflect.Struct { + return nil, fmt.Errorf("Encryptor invalid type %T, not a pointer to struct type", v) + } + meta := &EncryptMeta{} + meta.PlainFields = make(map[string]reflect.StructField) + numFields := rtElem.NumField() + for i := 0; i < numFields; i++ { + field := rtElem.Field(i) + encTag := field.Tag.Get(EncTagName) + if encTag == "" { + continue + } + if encTag == EncFieldIndicator { + if meta.EncField != nil { + return nil, fmt.Errorf("Encryptor, type %T has two enc fields set (*)", v) + } + if !isByteArrayType(field.Type) { + return nil, fmt.Errorf("Encryptor, type %T enc field %q is not []byte", v, field.Name) + } + meta.EncField = &field + continue + } + if _, found := meta.PlainFields[encTag]; found { + return nil, fmt.Errorf("Encryptor, type %T has two enc fields with tag %q", v, encTag) + } + meta.PlainFields[encTag] = field + } + if meta.EncField == nil { + return nil, fmt.Errorf("Encryptor, type %T has no enc (*) field", v) + } + return meta, nil +} + +func (enc *Encryptor) EncryptODS(v HasOData) error { + odata := v.GetOData() + return enc.EncryptStructFields(v, odata) +} + +func (enc *Encryptor) DecryptODS(v HasOData) error { + odata := v.GetOData() + return enc.DecryptStructFields(v, odata) +} + +func (enc *Encryptor) EncryptStructFields(v interface{}, odata string) error { + encMeta, err := metaFromType(v) + if err != nil { + return err + } + rvPtr := reflect.ValueOf(v) + rv := rvPtr.Elem() + m := make(map[string]interface{}) + for jsonKey, field := range encMeta.PlainFields { + fieldVal := rv.FieldByIndex(field.Index) + m[jsonKey] = fieldVal.Interface() + } + barr, err := json.Marshal(m) + if err != nil { + return err + } + cipherText, err := enc.EncryptData(barr, odata) + if err != nil { + return err + } + encFieldValue := rv.FieldByIndex(encMeta.EncField.Index) + encFieldValue.SetBytes(cipherText) + return nil +} + +func (enc *Encryptor) DecryptStructFields(v interface{}, odata string) error { + encMeta, err := metaFromType(v) + if err != nil { + return err + } + rvPtr := reflect.ValueOf(v) + rv := rvPtr.Elem() + cipherText := rv.FieldByIndex(encMeta.EncField.Index).Bytes() + m, err := enc.DecryptData(cipherText, odata) + if err != nil { + return err + } + for jsonKey, field := range encMeta.PlainFields { + val := m[jsonKey] + rv.FieldByIndex(field.Index).Set(reflect.ValueOf(val)) + } + return nil +} From f2529581e18c847a4387eee830311a41e1a3db7d Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Mar 2023 17:16:06 -0800 Subject: [PATCH 287/397] checkpoint, cloud sessions --- db/migrations/000008_cloudsession.down.sql | 4 ++ db/migrations/000008_cloudsession.up.sql | 13 ++++ pkg/pcloud/pcloud.go | 80 +++++++++++++++++++++- pkg/promptenc/promptenc.go | 18 +++++ pkg/sstore/dbops.go | 4 +- pkg/sstore/sstore.go | 17 ++++- 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 db/migrations/000008_cloudsession.down.sql create mode 100644 db/migrations/000008_cloudsession.up.sql diff --git a/db/migrations/000008_cloudsession.down.sql b/db/migrations/000008_cloudsession.down.sql new file mode 100644 index 000000000..afb70c9b8 --- /dev/null +++ b/db/migrations/000008_cloudsession.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE session ADD COLUMN accesskey DEFAULT ''; +ALTER TABLE session ADD COLUMN ownerid DEFAULT ''; + +DROP TABLE cloud_session; diff --git a/db/migrations/000008_cloudsession.up.sql b/db/migrations/000008_cloudsession.up.sql new file mode 100644 index 000000000..4fba64645 --- /dev/null +++ b/db/migrations/000008_cloudsession.up.sql @@ -0,0 +1,13 @@ +ALTER TABLE session DROP COLUMN accesskey; +ALTER TABLE session DROP COLUMN ownerid; + +CREATE TABLE cloud_session ( + sessionid varchar(36) PRIMARY KEY, + viewkey varchar(50) NOT NULL, + writekey varchar(50) NOT NULL, + enckey varchar(100) NOT NULL, + enctype varchar(50) NOT NULL, + vts bigint NOT NULL, + acl json NOT NULL +); + diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index e55d42307..3ca08b8fb 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -20,6 +20,10 @@ const PCloudEndpoint = "https://api.getprompt.dev/central" const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 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"` @@ -32,6 +36,27 @@ type TelemetryInputType struct { 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) +} + +type AuthInfo struct { + UserId string `json:"userid"` + ClientId string `json:"clientid"` + AuthKey string `json:"authkey"` +} + func GetEndpoint() string { if !scbase.IsDevMode() { return PCloudEndpoint @@ -43,7 +68,31 @@ func GetEndpoint() string { return endpoint } -func makePostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { +func makeAuthPostReq(ctx context.Context, apiUrl string, authInfo AuthInfo, data interface{}) (*http.Request, error) { + var dataReader io.Reader + if data != nil { + byteArr, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err) + } + dataReader = bytes.NewReader(byteArr) + } + fullUrl := GetEndpoint() + apiUrl + req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader) + if err != nil { + return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion)) + req.Header.Set("X-PromptAPIUrl", apiUrl) + req.Header.Set("X-PromptUserId", authInfo.UserId) + req.Header.Set("X-PromptClientId", authInfo.ClientId) + req.Header.Set("X-PromptAuthKey", authInfo.AuthKey) + req.Close = true + return req, nil +} + +func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { var dataReader io.Reader if data != nil { byteArr, err := json.Marshal(data) @@ -106,7 +155,7 @@ func SendTelemetry(ctx context.Context, force bool) error { log.Printf("sending telemetry data\n") dayStr := sstore.GetCurDayStr() input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, CurDay: dayStr, Activity: activity} - req, err := makePostReq(ctx, "/telemetry", input) + req, err := makeAnonPostReq(ctx, TelemetryUrl, input) if err != nil { return err } @@ -126,7 +175,32 @@ func SendNoTelemetryUpdate(ctx context.Context, noTelemetryVal bool) error { if err != nil { return fmt.Errorf("cannot retrieve client data: %v", err) } - req, err := makePostReq(ctx, "/no-telemetry", NoTelemetryInputType{ClientId: clientData.ClientId, Value: noTelemetryVal}) + req, err := makeAnonPostReq(ctx, NoTelemetryUrl, NoTelemetryInputType{ClientId: clientData.ClientId, Value: noTelemetryVal}) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } + return nil +} + +func getAuthInfo(ctx context.Context) (AuthInfo, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return AuthInfo{}, fmt.Errorf("cannot retrieve client data: %v", err) + } + return AuthInfo{UserId: clientData.UserId, ClientId: clientData.ClientId}, nil +} + +func CreateCloudSession(ctx context.Context) error { + authInfo, err := getAuthInfo(ctx) + if err != nil { + return err + } + fmt.Printf("authinfo: %v\n", authInfo) + req, err := makeAuthPostReq(ctx, CreateCloudSessionUrl, authInfo, nil) if err != nil { return err } diff --git a/pkg/promptenc/promptenc.go b/pkg/promptenc/promptenc.go index f7338e34e..bd74216da 100644 --- a/pkg/promptenc/promptenc.go +++ b/pkg/promptenc/promptenc.go @@ -3,6 +3,7 @@ package promptenc import ( "crypto/cipher" "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "io" @@ -42,6 +43,23 @@ func MakeRandomEncryptor() (*Encryptor, error) { return rtn, nil } +func MakeEncryptor(key []byte) (*Encryptor, error) { + rtn := &Encryptor{Key: key} + rtn.AEAD, err = ccp.NewX(rtn.Key) + if err != nil { + return nil, err + } + return rtn, nil +} + +func MakeEncryptorB64(key64 string) (*Encryptor, error) { + keyBytes, err := base64.RawURLEncoding.DecodeString(key64) + if err != nil { + return nil, err + } + return MakeEncryptor(keyBytes) +} + func (enc *Encryptor) EncryptData(plainText []byte, odata string) ([]byte, error) { outputBuf := make([]byte, enc.AEAD.NonceSize()+enc.AEAD.Overhead()+len(plainText)) nonce := outputBuf[0:enc.AEAD.NonceSize()] diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 780406e86..30b55a4e3 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -527,8 +527,8 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo names := tx.SelectStrings(`SELECT name FROM session`) sessionName = fmtUniqueName(sessionName, "session-%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, ownerid, sharemode, accesskey) - VALUES (?, ?, '', ?, ?, 0, 0, '', 'local', '')` + query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, archivedts, sharemode) + VALUES (?, ?, '', ?, ?, 0, 0, 'local')` tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, 0) _, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 9fe5c1ba9..45e39db50 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -212,14 +212,17 @@ func ClientDataFromMap(m map[string]interface{}) *ClientData { return &c } +type CloudAclType struct { + UserId string `json:"userid"` + Role string `json:"role"` +} + type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` SessionIdx int64 `json:"sessionidx"` ActiveScreenId string `json:"activescreenid"` - OwnerId string `json:"ownerid"` ShareMode string `json:"sharemode"` - AccessKey string `json:"-"` NotifyNum int64 `json:"notifynum"` Archived bool `json:"archived,omitempty"` ArchivedTs int64 `json:"archivedts,omitempty"` @@ -231,6 +234,16 @@ 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 +} + type SessionStatsType struct { SessionId string `json:"sessionid"` NumScreens int `json:"numscreens"` From db5cecb1da1bd792b83e8fad2ba5e3f34a76d2d7 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Mar 2023 18:44:01 -0800 Subject: [PATCH 288/397] first use of go generics for DB mapping functions. add cloud session tables. refactor some of dbops to use new generics. --- db/migrations/000008_cloudsession.down.sql | 1 + db/migrations/000008_cloudsession.up.sql | 7 + pkg/cmdrunner/cmdrunner.go | 19 +- pkg/pcloud/pcloud.go | 1 - pkg/promptenc/promptenc.go | 1 + pkg/sstore/dbops.go | 169 +++++++---------- pkg/sstore/map.go | 200 +++++++++++++++++++++ pkg/sstore/sstore.go | 152 +++++++--------- 8 files changed, 351 insertions(+), 199 deletions(-) create mode 100644 pkg/sstore/map.go diff --git a/db/migrations/000008_cloudsession.down.sql b/db/migrations/000008_cloudsession.down.sql index afb70c9b8..bf15963a7 100644 --- a/db/migrations/000008_cloudsession.down.sql +++ b/db/migrations/000008_cloudsession.down.sql @@ -2,3 +2,4 @@ ALTER TABLE session ADD COLUMN accesskey DEFAULT ''; ALTER TABLE session ADD COLUMN ownerid DEFAULT ''; DROP TABLE cloud_session; +DROP TABLE cloud_update; diff --git a/db/migrations/000008_cloudsession.up.sql b/db/migrations/000008_cloudsession.up.sql index 4fba64645..860e61b05 100644 --- a/db/migrations/000008_cloudsession.up.sql +++ b/db/migrations/000008_cloudsession.up.sql @@ -11,3 +11,10 @@ CREATE TABLE cloud_session ( acl json NOT NULL ); +CREATE TABLE cloud_update ( + updateid varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + updatetype varchar(50) NOT NULL, + updatekeys json NOT NULL +); + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a63f6757c..68e4485cf 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -128,6 +128,7 @@ func init() { registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("session:show", SessionShowCommand) registerCmdFn("session:openshared", SessionOpenSharedCommand) + registerCmdFn("session:opencloud", SessionOpenCloudCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:archive", ScreenArchiveCommand) @@ -1546,6 +1547,22 @@ func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketT return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)") } +func SessionOpenCloudCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + activate := resolveBool(pk.Kwargs["activate"], true) + newName := pk.Kwargs["name"] + if newName != "" { + err := validateName(newName, "session") + if err != nil { + return nil, err + } + } + update, err := sstore.InsertSessionWithName(ctx, newName, sstore.ShareModeShared, activate) + if err != nil { + return nil, err + } + return update, nil +} + func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { activate := resolveBool(pk.Kwargs["activate"], true) newName := pk.Kwargs["name"] @@ -1555,7 +1572,7 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, err } } - update, err := sstore.InsertSessionWithName(ctx, newName, activate) + update, err := sstore.InsertSessionWithName(ctx, newName, sstore.ShareModeLocal, activate) if err != nil { return nil, err } diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 3ca08b8fb..388433bd2 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -199,7 +199,6 @@ func CreateCloudSession(ctx context.Context) error { if err != nil { return err } - fmt.Printf("authinfo: %v\n", authInfo) req, err := makeAuthPostReq(ctx, CreateCloudSessionUrl, authInfo, nil) if err != nil { return err diff --git a/pkg/promptenc/promptenc.go b/pkg/promptenc/promptenc.go index bd74216da..e5ac25b3d 100644 --- a/pkg/promptenc/promptenc.go +++ b/pkg/promptenc/promptenc.go @@ -44,6 +44,7 @@ func MakeRandomEncryptor() (*Encryptor, error) { } func MakeEncryptor(key []byte) (*Encryptor, error) { + var err error rtn := &Encryptor{Key: key} rtn.AEAD, err = ccp.NewX(rtn.Key) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 30b55a4e3..2135b7f24 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -65,7 +65,7 @@ func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { query := `SELECT * FROM remote ORDER BY remoteidx` marr := tx.SelectMaps(query) for _, m := range marr { - rtn = append(rtn, RemoteFromMap(m)) + rtn = append(rtn, FromMap[*RemoteType](m)) } return nil }) @@ -80,7 +80,7 @@ func GetRemoteByAlias(ctx context.Context, alias string) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remotealias = ?` m := tx.GetMap(query, alias) - remote = RemoteFromMap(m) + remote = FromMap[*RemoteType](m) return nil }) if err != nil { @@ -94,7 +94,7 @@ func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remoteid = ?` m := tx.GetMap(query, remoteId) - remote = RemoteFromMap(m) + remote = FromMap[*RemoteType](m) return nil }) if err != nil { @@ -108,7 +108,7 @@ func GetLocalRemote(ctx context.Context) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE local` m := tx.GetMap(query) - remote = RemoteFromMap(m) + remote = FromMap[*RemoteType](m) return nil }) if err != nil { @@ -121,8 +121,7 @@ func GetRemoteByCanonicalName(ctx context.Context, cname string) (*RemoteType, e var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remotecanonicalname = ?` - m := tx.GetMap(query, cname) - remote = RemoteFromMap(m) + remote = GetMapGen[*RemoteType](tx, query, cname) return nil }) if err != nil { @@ -135,8 +134,7 @@ func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE physicalid = ?` - m := tx.GetMap(query, physicalId) - remote = RemoteFromMap(m) + remote = GetMapGen[*RemoteType](tx, query, physicalId) return nil }) if err != nil { @@ -327,7 +325,7 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { - hitem := HistoryItemFromMap(m) + hitem := FromMap[*HistoryItemType](m) rtn[idx] = hitem } return rtn, nil @@ -434,9 +432,8 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { screen.Windows = append(screen.Windows, sw) } query = `SELECT * FROM remote_instance` - riMaps := tx.SelectMaps(query) - for _, m := range riMaps { - ri := RIFromMap(m) + riArr := SelectMapsGen[*RemoteInstance](tx, query) + for _, ri := range riArr { s := sessionMap[ri.SessionId] if s != nil { s.Remotes = append(s.Remotes, ri) @@ -460,14 +457,11 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win if m == nil { return nil } - rtnWindow = WindowFromMap(m) + rtnWindow = FromMap[*WindowType](m) query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` tx.Select(&rtnWindow.Lines, query, sessionId, windowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` - cmdMaps := tx.SelectMaps(query, sessionId, windowId) - for _, m := range cmdMaps { - rtnWindow.Cmds = append(rtnWindow.Cmds, CmdFromMap(m)) - } + rtnWindow.Cmds = SelectMapsGen[*CmdType](tx, query, sessionId, windowId) return nil }) return rtnWindow, err @@ -519,9 +513,27 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return session, nil } +func InsertCloudSession(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { + var updateRtn *ModelUpdate + txErr := WithTx(ctx, func(tx *TxWrap) error { + var err error + updateRtn, err = InsertSessionWithName(tx.Context(), sessionName, shareMode, activate) + if err != nil { + return err + } + sessionId := updateRtn.Sessions[0].SessionId + fmt.Printf("sessionid: %v\n", sessionId) + return nil + }) + if txErr != nil { + return nil, txErr + } + return updateRtn, nil +} + // also creates default window, returns sessionId // if sessionName == "", it will be generated -func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (UpdatePacket, error) { +func InsertSessionWithName(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { newSessionId := scbase.GenPromptUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) @@ -553,7 +565,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo if activate { update.ActiveSessionId = newSessionId } - return update, nil + return &update, nil } func SetActiveSessionId(ctx context.Context, sessionId string) error { @@ -757,8 +769,7 @@ func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, lineRtn = &lineVal if lineVal.CmdId != "" { query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - m := tx.GetMap(query, sessionId, lineVal.CmdId) - cmdRtn = CmdFromMap(m) + cmdRtn = GetMapGen[*CmdType](tx, query, sessionId, lineVal.CmdId) } return nil }) @@ -784,8 +795,7 @@ func GetLineCmdByCmdId(ctx context.Context, sessionId string, windowId string, c } lineRtn = &lineVal query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - m := tx.GetMap(query, sessionId, cmdId) - cmdRtn = CmdFromMap(m) + cmdRtn = GetMapGen[*CmdType](tx, query, sessionId, cmdId) return nil }) if txErr != nil { @@ -834,8 +844,7 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, var cmd *CmdType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - m := tx.GetMap(query, sessionId, cmdId) - cmd = CmdFromMap(m) + cmd = GetMapGen[*CmdType](tx, query, sessionId, cmdId) return nil }) if err != nil { @@ -1176,8 +1185,7 @@ func GetRemoteInstance(ctx context.Context, sessionId string, windowId string, r var ri *RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) - ri = RIFromMap(m) + ri = GetMapGen[*RemoteInstance](tx, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) return nil }) if txErr != nil { @@ -1223,8 +1231,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r return fmt.Errorf("cannot update remote instance state: %w", err) } query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - m := tx.GetMap(query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) - ri = RIFromMap(m) + ri = GetMapGen[*RemoteInstance](tx, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if ri == nil { ri = &RemoteInstance{ RIId: scbase.GenPromptUUID(), @@ -1423,10 +1430,7 @@ func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string var rtn []*CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?) AND status = ?` - cmdMaps := tx.SelectMaps(query, sessionId, windowId, CmdStatusRunning) - for _, m := range cmdMaps { - rtn = append(rtn, CmdFromMap(m)) - } + rtn = SelectMapsGen[*CmdType](tx, query, sessionId, windowId, CmdStatusRunning) return nil }) if txErr != nil { @@ -1870,8 +1874,7 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, } for idx, diffHash := range ssPtr.DiffHashArr { query = `SELECT * FROM state_diff WHERE diffhash = ?` - m := tx.GetMap(query, diffHash) - stateDiff := StateDiffFromMap(m) + stateDiff := GetMapGen[*StateDiff](tx, query, diffHash) if stateDiff == nil { return fmt.Errorf("ShellStateDiff %s not found", diffHash) } @@ -1986,13 +1989,7 @@ func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([] var rtn []*RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote_instance WHERE sessionid = ? AND (windowid = '' OR windowid = ?)` - riMaps := tx.SelectMaps(query, sessionId, windowId) - for _, m := range riMaps { - ri := RIFromMap(m) - if ri != nil { - rtn = append(rtn, ri) - } - } + rtn = SelectMapsGen[*RemoteInstance](tx, query, sessionId, windowId) return nil }) if txErr != nil { @@ -2155,20 +2152,14 @@ func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { var bms []*BookmarkType txErr := WithTx(ctx, func(tx *TxWrap) error { var query string - var marr []map[string]interface{} if tag == "" { query = `SELECT * FROM bookmark` - marr = tx.SelectMaps(query) + bms = SelectMapsGen[*BookmarkType](tx, query) } else { query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)` - marr = tx.SelectMaps(query, tag) - } - bmMap := make(map[string]*BookmarkType) - for _, m := range marr { - bm := BookmarkFromMap(m) - bms = append(bms, bm) - bmMap[bm.BookmarkId] = bm + bms = SelectMapsGen[*BookmarkType](tx, query, tag) } + bmMap := MakeGenMap(bms) var orders []bookmarkOrderType query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?` tx.Select(&orders, query, tag) @@ -2199,8 +2190,7 @@ func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*Bookm var rtn *BookmarkType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM bookmark WHERE bookmarkid = ?` - m := tx.GetMap(query, bookmarkId) - rtn = BookmarkFromMap(m) + rtn = GetMapGen[*BookmarkType](tx, query, bookmarkId) if rtn == nil { return nil } @@ -2329,30 +2319,24 @@ func DeleteBookmark(ctx context.Context, bookmarkId string) error { } func CreatePlaybook(ctx context.Context, name string) (*PlaybookType, error) { - var rtn *PlaybookType - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTxRtn(ctx, func(tx *TxWrap) (*PlaybookType, error) { query := `SELECT playbookid FROM playbook WHERE name = ?` if tx.Exists(query, name) { - return fmt.Errorf("playbook %q already exists", name) + return nil, fmt.Errorf("playbook %q already exists", name) } - rtn = &PlaybookType{} + rtn := &PlaybookType{} rtn.PlaybookId = uuid.New().String() rtn.PlaybookName = name query = `INSERT INTO playbook ( playbookid, playbookname, description, entryids) VALUES (:playbookid,:playbookname,:description,:entryids)` tx.Exec(query, rtn.ToMap()) - return nil + return rtn, nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil } func selectPlaybook(tx *TxWrap, playbookId string) *PlaybookType { query := `SELECT * FROM playbook where playbookid = ?` - m := tx.GetMap(query, playbookId) - playbook := PlaybookFromMap(m) + playbook := GetMapGen[*PlaybookType](tx, query, playbookId) return playbook } @@ -2360,7 +2344,7 @@ func AddPlaybookEntry(ctx context.Context, entry *PlaybookEntry) error { if entry.EntryId == "" { return fmt.Errorf("invalid entryid") } - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTx(ctx, func(tx *TxWrap) error { playbook := selectPlaybook(tx, entry.PlaybookId) if playbook == nil { return fmt.Errorf("cannot add entry, playbook does not exist") @@ -2377,11 +2361,10 @@ func AddPlaybookEntry(ctx context.Context, entry *PlaybookEntry) error { tx.Exec(query, quickJsonArr(playbook.EntryIds), entry.PlaybookId) return nil }) - return txErr } func RemovePlaybookEntry(ctx context.Context, playbookId string, entryId string) error { - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTx(ctx, func(tx *TxWrap) error { playbook := selectPlaybook(tx, playbookId) if playbook == nil { return fmt.Errorf("cannot remove playbook entry, playbook does not exist") @@ -2397,25 +2380,19 @@ func RemovePlaybookEntry(ctx context.Context, playbookId string, entryId string) tx.Exec(query, quickJsonArr(playbook.EntryIds), playbookId) return nil }) - return txErr } func GetPlaybookById(ctx context.Context, playbookId string) (*PlaybookType, error) { - var rtn *PlaybookType - txErr := WithTx(ctx, func(tx *TxWrap) error { - rtn = selectPlaybook(tx, playbookId) + return WithTxRtn(ctx, func(tx *TxWrap) (*PlaybookType, error) { + rtn := selectPlaybook(tx, playbookId) if rtn == nil { - return nil + return nil, nil } query := `SELECT * FROM playbook_entry WHERE playbookid = ?` tx.Select(&rtn.Entries, query, playbookId) rtn.OrderEntries() - return nil + return rtn, nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil } func getLineIdsFromHistoryItems(historyItems []*HistoryItemType) []string { @@ -2439,55 +2416,33 @@ func getCmdIdsFromHistoryItems(historyItems []*HistoryItemType) []string { } func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryItemType) ([]*LineType, []*CmdType, error) { - var lineArr []*LineType - var cmdArr []*CmdType if len(historyItems) == 0 { return nil, nil, nil } - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTxRtn3(ctx, func(tx *TxWrap) ([]*LineType, []*CmdType, error) { + var lineArr []*LineType query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` tx.Select(&lineArr, query, quickJsonArr(getLineIdsFromHistoryItems(historyItems))) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT value FROM json_each(?))` - marr := tx.SelectMaps(query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) - for _, m := range marr { - cmd := CmdFromMap(m) - if cmd != nil { - cmdArr = append(cmdArr, cmd) - } - } - return nil + cmdArr := SelectMapsGen[*CmdType](tx, query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) + return lineArr, cmdArr, nil }) - if txErr != nil { - return nil, nil, txErr - } - return lineArr, cmdArr, nil } func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItemType, error) { - var rtn []*HistoryItemType - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTxRtn(ctx, func(tx *TxWrap) ([]*HistoryItemType, error) { query := `SELECT * FROM history WHERE historyid IN (SELECT value FROM json_each(?))` - marr := tx.SelectMaps(query, quickJsonArr(historyIds)) - for _, m := range marr { - hitem := HistoryItemFromMap(m) - if hitem != nil { - rtn = append(rtn, hitem) - } - } + rtn := SelectMapsGen[*HistoryItemType](tx, query, quickJsonArr(historyIds)) 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.SessionId, []string{hitem.LineId}) if err != nil { - return err + return nil, err } } } - return nil + return rtn, nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil } diff --git a/pkg/sstore/map.go b/pkg/sstore/map.go new file mode 100644 index 000000000..136445dc4 --- /dev/null +++ b/pkg/sstore/map.go @@ -0,0 +1,200 @@ +package sstore + +import ( + "context" + "fmt" + "reflect" + "strings" +) + +type DBMappable interface { + UseDBMap() +} + +type MapConverter interface { + ToMap() map[string]interface{} + FromMap(map[string]interface{}) bool +} + +type HasSimpleKey interface { + GetSimpleKey() string +} + +type MapConverterPtr[T any] interface { + MapConverter + *T +} + +type DBMappablePtr[T any] interface { + DBMappable + *T +} + +func FromMap[PT MapConverterPtr[T], T any](m map[string]any) PT { + if len(m) == 0 { + return nil + } + rtn := PT(new(T)) + ok := rtn.FromMap(m) + if !ok { + return nil + } + return rtn +} + +func GetMapGen[PT MapConverterPtr[T], T any](tx *TxWrap, query string, args ...interface{}) PT { + m := tx.GetMap(query, args...) + return FromMap[PT](m) +} + +func GetMappable[PT DBMappablePtr[T], T any](tx *TxWrap, query string, args ...interface{}) PT { + rtn := PT(new(T)) + m := tx.GetMap(query, args...) + if len(m) == 0 { + return nil + } + FromDBMap(rtn, m) + return rtn +} + +func SelectMapsGen[PT MapConverterPtr[T], T any](tx *TxWrap, query string, args ...interface{}) []PT { + var rtn []PT + marr := tx.SelectMaps(query, args...) + for _, m := range marr { + val := FromMap[PT](m) + if val != nil { + rtn = append(rtn, val) + } + } + return rtn +} + +func MakeGenMap[T HasSimpleKey](arr []T) map[string]T { + rtn := make(map[string]T) + for _, val := range arr { + rtn[val.GetSimpleKey()] = val + } + return rtn +} + +func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { + var rtn RT + txErr := WithTx(ctx, func(tx *TxWrap) error { + temp, err := fn(tx) + if err != nil { + return err + } + rtn = temp + return nil + }) + return rtn, txErr +} + +func WithTxRtn3[RT1 any, RT2 any](ctx context.Context, fn func(tx *TxWrap) (RT1, RT2, error)) (RT1, RT2, error) { + var rtn1 RT1 + var rtn2 RT2 + txErr := WithTx(ctx, func(tx *TxWrap) error { + temp1, temp2, err := fn(tx) + if err != nil { + return err + } + rtn1 = temp1 + rtn2 = temp2 + return nil + }) + return rtn1, rtn2, txErr +} + +func isStructType(rt reflect.Type) bool { + if rt.Kind() == reflect.Struct { + return true + } + if rt.Kind() == reflect.Pointer && rt.Elem().Kind() == reflect.Struct { + return true + } + return false +} + +func isByteArrayType(t reflect.Type) bool { + return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 +} + +func ToDBMap(v DBMappable) map[string]interface{} { + if v == nil { + return nil + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructToDBMap", v)) + } + rt := rv.Type() + m := make(map[string]interface{}) + numFields := rt.NumField() + for i := 0; i < numFields; i++ { + field := rt.Field(i) + fieldVal := rv.FieldByIndex(field.Index) + dbName := field.Tag.Get("dbmap") + if dbName == "" { + dbName = strings.ToLower(field.Name) + } + if dbName == "-" { + continue + } + if field.Type.Kind() == reflect.Slice { + m[dbName] = quickJsonArr(fieldVal.Interface()) + } else if isStructType(field.Type) { + m[dbName] = quickJson(fieldVal.Interface()) + } else { + m[dbName] = fieldVal.Interface() + } + } + return m +} + +func FromDBMap(v DBMappable, m map[string]interface{}) { + if v == nil { + panic("StructFromDBMap, v cannot be nil") + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructFromDBMap", v)) + } + rt := rv.Type() + numFields := rt.NumField() + for i := 0; i < numFields; i++ { + field := rt.Field(i) + fieldVal := rv.FieldByIndex(field.Index) + dbName := field.Tag.Get("dbmap") + if dbName == "" { + dbName = strings.ToLower(field.Name) + } + if dbName == "-" { + continue + } + if isByteArrayType(field.Type) { + barrVal := fieldVal.Addr().Interface() + quickSetBytes(barrVal.(*[]byte), m, dbName) + } else if field.Type.Kind() == reflect.Slice { + quickSetJsonArr(fieldVal.Addr().Interface(), m, dbName) + } else if isStructType(field.Type) { + quickSetJson(fieldVal.Addr().Interface(), m, dbName) + } else if field.Type.Kind() == reflect.String { + strVal := fieldVal.Addr().Interface() + quickSetStr(strVal.(*string), m, dbName) + } else if field.Type.Kind() == reflect.Int64 { + intVal := fieldVal.Addr().Interface() + quickSetInt64(intVal.(*int64), m, dbName) + } else if field.Type.Kind() == reflect.Bool { + boolVal := fieldVal.Addr().Interface() + quickSetBool(boolVal.(*bool), m, dbName) + } else { + panic(fmt.Sprintf("StructFromDBMap invalid field type %v in %T", fieldVal.Type(), v)) + } + } +} diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 45e39db50..76d056633 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -175,42 +175,15 @@ type ClientData struct { UserId string `json:"userid"` UserPrivateKeyBytes []byte `json:"-"` UserPublicKeyBytes []byte `json:"-"` - UserPrivateKey *ecdsa.PrivateKey `json:"-"` - UserPublicKey *ecdsa.PublicKey `json:"-"` + UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` + UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` ActiveSessionId string `json:"activesessionid"` WinSize ClientWinSizeType `json:"winsize"` ClientOpts ClientOptsType `json:"clientopts"` FeOpts FeOptsType `json:"feopts"` } -func (c *ClientData) ToMap() map[string]interface{} { - rtn := make(map[string]interface{}) - rtn["clientid"] = c.ClientId - rtn["userid"] = c.UserId - rtn["userprivatekeybytes"] = c.UserPrivateKeyBytes - rtn["userpublickeybytes"] = c.UserPublicKeyBytes - rtn["activesessionid"] = c.ActiveSessionId - rtn["winsize"] = quickJson(c.WinSize) - rtn["clientopts"] = quickJson(c.ClientOpts) - rtn["feopts"] = quickJson(c.FeOpts) - return rtn -} - -func ClientDataFromMap(m map[string]interface{}) *ClientData { - if len(m) == 0 { - return nil - } - var c ClientData - quickSetStr(&c.ClientId, m, "clientid") - quickSetStr(&c.UserId, m, "userid") - quickSetBytes(&c.UserPrivateKeyBytes, m, "userprivatekeybytes") - quickSetBytes(&c.UserPublicKeyBytes, m, "userpublickeybytes") - quickSetStr(&c.ActiveSessionId, m, "activesessionid") - quickSetJson(&c.WinSize, m, "winsize") - quickSetJson(&c.ClientOpts, m, "clientopts") - quickSetJson(&c.FeOpts, m, "feopts") - return &c -} +func (c ClientData) UseDBMap() {} type CloudAclType struct { UserId string `json:"userid"` @@ -244,6 +217,36 @@ type CloudSessionType struct { 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"` @@ -370,11 +373,7 @@ func (w *WindowType) ToMap() map[string]interface{} { return rtn } -func WindowFromMap(m map[string]interface{}) *WindowType { - if len(m) == 0 { - return nil - } - var w WindowType +func (w *WindowType) FromMap(m map[string]interface{}) bool { quickSetStr(&w.SessionId, m, "sessionid") quickSetStr(&w.WindowId, m, "windowid") quickSetStr(&w.CurRemote.OwnerId, m, "curremoteownerid") @@ -385,7 +384,7 @@ func WindowFromMap(m map[string]interface{}) *WindowType { quickSetStr(&w.OwnerId, m, "ownerid") quickSetStr(&w.ShareMode, m, "sharemode") quickSetJson(&w.ShareOpts, m, "shareopts") - return &w + return true } func (h *HistoryItemType) ToMap() map[string]interface{} { @@ -408,11 +407,7 @@ func (h *HistoryItemType) ToMap() map[string]interface{} { return rtn } -func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { - if len(m) == 0 { - return nil - } - var h HistoryItemType +func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { quickSetStr(&h.HistoryId, m, "historyid") quickSetInt64(&h.Ts, m, "ts") quickSetStr(&h.UserId, m, "userid") @@ -429,7 +424,7 @@ func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { quickSetBool(&h.IsMetaCmd, m, "ismetacmd") quickSetStr(&h.HistoryNum, m, "historynum") quickSetBool(&h.Incognito, m, "incognito") - return &h + return true } type ScreenOptsType struct { @@ -624,17 +619,13 @@ type StateDiff struct { Data []byte } -func StateDiffFromMap(m map[string]interface{}) *StateDiff { - if len(m) == 0 { - return nil - } - var sd StateDiff +func (sd *StateDiff) FromMap(m map[string]interface{}) bool { quickSetStr(&sd.DiffHash, m, "diffhash") quickSetInt64(&sd.Ts, m, "ts") quickSetStr(&sd.BaseHash, m, "basehash") quickSetJsonArr(&sd.DiffHashArr, m, "diffhasharr") quickSetBytes(&sd.Data, m, "data") - return &sd + return true } func (sd *StateDiff) ToMap() map[string]interface{} { @@ -659,11 +650,7 @@ func FeStateFromShellState(state *packet.ShellState) *FeStateType { return &FeStateType{Cwd: state.Cwd} } -func RIFromMap(m map[string]interface{}) *RemoteInstance { - if len(m) == 0 { - return nil - } - var ri RemoteInstance +func (ri *RemoteInstance) FromMap(m map[string]interface{}) bool { quickSetStr(&ri.RIId, m, "riid") quickSetStr(&ri.Name, m, "name") quickSetStr(&ri.SessionId, m, "sessionid") @@ -673,7 +660,7 @@ func RIFromMap(m map[string]interface{}) *RemoteInstance { quickSetJson(&ri.FeState, m, "festate") quickSetStr(&ri.StateBaseHash, m, "statebasehash") quickSetJsonArr(&ri.StateDiffHashArr, m, "statediffhasharr") - return &ri + return true } func (ri *RemoteInstance) ToMap() map[string]interface{} { @@ -731,16 +718,12 @@ func (p *PlaybookType) ToMap() map[string]interface{} { return rtn } -func PlaybookFromMap(m map[string]interface{}) *PlaybookType { - if len(m) == 0 { - return nil - } - var p PlaybookType +func (p *PlaybookType) FromMap(m map[string]interface{}) bool { quickSetStr(&p.PlaybookId, m, "playbookid") quickSetStr(&p.PlaybookName, m, "playbookname") quickSetStr(&p.Description, m, "description") quickSetJsonArr(&p.Entries, m, "entries") - return &p + return true } // reorders p.Entries to match p.EntryIds @@ -800,6 +783,10 @@ type BookmarkType struct { Remove bool `json:"remove,omitempty"` } +func (bm *BookmarkType) GetSimpleKey() string { + return bm.BookmarkId +} + func (bm *BookmarkType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["bookmarkid"] = bm.BookmarkId @@ -811,18 +798,14 @@ func (bm *BookmarkType) ToMap() map[string]interface{} { return rtn } -func BookmarkFromMap(m map[string]interface{}) *BookmarkType { - if len(m) == 0 { - return nil - } - var bm BookmarkType +func (bm *BookmarkType) FromMap(m map[string]interface{}) bool { quickSetStr(&bm.BookmarkId, m, "bookmarkid") quickSetInt64(&bm.CreatedTs, m, "createdts") quickSetStr(&bm.Alias, m, "alias") quickSetStr(&bm.CmdStr, m, "cmdstr") quickSetStr(&bm.Description, m, "description") quickSetJsonArr(&bm.Tags, m, "tags") - return &bm + return true } type ResolveItem struct { @@ -925,11 +908,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { return rtn } -func RemoteFromMap(m map[string]interface{}) *RemoteType { - if len(m) == 0 { - return nil - } - var r RemoteType +func (r *RemoteType) FromMap(m map[string]interface{}) bool { quickSetStr(&r.RemoteId, m, "remoteid") quickSetStr(&r.PhysicalId, m, "physicalid") quickSetStr(&r.RemoteType, m, "remotetype") @@ -946,7 +925,7 @@ func RemoteFromMap(m map[string]interface{}) *RemoteType { quickSetBool(&r.Archived, m, "archived") quickSetInt64(&r.RemoteIdx, m, "remoteidx") quickSetBool(&r.Local, m, "local") - return &r + return true } func (cmd *CmdType) ToMap() map[string]interface{} { @@ -972,11 +951,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { return rtn } -func CmdFromMap(m map[string]interface{}) *CmdType { - if len(m) == 0 { - return nil - } - var cmd CmdType +func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetStr(&cmd.SessionId, m, "sessionid") quickSetStr(&cmd.CmdId, m, "cmdid") quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") @@ -995,7 +970,7 @@ func CmdFromMap(m map[string]interface{}) *CmdType { quickSetBool(&cmd.RtnState, m, "rtnstate") quickSetStr(&cmd.RtnStatePtr.BaseHash, m, "rtnbasehash") quickSetJsonArr(&cmd.RtnStatePtr.DiffHashArr, m, "rtndiffhasharr") - return &cmd + return true } func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId string, renderer string) *LineType { @@ -1116,7 +1091,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { if session != nil { return session, nil } - _, err = InsertSessionWithName(ctx, DefaultSessionName, true) + _, err = InsertSessionWithName(ctx, DefaultSessionName, ShareModeLocal, true) if err != nil { return nil, err } @@ -1147,32 +1122,29 @@ func createClientData(tx *TxWrap) error { } query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` - tx.NamedExec(query, c.ToMap()) + tx.NamedExec(query, ToDBMap(c)) log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } func EnsureClientData(ctx context.Context) (*ClientData, error) { - var rtn ClientData - err := WithTx(ctx, func(tx *TxWrap) error { + rtn, err := WithTxRtn(ctx, func(tx *TxWrap) (*ClientData, error) { query := `SELECT count(*) FROM client` count := tx.GetInt(query) if count > 1 { - return fmt.Errorf("invalid client database, multiple (%d) rows in client table", count) + return nil, fmt.Errorf("invalid client database, multiple (%d) rows in client table", count) } if count == 0 { createErr := createClientData(tx) if createErr != nil { - return createErr + return nil, createErr } } - m := tx.GetMap(`SELECT * FROM client`) - cdata := ClientDataFromMap(m) + cdata := GetMappable[*ClientData](tx, `SELECT * FROM client`) if cdata == nil { - return fmt.Errorf("no client data found") + return nil, fmt.Errorf("no client data found") } - rtn = *cdata - return nil + return cdata, nil }) if err != nil { return nil, err @@ -1196,7 +1168,7 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { if !ok { return nil, fmt.Errorf("invalid client data, wrong public key type: %T", pubKey) } - return &rtn, nil + return rtn, nil } func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { From 34d4962b7a4dbd51893a32898d5a2a74329cc207 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 12 Mar 2023 14:42:18 -0700 Subject: [PATCH 289/397] more logging, backup db on migration, fix issue with dbmapper (writing byte arrays) --- cmd/main-server.go | 4 ++- pkg/sstore/map.go | 4 ++- pkg/sstore/migrate.go | 58 ++++++++++++++++++++++++++++++++++++++----- pkg/sstore/sstore.go | 10 ++++++-- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index b582908ec..1d3a1caf1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -449,9 +449,10 @@ func stdinReadWatch() { for { _, err := os.Stdin.Read(buf) if err != nil { - log.Printf("stdin closed/error, shutting down: %v\n", err) + log.Printf("[prompt] stdin closed/error, shutting down: %v\n", err) sendTelemetryWrapper() time.Sleep(1 * time.Second) + log.Printf("[prompt] *** shutting down local server\n") syscall.Kill(syscall.Getpid(), syscall.SIGINT) break } @@ -473,6 +474,7 @@ func main() { } scHomeDir := scbase.GetPromptHomeDir() + log.Printf("[prompt] *** starting local server\n") log.Printf("[prompt] local server version %s+%s\n", scbase.PromptVersion, scbase.BuildTime) log.Printf("[prompt] homedir = %q\n", scHomeDir) diff --git a/pkg/sstore/map.go b/pkg/sstore/map.go index 136445dc4..6f97600e2 100644 --- a/pkg/sstore/map.go +++ b/pkg/sstore/map.go @@ -143,7 +143,9 @@ func ToDBMap(v DBMappable) map[string]interface{} { if dbName == "-" { continue } - if field.Type.Kind() == reflect.Slice { + if isByteArrayType(field.Type) { + m[dbName] = fieldVal.Interface() + } else if field.Type.Kind() == reflect.Slice { m[dbName] = quickJsonArr(fieldVal.Interface()) } else if isStructType(field.Type) { m[dbName] = quickJson(fieldVal.Interface()) diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 5dcff1c1c..1326b90f9 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -2,7 +2,9 @@ package sstore import ( "fmt" + "io" "log" + "os" "strconv" "time" @@ -15,27 +17,71 @@ import ( "github.com/golang-migrate/migrate/v4" ) +const MaxMigration = 8 +const MigratePrimaryScreenVersion = 9 + func MakeMigrate() (*migrate.Migrate, error) { fsVar, err := iofs.New(sh2db.MigrationFS, "migrations") if err != nil { return nil, fmt.Errorf("opening iofs: %w", err) } // migrationPathUrl := fmt.Sprintf("file://%s", path.Join(wd, "db", "migrations")) - dbUrl := fmt.Sprintf("sqlite3://%s", GetSessionDBName()) + dbUrl := fmt.Sprintf("sqlite3://%s", GetDBName()) m, err := migrate.NewWithSourceInstance("iofs", fsVar, dbUrl) // m, err := migrate.New(migrationPathUrl, dbUrl) if err != nil { - return nil, fmt.Errorf("making migration db[%s]: %w", GetSessionDBName(), err) + return nil, fmt.Errorf("making migration db[%s]: %w", GetDBName(), err) } return m, nil } +func copyFile(srcFile string, dstFile string) error { + if srcFile == dstFile { + return fmt.Errorf("cannot copy %s to itself", srcFile) + } + srcFd, err := os.Open(srcFile) + if err != nil { + return fmt.Errorf("cannot open %s: %v", err) + } + defer srcFd.Close() + dstFd, err := os.OpenFile(dstFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("cannot open destination file %s: %v", err) + } + _, err = io.Copy(dstFd, srcFd) + if err != nil { + dstFd.Close() + return fmt.Errorf("error copying file: %v", err) + } + return dstFd.Close() +} + func MigrateUp() error { m, err := MakeMigrate() if err != nil { return err } - err = m.Up() + curVersion, dirty, err := m.Version() + if err == migrate.ErrNilVersion { + curVersion = 0 + err = nil + } + if dirty { + return fmt.Errorf("cannot migrate up, database is dirty") + } + if err != nil { + return fmt.Errorf("cannot get current migration version: %v", err) + } + if curVersion >= MaxMigration { + return nil + } + log.Printf("[db] migrating from %d to %d\n", curVersion, MaxMigration) + log.Printf("[db] backing up database %s to %s\n", DBFileName, DBFileNameBackup) + err = copyFile(GetDBName(), GetDBBackupName()) + if err != nil { + return fmt.Errorf("error creating database backup: %v", err) + } + err = m.Migrate(MaxMigration) if err != nil { return err } @@ -100,17 +146,17 @@ func MigratePrintVersion() error { func MigrateCommandOpts(opts []string) error { var err error if opts[0] == "--migrate-up" { - fmt.Printf("migrate-up %v\n", GetSessionDBName()) + fmt.Printf("migrate-up %v\n", GetDBName()) time.Sleep(3 * time.Second) err = MigrateUp() } else if opts[0] == "--migrate-down" { - fmt.Printf("migrate-down %v\n", GetSessionDBName()) + fmt.Printf("migrate-down %v\n", GetDBName()) time.Sleep(3 * time.Second) err = MigrateDown() } else if opts[0] == "--migrate-goto" { n, err := strconv.Atoi(opts[1]) if err == nil { - fmt.Printf("migrate-goto %v => %d\n", GetSessionDBName(), n) + fmt.Printf("migrate-goto %v => %d\n", GetDBName(), n) time.Sleep(3 * time.Second) err = MigrateGoto(uint(n)) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 76d056633..7fa6633f3 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -31,6 +31,7 @@ const LineTypeCmd = "cmd" const LineTypeText = "text" const LineNoHeight = -1 const DBFileName = "prompt.db" +const DBFileNameBackup = "backup.prompt.db" const DefaultSessionName = "default" const DefaultWindowName = "default" @@ -83,11 +84,16 @@ var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error -func GetSessionDBName() string { +func GetDBName() string { scHome := scbase.GetPromptHomeDir() return path.Join(scHome, DBFileName) } +func GetDBBackupName() string { + scHome := scbase.GetPromptHomeDir() + return path.Join(scHome, DBFileNameBackup) +} + func IsValidConnectMode(mode string) bool { return mode == ConnectModeStartup || mode == ConnectModeAuto || mode == ConnectModeManual } @@ -99,7 +105,7 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { globalDBLock.Lock() defer globalDBLock.Unlock() if globalDB == nil && globalDBErr == nil { - dbName := GetSessionDBName() + dbName := GetDBName() globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) if globalDBErr != nil { globalDBErr = fmt.Errorf("opening db[%s]: %w", dbName, globalDBErr) From 861cf8d12659090896048cdc1a575cc9e50156e7 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 12 Mar 2023 15:16:49 -0700 Subject: [PATCH 290/397] fix shutdown loop -- have to ignore SIGHUP --- cmd/main-server.go | 17 ++++++++++++++++- pkg/sstore/sstore.go | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 1d3a1caf1..d9dfca764 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "os" + "os/signal" "runtime/debug" "strconv" "strings" @@ -451,8 +452,10 @@ func stdinReadWatch() { if err != nil { log.Printf("[prompt] stdin closed/error, shutting down: %v\n", err) sendTelemetryWrapper() - time.Sleep(1 * time.Second) + log.Printf("[prompt] closing db connection\n") + sstore.CloseDB() log.Printf("[prompt] *** shutting down local server\n") + time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) break } @@ -461,6 +464,17 @@ func stdinReadWatch() { syscall.Kill(syscall.Getpid(), syscall.SIGKILL) } +// ignore SIGHUP +func installSignalHandlers() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGHUP) + go func() { + for sig := range sigCh { + fmt.Printf("[prompt] got signal %v (ignoring)\n", sig) + } + }() +} + func main() { scbase.BuildTime = BuildTime @@ -537,6 +551,7 @@ func main() { if err != nil { log.Printf("[error] updating activity: %v\n", err) } + installSignalHandlers() go telemetryLoop() go stdinReadWatch() go runWebSocketServer() diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 7fa6633f3..9593b2351 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -117,6 +117,19 @@ func GetDB(ctx context.Context) (*sqlx.DB, error) { return globalDB, globalDBErr } +func CloseDB() { + globalDBLock.Lock() + defer globalDBLock.Unlock() + if globalDB == nil { + return + } + err := globalDB.Close() + if err != nil { + log.Printf("[db] error closing database: %v\n", err) + } + globalDB = nil +} + type ClientWinSizeType struct { Width int `json:"width"` Height int `json:"height"` From ee2fd8e98fa3ed32e8994a36348ff629b569d14e Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 12 Mar 2023 15:24:53 -0700 Subject: [PATCH 291/397] let SIGHUP or stdin close trigger a shutdown --- cmd/main-server.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index d9dfca764..795d178b8 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -52,6 +52,7 @@ var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string var BuildTime = "0" +var shutdownOnce sync.Once type ClientActiveState struct { Fg bool `json:"fg"` @@ -450,18 +451,10 @@ func stdinReadWatch() { for { _, err := os.Stdin.Read(buf) if err != nil { - log.Printf("[prompt] stdin closed/error, shutting down: %v\n", err) - sendTelemetryWrapper() - log.Printf("[prompt] closing db connection\n") - sstore.CloseDB() - log.Printf("[prompt] *** shutting down local server\n") - time.Sleep(1 * time.Second) - syscall.Kill(syscall.Getpid(), syscall.SIGINT) + doShutdown(fmt.Sprintf("stdin closed/error (%v)", err)) break } } - time.Sleep(10 * time.Second) - syscall.Kill(syscall.Getpid(), syscall.SIGKILL) } // ignore SIGHUP @@ -470,11 +463,26 @@ func installSignalHandlers() { signal.Notify(sigCh, syscall.SIGHUP) go func() { for sig := range sigCh { - fmt.Printf("[prompt] got signal %v (ignoring)\n", sig) + doShutdown(fmt.Sprintf("got signal %v", sig)) + break } }() } +func doShutdown(reason string) { + shutdownOnce.Do(func() { + log.Printf("[prompt] local server %v, start shutdown\n", reason) + sendTelemetryWrapper() + log.Printf("[prompt] closing db connection\n") + sstore.CloseDB() + log.Printf("[prompt] *** shutting down local server\n") + time.Sleep(1 * time.Second) + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + time.Sleep(5 * time.Second) + syscall.Kill(syscall.Getpid(), syscall.SIGKILL) + }) +} + func main() { scbase.BuildTime = BuildTime From e154aacaad2e1a97b6c6a0d4e3f97ffef3fbff6e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 01:52:30 -0700 Subject: [PATCH 292/397] checkpoint, backend changes for consolidating window and screen_window to screen --- cmd/main-server.go | 21 +- db/migrations/000009_screenprimary.down.sql | 3 + db/migrations/000009_screenprimary.up.sql | 56 ++++ pkg/cmdrunner/cmdrunner.go | 266 +++++++-------- pkg/mapqueue/mapqueue.go | 99 ++++++ pkg/remote/remote.go | 4 +- pkg/scws/scws.go | 17 +- pkg/sstore/dbops.go | 337 ++++++++------------ pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 188 ++++------- pkg/sstore/updatebus.go | 35 +- 11 files changed, 519 insertions(+), 509 deletions(-) create mode 100644 db/migrations/000009_screenprimary.down.sql create mode 100644 db/migrations/000009_screenprimary.up.sql create mode 100644 pkg/mapqueue/mapqueue.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 795d178b8..9092626c2 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -193,25 +193,20 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) { return } -// params: sessionid, windowid -func HandleGetWindow(w http.ResponseWriter, r *http.Request) { +// params: screenid +func HandleGetFullScreen(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") - windowId := qvals.Get("windowid") - if _, err := uuid.Parse(sessionId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid sessionid: %w", err)) + screenId := qvals.Get("screenid") + if _, err := uuid.Parse(screenId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid screenid: %w", err)) return } - if _, err := uuid.Parse(windowId); err != nil { - WriteJsonError(w, fmt.Errorf("invalid windowid: %w", err)) - return - } - window, err := sstore.GetWindowById(r.Context(), sessionId, windowId) + screen, err := sstore.GetFullScreenById(r.Context(), screenId) if err != nil { WriteJsonError(w, err) return } - WriteJsonSuccess(w, window) + WriteJsonSuccess(w, screen) return } @@ -567,7 +562,7 @@ func main() { gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty)) gr.HandleFunc("/api/rtnstate", AuthKeyWrap(HandleRtnState)) - gr.HandleFunc("/api/get-window", AuthKeyWrap(HandleGetWindow)) + gr.HandleFunc("/api/get-full-screen", AuthKeyWrap(HandleGetFullScreen)) gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) diff --git a/db/migrations/000009_screenprimary.down.sql b/db/migrations/000009_screenprimary.down.sql new file mode 100644 index 000000000..1c2aaf962 --- /dev/null +++ b/db/migrations/000009_screenprimary.down.sql @@ -0,0 +1,3 @@ +-- invalid, will throw an error, cannot migrate down past 9 +SELECT x; + diff --git a/db/migrations/000009_screenprimary.up.sql b/db/migrations/000009_screenprimary.up.sql new file mode 100644 index 000000000..48efa4f84 --- /dev/null +++ b/db/migrations/000009_screenprimary.up.sql @@ -0,0 +1,56 @@ +CREATE TABLE new_screen ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + windowid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + screenidx int NOT NULL, + screenopts json NOT NULL, + ownerid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + curremoteownerid varchar(36) NOT NULL, + curremoteid varchar(36) NOT NULL, + curremotename varchar(50) NOT NULL, + nextlinenum int NOT NULL, + selectedline int NOT NULL, + anchor json NOT NULL, + focustype varchar(12) NOT NULL, + archived boolean NOT NULL, + archivedts bigint NOT NULL, + PRIMARY KEY (sessionid, screenid) +); + +INSERT INTO new_screen +SELECT + s.sessionid, + s.screenid, + w.windowid, + s.name, + s.screenidx, + json_patch(s.screenopts, w.winopts), + s.ownerid, + s.sharemode, + w.curremoteownerid, + w.curremoteid, + w.curremotename, + w.nextlinenum, + sw.selectedline, + sw.anchor, + sw.focustype, + s.archived, + s.archivedts +FROM + screen s, + screen_window sw, + window w +WHERE + s.screenid = sw.screenid + AND sw.windowid = w.windowid +; + +DROP TABLE screen; +DROP TABLE screen_window; +DROP TABLE window; + +ALTER TABLE new_screen RENAME TO screen; + + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 68e4485cf..3e7470b5a 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -151,7 +151,6 @@ func init() { registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) registerCmdFn("remote:reset", RemoteResetCommand) - registerCmdFn("sw:set", SwSetCommand) registerCmdFn("sw:resize", SwResizeCommand) // sw:resize @@ -458,7 +457,7 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/screen:archive cannot re-open screen: %v", err) } - screen, err := sstore.GetScreenById(ctx, ids.SessionId, screenId) + screen, err := sstore.GetScreenById(ctx, screenId) if err != nil { return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err) } @@ -512,17 +511,17 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return nil, err } var varsUpdated []string + var setNonAnchor bool // anchor does not receive an update + updateMap := make(map[string]interface{}) if pk.Kwargs["name"] != "" { newName := pk.Kwargs["name"] err = validateName(newName, "screen") if err != nil { return nil, err } - err = sstore.SetScreenName(ctx, ids.SessionId, ids.ScreenId, newName) - if err != nil { - return nil, fmt.Errorf("setting screen name: %v", err) - } + updateMap[sstore.ScreenField_Name] = newName varsUpdated = append(varsUpdated, "name") + setNonAnchor = true } if pk.Kwargs["tabcolor"] != "" { color := pk.Kwargs["tabcolor"] @@ -530,39 +529,67 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if err != nil { return nil, err } - screenObj, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) - if err != nil { - return nil, err - } - opts := screenObj.ScreenOpts - if opts == nil { - opts = &sstore.ScreenOptsType{} - } - opts.TabColor = color - err = sstore.SetScreenOpts(ctx, ids.SessionId, ids.ScreenId, opts) - if err != nil { - return nil, fmt.Errorf("setting screen opts: %v", err) - } + updateMap[sstore.ScreenField_TabColor] = color varsUpdated = append(varsUpdated, "tabcolor") + setNonAnchor = true } if pk.Kwargs["pos"] != "" { - varsUpdated = append(varsUpdated, "pos") + setNonAnchor = true + } + if pk.Kwargs["focus"] != "" { + focusVal := pk.Kwargs["focus"] + if focusVal != sstore.ScreenFocusInput && focusVal != sstore.ScreenFocusCmd && focusVal != sstore.ScreenFocusCmdFg { + return nil, fmt.Errorf("/screen:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.ScreenFocusInput, sstore.ScreenFocusCmd, sstore.ScreenFocusCmdFg}, "or", false)) + } + updateMap[sstore.ScreenField_Focus] = focusVal + setNonAnchor = true + } + if pk.Kwargs["line"] != "" { + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("/screen:set cannot get screen: %v", err) + } + var selectedLineStr string + if screen.SelectedLine > 0 { + selectedLineStr = strconv.Itoa(int(screen.SelectedLine)) + } + ritem, err := resolveLine(ctx, screen.SessionId, screen.WindowId, pk.Kwargs["line"], selectedLineStr) + if err != nil { + return nil, fmt.Errorf("/screen:set error resolving line: %v", err) + } + if ritem == nil { + return nil, fmt.Errorf("/screen:set could not resolve line %q", pk.Kwargs["line"]) + } + setNonAnchor = true + updateMap[sstore.ScreenField_SelectedLine] = ritem.Num + } + if pk.Kwargs["anchor"] != "" { + m := swAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"]) + if m == nil { + return nil, fmt.Errorf("/screen:set invalid anchor argument (must be [line] or [line]:[offset])") + } + anchorLine, _ := strconv.Atoi(m[1]) + updateMap[sstore.ScreenField_AnchorLine] = anchorLine + if m[2] != "" { + anchorOffset, _ := strconv.Atoi(m[2]) + updateMap[sstore.ScreenField_AnchorOffset] = anchorOffset + } else { + updateMap[sstore.ScreenField_AnchorOffset] = 0 + } } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor"}, "or", false)) + return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line"}, "or", false)) } - screenObj, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) + screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { - return nil, err + return nil, fmt.Errorf("error updating screen: %v", err) } - bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) - if err != nil { - return nil, fmt.Errorf("/screen:set cannot retrieve session: %v", err) + if !setNonAnchor { + return nil, nil } - bareSession.Screens = append(bareSession.Screens, screenObj) update := sstore.ModelUpdate{ - Sessions: []*sstore.SessionType{bareSession}, + Screens: []*sstore.ScreenType{screen}, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)), TimeoutMs: 2000, @@ -593,69 +620,8 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor var swAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") -func SwSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) - if err != nil { - return nil, fmt.Errorf("/sw:set cannot resolve current screen-window: %w", err) - } - var setNonST bool // scrolltop does not receive an update - updateMap := make(map[string]interface{}) - if pk.Kwargs["anchor"] != "" { - m := swAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"]) - if m == nil { - return nil, fmt.Errorf("/sw:set invalid anchor argument (must be [line] or [line]:[offset])") - } - anchorLine, _ := strconv.Atoi(m[1]) - updateMap[sstore.SWField_AnchorLine] = anchorLine - if m[2] != "" { - anchorOffset, _ := strconv.Atoi(m[2]) - updateMap[sstore.SWField_AnchorOffset] = anchorOffset - } else { - updateMap[sstore.SWField_AnchorOffset] = 0 - } - } - if pk.Kwargs["focus"] != "" { - focusVal := pk.Kwargs["focus"] - if focusVal != sstore.SWFocusInput && focusVal != sstore.SWFocusCmd && focusVal != sstore.SWFocusCmdFg { - return nil, fmt.Errorf("/sw:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.SWFocusInput, sstore.SWFocusCmd, sstore.SWFocusCmdFg}, "or", false)) - } - updateMap[sstore.SWField_Focus] = focusVal - setNonST = true - } - if pk.Kwargs["line"] != "" { - sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) - if err != nil { - return nil, fmt.Errorf("/sw:set cannot get screen-window: %v", err) - } - var selectedLineStr string - if sw.SelectedLine > 0 { - selectedLineStr = strconv.Itoa(sw.SelectedLine) - } - ritem, err := resolveLine(ctx, ids.SessionId, ids.WindowId, pk.Kwargs["line"], selectedLineStr) - if err != nil { - return nil, fmt.Errorf("/sw:set error resolving line: %v", err) - } - if ritem == nil { - return nil, fmt.Errorf("/sw:set could not resolve line %q", pk.Kwargs["line"]) - } - setNonST = true - updateMap[sstore.SWField_SelectedLine] = ritem.Num - } - if len(updateMap) == 0 { - return nil, fmt.Errorf("/sw:set no updates, can set %s", formatStrs([]string{"line", "scrolltop", "focus"}, "or", false)) - } - sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) - if err != nil { - return nil, fmt.Errorf("/sw:set failed to update: %v", err) - } - if !setNonST { - return nil, nil - } - return sstore.ModelUpdate{ScreenWindows: []*sstore.ScreenWindowType{sw}}, nil -} - func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -669,7 +635,7 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -683,7 +649,7 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke } func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -696,7 +662,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -946,7 +912,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -976,7 +942,7 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -1039,29 +1005,20 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, err } - screen, err := sstore.GetScreenById(ctx, ids.SessionId, ids.ScreenId) - if err != nil { - return nil, fmt.Errorf("error retrieving screen: %v", err) - } localRemote := remote.GetLocalRemote() if localRemote == nil { return nil, fmt.Errorf("error getting local remote (not found)") } rptr := sstore.RemotePtrType{RemoteId: localRemote.RemoteId} - var windows []*sstore.WindowType sessionUpdate := &sstore.SessionType{SessionId: ids.SessionId} - for _, sw := range screen.Windows { - ris, err := sstore.WindowReset(ctx, ids.SessionId, sw.WindowId) - if err != nil { - return nil, fmt.Errorf("error resetting screen window: %v", err) - } - sessionUpdate.Remotes = append(sessionUpdate.Remotes, ris...) - err = sstore.UpdateCurRemote(ctx, ids.SessionId, sw.WindowId, rptr) - if err != nil { - return nil, fmt.Errorf("cannot reset window remote back to local: %w", err) - } - winUpdate := &sstore.WindowType{SessionId: ids.SessionId, WindowId: sw.WindowId, CurRemote: rptr} - windows = append(windows, winUpdate) + ris, err := sstore.ScreenReset(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("error resetting screen window: %v", err) + } + sessionUpdate.Remotes = append(sessionUpdate.Remotes, ris...) + err = sstore.UpdateCurRemote(ctx, ids.ScreenId, rptr) + if err != nil { + return nil, fmt.Errorf("cannot reset screen remote back to local: %w", err) } outputStr := "reset screen state (all remote state reset)" cmd, err := makeStaticCmd(ctx, "screen:reset", ids, pk.GetRawStr(), []byte(outputStr)) @@ -1075,13 +1032,12 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, err } update.Interactive = pk.Interactive - update.Windows = windows update.Sessions = []*sstore.SessionType{sessionUpdate} return update, nil } func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) if err != nil { return nil, err } @@ -1091,13 +1047,16 @@ func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName) localRemote := remote.GetLocalRemote() - if localRemote != nil { - update.Windows = []*sstore.WindowType{&sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - CurRemote: sstore.RemotePtrType{RemoteId: localRemote.GetRemoteId()}, - }} + rptr := sstore.RemotePtrType{RemoteId: localRemote.GetRemoteId()} + err = sstore.UpdateCurRemote(ctx, ids.ScreenId, rptr) + if err != nil { + return nil, fmt.Errorf("cannot switch remote back to local: %w", err) } + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("cannot get updated screen: %w", err) + } + update.Screens = []*sstore.ScreenType{screen} return update, nil } @@ -1173,7 +1132,7 @@ func GetFullRemoteDisplayName(rptr *sstore.RemotePtrType, rstate *remote.RemoteR } func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err) } @@ -1191,7 +1150,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if rstate.Archived { return nil, fmt.Errorf("/%s error: remote %q cannot switch to archived remote", GetCmdStr(pk), newRemote) } - err = sstore.UpdateCurRemote(ctx, ids.SessionId, ids.WindowId, *rptr) + err = sstore.UpdateCurRemote(ctx, ids.ScreenId, *rptr) if err != nil { return nil, fmt.Errorf("/%s error: cannot update curremote: %w", GetCmdStr(pk), err) } @@ -1206,11 +1165,6 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update.Windows = []*sstore.WindowType{&sstore.WindowType{ - SessionId: ids.SessionId, - WindowId: ids.WindowId, - CurRemote: *rptr, - }} update.Interactive = pk.Interactive return update, nil } @@ -1252,27 +1206,27 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re if err != nil { return nil, err } - sw, err := sstore.GetScreenWindowByIds(ctx, ids.SessionId, ids.ScreenId, ids.WindowId) + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) if err != nil { // ignore error here, because the command has already run (nothing to do) - log.Printf("%s error getting screen-window: %v\n", metaCmd, err) + log.Printf("%s error getting screen: %v\n", metaCmd, err) } - if sw != nil { + if screen != nil { updateMap := make(map[string]interface{}) - updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum + updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum if shouldFocus { - updateMap[sstore.SWField_Focus] = sstore.SWFocusCmdFg + updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmdFg } - sw, err = sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + screen, err = sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { // ignore error again (nothing to do) - log.Printf("%s error updating screen-window selected line: %v\n", metaCmd, err) + log.Printf("%s error updating screen selected line: %v\n", metaCmd, err) } } update := &sstore.ModelUpdate{ - Line: rtnLine, - Cmd: cmd, - ScreenWindows: []*sstore.ScreenWindowType{sw}, + Line: rtnLine, + Cmd: cmd, + Screens: []*sstore.ScreenType{screen}, } updateHistoryContext(ctx, rtnLine, cmd) return update, nil @@ -1374,7 +1328,7 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str if !packet.IsValidCompGenType(compType) { return nil, false, fmt.Errorf("/_compgen invalid type '%s'", compType) } - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window|R_RemoteConnected) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) if err != nil { return nil, false, fmt.Errorf("/_compgen error: %w", err) } @@ -1447,7 +1401,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, fmt.Errorf("/comment error: %w", err) } @@ -1461,14 +1415,14 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } updateHistoryContext(ctx, rtnLine, nil) updateMap := make(map[string]interface{}) - updateMap[sstore.SWField_SelectedLine] = rtnLine.LineNum - updateMap[sstore.SWField_Focus] = sstore.SWFocusInput - sw, err := sstore.UpdateScreenWindow(ctx, ids.SessionId, ids.ScreenId, ids.WindowId, updateMap) + updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum + updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput + screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { // ignore error again (nothing to do) log.Printf("/comment error updating screen-window selected line: %v\n", err) } - update := sstore.ModelUpdate{Line: rtnLine, ScreenWindows: []*sstore.ScreenWindowType{sw}} + update := sstore.ModelUpdate{Line: rtnLine, Screens: []*sstore.ScreenType{screen}} return update, nil } @@ -1809,22 +1763,22 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return nil, err } if resolveBool(pk.Kwargs["purge"], false) { - update, err := sstore.PurgeWindowLines(ctx, ids.SessionId, ids.WindowId) + update, err := sstore.PurgeScreenLines(ctx, ids.ScreenId) if err != nil { - return nil, fmt.Errorf("clearing window: %v", err) + return nil, fmt.Errorf("clearing screen: %v", err) } update.Info = &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("window cleared (all lines purged)"), + InfoMsg: fmt.Sprintf("screen cleared (all lines purged)"), TimeoutMs: 2000, } return update, nil } else { - update, err := sstore.ArchiveWindowLines(ctx, ids.SessionId, ids.WindowId) + update, err := sstore.ArchiveScreenLines(ctx, ids.ScreenId) if err != nil { - return nil, fmt.Errorf("clearing window: %v", err) + return nil, fmt.Errorf("clearing screen: %v", err) } update.Info = &sstore.InfoMsgType{ - InfoMsg: fmt.Sprintf("window cleared"), + InfoMsg: fmt.Sprintf("screen cleared"), TimeoutMs: 2000, } return update, nil @@ -2121,11 +2075,11 @@ func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:view invalid screen arg: %v", err) } - screen, err := sstore.GetScreenById(ctx, sessionId, screenRItem.Id) + screen, err := sstore.GetScreenById(ctx, screenRItem.Id) if err != nil { return nil, fmt.Errorf("/line:view could not get screen: %v", err) } - lineRItem, err := resolveLine(ctx, sessionId, screen.ActiveWindowId, lineArg, "") + lineRItem, err := resolveLine(ctx, sessionId, screen.WindowId, lineArg, "") if err != nil { return nil, fmt.Errorf("/line:view invalid line arg: %v", err) } @@ -2134,14 +2088,14 @@ func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, err } updateMap := make(map[string]interface{}) - updateMap[sstore.SWField_SelectedLine] = lineRItem.Num - updateMap[sstore.SWField_AnchorLine] = lineRItem.Num - updateMap[sstore.SWField_AnchorOffset] = 0 - sw, err := sstore.UpdateScreenWindow(ctx, sessionId, screenRItem.Id, screen.ActiveWindowId, updateMap) + updateMap[sstore.ScreenField_SelectedLine] = lineRItem.Num + updateMap[sstore.ScreenField_AnchorLine] = lineRItem.Num + updateMap[sstore.ScreenField_AnchorOffset] = 0 + screen, err = sstore.UpdateScreen(ctx, screenRItem.Id, updateMap) if err != nil { return nil, err } - update.ScreenWindows = []*sstore.ScreenWindowType{sw} + update.Screens = []*sstore.ScreenType{screen} return update, nil } diff --git a/pkg/mapqueue/mapqueue.go b/pkg/mapqueue/mapqueue.go new file mode 100644 index 000000000..aa5d4d9ac --- /dev/null +++ b/pkg/mapqueue/mapqueue.go @@ -0,0 +1,99 @@ +package mapqueue + +import ( + "fmt" + "log" + "runtime/debug" + "sync" +) + +type MQEntry struct { + Lock *sync.Mutex + Running bool + Queue chan func() +} + +type MapQueue struct { + Lock *sync.Mutex + M map[string]*MQEntry + QueueSize int +} + +func MakeMapQueue(queueSize int) *MapQueue { + rtn := &MapQueue{ + Lock: &sync.Mutex{}, + M: make(map[string]*MQEntry), + QueueSize: queueSize, + } + return rtn +} + +func (mq *MapQueue) getEntry(id string) *MQEntry { + mq.Lock.Lock() + defer mq.Lock.Unlock() + entry := mq.M[id] + if entry == nil { + entry = &MQEntry{ + Lock: &sync.Mutex{}, + Running: false, + Queue: make(chan func(), mq.QueueSize), + } + mq.M[id] = entry + } + return entry +} + +func (entry *MQEntry) add(fn func()) error { + select { + case entry.Queue <- fn: + break + default: + return fmt.Errorf("input queue full") + } + entry.tryRun() + return nil +} + +func runFn(fn func()) { + defer func() { + r := recover() + if r == nil { + return + } + log.Printf("[error] panic in MQEntry runFn: %v\n", r) + debug.PrintStack() + return + }() + fn() +} + +func (entry *MQEntry) tryRun() { + entry.Lock.Lock() + defer entry.Lock.Unlock() + if entry.Running { + return + } + if len(entry.Queue) > 0 { + entry.Running = true + go entry.run() + } +} + +func (entry *MQEntry) run() { + for fn := range entry.Queue { + runFn(fn) + } + entry.Lock.Lock() + entry.Running = false + entry.Lock.Unlock() + entry.tryRun() +} + +func (mq *MapQueue) Enqueue(id string, fn func()) error { + entry := mq.getEntry(id) + err := entry.add(fn) + if err != nil { + return fmt.Errorf("cannot enqueue: %v", err) + } + return nil +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 45477f8f0..b0ce19bf0 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1499,12 +1499,12 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return } - sws, err := sstore.UpdateSWsWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + screens, err := sstore.UpdateScreensWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) if err != nil { msh.WriteToPtyBuffer("*error trying to update cmd-fg screen windows: %v\n", err) // fall-through (nothing to do) } - update.ScreenWindows = sws + update.Screens = screens rct := msh.GetRunningCmd(donePk.CK) var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index b84fb4ba6..50bb7388b 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/mapqueue" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -17,6 +18,13 @@ import ( const WSStatePacketChSize = 20 const MaxInputDataSize = 1000 +const RemoteInputQueueSize = 100 + +var RemoteInputMapQueue *mapqueue.MapQueue + +func init() { + RemoteInputMapQueue = mapqueue.MakeMapQueue(RemoteInputQueueSize) +} type WSState struct { Lock *sync.Mutex @@ -227,13 +235,16 @@ func (ws *WSState) RunWSRead() { log.Printf("[error] invalid input packet, remoteid is not set\n") continue } - go func() { - // TODO enforce a strong ordering (channel with list) + err := RemoteInputMapQueue.Enqueue(feInputPk.Remote.RemoteId, func() { err = sendCmdInput(feInputPk) if err != nil { log.Printf("[error] sending command input: %v\n", err) } - }() + }) + if err != nil { + log.Printf("[error] could not queue sendCmdInput: %v\n", err) + continue + } continue } if pk.GetType() == scpacket.RemoteInputPacketStr { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 2135b7f24..e9dbb5680 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -405,9 +405,8 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { sessionMap[session.SessionId] = session session.Full = true } - var screens []*ScreenType query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` - tx.Select(&screens, query) + screens := SelectMapsGen[*ScreenType](tx, query) screenMap := make(map[string][]*ScreenType) for _, screen := range screens { screenArr := screenMap[screen.SessionId] @@ -417,20 +416,6 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { for _, session := range rtn { session.Screens = screenMap[session.SessionId] } - var sws []*ScreenWindowType - query = `SELECT * FROM screen_window` - tx.Select(&sws, query) - screenIdMap := make(map[string]*ScreenType) - for _, screen := range screens { - screenIdMap[screen.SessionId+screen.ScreenId] = screen - } - for _, sw := range sws { - screen := screenIdMap[sw.SessionId+sw.ScreenId] - if screen == nil { - continue - } - screen.Windows = append(screen.Windows, sw) - } query = `SELECT * FROM remote_instance` riArr := SelectMapsGen[*RemoteInstance](tx, query) for _, ri := range riArr { @@ -449,22 +434,20 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { return &ModelUpdate{Sessions: rtn, ActiveSessionId: activeSessionId}, nil } -func GetWindowById(ctx context.Context, sessionId string, windowId string) (*WindowType, error) { - var rtnWindow *WindowType - err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM window WHERE sessionid = ? AND windowid = ?` - m := tx.GetMap(query, sessionId, windowId) - if m == nil { - return nil +func GetFullScreenById(ctx context.Context, screenId string) (*ScreenType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { + query := `SELECT * FROM screen WHERE screenid = ?` + screen := GetMapGen[*ScreenType](tx, query, screenId) + if screen == nil { + return nil, nil } - rtnWindow = FromMap[*WindowType](m) query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` - tx.Select(&rtnWindow.Lines, query, sessionId, windowId) + tx.Select(&screen.Lines, query, screen.SessionId, screen.WindowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` - rtnWindow.Cmds = SelectMapsGen[*CmdType](tx, query, sessionId, windowId) - return nil + screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.SessionId, screen.WindowId) + screen.Full = true + return screen, nil }) - return rtnWindow, err } // includes archived screens (does not include screen windows) @@ -649,11 +632,11 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, if !tx.Exists(query, sessionId) { return fmt.Errorf("cannot create screen, no session found (or session archived)") } - remoteId := tx.GetString(`SELECT remoteid FROM remote WHERE remotealias = ?`, LocalRemoteAlias) - if remoteId == "" { + localRemoteId := tx.GetString(`SELECT remoteid FROM remote WHERE remotealias = ?`, LocalRemoteAlias) + if localRemoteId == "" { return fmt.Errorf("cannot create screen, no local remote found") } - newWindowId := txCreateWindow(tx, sessionId, RemotePtrType{RemoteId: remoteId}) + newWindowId := scbase.GenPromptUUID() maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) var screenName string if origScreenName == "" { @@ -663,11 +646,25 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, screenName = origScreenName } newScreenId = scbase.GenPromptUUID() - query = `INSERT INTO screen (sessionid, screenid, name, activewindowid, screenidx, screenopts, ownerid, sharemode, incognito, archived, archivedts) VALUES (?, ?, ?, ?, ?, ?, '', 'local', 0, 0, 0)` - tx.Exec(query, sessionId, newScreenId, screenName, newWindowId, maxScreenIdx+1, ScreenOptsType{}) - layout := LayoutType{Type: LayoutFull} - query = `INSERT INTO screen_window (sessionid, screenid, windowid, name, layout, selectedline, anchor, focustype) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - tx.Exec(query, sessionId, newScreenId, newWindowId, DefaultScreenWindowName, layout, 0, SWAnchorType{}, "input") + screen := &ScreenType{ + SessionId: sessionId, + ScreenId: newScreenId, + WindowId: newWindowId, + Name: screenName, + ScreenIdx: int64(maxScreenIdx) + 1, + ScreenOpts: ScreenOptsType{}, + OwnerId: "", + ShareMode: ShareModeLocal, + CurRemote: RemotePtrType{RemoteId: localRemoteId}, + NextLineNum: 1, + SelectedLine: 0, + Anchor: ScreenAnchorType{}, + FocusType: ScreenFocusInput, + Archived: false, + ArchivedTs: 0, + } + query = `INSERT INTO screen (sessionid, screenid, windowid, name, screenidx, screenopts, ownerid, sharemode, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) VALUES (:sessionid,:screenid,:windowid,:name,:screenidx,:screenopts,:ownerid,:sharemode,: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) @@ -677,7 +674,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, if txErr != nil { return nil, txErr } - newScreen, err := GetScreenById(ctx, sessionId, newScreenId) + newScreen, err := GetScreenById(ctx, newScreenId) if err != nil { return nil, err } @@ -689,42 +686,12 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil } -func GetScreenById(ctx context.Context, sessionId string, screenId string) (*ScreenType, error) { - var rtnScreen *ScreenType - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM screen WHERE sessionid = ? AND screenid = ?` - var screen ScreenType - found := tx.Get(&screen, query, sessionId, screenId) - if !found { - return nil - } - rtnScreen = &screen - query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ?` - tx.Select(&screen.Windows, query, sessionId, screenId) - screen.Full = true - return nil +func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { + query := `SELECT * FROM screen WHERE screenid = ?` + screen := GetMapGen[*ScreenType](tx, query, screenId) + return screen, nil }) - if txErr != nil { - return nil, txErr - } - return rtnScreen, nil -} - -func txCreateWindow(tx *TxWrap, sessionId string, curRemote RemotePtrType) string { - w := &WindowType{ - SessionId: sessionId, - WindowId: scbase.GenPromptUUID(), - CurRemote: curRemote, - NextLineNum: 1, - WinOpts: WindowOptsType{}, - ShareMode: ShareModeLocal, - ShareOpts: WindowShareOptsType{}, - } - wmap := w.ToMap() - query := `INSERT INTO window ( sessionid, windowid, curremoteownerid, curremoteid, curremotename, nextlinenum, winopts, ownerid, sharemode, shareopts) - VALUES (:sessionid,:windowid,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:winopts,:ownerid,:sharemode,:shareopts)` - tx.NamedExec(query, wmap) - return w.WindowId } func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lineArg string) (string, error) { @@ -1071,7 +1038,7 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda return nil, txErr } update := ModelUpdate{Sessions: []*SessionType{bareSession}} - newScreen, _ := GetScreenById(ctx, sessionId, screenId) + newScreen, _ := GetScreenById(ctx, screenId) if newScreen != nil { bareSession.Screens = append(bareSession.Screens, newScreen) } @@ -1264,17 +1231,16 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r return ri, txErr } -func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) error { - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("cannot update curremote: no window found") +func UpdateCurRemote(ctx context.Context, screenId string, remotePtr RemotePtrType) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { + return fmt.Errorf("cannot update curremote: no screen found") } - query = `UPDATE window SET curremoteownerid = ?, curremoteid = ?, curremotename = ? WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name, sessionId, windowId) + query = `UPDATE screen SET curremoteownerid = ?, curremoteid = ?, curremotename = ? WHERE screenid = ?` + tx.Exec(query, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name, screenId) return nil }) - return txErr } func reorderStrings(strs []string, toMove string, newIndex int) []string { @@ -1353,77 +1319,65 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name return txErr } -func SetScreenOpts(ctx context.Context, sessionId string, screenId string, opts *ScreenOptsType) error { - if opts == nil { - return fmt.Errorf("invalid screen opts cannot be nil") - } +func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` - if !tx.Exists(query, sessionId, screenId) { - return fmt.Errorf("screen does not exist") - } - query = `UPDATE screen SET screenopts = ? WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, opts, sessionId, screenId) - return nil - }) - return txErr -} - -func ArchiveWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("window does not exist") + query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` + var swkeys SWKeys + tx.Get(&swkeys, query, screenId) + if swkeys.SessionId == "" || swkeys.WindowId == "" { + return fmt.Errorf("screen windowid does not exist") } query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) + tx.Exec(query, swkeys.SessionId, swkeys.WindowId) return nil }) if txErr != nil { return nil, txErr } - win, err := GetWindowById(ctx, sessionId, windowId) + screen, err := GetFullScreenById(ctx, screenId) if err != nil { return nil, err } - return &ModelUpdate{Windows: []*WindowType{win}}, nil + return &ModelUpdate{Screens: []*ScreenType{screen}}, nil } -func PurgeWindowLines(ctx context.Context, sessionId string, windowId string) (*ModelUpdate, error) { +func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { var lineIds []string + var swkeys SWKeys txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("window does not exist") + query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` + tx.Get(&swkeys, query, screenId) + if swkeys.SessionId == "" || swkeys.WindowId == "" { + return fmt.Errorf("screen windowid does not exist") } query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` - lineIds = tx.SelectStrings(query, sessionId, windowId) + lineIds = tx.SelectStrings(query, swkeys.SessionId, swkeys.WindowId) query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) + tx.Exec(query, swkeys.SessionId, swkeys.WindowId) query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - query = `UPDATE window SET nextlinenum = 1 WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) + tx.Exec(query, swkeys.SessionId, swkeys.WindowId) + query = `UPDATE screen SET nextlinenum = 1 WHERE screenid = ?` + tx.Exec(query, screenId) return nil }) if txErr != nil { return nil, txErr } - go cleanSessionCmds(context.Background(), sessionId) - win, err := GetWindowById(ctx, sessionId, windowId) + go cleanSessionCmds(context.Background(), swkeys.SessionId) + screen, err := GetFullScreenById(ctx, screenId) if err != nil { return nil, err } for _, lineId := range lineIds { line := &LineType{ - SessionId: sessionId, - WindowId: windowId, + SessionId: swkeys.SessionId, + WindowId: swkeys.WindowId, LineId: lineId, Remove: true, } - win.Lines = append(win.Lines, line) + screen.Lines = append(screen.Lines, line) } - return &ModelUpdate{Windows: []*WindowType{win}}, nil + return &ModelUpdate{Screens: []*ScreenType{screen}}, nil } func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string) ([]*CmdType, error) { @@ -1449,24 +1403,25 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term } // returns riids of deleted RIs -func WindowReset(ctx context.Context, sessionId string, windowId string) ([]*RemoteInstance, error) { - var delRis []*RemoteInstance - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("window does not exist") +func ScreenReset(ctx context.Context, screenId string) ([]*RemoteInstance, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]*RemoteInstance, error) { + var swkeys SWKeys + query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` + tx.Get(&swkeys, query, screenId) + if swkeys.SessionId == "" || swkeys.WindowId == "" { + return nil, fmt.Errorf("screen does not exist") } query = `SELECT riid FROM remote_instance WHERE sessionid = ? AND windowid = ?` - riids := tx.SelectStrings(query, sessionId, windowId) + riids := tx.SelectStrings(query, swkeys.SessionId, swkeys.WindowId) + var delRis []*RemoteInstance for _, riid := range riids { - ri := &RemoteInstance{SessionId: sessionId, WindowId: windowId, RIId: riid, Remove: true} + ri := &RemoteInstance{SessionId: swkeys.SessionId, WindowId: swkeys.WindowId, RIId: riid, Remove: true} delRis = append(delRis, ri) } query = `DELETE FROM remote_instance WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - return nil + tx.Exec(query, swkeys.SessionId, swkeys.WindowId) + return delRis, nil }) - return delRis, txErr } func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) { @@ -1480,10 +1435,6 @@ func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) tx.Exec(query, sessionId) query = `DELETE FROM screen WHERE sessionid = ?` tx.Exec(query, sessionId) - query = `DELETE FROM screen_window WHERE sessionid = ?` - tx.Exec(query, sessionId) - query = `DELETE FROM window WHERE sessionid = ?` - tx.Exec(query, sessionId) query = `DELETE FROM history WHERE sessionid = ?` tx.Exec(query, sessionId) query = `DELETE FROM line WHERE sessionid = ?` @@ -1693,64 +1644,55 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter } const ( - SWField_AnchorLine = "anchorline" // int - SWField_AnchorOffset = "anchoroffset" // int - SWField_SelectedLine = "selectedline" // int - SWField_Focus = "focustype" // string + ScreenField_AnchorLine = "anchorline" // int + ScreenField_AnchorOffset = "anchoroffset" // int + ScreenField_SelectedLine = "selectedline" // int + ScreenField_Focus = "focustype" // string + ScreenField_TabColor = "tabcolor" // string + ScreenField_PTerm = "pterm" // string + ScreenField_Name = "name" // string ) -func UpdateScreenWindow(ctx context.Context, sessionId string, screenId string, windowId string, editMap map[string]interface{}) (*ScreenWindowType, error) { - var rtn *ScreenWindowType +func UpdateScreen(ctx context.Context, screenId string, editMap map[string]interface{}) (*ScreenType, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, screenId, windowId) { - return fmt.Errorf("screen-window not found") + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { + return fmt.Errorf("screen not found") } - if anchorLine, found := editMap[SWField_AnchorLine]; found { - query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchorline', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.Exec(query, anchorLine, sessionId, screenId, windowId) + if anchorLine, found := editMap[ScreenField_AnchorLine]; found { + query = `UPDATE screen SET anchor = json_set(anchor, '$.anchorline', ?) WHERE screenid = ?` + tx.Exec(query, anchorLine, screenId) } - if anchorOffset, found := editMap[SWField_AnchorOffset]; found { - query = `UPDATE screen_window SET anchor = json_set(anchor, '$.anchoroffset', ?) WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.Exec(query, anchorOffset, sessionId, screenId, windowId) + if anchorOffset, found := editMap[ScreenField_AnchorOffset]; found { + query = `UPDATE screen SET anchor = json_set(anchor, '$.anchoroffset', ?) WHERE screenid = ?` + tx.Exec(query, anchorOffset, screenId) } - if sline, found := editMap[SWField_SelectedLine]; found { - query = `UPDATE screen_window SET selectedline = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.Exec(query, sline, sessionId, screenId, windowId) + if sline, found := editMap[ScreenField_SelectedLine]; found { + query = `UPDATE screen SET selectedline = ? WHERE screenid = ?` + tx.Exec(query, sline, screenId) } - if focusType, found := editMap[SWField_Focus]; found { - query = `UPDATE screen_window SET focustype = ? WHERE sessionid = ? AND screenid = ? AND windowid = ?` - tx.Exec(query, focusType, sessionId, screenId, windowId) + if focusType, found := editMap[ScreenField_Focus]; found { + query = `UPDATE screen SET focustype = ? WHERE screenid = ?` + tx.Exec(query, focusType, screenId) } - var sw ScreenWindowType - query = `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` - found := tx.Get(&sw, query, sessionId, screenId, windowId) - if found { - rtn = &sw + if tabColor, found := editMap[ScreenField_TabColor]; found { + query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabcolor', ?) WHERE screenid = ?` + tx.Exec(query, tabColor, screenId) + } + if pterm, found := editMap[ScreenField_PTerm]; found { + query = `UPDATE screen SET screenopts = json_set(screenopts, '$.pterm', ?) WHERE screenid = ?` + tx.Exec(query, pterm, screenId) + } + if name, found := editMap[ScreenField_Name]; found { + query = `UPDATE screen SET name = ? WHERE screenid = ?` + tx.Exec(query, name, screenId) } return nil }) if txErr != nil { return nil, txErr } - return rtn, nil -} - -func GetScreenWindowByIds(ctx context.Context, sessionId string, screenId string, windowId string) (*ScreenWindowType, error) { - var rtn *ScreenWindowType - txErr := WithTx(ctx, func(tx *TxWrap) error { - var sw ScreenWindowType - query := `SELECT * FROM screen_window WHERE sessionid = ? AND screenid = ? AND windowid = ?` - found := tx.Get(&sw, query, sessionId, screenId, windowId) - if found { - rtn = &sw - } - return nil - }) - if txErr != nil { - return nil, txErr - } - return rtn, nil + return GetScreenById(ctx, screenId) } func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) ([]ResolveItem, error) { @@ -1766,33 +1708,32 @@ func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) return rtn, nil } -func UpdateSWsWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([]*ScreenWindowType, error) { - var rtn []*ScreenWindowType +func UpdateScreensWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([]*ScreenType, error) { + var rtn []*ScreenType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid, screenid, windowid - FROM screen_window sw - WHERE - sessionid = ? - AND focustype = 'cmd-fg' - AND selectedline IN (SELECT linenum - FROM line l - WHERE l.sessionid = sw.sessionid - AND l.windowid = sw.windowid - AND l.cmdid = ? - )` - var swKeys []SWKey - tx.Select(&swKeys, query, sessionId, cmdId) - if len(swKeys) == 0 { + query := `SELECT screenid + FROM screen s + WHERE + s.sessionid = ? + AND s.focustype = 'cmd-fg' + AND s.selectedline IN (SELECT linenum + FROM line l + WHERE l.sessionid = s.sessionid + AND l.windowid = s.windowid + AND l.cmdid = ? + )` + screenIds := tx.SelectStrings(query, sessionId, cmdId) + if len(screenIds) == 0 { return nil } - for _, key := range swKeys { + for _, screenId := range screenIds { editMap := make(map[string]interface{}) - editMap[SWField_Focus] = SWFocusInput - sw, err := UpdateScreenWindow(tx.Context(), key.SessionId, key.ScreenId, key.WindowId, editMap) + editMap[ScreenField_Focus] = ScreenFocusInput + screen, err := UpdateScreen(tx.Context(), screenId, editMap) if err != nil { return err } - rtn = append(rtn, sw) + rtn = append(rtn, screen) } return nil }) diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 1326b90f9..c49f46bf8 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 8 +const MaxMigration = 9 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 9593b2351..344bf0258 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -73,9 +73,9 @@ const ( ) const ( - SWFocusInput = "input" - SWFocusCmd = "cmd" - SWFocusCmdFg = "cmd-fg" + ScreenFocusInput = "input" + ScreenFocusCmd = "cmd" + ScreenFocusCmdFg = "cmd-fg" ) const MaxTzNameLen = 50 @@ -276,29 +276,6 @@ type SessionStatsType struct { DiskStats SessionDiskSizeType `json:"diskstats"` } -type WindowOptsType struct { - PTerm string `json:"pterm,omitempty"` -} - -func (opts *WindowOptsType) Scan(val interface{}) error { - return quickScanJson(opts, val) -} - -func (opts WindowOptsType) Value() (driver.Value, error) { - return quickValueJson(opts) -} - -type WindowShareOptsType struct { -} - -func (opts *WindowShareOptsType) Scan(val interface{}) error { - return quickScanJson(opts, val) -} - -func (opts WindowShareOptsType) Value() (driver.Value, error) { - return quickValueJson(opts) -} - var RemoteNameRe = regexp.MustCompile("^\\*?[a-zA-Z0-9_-]+$") type RemotePtrType struct { @@ -361,51 +338,6 @@ func (r RemotePtrType) MakeFullRemoteRef() string { return fmt.Sprintf("@%s:%s:%s", r.OwnerId, r.RemoteId, r.Name) } -type WindowType struct { - SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` - CurRemote RemotePtrType `json:"curremote"` - WinOpts WindowOptsType `json:"winopts"` - OwnerId string `json:"ownerid"` - NextLineNum int64 `json:"nextlinenum"` - ShareMode string `json:"sharemode"` - ShareOpts WindowShareOptsType `json:"shareopts"` - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` - - // only for updates - Remove bool `json:"remove,omitempty"` -} - -func (w *WindowType) ToMap() map[string]interface{} { - rtn := make(map[string]interface{}) - rtn["sessionid"] = w.SessionId - rtn["windowid"] = w.WindowId - rtn["curremoteownerid"] = w.CurRemote.OwnerId - rtn["curremoteid"] = w.CurRemote.RemoteId - rtn["curremotename"] = w.CurRemote.Name - rtn["nextlinenum"] = w.NextLineNum - rtn["winopts"] = quickJson(w.WinOpts) - rtn["ownerid"] = w.OwnerId - rtn["sharemode"] = w.ShareMode - rtn["shareopts"] = quickJson(w.ShareOpts) - return rtn -} - -func (w *WindowType) FromMap(m map[string]interface{}) bool { - quickSetStr(&w.SessionId, m, "sessionid") - quickSetStr(&w.WindowId, m, "windowid") - quickSetStr(&w.CurRemote.OwnerId, m, "curremoteownerid") - quickSetStr(&w.CurRemote.RemoteId, m, "curremoteid") - quickSetStr(&w.CurRemote.Name, m, "curremotename") - quickSetInt64(&w.NextLineNum, m, "nextlinenum") - quickSetJson(&w.WinOpts, m, "winopts") - quickSetStr(&w.OwnerId, m, "ownerid") - quickSetStr(&w.ShareMode, m, "sharemode") - quickSetJson(&w.ShareOpts, m, "shareopts") - return true -} - func (h *HistoryItemType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["historyid"] = h.HistoryId @@ -448,35 +380,83 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { type ScreenOptsType struct { TabColor string `json:"tabcolor,omitempty"` + PTerm string `json:"pterm,omitempty"` } -func (opts *ScreenOptsType) Scan(val interface{}) error { - return quickScanJson(opts, val) -} - -func (opts ScreenOptsType) Value() (driver.Value, error) { - return quickValueJson(opts) +type SWKeys struct { + SessionId string + WindowId string } type ScreenType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - ScreenIdx int64 `json:"screenidx"` - Name string `json:"name"` - ActiveWindowId string `json:"activewindowid"` - ScreenOpts *ScreenOptsType `json:"screenopts"` - OwnerId string `json:"ownerid"` - ShareMode string `json:"sharemode"` - Incognito bool `json:"incognito,omitempty"` - Archived bool `json:"archived,omitempty"` - ArchivedTs int64 `json:"archivedts,omitempty"` - Windows []*ScreenWindowType `json:"windows"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Name string `json:"name"` + ScreenIdx int64 `json:"screenidx"` + ScreenOpts ScreenOptsType `json:"screenopts"` + OwnerId string `json:"ownerid"` + ShareMode string `json:"sharemode"` + CurRemote RemotePtrType `json:"curremote"` + NextLineNum int64 `json:"nextlinenum"` + SelectedLine int64 `json:"selectedline"` + Anchor ScreenAnchorType `json:"anchor"` + FocusType string `json:"focustype"` + Archived bool `json:"archived,omitempty"` + ArchivedTs int64 `json:"archivedts,omitempty"` + + // only for "full" + Lines []*LineType `json:"lines"` + Cmds []*CmdType `json:"cmds"` // only for updates Remove bool `json:"remove,omitempty"` Full bool `json:"full,omitempty"` } +func (s *ScreenType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["sessionid"] = s.SessionId + rtn["screenid"] = s.ScreenId + rtn["windowid"] = s.WindowId + rtn["name"] = s.Name + rtn["screenidx"] = s.ScreenIdx + rtn["screenopts"] = quickJson(s.ScreenOpts) + rtn["ownerid"] = s.OwnerId + rtn["sharemode"] = s.ShareMode + rtn["curremoteownerid"] = s.CurRemote.OwnerId + rtn["curremoteid"] = s.CurRemote.RemoteId + rtn["curremotename"] = s.CurRemote.Name + rtn["nextlinenum"] = s.NextLineNum + rtn["selectedline"] = s.SelectedLine + rtn["anchor"] = quickJson(s.Anchor) + rtn["focustype"] = s.FocusType + rtn["archived"] = s.Archived + rtn["archivedts"] = s.ArchivedTs + return rtn +} + +func (s *ScreenType) FromMap(m map[string]interface{}) bool { + quickSetStr(&s.SessionId, m, "sessionid") + quickSetStr(&s.ScreenId, m, "screenid") + quickSetStr(&s.WindowId, m, "windowid") + quickSetStr(&s.Name, m, "name") + quickSetInt64(&s.ScreenIdx, m, "screenidx") + quickSetJson(&s.ScreenOpts, m, "screenopts") + quickSetStr(&s.OwnerId, m, "ownerid") + quickSetStr(&s.ShareMode, m, "sharemode") + quickSetStr(&s.CurRemote.OwnerId, m, "curremoteownerid") + quickSetStr(&s.CurRemote.RemoteId, m, "curremoteid") + quickSetStr(&s.CurRemote.Name, m, "curremotename") + quickSetInt64(&s.NextLineNum, m, "nextlinenum") + quickSetInt64(&s.SelectedLine, m, "selectedline") + quickSetJson(&s.Anchor, m, "anchor") + quickSetStr(&s.FocusType, m, "focustype") + quickSetBool(&s.Archived, m, "archived") + quickSetInt64(&s.ArchivedTs, m, "archivedts") + return true +} + const ( LayoutFull = "full" ) @@ -502,39 +482,11 @@ func (l LayoutType) Value() (driver.Value, error) { return quickValueJson(l) } -type SWAnchorType struct { +type ScreenAnchorType struct { AnchorLine int `json:"anchorline,omitempty"` AnchorOffset int `json:"anchoroffset,omitempty"` } -func (a *SWAnchorType) Scan(val interface{}) error { - return quickScanJson(a, val) -} - -func (a SWAnchorType) Value() (driver.Value, error) { - return quickValueJson(a) -} - -type SWKey struct { - SessionId string - ScreenId string - WindowId string -} - -type ScreenWindowType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - Name string `json:"name"` - Layout LayoutType `json:"layout"` - SelectedLine int `json:"selectedline"` - Anchor SWAnchorType `json:"anchor"` - FocusType string `json:"focustype"` - - // only for updates - Remove bool `json:"remove,omitempty"` -} - type HistoryItemType struct { HistoryId string `json:"historyid"` Ts int64 `json:"ts"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 36608d041..fa7e69741 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -30,24 +30,23 @@ func (PtyDataUpdate) UpdateType() string { } type ModelUpdate struct { - Sessions []*SessionType `json:"sessions,omitempty"` - ActiveSessionId string `json:"activesessionid,omitempty"` - Windows []*WindowType `json:"windows,omitempty"` - ScreenWindows []*ScreenWindowType `json:"screenwindows,omitempty"` - Line *LineType `json:"line,omitempty"` - Lines []*LineType `json:"lines,omitempty"` - Cmd *CmdType `json:"cmd,omitempty"` - CmdLine *CmdLineType `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"` - HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` - ClientData *ClientData `json:"clientdata,omitempty"` + Sessions []*SessionType `json:"sessions,omitempty"` + ActiveSessionId string `json:"activesessionid,omitempty"` + Screens []*ScreenType `json:"screens,omitempty"` + Line *LineType `json:"line,omitempty"` + Lines []*LineType `json:"lines,omitempty"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *CmdLineType `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"` + HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` + ClientData *ClientData `json:"clientdata,omitempty"` } func (ModelUpdate) UpdateType() string { From fd3315713005b33e7449715ade32fc7ff92480e3 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 02:09:29 -0700 Subject: [PATCH 293/397] more window/screen_window references removed --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/sstore/dbops.go | 110 ++++++++++++++----------------------- pkg/sstore/sstore.go | 1 - 3 files changed, 42 insertions(+), 71 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3e7470b5a..d300df7ae 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -478,7 +478,7 @@ func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("/screen:purge cannot purge screen: %w", err) } - update, err := sstore.DeleteScreen(ctx, ids.SessionId, ids.ScreenId) + update, err := sstore.DeleteScreen(ctx, ids.ScreenId) if err != nil { return nil, err } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e9dbb5680..dbf0b0e0a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -720,55 +720,34 @@ func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lin } func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, *CmdType, error) { - var lineRtn *LineType - var cmdRtn *CmdType - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("window not found") - } + return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` + query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` found := tx.Get(&lineVal, query, sessionId, windowId, lineId) if !found { - return nil + return nil, nil, nil } - lineRtn = &lineVal + var cmdRtn *CmdType if lineVal.CmdId != "" { query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` cmdRtn = GetMapGen[*CmdType](tx, query, sessionId, lineVal.CmdId) } - return nil + return &lineVal, cmdRtn, nil }) - if txErr != nil { - return nil, nil, txErr - } - return lineRtn, cmdRtn, nil } func GetLineCmdByCmdId(ctx context.Context, sessionId string, windowId string, cmdId string) (*LineType, *CmdType, error) { - var lineRtn *LineType - var cmdRtn *CmdType - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("window not found") - } + return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND cmdid = ?` + query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND cmdid = ?` found := tx.Get(&lineVal, query, sessionId, windowId, cmdId) if !found { - return nil + return nil, nil, nil } - lineRtn = &lineVal query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - cmdRtn = GetMapGen[*CmdType](tx, query, sessionId, cmdId) - return nil + cmdRtn := GetMapGen[*CmdType](tx, query, sessionId, cmdId) + return &lineVal, cmdRtn, nil }) - if txErr != nil { - return nil, nil, txErr - } - return lineRtn, cmdRtn, nil } func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { @@ -782,17 +761,17 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { return fmt.Errorf("line should not hage linenum set") } return WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + query := `SELECT screenid FROM screen WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, line.SessionId, line.WindowId) { - return fmt.Errorf("window not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) + return fmt.Errorf("screen not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) } - query = `SELECT nextlinenum FROM window WHERE sessionid = ? AND windowid = ?` + query = `SELECT nextlinenum FROM screen WHERE sessionid = ? AND windowid = ?` nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) line.LineNum = int64(nextLineNum) query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked, pinned) VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked,:pinned)` tx.NamedExec(query, line) - query = `UPDATE window SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` + query = `UPDATE screen SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` tx.Exec(query, nextLineNum+1, line.SessionId, line.WindowId) if cmd != nil { cmd.OrigTermOpts = cmd.TermOpts @@ -887,7 +866,7 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err func ReInitFocus(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE screen_window SET focustype = 'input'` + query := `UPDATE screen SET focustype = 'input'` tx.Exec(query) return nil }) @@ -981,26 +960,17 @@ func cleanSessionCmds(ctx context.Context, sessionId string) error { return nil } -func CleanWindows(sessionId string) { +func CleanWindow(sessionId string, windowId string) { // NOTE: context.Background() here! (this could take a long time, and is async) txErr := WithTx(context.Background(), func(tx *TxWrap) error { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid NOT IN (SELECT windowid FROM screen_window WHERE sessionid = ?)` - removedWindowIds := tx.SelectStrings(query, sessionId, sessionId) - if len(removedWindowIds) == 0 { - return nil - } - for _, windowId := range removedWindowIds { - query = `DELETE FROM window WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - } + query := `DELETE FROM history WHERE sessionid = ? AND windowid = ?` + tx.Exec(query, sessionId, windowId) + query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` + tx.Exec(query, sessionId, windowId) return cleanSessionCmds(tx.Context(), sessionId) }) if txErr != nil { - fmt.Printf("ERROR cleaning windows sessionid:%s: %v\n", sessionId, txErr) + fmt.Printf("ERROR cleaning session:%s window:%s : %v\n", sessionId, windowId, txErr) } } @@ -1059,38 +1029,42 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err return txErr } -func DeleteScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { +func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { + var swkeys SWKeys txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` - if !tx.Exists(query, sessionId, screenId) { + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { return fmt.Errorf("cannot purge screen (not found)") } + query = `SELECT sessionid, windowid FROM screen WHERE screenid = ?` + tx.Get(&swkeys, query, screenId) + if swkeys.SessionId == "" || swkeys.WindowId == "" { + return fmt.Errorf("cannot purge screen (no windowid)") + } query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived` - numScreens := tx.GetInt(query, sessionId) + numScreens := tx.GetInt(query, swkeys.SessionId) if numScreens <= 1 { return fmt.Errorf("cannot purge the last screen in a session") } - isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId) + isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, swkeys.SessionId, screenId) if isActive { - screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, swkeys.SessionId) nextId := getNextId(screenIds, screenId) - tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, swkeys.SessionId) } - query = `DELETE FROM screen_window WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) - query = `DELETE FROM screen WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) + query = `DELETE FROM screen WHERE screenid = ?` + tx.Exec(query, screenId) return nil }) if txErr != nil { return nil, txErr } - go CleanWindows(sessionId) - bareSession, err := GetBareSessionById(ctx, sessionId) + go CleanWindow(swkeys.SessionId, swkeys.WindowId) + bareSession, err := GetBareSessionById(ctx, swkeys.SessionId) if err != nil { return nil, err } - bareSession.Screens = append(bareSession.Screens, &ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}) + bareSession.Screens = append(bareSession.Screens, &ScreenType{SessionId: swkeys.SessionId, ScreenId: screenId, Remove: true}) update := ModelUpdate{Sessions: []*SessionType{bareSession}} return update, nil } @@ -1137,9 +1111,9 @@ func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error } return nil } else { - query := `SELECT windowid FROM window WHERE sessionid = ? AND windowid = ?` + query := `SELECT windowid FROM screen WHERE sessionid = ? AND windowid = ?` if !tx.Exists(query, sessionId, windowId) { - return fmt.Errorf("no window found") + return fmt.Errorf("no screen found") } return nil } @@ -1566,8 +1540,6 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, rtn.NumScreens = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM screen WHERE sessionid = ? AND archived` rtn.NumArchivedScreens = tx.GetInt(query, sessionId) - query = `SELECT count(*) FROM window WHERE sessionid = ?` - rtn.NumWindows = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM line WHERE sessionid = ?` rtn.NumLines = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM cmd WHERE sessionid = ?` diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 344bf0258..2b6d3e76a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -270,7 +270,6 @@ type SessionStatsType struct { SessionId string `json:"sessionid"` NumScreens int `json:"numscreens"` NumArchivedScreens int `json:"numarchivedscreens"` - NumWindows int `json:"numwindows"` NumLines int `json:"numlines"` NumCmds int `json:"numcmds"` DiskStats SessionDiskSizeType `json:"diskstats"` From 1059a10727c1e2ac84bef76b759f64ca2243f94f Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 10:50:29 -0700 Subject: [PATCH 294/397] remove screens from session. add screenlinestype (replaces 'window' lines) --- cmd/main-server.go | 9 ++-- pkg/cmdrunner/cmdrunner.go | 7 +-- pkg/sstore/dbops.go | 98 +++++++++++++++++++------------------- pkg/sstore/sstore.go | 18 ++++--- pkg/sstore/updatebus.go | 1 + 5 files changed, 67 insertions(+), 66 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9092626c2..966919d39 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -194,19 +194,20 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) { } // params: screenid -func HandleGetFullScreen(w http.ResponseWriter, r *http.Request) { +func HandleGetScreenLines(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() screenId := qvals.Get("screenid") if _, err := uuid.Parse(screenId); err != nil { WriteJsonError(w, fmt.Errorf("invalid screenid: %w", err)) return } - screen, err := sstore.GetFullScreenById(r.Context(), screenId) + screenLines, err := sstore.GetScreenLinesById(r.Context(), screenId) if err != nil { WriteJsonError(w, err) return } - WriteJsonSuccess(w, screen) + update := &sstore.ModelUpdate{ScreenLines: screenLines} + WriteJsonSuccess(w, update) return } @@ -562,7 +563,7 @@ func main() { gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty)) gr.HandleFunc("/api/rtnstate", AuthKeyWrap(HandleRtnState)) - gr.HandleFunc("/api/get-full-screen", AuthKeyWrap(HandleGetFullScreen)) + gr.HandleFunc("/api/get-screen-lines", AuthKeyWrap(HandleGetScreenLines)) gr.HandleFunc("/api/run-command", AuthKeyWrap(HandleRunCommand)).Methods("POST") gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d300df7ae..08cb61ef8 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -461,13 +461,8 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err) } - bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) - if err != nil { - return nil, fmt.Errorf("/screen:archive cannot retrieve updated session obj: %v", err) - } - bareSession.Screens = append(bareSession.Screens, screen) update := sstore.ModelUpdate{ - Sessions: []*sstore.SessionType{bareSession}, + Screens: []*sstore.ScreenType{screen}, } return update, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index dbf0b0e0a..20c9aef1f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -395,27 +395,17 @@ func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, er } func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { - var rtn []*SessionType - var activeSessionId string - txErr := WithTx(ctx, func(tx *TxWrap) error { + return WithTxRtn(ctx, func(tx *TxWrap) (*ModelUpdate, error) { + update := &ModelUpdate{} query := `SELECT * FROM session ORDER BY archived, sessionidx, archivedts` - tx.Select(&rtn, query) + tx.Select(&update.Sessions, query) sessionMap := make(map[string]*SessionType) - for _, session := range rtn { + for _, session := range update.Sessions { sessionMap[session.SessionId] = session session.Full = true } query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` - screens := SelectMapsGen[*ScreenType](tx, query) - screenMap := make(map[string][]*ScreenType) - for _, screen := range screens { - screenArr := screenMap[screen.SessionId] - screenArr = append(screenArr, screen) - screenMap[screen.SessionId] = screenArr - } - for _, session := range rtn { - session.Screens = screenMap[session.SessionId] - } + update.Screens = SelectMapsGen[*ScreenType](tx, query) query = `SELECT * FROM remote_instance` riArr := SelectMapsGen[*RemoteInstance](tx, query) for _, ri := range riArr { @@ -425,19 +415,15 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { } } query = `SELECT activesessionid FROM client` - activeSessionId = tx.GetString(query) - return nil + update.ActiveSessionId = tx.GetString(query) + return update, nil }) - if txErr != nil { - return nil, txErr - } - return &ModelUpdate{Sessions: rtn, ActiveSessionId: activeSessionId}, nil } -func GetFullScreenById(ctx context.Context, screenId string) (*ScreenType, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { - query := `SELECT * FROM screen WHERE screenid = ?` - screen := GetMapGen[*ScreenType](tx, query, screenId) +func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenLinesType, error) { + query := `SELECT sessionid, screenid, windowid FROM screen WHERE screenid = ?` + screen := GetMappable[*ScreenLinesType](tx, query, screenId) if screen == nil { return nil, nil } @@ -445,7 +431,6 @@ func GetFullScreenById(ctx context.Context, screenId string) (*ScreenType, error tx.Select(&screen.Lines, query, screen.SessionId, screen.WindowId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.SessionId, screen.WindowId) - screen.Full = true return screen, nil }) } @@ -678,12 +663,15 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, if err != nil { return nil, err } - bareSession, err := GetBareSessionById(ctx, sessionId) - if err != nil { - return nil, err + update := ModelUpdate{Screens: []*ScreenType{newScreen}} + if activate { + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, txErr + } + update.Sessions = []*SessionType{bareSession} } - bareSession.Screens = append(bareSession.Screens, newScreen) - return ModelUpdate{Sessions: []*SessionType{bareSession}}, nil + return update, nil } func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { @@ -975,6 +963,7 @@ func CleanWindow(sessionId string, windowId string) { } func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { + var isActive bool txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, sessionId, screenId) { @@ -992,7 +981,7 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda } 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) + 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 := getNextId(screenIds, screenId) @@ -1003,14 +992,17 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda if txErr != nil { return nil, txErr } - bareSession, err := GetBareSessionById(ctx, sessionId) + newScreen, err := GetScreenById(ctx, screenId) if err != nil { - return nil, txErr + return nil, fmt.Errorf("cannot retrive archived screen: %w", err) } - update := ModelUpdate{Sessions: []*SessionType{bareSession}} - newScreen, _ := GetScreenById(ctx, screenId) - if newScreen != nil { - bareSession.Screens = append(bareSession.Screens, newScreen) + update := ModelUpdate{Screens: []*ScreenType{newScreen}} + if isActive { + bareSession, err := GetBareSessionById(ctx, sessionId) + if err != nil { + return nil, err + } + update.Sessions = []*SessionType{bareSession} } return update, nil } @@ -1031,6 +1023,7 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { var swkeys SWKeys + var isActive bool txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE screenid = ?` if !tx.Exists(query, screenId) { @@ -1046,7 +1039,7 @@ func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { if numScreens <= 1 { return fmt.Errorf("cannot purge the last screen in a session") } - isActive := tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, swkeys.SessionId, screenId) + isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, swkeys.SessionId, screenId) if isActive { screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, swkeys.SessionId) nextId := getNextId(screenIds, screenId) @@ -1060,12 +1053,15 @@ func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { return nil, txErr } go CleanWindow(swkeys.SessionId, swkeys.WindowId) - bareSession, err := GetBareSessionById(ctx, swkeys.SessionId) - if err != nil { - return nil, err + update := ModelUpdate{} + update.Screens = []*ScreenType{&ScreenType{SessionId: swkeys.SessionId, ScreenId: screenId, Remove: true}} + if isActive { + bareSession, err := GetBareSessionById(ctx, swkeys.SessionId) + if err != nil { + return nil, err + } + update.Sessions = []*SessionType{bareSession} } - bareSession.Screens = append(bareSession.Screens, &ScreenType{SessionId: swkeys.SessionId, ScreenId: screenId, Remove: true}) - update := ModelUpdate{Sessions: []*SessionType{bareSession}} return update, nil } @@ -1308,11 +1304,11 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err if txErr != nil { return nil, txErr } - screen, err := GetFullScreenById(ctx, screenId) + screenLines, err := GetScreenLinesById(ctx, screenId) if err != nil { return nil, err } - return &ModelUpdate{Screens: []*ScreenType{screen}}, nil + return &ModelUpdate{ScreenLines: screenLines}, nil } func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { @@ -1338,7 +1334,11 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error return nil, txErr } go cleanSessionCmds(context.Background(), swkeys.SessionId) - screen, err := GetFullScreenById(ctx, screenId) + screen, err := GetScreenById(ctx, screenId) + if err != nil { + return nil, err + } + screenLines, err := GetScreenLinesById(ctx, screenId) if err != nil { return nil, err } @@ -1349,9 +1349,9 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error LineId: lineId, Remove: true, } - screen.Lines = append(screen.Lines, line) + screenLines.Lines = append(screenLines.Lines, line) } - return &ModelUpdate{Screens: []*ScreenType{screen}}, nil + return &ModelUpdate{Screens: []*ScreenType{screen}, ScreenLines: screenLines}, nil } func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string) ([]*CmdType, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 2b6d3e76a..90b7666d0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -202,7 +202,7 @@ type ClientData struct { FeOpts FeOptsType `json:"feopts"` } -func (c ClientData) UseDBMap() {} +func (ClientData) UseDBMap() {} type CloudAclType struct { UserId string `json:"userid"` @@ -218,7 +218,6 @@ type SessionType struct { NotifyNum int64 `json:"notifynum"` Archived bool `json:"archived,omitempty"` ArchivedTs int64 `json:"archivedts,omitempty"` - Screens []*ScreenType `json:"screens"` Remotes []*RemoteInstance `json:"remotes"` // only for updates @@ -387,6 +386,16 @@ type SWKeys struct { WindowId string } +type ScreenLinesType struct { + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + Lines []*LineType `json:"lines" dbmap:"-"` + Cmds []*CmdType `json:"cmds" dbmap:"-"` +} + +func (ScreenLinesType) UseDBMap() {} + type ScreenType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` @@ -404,13 +413,8 @@ type ScreenType struct { Archived bool `json:"archived,omitempty"` ArchivedTs int64 `json:"archivedts,omitempty"` - // only for "full" - Lines []*LineType `json:"lines"` - Cmds []*CmdType `json:"cmds"` - // only for updates Remove bool `json:"remove,omitempty"` - Full bool `json:"full,omitempty"` } func (s *ScreenType) ToMap() map[string]interface{} { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index fa7e69741..9855b82e1 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -33,6 +33,7 @@ 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"` From 49e56f91cdf2ca80de6fb550cee3116dfa0f3c0f Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 12:10:23 -0700 Subject: [PATCH 295/397] bug fixes for screen primary --- cmd/main-server.go | 3 +-- pkg/cmdrunner/cmdrunner.go | 11 +++++------ pkg/cmdrunner/resolver.go | 2 +- pkg/sstore/dbops.go | 17 +++++++++++------ pkg/sstore/sstore.go | 1 + 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 966919d39..f6c2e374a 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -206,8 +206,7 @@ func HandleGetScreenLines(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, err) return } - update := &sstore.ModelUpdate{ScreenLines: screenLines} - WriteJsonSuccess(w, update) + WriteJsonSuccess(w, screenLines) return } diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 08cb61ef8..a6f4d28e4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -151,10 +151,7 @@ func init() { registerCmdFn("remote:installcancel", RemoteInstallCancelCommand) registerCmdFn("remote:reset", RemoteResetCommand) - registerCmdFn("sw:resize", SwResizeCommand) - - // sw:resize - registerCmdFn("window:resize", SwResizeCommand) + registerCmdFn("screen:resize", ScreenResizeCommand) registerCmdFn("line", LineCommand) registerCmdFn("line:show", LineShowCommand) @@ -556,6 +553,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if ritem == nil { return nil, fmt.Errorf("/screen:set could not resolve line %q", pk.Kwargs["line"]) } + varsUpdated = append(varsUpdated, "line") setNonAnchor = true updateMap[sstore.ScreenField_SelectedLine] = ritem.Num } @@ -565,6 +563,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return nil, fmt.Errorf("/screen:set invalid anchor argument (must be [line] or [line]:[offset])") } anchorLine, _ := strconv.Atoi(m[1]) + varsUpdated = append(varsUpdated, "anchor") updateMap[sstore.ScreenField_AnchorLine] = anchorLine if m[2] != "" { anchorOffset, _ := strconv.Atoi(m[2]) @@ -970,7 +969,7 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) - screenArr, err := sstore.GetBareSessionScreens(ctx, ids.SessionId) + screenArr, err := sstore.GetSessionScreens(ctx, ids.SessionId) if err != nil { return nil, fmt.Errorf("/screen:showall error getting screen list: %v", err) } @@ -1991,7 +1990,7 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) return nil } -func SwResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { +func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return nil, err diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 8d15d0640..05a11b39b 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -291,7 +291,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i } func resolveSessionScreen(ctx context.Context, sessionId string, screenArg string, curScreenArg string) (*ResolveItem, error) { - screens, err := sstore.GetBareSessionScreens(ctx, sessionId) + screens, err := sstore.GetSessionScreens(ctx, sessionId) if err != nil { return nil, fmt.Errorf("could not retreive screens for session=%s: %v", sessionId, err) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 20c9aef1f..07fd4bed5 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -406,6 +406,9 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { } query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` update.Screens = SelectMapsGen[*ScreenType](tx, query) + for _, screen := range update.Screens { + screen.Full = true + } query = `SELECT * FROM remote_instance` riArr := SelectMapsGen[*RemoteInstance](tx, query) for _, ri := range riArr { @@ -436,14 +439,15 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, } // includes archived screens (does not include screen windows) -func GetBareSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { - var rtn []*ScreenType - txErr := WithTx(ctx, func(tx *TxWrap) error { +func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenType, error) { query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` - tx.Select(&rtn, query, sessionId) - return nil + rtn := SelectMapsGen[*ScreenType](tx, query, sessionId) + for _, screen := range rtn { + screen.Full = true + } + return rtn, nil }) - return rtn, txErr } func GetSessionById(ctx context.Context, id string) (*SessionType, error) { @@ -678,6 +682,7 @@ func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { query := `SELECT * FROM screen WHERE screenid = ?` screen := GetMapGen[*ScreenType](tx, query, screenId) + screen.Full = true return screen, nil }) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 90b7666d0..fb5813c8c 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -414,6 +414,7 @@ type ScreenType struct { ArchivedTs int64 `json:"archivedts,omitempty"` // only for updates + Full bool `json:"full,omitempty"` Remove bool `json:"remove,omitempty"` } From f8abfcb5632bcfd8da0bfcb3072d0f6945fb85df Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 12:23:36 -0700 Subject: [PATCH 296/397] bug fixes --- pkg/cmdrunner/cmdrunner.go | 1 + pkg/sstore/dbops.go | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a6f4d28e4..29e97303b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -534,6 +534,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if focusVal != sstore.ScreenFocusInput && focusVal != sstore.ScreenFocusCmd && focusVal != sstore.ScreenFocusCmdFg { return nil, fmt.Errorf("/screen:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.ScreenFocusInput, sstore.ScreenFocusCmd, sstore.ScreenFocusCmdFg}, "or", false)) } + varsUpdated = append(varsUpdated, "focus") updateMap[sstore.ScreenField_Focus] = focusVal setNonAnchor = true } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 07fd4bed5..885780eb1 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -506,6 +506,7 @@ func InsertCloudSession(ctx context.Context, sessionName string, shareMode strin // also creates default window, returns sessionId // if sessionName == "", it will be generated func InsertSessionWithName(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { + var newScreen *ScreenType newSessionId := scbase.GenPromptUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) @@ -514,10 +515,11 @@ func InsertSessionWithName(ctx context.Context, sessionName string, shareMode st query := `INSERT INTO session (sessionid, name, activescreenid, sessionidx, notifynum, archived, archivedts, sharemode) VALUES (?, ?, '', ?, ?, 0, 0, 'local')` tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, 0) - _, err := InsertScreen(tx.Context(), newSessionId, "", true) + screenUpdate, err := InsertScreen(tx.Context(), newSessionId, "", true) if err != nil { return err } + newScreen = screenUpdate.Screens[0] if activate { query = `UPDATE client SET activesessionid = ?` tx.Exec(query, newSessionId) @@ -533,6 +535,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, shareMode st } update := ModelUpdate{ Sessions: []*SessionType{session}, + Screens: []*ScreenType{newScreen}, } if activate { update.ActiveSessionId = newSessionId @@ -614,7 +617,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin } } -func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (UpdatePacket, error) { +func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (*ModelUpdate, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` @@ -667,7 +670,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, if err != nil { return nil, err } - update := ModelUpdate{Screens: []*ScreenType{newScreen}} + update := &ModelUpdate{Screens: []*ScreenType{newScreen}} if activate { bareSession, err := GetBareSessionById(ctx, sessionId) if err != nil { From 2a7a5c739c695ea0a64dfc592d8ad717fd6d2917 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Mar 2023 15:54:35 -0700 Subject: [PATCH 297/397] remove incognito check for now --- pkg/sstore/dbops.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 885780eb1..7e3a1eafe 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -196,13 +196,7 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { } func IsIncognitoScreen(ctx context.Context, sessionId string, screenId string) (bool, error) { - var rtn bool - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT incognito FROM screen WHERE sessionid = ? AND screenid = ?` - tx.Get(&rtn, query, sessionId, screenId) - return nil - }) - return rtn, txErr + return false, nil } const HistoryQueryChunkSize = 1000 From ed607237204d2d0aa172871100ff7515634dde63 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Mar 2023 16:37:22 -0700 Subject: [PATCH 298/397] remove windowid --- cmd/main-server.go | 2 +- db/migrations/000010_removewindowid.down.sql | 0 db/migrations/000010_removewindowid.up.sql | 17 ++ db/schema.sql | 117 ++++----- pkg/cmdrunner/cmdrunner.go | 145 ++++++----- pkg/cmdrunner/resolver.go | 41 +--- pkg/cmdrunner/shparse.go | 4 +- pkg/remote/remote.go | 18 +- pkg/scpacket/scpacket.go | 1 - pkg/sstore/dbops.go | 240 +++++++++---------- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 40 +--- pkg/sstore/updatebus.go | 2 +- 13 files changed, 302 insertions(+), 327 deletions(-) create mode 100644 db/migrations/000010_removewindowid.down.sql create mode 100644 db/migrations/000010_removewindowid.up.sql diff --git a/cmd/main-server.go b/cmd/main-server.go index f6c2e374a..57f3a618d 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -546,7 +546,7 @@ func main() { } err = sstore.ReInitFocus(context.Background()) if err != nil { - log.Printf("[error] resetting window focus: %v\n", err) + log.Printf("[error] resetting screen focus: %v\n", err) } log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) diff --git a/db/migrations/000010_removewindowid.down.sql b/db/migrations/000010_removewindowid.down.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db/migrations/000010_removewindowid.up.sql b/db/migrations/000010_removewindowid.up.sql new file mode 100644 index 000000000..c131c7b51 --- /dev/null +++ b/db/migrations/000010_removewindowid.up.sql @@ -0,0 +1,17 @@ +ALTER TABLE remote_instance RENAME COLUMN windowid TO screenid; +ALTER TABLE line RENAME COLUMN windowid TO screenid; + +UPDATE remote_instance +SET screenid = (SELECT screen.screenid FROM screen WHERE screen.windowid = remote_instance.screenid) +WHERE screenid <> '' +; + +UPDATE line +SET screenid = (SELECT screen.screenid FROM screen WHERE screen.windowid = line.screenid) +WHERE screenid <> '' +; + +ALTER TABLE history DROP COLUMN windowid; +ALTER TABLE screen DROP COLUMN windowid; + + diff --git a/db/schema.sql b/db/schema.sql index aef3ecc2e..059edcc63 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -16,53 +16,12 @@ CREATE TABLE session ( notifynum int NOT NULL, archived boolean NOT NULL, archivedts bigint NOT NULL, - ownerid varchar(36) NOT NULL, - sharemode varchar(12) NOT NULL, - accesskey varchar(36) NOT NULL -); -CREATE TABLE window ( - sessionid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, - curremoteownerid varchar(36) NOT NULL, - curremoteid varchar(36) NOT NULL, - curremotename varchar(50) NOT NULL, - nextlinenum int NOT NULL, - winopts json NOT NULL, - ownerid varchar(36) NOT NULL, - sharemode varchar(12) NOT NULL, - shareopts json NOT NULL, - PRIMARY KEY (sessionid, windowid) -); -CREATE TABLE screen ( - sessionid varchar(36) NOT NULL, - screenid varchar(36) NOT NULL, - name varchar(50) NOT NULL, - activewindowid varchar(36) NOT NULL, - screenidx int NOT NULL, - screenopts json NOT NULL, - ownerid varchar(36) NOT NULL, - sharemode varchar(12) NOT NULL, - incognito boolean NOT NULL, - archived boolean NOT NULL, - archivedts bigint NOT NULL, - PRIMARY KEY (sessionid, screenid) -); -CREATE TABLE screen_window ( - sessionid varchar(36) NOT NULL, - screenid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, - name varchar(50) NOT NULL, - layout json NOT NULL, - selectedline int NOT NULL, - anchor json NOT NULL, - focustype varchar(12) NOT NULL, - PRIMARY KEY (sessionid, screenid, windowid) -); + sharemode varchar(12) NOT NULL); CREATE TABLE remote_instance ( riid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, sessionid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, festate json NOT NULL, @@ -84,7 +43,7 @@ CREATE TABLE state_diff ( ); CREATE TABLE line ( sessionid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, userid varchar(36) NOT NULL, lineid varchar(36) NOT NULL, ts bigint NOT NULL, @@ -98,7 +57,7 @@ CREATE TABLE line ( contentheight int NOT NULL, star int NOT NULL, archived boolean NOT NULL, renderer varchar(50) NOT NULL DEFAULT '', bookmarked boolean NOT NULL DEFAULT 0, pinned boolean NOT NULL DEFAULT 0, - PRIMARY KEY (sessionid, windowid, lineid) + PRIMARY KEY (sessionid, screenid, lineid) ); CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, @@ -145,7 +104,6 @@ CREATE TABLE history ( userid varchar(36) NOT NULL, sessionid varchar(36) NOT NULL, screenid varchar(36) NOT NULL, - windowid varchar(36) NOT NULL, lineid int NOT NULL, remoteownerid varchar(36) NOT NULL, remoteid varchar(36) NOT NULL, @@ -156,15 +114,6 @@ CREATE TABLE history ( ismetacmd boolean, incognito boolean ); -CREATE TABLE activity ( - day varchar(20) PRIMARY KEY, - uploaded boolean NOT NULL, - tdata json NOT NULL, - tzname varchar(50) NOT NULL, - tzoffset int NOT NULL, - clientversion varchar(20) NOT NULL, - clientarch varchar(20) NOT NULL -, buildtime varchar(20) NOT NULL DEFAULT '-', osrelease varchar(20) NOT NULL DEFAULT '-'); CREATE TABLE bookmark ( bookmarkid varchar(36) PRIMARY KEY, createdts bigint NOT NULL, @@ -185,3 +134,61 @@ CREATE TABLE bookmark_cmd ( cmdid varchar(36) NOT NULL, PRIMARY KEY (bookmarkid, sessionid, cmdid) ); +CREATE TABLE activity ( + day varchar(20) PRIMARY KEY, + uploaded boolean NOT NULL, + tdata json NOT NULL, + tzname varchar(50) NOT NULL, + tzoffset int NOT NULL, + clientversion varchar(50) NOT NULL, + clientarch varchar(50) NOT NULL +, buildtime varchar(20) NOT NULL DEFAULT '-', osrelease varchar(20) NOT NULL DEFAULT '-'); +CREATE TABLE playbook ( + playbookid varchar(36) PRIMARY KEY, + playbookname varchar(100) NOT NULL, + description text NOT NULL, + entryids json NOT NULL +); +CREATE TABLE playbook_entry ( + entryid varchar(36) PRIMARY KEY, + playbookid varchar(36) NOT NULL, + description text NOT NULL, + alias varchar(50) NOT NULL, + cmdstr text NOT NULL, + createdts bigint NOT NULL, + updatedts bigint NOT NULL +); +CREATE TABLE cloud_session ( + sessionid varchar(36) PRIMARY KEY, + viewkey varchar(50) NOT NULL, + writekey varchar(50) NOT NULL, + enckey varchar(100) NOT NULL, + enctype varchar(50) NOT NULL, + vts bigint NOT NULL, + acl json NOT NULL +); +CREATE TABLE cloud_update ( + updateid varchar(36) PRIMARY KEY, + ts bigint NOT NULL, + updatetype varchar(50) NOT NULL, + updatekeys json NOT NULL +); +CREATE TABLE IF NOT EXISTS "screen" ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + screenidx int NOT NULL, + screenopts json NOT NULL, + ownerid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + curremoteownerid varchar(36) NOT NULL, + curremoteid varchar(36) NOT NULL, + curremotename varchar(50) NOT NULL, + nextlinenum int NOT NULL, + selectedline int NOT NULL, + anchor json NOT NULL, + focustype varchar(12) NOT NULL, + archived boolean NOT NULL, + archivedts bigint NOT NULL, + PRIMARY KEY (sessionid, screenid) +); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 29e97303b..332c87793 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -28,7 +28,7 @@ import ( ) const ( - HistoryTypeWindow = "window" + HistoryTypeScreen = "screen" HistoryTypeSession = "session" HistoryTypeGlobal = "global" ) @@ -51,25 +51,23 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var WindowCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} +var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry", "bookmark", "bookmarks"} var SetVarNameMap map[string]string = map[string]string{ "tabcolor": "screen.tabcolor", - "pterm": "window.pterm", - "anchor": "sw.anchor", - "focus": "sw.focus", - "line": "sw.line", + "pterm": "screen.pterm", + "anchor": "screen.anchor", + "focus": "screen.focus", + "line": "screen.line", } var SetVarScopes = []SetVarScope{ SetVarScope{ScopeName: "global", VarNames: []string{}}, SetVarScope{ScopeName: "client", VarNames: []string{"telemetry"}}, SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}}, - SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos"}}, - SetVarScope{ScopeName: "window", VarNames: []string{"pterm"}}, - SetVarScope{ScopeName: "sw", VarNames: []string{"anchor", "focus", "line"}}, + SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos", "pterm", "anchor", "focus", "line"}}, SetVarScope{ScopeName: "line", VarNames: []string{}}, // connection = remote, remote = remoteinstance SetVarScope{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}}, @@ -322,7 +320,7 @@ func resolveNonNegInt(arg string, def int) (int, error) { } func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { return nil, fmt.Errorf("/run error: %w", err) } @@ -340,7 +338,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } runPacket.Command = strings.TrimSpace(cmdStr) runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd) - cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, runPacket) + cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket) if callback != nil { defer callback() } @@ -358,7 +356,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, historyContext historyContextType, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return err } @@ -372,7 +370,6 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history UserId: DefaultUserId, SessionId: ids.SessionId, ScreenId: ids.ScreenId, - WindowId: ids.WindowId, LineId: historyContext.LineId, HadError: hadError, CmdId: historyContext.CmdId, @@ -547,7 +544,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if screen.SelectedLine > 0 { selectedLineStr = strconv.Itoa(int(screen.SelectedLine)) } - ritem, err := resolveLine(ctx, screen.SessionId, screen.WindowId, pk.Kwargs["line"], selectedLineStr) + ritem, err := resolveLine(ctx, screen.SessionId, screen.ScreenId, pk.Kwargs["line"], selectedLineStr) if err != nil { return nil, fmt.Errorf("/screen:set error resolving line: %v", err) } @@ -559,7 +556,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss updateMap[sstore.ScreenField_SelectedLine] = ritem.Num } if pk.Kwargs["anchor"] != "" { - m := swAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"]) + m := screenAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"]) if m == nil { return nil, fmt.Errorf("/screen:set invalid anchor argument (must be [line] or [line]:[offset])") } @@ -613,10 +610,10 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } -var swAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") +var screenAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -630,7 +627,7 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -644,7 +641,7 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke } func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -657,7 +654,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -907,7 +904,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -937,7 +934,7 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -1008,7 +1005,7 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( sessionUpdate := &sstore.SessionType{SessionId: ids.SessionId} ris, err := sstore.ScreenReset(ctx, ids.ScreenId) if err != nil { - return nil, fmt.Errorf("error resetting screen window: %v", err) + return nil, fmt.Errorf("error resetting screen: %v", err) } sessionUpdate.Remotes = append(sessionUpdate.Remotes, ris...) err = sstore.UpdateCurRemote(ctx, ids.ScreenId, rptr) @@ -1032,7 +1029,7 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( } func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -1061,7 +1058,7 @@ func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids resolvedIds) (sstore.UpdatePacket, error) { var buf bytes.Buffer - riArr, err := sstore.GetRIsForWindow(ctx, ids.SessionId, ids.WindowId) + riArr, err := sstore.GetRIsForScreen(ctx, ids.SessionId, ids.ScreenId) if err != nil { return nil, fmt.Errorf("cannot get remote instances: %w", err) } @@ -1127,7 +1124,7 @@ func GetFullRemoteDisplayName(rptr *sstore.RemotePtrType, rstate *remote.RemoteR } func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err) } @@ -1135,7 +1132,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if newRemote == "" { return crShowCommand(ctx, pk, ids) } - _, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.WindowId) + _, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.ScreenId) if err != nil { return nil, err } @@ -1197,7 +1194,7 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr } func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType) (*sstore.ModelUpdate, error) { - rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, cmd, "") + rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, cmd, "") if err != nil { return nil, err } @@ -1323,7 +1320,7 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str if !packet.IsValidCompGenType(compType) { return nil, false, fmt.Errorf("/_compgen invalid type '%s'", compType) } - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_RemoteConnected) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { return nil, false, fmt.Errorf("/_compgen error: %w", err) } @@ -1396,7 +1393,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, fmt.Errorf("/comment error: %w", err) } @@ -1404,7 +1401,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if strings.TrimSpace(text) == "" { return nil, fmt.Errorf("cannot post empty comment") } - rtnLine, err := sstore.AddCommentLine(ctx, ids.SessionId, ids.WindowId, DefaultUserId, text) + rtnLine, err := sstore.AddCommentLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, text) if err != nil { return nil, err } @@ -1415,7 +1412,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { // ignore error again (nothing to do) - log.Printf("/comment error updating screen-window selected line: %v\n", err) + log.Printf("/comment error updating screen selected line: %v\n", err) } update := sstore.ModelUpdate{Line: rtnLine, Screens: []*sstore.ScreenType{screen}} return update, nil @@ -1720,7 +1717,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -1732,7 +1729,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, fmt.Errorf("invalid initpk received from remote (no remote state)") } feState := sstore.FeStateFromShellState(initPk.State) - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.WindowId, ids.Remote.RemotePtr, *feState, initPk.State, nil) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, *feState, initPk.State, nil) if err != nil { return nil, err } @@ -1753,7 +1750,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( } func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -1804,7 +1801,7 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } lineObj := &sstore.LineType{ SessionId: historyItem.SessionId, - WindowId: historyItem.WindowId, + ScreenId: historyItem.ScreenId, LineId: historyItem.LineId, Remove: true, } @@ -1910,7 +1907,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType const DefaultMaxHistoryItems = 10000 func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) if err != nil { return nil, err } @@ -1924,22 +1921,22 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if maxItems == 0 { maxItems = DefaultMaxHistoryItems } - htype := HistoryTypeWindow + htype := HistoryTypeScreen hSessionId := ids.SessionId - hWindowId := ids.WindowId + hScreenId := ids.ScreenId if pk.Kwargs["type"] != "" { htype = pk.Kwargs["type"] - if htype != HistoryTypeWindow && htype != HistoryTypeSession && htype != HistoryTypeGlobal { - return nil, fmt.Errorf("invalid history type '%s', valid types: %s", htype, formatStrs([]string{HistoryTypeWindow, HistoryTypeSession, HistoryTypeGlobal}, "or", false)) + if htype != HistoryTypeScreen && htype != HistoryTypeSession && htype != HistoryTypeGlobal { + return nil, fmt.Errorf("invalid history type '%s', valid types: %s", htype, formatStrs([]string{HistoryTypeScreen, HistoryTypeSession, HistoryTypeGlobal}, "or", false)) } } if htype == HistoryTypeGlobal { hSessionId = "" - hWindowId = "" + hScreenId = "" } else if htype == HistoryTypeSession { - hWindowId = "" + hScreenId = "" } - hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, WindowId: hWindowId} + hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId} hresult, err := sstore.GetHistoryItems(ctx, hopts) if err != nil { return nil, err @@ -1955,7 +1952,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto update.History = &sstore.HistoryInfoType{ HistoryType: htype, SessionId: ids.SessionId, - WindowId: ids.WindowId, + ScreenId: ids.ScreenId, Items: hresult.Items, Show: show, } @@ -1992,25 +1989,25 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) } func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } colsStr := pk.Kwargs["cols"] if colsStr == "" { - return nil, fmt.Errorf("/sw:resize requires a numeric 'cols' argument") + return nil, fmt.Errorf("/screen:resize requires a numeric 'cols' argument") } cols, err := strconv.Atoi(colsStr) if err != nil { - return nil, fmt.Errorf("/sw:resize requires a numeric 'cols' argument: %v", err) + return nil, fmt.Errorf("/screen:resize requires a numeric 'cols' argument: %v", err) } if cols <= 0 { - return nil, fmt.Errorf("/sw:resize invalid zero/negative 'cols' argument") + return nil, fmt.Errorf("/screen:resize invalid zero/negative 'cols' argument") } cols = base.BoundInt(cols, shexec.MinTermCols, shexec.MaxTermCols) - runningCmds, err := sstore.GetRunningWindowCmds(ctx, ids.SessionId, ids.WindowId) + runningCmds, err := sstore.GetRunningScreenCmds(ctx, ids.SessionId, ids.ScreenId) if err != nil { - return nil, fmt.Errorf("/sw:resize cannot get running commands: %v", err) + return nil, fmt.Errorf("/screen:resize cannot get running commands: %v", err) } if len(runningCmds) == 0 { return nil, nil @@ -2028,7 +2025,7 @@ func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2036,7 +2033,7 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("/line:setheight requires 2 arguments (linearg and height)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2074,7 +2071,7 @@ func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:view could not get screen: %v", err) } - lineRItem, err := resolveLine(ctx, sessionId, screen.WindowId, lineArg, "") + lineRItem, err := resolveLine(ctx, sessionId, screen.ScreenId, lineArg, "") if err != nil { return nil, fmt.Errorf("/line:view invalid line arg: %v", err) } @@ -2179,7 +2176,7 @@ func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2187,14 +2184,14 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("/line:bookmark requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) + lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) } @@ -2215,7 +2212,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("cannot insert bookmark: %v", err) } - newLineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) + newLineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) } @@ -2231,7 +2228,7 @@ func LinePinCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2242,7 +2239,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, fmt.Errorf("/line:star only takes up to 2 arguments (line-number and star-value)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2260,7 +2257,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:star error updating star value: %v", err) } - lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:star error getting line: %v", err) } @@ -2272,7 +2269,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst } func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2280,7 +2277,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, fmt.Errorf("/line:archive requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2295,7 +2292,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err) } - lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.WindowId, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:archive error getting line: %v", err) } @@ -2307,7 +2304,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( } func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2316,7 +2313,7 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } var lineIds []string for _, lineArg := range pk.Args { - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2333,7 +2330,7 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss for _, lineId := range lineIds { lineObj := &sstore.LineType{ SessionId: ids.SessionId, - WindowId: ids.WindowId, + ScreenId: ids.ScreenId, LineId: lineId, Remove: true, } @@ -2343,7 +2340,7 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2351,14 +2348,14 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, fmt.Errorf("/line:show requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("error getting line: %v", err) } @@ -2438,7 +2435,7 @@ func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Window) + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { return nil, err } @@ -2449,11 +2446,11 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/signal requires a second argument (signal name)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.WindowId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } - line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.WindowId, lineId) + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("error getting line: %v", err) } @@ -2837,7 +2834,7 @@ func isValidInScope(scopeName string, varName string) bool { } // returns (is-valid, scope, name) -// TODO write a full resolver to allow for indexed arguments. e.g. session[1].screen[1].window.pterm="25x80" +// TODO write a full resolver to allow for indexed arguments. e.g. session[1].screen[1].screen.pterm="25x80" func resolveSetArg(argName string) (bool, string, string) { dotIdx := strings.Index(argName, ".") if dotIdx == -1 { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 05a11b39b..67f3ed6c0 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -17,7 +17,6 @@ import ( const ( R_Session = 1 R_Screen = 2 - R_Window = 4 R_Remote = 8 R_RemoteConnected = 16 ) @@ -25,7 +24,6 @@ const ( type resolvedIds struct { SessionId string ScreenId string - WindowId string Remote *ResolvedRemote } @@ -203,7 +201,6 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if uictx != nil { rtn.SessionId = uictx.SessionId rtn.ScreenId = uictx.ScreenId - rtn.WindowId = uictx.WindowId } if pk.Kwargs["session"] != "" { sessionId, err := resolveSessionArg(pk.Kwargs["session"]) @@ -223,15 +220,6 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i rtn.ScreenId = screenId } } - if pk.Kwargs["window"] != "" { - windowId, err := resolveWindowArg(rtn.SessionId, rtn.ScreenId, pk.Kwargs["window"]) - if err != nil { - return rtn, err - } - if windowId != "" { - rtn.WindowId = windowId - } - } var rptr *sstore.RemotePtrType var err error if pk.Kwargs["remote"] != "" { @@ -250,7 +238,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if err != nil { return rtn, fmt.Errorf("invalid resolved remote: %v", err) } - rr, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.WindowId) + rr, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) if err != nil { return rtn, err } @@ -262,9 +250,6 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if rtype&R_Screen > 0 && rtn.ScreenId == "" { return rtn, fmt.Errorf("no screen") } - if rtype&R_Window > 0 && rtn.WindowId == "" { - return rtn, fmt.Errorf("no window") - } if (rtype&R_Remote > 0 || rtype&R_RemoteConnected > 0) && rtn.Remote == nil { return rtn, fmt.Errorf("no remote") } @@ -274,7 +259,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if err != nil { return rtn, fmt.Errorf("error trying to auto-connect remote [%s]: %w", rtn.Remote.DisplayName, err) } - rrNew, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.WindowId) + rrNew, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) if err != nil { return rtn, err } @@ -312,8 +297,8 @@ func resolveSession(ctx context.Context, sessionArg string, curSessionArg string return ritem, nil } -func resolveLine(ctx context.Context, sessionId string, windowId string, lineArg string, curLineArg string) (*ResolveItem, error) { - lines, err := sstore.GetLineResolveItems(ctx, sessionId, windowId) +func resolveLine(ctx context.Context, sessionId string, screenId string, lineArg string, curLineArg string) (*ResolveItem, error) { + lines, err := sstore.GetLineResolveItems(ctx, sessionId, screenId) if err != nil { return nil, fmt.Errorf("could not get lines: %v", err) } @@ -402,16 +387,6 @@ func resolveSessionId(pk *scpacket.FeCommandPacketType) (string, error) { return sessionId, nil } -func resolveWindowArg(sessionId string, screenId string, windowArg string) (string, error) { - if windowArg == "" { - return "", nil - } - if _, err := uuid.Parse(windowArg); err != nil { - return "", fmt.Errorf("invalid window arg specified (must be windowid) '%s'", windowArg) - } - return windowArg, nil -} - func resolveSessionArg(sessionArg string) (string, error) { if sessionArg == "" { return "", nil @@ -471,7 +446,7 @@ func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { return fields[0], fields[1], fields[2], nil } -func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, windowId string) (*ResolvedRemote, error) { +func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, screenId string) (*ResolvedRemote, error) { if rptr == nil || rptr.RemoteId == "" { return nil, nil } @@ -491,8 +466,8 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi StatePtr: nil, FeState: nil, } - if sessionId != "" && windowId != "" { - ri, err := sstore.GetRemoteInstance(ctx, sessionId, windowId, *rptr) + if sessionId != "" && screenId != "" { + ri, err := sstore.GetRemoteInstance(ctx, sessionId, screenId, *rptr) if err != nil { log.Printf("ERROR resolving remote state '%s': %v\n", displayName, err) // continue with state set to nil @@ -510,7 +485,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi } // returns (remoteDisplayName, remoteptr, state, rstate, err) -func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, windowId string) (string, *sstore.RemotePtrType, *remote.RemoteRuntimeState, error) { +func resolveRemote(ctx context.Context, fullRemoteRef string, sessionId string, screenId string) (string, *sstore.RemotePtrType, *remote.RemoteRuntimeState, error) { if fullRemoteRef == "" { return "", nil, nil, nil } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index f6dfbabe5..8d5aea0ee 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -69,8 +69,6 @@ func SubMetaCmd(cmd string) string { switch cmd { case "s": return "screen" - case "w": - return "window" case "r": return "run" case "c": @@ -245,7 +243,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("parsing metacmd, position %v", err) } - envMap := make(map[string]string) // later we can add vars like session, window, screen, remote, and user + envMap := make(map[string]string) // later we can add vars like session, screen, remote, and user cfg := shexec.GetParserConfig(envMap) // process arguments for idx, w := range words { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index b0ce19bf0..23d3d9716 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -117,7 +117,7 @@ type MShellProc struct { type RunCmdType struct { SessionId string - WindowId string + ScreenId string RemotePtr sstore.RemotePtrType RunPacket *packet.RunPacketType } @@ -1250,10 +1250,10 @@ func (msh *MShellProc) removePendingStateCmd(riName string, ck base.CommandKey) } // returns (cmdtype, allow-updates-callback, err) -func RunCommand(ctx context.Context, sessionId string, windowId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { +func RunCommand(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) { rct := RunCmdType{ SessionId: sessionId, - WindowId: windowId, + ScreenId: screenId, RemotePtr: remotePtr, RunPacket: runPacket, } @@ -1279,7 +1279,7 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt } ok, existingPSC := msh.testAndSetPendingStateCmd(remotePtr.Name, newPSC) if !ok { - line, _, err := sstore.GetLineCmdByCmdId(ctx, sessionId, windowId, existingPSC.GetCmdId()) + line, _, err := sstore.GetLineCmdByCmdId(ctx, sessionId, screenId, existingPSC.GetCmdId()) if err != nil { return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) } @@ -1298,7 +1298,7 @@ func RunCommand(ctx context.Context, sessionId string, windowId string, remotePt } }() // get current remote-instance state - statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, windowId, remotePtr) + statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr) if err != nil { return nil, nil, fmt.Errorf("cannot get current remote stateptr: %w", err) } @@ -1371,7 +1371,7 @@ func (msh *MShellProc) reExecSingle(rct RunCmdType) { // TODO fixme ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second) defer cancelFn() - _, callback, _ := RunCommand(ctx, rct.SessionId, rct.WindowId, rct.RemotePtr, rct.RunPacket) + _, callback, _ := RunCommand(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, rct.RunPacket) if callback != nil { defer callback() } @@ -1501,7 +1501,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { } screens, err := sstore.UpdateScreensWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) if err != nil { - msh.WriteToPtyBuffer("*error trying to update cmd-fg screen windows: %v\n", err) + msh.WriteToPtyBuffer("*error trying to update cmd-fg screens: %v\n", err) // fall-through (nothing to do) } update.Screens = screens @@ -1509,7 +1509,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { feState := sstore.FeStateFromShellState(donePk.FinalState) - remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, donePk.FinalState, nil) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, *feState, donePk.FinalState, nil) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) @@ -1524,7 +1524,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) } else { - remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.WindowId, rct.RemotePtr, *feState, nil, donePk.FinalStateDiff) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, *feState, nil, donePk.FinalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 8c1b81843..3e2bf6330 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -52,7 +52,6 @@ func (pk *FeCommandPacketType) GetRawStr() string { type UIContextType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` Remote *sstore.RemotePtrType `json:"remote,omitempty"` WinSize *packet.WinSize `json:"winsize,omitempty"` Build string `json:"build,omitempty"` diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 7e3a1eafe..b0986d0d6 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -17,7 +17,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -const HistoryCols = "historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito" +const HistoryCols = "historyid, ts, userid, sessionid, screenid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito" const DefaultMaxHistoryItems = 1000 type SingleConnDBGetter struct { @@ -187,8 +187,8 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { } txErr := WithTx(ctx, func(tx *TxWrap) error { query := `INSERT INTO history - ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito)` + ( historyid, ts, userid, sessionid, screenid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito)` tx.NamedExec(query, hitem.ToMap()) return nil }) @@ -268,17 +268,17 @@ func runHistoryQueryWithFilter(tx *TxWrap, opts HistoryQueryOpts) (*HistoryQuery } func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimit int) ([]*HistoryItemType, error) { - // check sessionid/windowid format because we are directly inserting them into the SQL + // check sessionid/screenid format because we are directly inserting them into the SQL if opts.SessionId != "" { _, err := uuid.Parse(opts.SessionId) if err != nil { return nil, fmt.Errorf("malformed sessionid") } } - if opts.WindowId != "" { - _, err := uuid.Parse(opts.WindowId) + if opts.ScreenId != "" { + _, err := uuid.Parse(opts.ScreenId) if err != nil { - return nil, fmt.Errorf("malformed windowid") + return nil, fmt.Errorf("malformed screenid") } } if opts.RemoteId != "" { @@ -290,8 +290,8 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi hnumStr := "" whereClause := "WHERE 1" var queryArgs []interface{} - if opts.SessionId != "" && opts.WindowId != "" { - whereClause += fmt.Sprintf(" AND sessionid = '%s' AND windowid = '%s'", opts.SessionId, opts.WindowId) + if opts.SessionId != "" && opts.ScreenId != "" { + whereClause += fmt.Sprintf(" AND sessionid = '%s' AND screenid = '%s'", opts.SessionId, opts.ScreenId) hnumStr = "w" } else if opts.SessionId != "" { whereClause += fmt.Sprintf(" AND sessionid = '%s'", opts.SessionId) @@ -419,20 +419,20 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenLinesType, error) { - query := `SELECT sessionid, screenid, windowid FROM screen WHERE screenid = ?` + query := `SELECT sessionid, screenid FROM screen WHERE screenid = ?` screen := GetMappable[*ScreenLinesType](tx, query, screenId) if screen == nil { return nil, nil } - query = `SELECT * FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` - tx.Select(&screen.Lines, query, screen.SessionId, screen.WindowId) - query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?)` - screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.SessionId, screen.WindowId) + query = `SELECT * FROM line WHERE sessionid = ? AND screenid = ? ORDER BY linenum` + tx.Select(&screen.Lines, query, screen.SessionId, screen.ScreenId) + query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND screenid = ?)` + screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.SessionId, screen.ScreenId) return screen, nil }) } -// includes archived screens (does not include screen windows) +// includes archived screens func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenType, error) { query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` @@ -497,7 +497,7 @@ func InsertCloudSession(ctx context.Context, sessionName string, shareMode strin return updateRtn, nil } -// also creates default window, returns sessionId +// returns sessionId // if sessionName == "", it will be generated func InsertSessionWithName(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { var newScreen *ScreenType @@ -622,7 +622,6 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, if localRemoteId == "" { return fmt.Errorf("cannot create screen, no local remote found") } - newWindowId := scbase.GenPromptUUID() maxScreenIdx := tx.GetInt(`SELECT COALESCE(max(screenidx), 0) FROM screen WHERE sessionid = ? AND NOT archived`, sessionId) var screenName string if origScreenName == "" { @@ -635,7 +634,6 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, screen := &ScreenType{ SessionId: sessionId, ScreenId: newScreenId, - WindowId: newWindowId, Name: screenName, ScreenIdx: int64(maxScreenIdx) + 1, ScreenOpts: ScreenOptsType{}, @@ -649,7 +647,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, Archived: false, ArchivedTs: 0, } - query = `INSERT INTO screen (sessionid, screenid, windowid, name, screenidx, screenopts, ownerid, sharemode, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) VALUES (:sessionid,:screenid,:windowid,:name,:screenidx,:screenopts,:ownerid,:sharemode,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)` + query = `INSERT INTO screen (sessionid, screenid, name, screenidx, screenopts, ownerid, sharemode, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,:ownerid,:sharemode,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)` tx.NamedExec(query, screen.ToMap()) if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` @@ -684,22 +682,22 @@ func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { }) } -func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lineArg string) (string, error) { +func FindLineIdByArg(ctx context.Context, sessionId string, screenId string, lineArg string) (string, error) { var lineId string txErr := WithTx(ctx, func(tx *TxWrap) error { lineNum, err := strconv.Atoi(lineArg) if err == nil { // valid linenum - query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND linenum = ?` - lineId = tx.GetString(query, sessionId, windowId, lineNum) + query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND linenum = ?` + lineId = tx.GetString(query, sessionId, screenId, lineNum) } else if len(lineArg) == 8 { // prefix id string match - query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND substr(lineid, 1, 8) = ?` - lineId = tx.GetString(query, sessionId, windowId, lineArg) + query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND substr(lineid, 1, 8) = ?` + lineId = tx.GetString(query, sessionId, screenId, lineArg) } else { // id match - query := `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` - lineId = tx.GetString(query, sessionId, windowId, lineArg) + query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` + lineId = tx.GetString(query, sessionId, screenId, lineArg) } return nil }) @@ -709,11 +707,11 @@ func FindLineIdByArg(ctx context.Context, sessionId string, windowId string, lin return lineId, nil } -func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, *CmdType, error) { +func GetLineCmdByLineId(ctx context.Context, sessionId string, screenId string, lineId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` - found := tx.Get(&lineVal, query, sessionId, windowId, lineId) + query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` + found := tx.Get(&lineVal, query, sessionId, screenId, lineId) if !found { return nil, nil, nil } @@ -726,11 +724,11 @@ func GetLineCmdByLineId(ctx context.Context, sessionId string, windowId string, }) } -func GetLineCmdByCmdId(ctx context.Context, sessionId string, windowId string, cmdId string) (*LineType, *CmdType, error) { +func GetLineCmdByCmdId(ctx context.Context, sessionId string, screenId string, cmdId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND cmdid = ?` - found := tx.Get(&lineVal, query, sessionId, windowId, cmdId) + query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND cmdid = ?` + found := tx.Get(&lineVal, query, sessionId, screenId, cmdId) if !found { return nil, nil, nil } @@ -751,18 +749,18 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { return fmt.Errorf("line should not hage linenum set") } return WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, line.SessionId, line.WindowId) { - return fmt.Errorf("screen not found, cannot insert line[%s/%s]", line.SessionId, line.WindowId) + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + if !tx.Exists(query, line.SessionId, line.ScreenId) { + return fmt.Errorf("screen not found, cannot insert line[%s/%s]", line.SessionId, line.ScreenId) } - query = `SELECT nextlinenum FROM screen WHERE sessionid = ? AND windowid = ?` - nextLineNum := tx.GetInt(query, line.SessionId, line.WindowId) + query = `SELECT nextlinenum FROM screen WHERE sessionid = ? AND screenid = ?` + nextLineNum := tx.GetInt(query, line.SessionId, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, windowid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked, pinned) - VALUES (:sessionid,:windowid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked,:pinned)` + query = `INSERT INTO line ( sessionid, screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked, pinned) + VALUES (:sessionid,:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked,:pinned)` tx.NamedExec(query, line) - query = `UPDATE screen SET nextlinenum = ? WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, nextLineNum+1, line.SessionId, line.WindowId) + query = `UPDATE screen SET nextlinenum = ? WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, nextLineNum+1, line.SessionId, line.ScreenId) if cmd != nil { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() @@ -950,17 +948,17 @@ func cleanSessionCmds(ctx context.Context, sessionId string) error { return nil } -func CleanWindow(sessionId string, windowId string) { +func CleanScreen(sessionId string, screenId string) { // NOTE: context.Background() here! (this could take a long time, and is async) txErr := WithTx(context.Background(), func(tx *TxWrap) error { - query := `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) - query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, sessionId, windowId) + query := `DELETE FROM history WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) + query = `DELETE FROM line WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) return cleanSessionCmds(tx.Context(), sessionId) }) if txErr != nil { - fmt.Printf("ERROR cleaning session:%s window:%s : %v\n", sessionId, windowId, txErr) + fmt.Printf("ERROR cleaning session:%s screen:%s : %v\n", sessionId, screenId, txErr) } } @@ -1024,28 +1022,28 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err } func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { - var swkeys SWKeys + var sessionId string var isActive bool 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)") } - query = `SELECT sessionid, windowid FROM screen WHERE screenid = ?` - tx.Get(&swkeys, query, screenId) - if swkeys.SessionId == "" || swkeys.WindowId == "" { - return fmt.Errorf("cannot purge screen (no windowid)") + query = `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId = tx.GetString(query, screenId) + if sessionId == "" { + return fmt.Errorf("cannot purge screen (no sessionid)") } query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived` - numScreens := tx.GetInt(query, swkeys.SessionId) + numScreens := tx.GetInt(query, sessionId) if numScreens <= 1 { return fmt.Errorf("cannot purge the last screen in a session") } - isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, swkeys.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`, swkeys.SessionId) + screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId) nextId := getNextId(screenIds, screenId) - tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, swkeys.SessionId) + tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) } query = `DELETE FROM screen WHERE screenid = ?` tx.Exec(query, screenId) @@ -1054,11 +1052,11 @@ func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { if txErr != nil { return nil, txErr } - go CleanWindow(swkeys.SessionId, swkeys.WindowId) + go CleanScreen(sessionId, screenId) update := ModelUpdate{} - update.Screens = []*ScreenType{&ScreenType{SessionId: swkeys.SessionId, ScreenId: screenId, Remove: true}} + update.Screens = []*ScreenType{&ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}} if isActive { - bareSession, err := GetBareSessionById(ctx, swkeys.SessionId) + bareSession, err := GetBareSessionById(ctx, sessionId) if err != nil { return nil, err } @@ -1067,8 +1065,8 @@ func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { return update, nil } -func GetRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*packet.ShellState, *ShellStatePtr, error) { - ssptr, err := GetRemoteStatePtr(ctx, sessionId, windowId, remotePtr) +func GetRemoteState(ctx context.Context, sessionId string, screenId string, remotePtr RemotePtrType) (*packet.ShellState, *ShellStatePtr, error) { + ssptr, err := GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr) if err != nil { return nil, nil, err } @@ -1082,10 +1080,10 @@ func GetRemoteState(ctx context.Context, sessionId string, windowId string, remo return state, ssptr, err } -func GetRemoteStatePtr(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*ShellStatePtr, error) { +func GetRemoteStatePtr(ctx context.Context, sessionId string, screenId string, remotePtr RemotePtrType) (*ShellStatePtr, error) { var ssptr *ShellStatePtr txErr := WithTx(ctx, func(tx *TxWrap) error { - ri, err := GetRemoteInstance(tx.Context(), sessionId, windowId, remotePtr) + ri, err := GetRemoteInstance(tx.Context(), sessionId, screenId, remotePtr) if err != nil { return err } @@ -1101,30 +1099,30 @@ func GetRemoteStatePtr(ctx context.Context, sessionId string, windowId string, r return ssptr, nil } -func validateSessionWindow(tx *TxWrap, sessionId string, windowId string) error { - if windowId == "" { +func validateSessionScreen(tx *TxWrap, sessionId string, screenId string) error { + if screenId == "" { query := `SELECT sessionid FROM session WHERE sessionid = ?` if !tx.Exists(query, sessionId) { return fmt.Errorf("no session found") } return nil } else { - query := `SELECT windowid FROM screen WHERE sessionid = ? AND windowid = ?` - if !tx.Exists(query, sessionId, windowId) { + query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` + if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("no screen found") } return nil } } -func GetRemoteInstance(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType) (*RemoteInstance, error) { +func GetRemoteInstance(ctx context.Context, sessionId string, screenId string, remotePtr RemotePtrType) (*RemoteInstance, error) { if remotePtr.IsSessionScope() { - windowId = "" + screenId = "" } var ri *RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - ri = GetMapGen[*RemoteInstance](tx, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND screenid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + ri = GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) return nil }) if txErr != nil { @@ -1153,7 +1151,7 @@ func updateRIWithState(ctx context.Context, ri *RemoteInstance, stateBase *packe return nil } -func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, remotePtr RemotePtrType, feState FeStateType, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) (*RemoteInstance, error) { +func UpdateRemoteState(ctx context.Context, sessionId string, screenId string, remotePtr RemotePtrType, feState FeStateType, stateBase *packet.ShellState, stateDiff *packet.ShellStateDiff) (*RemoteInstance, error) { if stateBase == nil && stateDiff == nil { return nil, fmt.Errorf("UpdateRemoteState, must set state or diff") } @@ -1161,22 +1159,22 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r return nil, fmt.Errorf("UpdateRemoteState, cannot set state and diff") } if remotePtr.IsSessionScope() { - windowId = "" + screenId = "" } var ri *RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { - err := validateSessionWindow(tx, sessionId, windowId) + err := validateSessionScreen(tx, sessionId, screenId) if err != nil { return fmt.Errorf("cannot update remote instance state: %w", err) } - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - ri = GetMapGen[*RemoteInstance](tx, query, sessionId, windowId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND screenid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` + ri = GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if ri == nil { ri = &RemoteInstance{ RIId: scbase.GenPromptUUID(), Name: remotePtr.Name, SessionId: sessionId, - WindowId: windowId, + ScreenId: screenId, RemoteOwnerId: remotePtr.OwnerId, RemoteId: remotePtr.RemoteId, FeState: feState, @@ -1185,8 +1183,8 @@ func UpdateRemoteState(ctx context.Context, sessionId string, windowId string, r if err != nil { return err } - query = `INSERT INTO remote_instance ( riid, name, sessionid, windowid, remoteownerid, remoteid, festate, statebasehash, statediffhasharr) - VALUES (:riid,:name,:sessionid,:windowid,:remoteownerid,:remoteid,:festate,:statebasehash,:statediffhasharr)` + query = `INSERT INTO remote_instance ( riid, name, sessionid, screenid, remoteownerid, remoteid, festate, statebasehash, statediffhasharr) + VALUES (:riid,:name,:sessionid,:screenid,:remoteownerid,:remoteid,:festate,:statebasehash,:statediffhasharr)` tx.NamedExec(query, ri.ToMap()) return nil } else { @@ -1293,14 +1291,13 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` - var swkeys SWKeys - tx.Get(&swkeys, query, screenId) - if swkeys.SessionId == "" || swkeys.WindowId == "" { - return fmt.Errorf("screen windowid does not exist") + query := `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId := tx.GetString(query, screenId) + if sessionId == "" { + return fmt.Errorf("screen sessionid does not exist") } - query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, swkeys.SessionId, swkeys.WindowId) + query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) return nil }) if txErr != nil { @@ -1315,19 +1312,19 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { var lineIds []string - var swkeys SWKeys + var sessionId string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` - tx.Get(&swkeys, query, screenId) - if swkeys.SessionId == "" || swkeys.WindowId == "" { - return fmt.Errorf("screen windowid does not exist") + query := `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId = tx.GetString(query, screenId) + if sessionId == "" { + return fmt.Errorf("screen sessionid does not exist") } - query = `SELECT lineid FROM line WHERE sessionid = ? AND windowid = ?` - lineIds = tx.SelectStrings(query, swkeys.SessionId, swkeys.WindowId) - query = `DELETE FROM line WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, swkeys.SessionId, swkeys.WindowId) - query = `DELETE FROM history WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, swkeys.SessionId, swkeys.WindowId) + query = `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ?` + lineIds = tx.SelectStrings(query, sessionId, screenId) + query = `DELETE FROM line WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) + query = `DELETE FROM history WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) query = `UPDATE screen SET nextlinenum = 1 WHERE screenid = ?` tx.Exec(query, screenId) return nil @@ -1335,7 +1332,7 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error if txErr != nil { return nil, txErr } - go cleanSessionCmds(context.Background(), swkeys.SessionId) + go cleanSessionCmds(context.Background(), sessionId) screen, err := GetScreenById(ctx, screenId) if err != nil { return nil, err @@ -1346,8 +1343,8 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error } for _, lineId := range lineIds { line := &LineType{ - SessionId: swkeys.SessionId, - WindowId: swkeys.WindowId, + SessionId: sessionId, + ScreenId: screenId, LineId: lineId, Remove: true, } @@ -1356,11 +1353,11 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error return &ModelUpdate{Screens: []*ScreenType{screen}, ScreenLines: screenLines}, nil } -func GetRunningWindowCmds(ctx context.Context, sessionId string, windowId string) ([]*CmdType, error) { +func GetRunningScreenCmds(ctx context.Context, sessionId string, screenId string) ([]*CmdType, error) { var rtn []*CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND windowid = ?) AND status = ?` - rtn = SelectMapsGen[*CmdType](tx, query, sessionId, windowId, CmdStatusRunning) + query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND screenid = ?) AND status = ?` + rtn = SelectMapsGen[*CmdType](tx, query, sessionId, screenId, CmdStatusRunning) return nil }) if txErr != nil { @@ -1381,21 +1378,20 @@ func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, term // returns riids of deleted RIs func ScreenReset(ctx context.Context, screenId string) ([]*RemoteInstance, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]*RemoteInstance, error) { - var swkeys SWKeys - query := `SELECT sessionid, windowid FROM screen WHERE screenid = ?` - tx.Get(&swkeys, query, screenId) - if swkeys.SessionId == "" || swkeys.WindowId == "" { + query := `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId := tx.GetString(query, screenId) + if sessionId == "" { return nil, fmt.Errorf("screen does not exist") } - query = `SELECT riid FROM remote_instance WHERE sessionid = ? AND windowid = ?` - riids := tx.SelectStrings(query, swkeys.SessionId, swkeys.WindowId) + query = `SELECT riid FROM remote_instance WHERE sessionid = ? AND screenid = ?` + riids := tx.SelectStrings(query, sessionId, screenId) var delRis []*RemoteInstance for _, riid := range riids { - ri := &RemoteInstance{SessionId: swkeys.SessionId, WindowId: swkeys.WindowId, RIId: riid, Remove: true} + ri := &RemoteInstance{SessionId: sessionId, ScreenId: screenId, RIId: riid, Remove: true} delRis = append(delRis, ri) } - query = `DELETE FROM remote_instance WHERE sessionid = ? AND windowid = ?` - tx.Exec(query, swkeys.SessionId, swkeys.WindowId) + query = `DELETE FROM remote_instance WHERE sessionid = ? AND screenid = ?` + tx.Exec(query, sessionId, screenId) return delRis, nil }) } @@ -1669,11 +1665,11 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter return GetScreenById(ctx, screenId) } -func GetLineResolveItems(ctx context.Context, sessionId string, windowId string) ([]ResolveItem, error) { +func GetLineResolveItems(ctx context.Context, sessionId string, screenId string) ([]ResolveItem, error) { var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT lineid as id, linenum as num FROM line WHERE sessionid = ? AND windowid = ? ORDER BY linenum` - tx.Select(&rtn, query, sessionId, windowId) + query := `SELECT lineid as id, linenum as num FROM line WHERE sessionid = ? AND screenid = ? ORDER BY linenum` + tx.Select(&rtn, query, sessionId, screenId) return nil }) if txErr != nil { @@ -1693,7 +1689,7 @@ func UpdateScreensWithCmdFg(ctx context.Context, sessionId string, cmdId string) AND s.selectedline IN (SELECT linenum FROM line l WHERE l.sessionid = s.sessionid - AND l.windowid = s.windowid + AND l.screenid = s.screenid AND l.cmdid = ? )` screenIds := tx.SelectStrings(query, sessionId, cmdId) @@ -1840,12 +1836,12 @@ func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { } // can return nil, nil if line is not found -func GetLineById(ctx context.Context, sessionId string, windowId string, lineId string) (*LineType, error) { +func GetLineById(ctx context.Context, sessionId string, screenId string, lineId string) (*LineType, error) { var rtn *LineType txErr := WithTx(ctx, func(tx *TxWrap) error { var line LineType - query := `SELECT * FROM line WHERE sessionid = ? AND windowid = ? AND lineid = ?` - found := tx.Get(&line, query, sessionId, windowId, lineId) + query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` + found := tx.Get(&line, query, sessionId, screenId, lineId) if found { rtn = &line } @@ -1900,11 +1896,11 @@ func PurgeLinesByIds(ctx context.Context, sessionId string, lineIds []string) er return txErr } -func GetRIsForWindow(ctx context.Context, sessionId string, windowId string) ([]*RemoteInstance, error) { +func GetRIsForScreen(ctx context.Context, sessionId string, screenId string) ([]*RemoteInstance, error) { var rtn []*RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote_instance WHERE sessionid = ? AND (windowid = '' OR windowid = ?)` - rtn = SelectMapsGen[*RemoteInstance](tx, query, sessionId, windowId) + query := `SELECT * FROM remote_instance WHERE sessionid = ? AND (screenid = '' OR screenid = ?)` + rtn = SelectMapsGen[*RemoteInstance](tx, query, sessionId, screenId) return nil }) if txErr != nil { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index c49f46bf8..5e98e3e7a 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 9 +const MaxMigration = 10 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index fb5813c8c..de02764c6 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -34,9 +34,7 @@ const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const DefaultSessionName = "default" -const DefaultWindowName = "default" const LocalRemoteAlias = "local" -const DefaultScreenWindowName = "w1" const DefaultCwd = "~" @@ -343,7 +341,6 @@ func (h *HistoryItemType) ToMap() map[string]interface{} { rtn["userid"] = h.UserId rtn["sessionid"] = h.SessionId rtn["screenid"] = h.ScreenId - rtn["windowid"] = h.WindowId rtn["lineid"] = h.LineId rtn["haderror"] = h.HadError rtn["cmdid"] = h.CmdId @@ -362,7 +359,6 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { quickSetStr(&h.UserId, m, "userid") quickSetStr(&h.SessionId, m, "sessionid") quickSetStr(&h.ScreenId, m, "screenid") - quickSetStr(&h.WindowId, m, "windowid") quickSetStr(&h.LineId, m, "lineid") quickSetBool(&h.HadError, m, "haderror") quickSetStr(&h.CmdId, m, "cmdid") @@ -381,15 +377,9 @@ type ScreenOptsType struct { PTerm string `json:"pterm,omitempty"` } -type SWKeys struct { - SessionId string - WindowId string -} - type ScreenLinesType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` Lines []*LineType `json:"lines" dbmap:"-"` Cmds []*CmdType `json:"cmds" dbmap:"-"` } @@ -399,7 +389,6 @@ func (ScreenLinesType) UseDBMap() {} type ScreenType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` Name string `json:"name"` ScreenIdx int64 `json:"screenidx"` ScreenOpts ScreenOptsType `json:"screenopts"` @@ -422,7 +411,6 @@ func (s *ScreenType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = s.SessionId rtn["screenid"] = s.ScreenId - rtn["windowid"] = s.WindowId rtn["name"] = s.Name rtn["screenidx"] = s.ScreenIdx rtn["screenopts"] = quickJson(s.ScreenOpts) @@ -443,7 +431,6 @@ func (s *ScreenType) ToMap() map[string]interface{} { func (s *ScreenType) FromMap(m map[string]interface{}) bool { quickSetStr(&s.SessionId, m, "sessionid") quickSetStr(&s.ScreenId, m, "screenid") - quickSetStr(&s.WindowId, m, "windowid") quickSetStr(&s.Name, m, "name") quickSetInt64(&s.ScreenIdx, m, "screenidx") quickSetJson(&s.ScreenOpts, m, "screenopts") @@ -497,7 +484,6 @@ type HistoryItemType struct { UserId string `json:"userid"` SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` LineId string `json:"lineid"` HadError bool `json:"haderror"` CmdId string `json:"cmdid"` @@ -520,7 +506,7 @@ type HistoryQueryOpts struct { SearchText string SessionId string RemoteId string - WindowId string + ScreenId string NoMeta bool RawOffset int FilterFn func(*HistoryItemType) bool @@ -568,7 +554,7 @@ type RemoteInstance struct { RIId string `json:"riid"` Name string `json:"name"` SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` + ScreenId string `json:"screenid"` RemoteOwnerId string `json:"remoteownerid"` RemoteId string `json:"remoteid"` FeState FeStateType `json:"festate"` @@ -629,7 +615,7 @@ func (ri *RemoteInstance) FromMap(m map[string]interface{}) bool { quickSetStr(&ri.RIId, m, "riid") quickSetStr(&ri.Name, m, "name") quickSetStr(&ri.SessionId, m, "sessionid") - quickSetStr(&ri.WindowId, m, "windowid") + quickSetStr(&ri.ScreenId, m, "screenid") quickSetStr(&ri.RemoteOwnerId, m, "remoteownerid") quickSetStr(&ri.RemoteId, m, "remoteid") quickSetJson(&ri.FeState, m, "festate") @@ -643,7 +629,7 @@ func (ri *RemoteInstance) ToMap() map[string]interface{} { rtn["riid"] = ri.RIId rtn["name"] = ri.Name rtn["sessionid"] = ri.SessionId - rtn["windowid"] = ri.WindowId + rtn["screenid"] = ri.ScreenId rtn["remoteownerid"] = ri.RemoteOwnerId rtn["remoteid"] = ri.RemoteId rtn["festate"] = quickJson(ri.FeState) @@ -654,7 +640,7 @@ func (ri *RemoteInstance) ToMap() map[string]interface{} { type LineType struct { SessionId string `json:"sessionid"` - WindowId string `json:"windowid"` + ScreenId string `json:"screenid"` UserId string `json:"userid"` LineId string `json:"lineid"` Ts int64 `json:"ts"` @@ -948,10 +934,10 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool { return true } -func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId string, renderer string) *LineType { +func makeNewLineCmd(sessionId string, screenId string, userId string, cmdId string, renderer string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId - rtn.WindowId = windowId + rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() @@ -963,10 +949,10 @@ func makeNewLineCmd(sessionId string, windowId string, userId string, cmdId stri return rtn } -func makeNewLineText(sessionId string, windowId string, userId string, text string) *LineType { +func makeNewLineText(sessionId string, screenId string, userId string, text string) *LineType { rtn := &LineType{} rtn.SessionId = sessionId - rtn.WindowId = windowId + rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() rtn.Ts = time.Now().UnixMilli() @@ -977,8 +963,8 @@ func makeNewLineText(sessionId string, windowId string, userId string, text stri return rtn } -func AddCommentLine(ctx context.Context, sessionId string, windowId string, userId string, commentText string) (*LineType, error) { - rtnLine := makeNewLineText(sessionId, windowId, userId, commentText) +func AddCommentLine(ctx context.Context, sessionId string, screenId string, userId string, commentText string) (*LineType, error) { + rtnLine := makeNewLineText(sessionId, screenId, userId, commentText) err := InsertLine(ctx, rtnLine, nil) if err != nil { return nil, err @@ -986,8 +972,8 @@ func AddCommentLine(ctx context.Context, sessionId string, windowId string, user return rtnLine, nil } -func AddCmdLine(ctx context.Context, sessionId string, windowId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { - rtnLine := makeNewLineCmd(sessionId, windowId, userId, cmd.CmdId, renderer) +func AddCmdLine(ctx context.Context, sessionId string, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { + rtnLine := makeNewLineCmd(sessionId, screenId, userId, cmd.CmdId, renderer) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 9855b82e1..fa1a067ee 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -111,7 +111,7 @@ type InfoMsgType struct { type HistoryInfoType struct { HistoryType string `json:"historytype"` SessionId string `json:"sessionid,omitempty"` - WindowId string `json:"windowid,omitempty"` + ScreenId string `json:"screenid,omitempty"` Items []*HistoryItemType `json:"items"` Show bool `json:"show"` } From f86f0a78b121b1b24d70a113a1c43504ec7aa227 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 15 Mar 2023 12:27:17 -0700 Subject: [PATCH 299/397] restore historynum and add linenum to historyitems --- pkg/sstore/dbops.go | 25 +++++++++++++------------ pkg/sstore/sstore.go | 2 ++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index b0986d0d6..b61476e36 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -17,7 +17,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -const HistoryCols = "historyid, ts, userid, sessionid, screenid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito" +const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito" const DefaultMaxHistoryItems = 1000 type SingleConnDBGetter struct { @@ -287,35 +287,36 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi return nil, fmt.Errorf("malformed remoteid") } } - hnumStr := "" whereClause := "WHERE 1" var queryArgs []interface{} + hNumStr := "" if opts.SessionId != "" && opts.ScreenId != "" { - whereClause += fmt.Sprintf(" AND sessionid = '%s' AND screenid = '%s'", opts.SessionId, opts.ScreenId) - hnumStr = "w" + whereClause += fmt.Sprintf(" AND h.sessionid = '%s' AND h.screenid = '%s'", opts.SessionId, opts.ScreenId) + hNumStr = "" } else if opts.SessionId != "" { - whereClause += fmt.Sprintf(" AND sessionid = '%s'", opts.SessionId) - hnumStr = "s" + whereClause += fmt.Sprintf(" AND h.sessionid = '%s'", opts.SessionId) + hNumStr = "s" } else { - hnumStr = "g" + hNumStr = "g" } if opts.SearchText != "" { - whereClause += " AND cmdstr LIKE ? ESCAPE '\\'" + whereClause += " AND h.cmdstr LIKE ? ESCAPE '\\'" likeArg := opts.SearchText likeArg = strings.ReplaceAll(likeArg, "%", "\\%") likeArg = strings.ReplaceAll(likeArg, "_", "\\_") queryArgs = append(queryArgs, "%"+likeArg+"%") } if opts.FromTs > 0 { - whereClause += fmt.Sprintf(" AND ts <= %d", opts.FromTs) + whereClause += fmt.Sprintf(" AND h.ts <= %d", opts.FromTs) } if opts.RemoteId != "" { - whereClause += fmt.Sprintf(" AND remoteid = '%s'", opts.RemoteId) + whereClause += fmt.Sprintf(" AND h.remoteid = '%s'", opts.RemoteId) } if opts.NoMeta { - whereClause += " AND NOT ismetacmd" + whereClause += " AND NOT h.ismetacmd" } - query := fmt.Sprintf("SELECT %s, '%s' || row_number() OVER win AS historynum FROM history %s WINDOW win AS (ORDER BY ts, historyid) ORDER BY ts DESC, historyid DESC LIMIT %d OFFSET %d", HistoryCols, hnumStr, whereClause, itemLimit, realOffset) + query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum, l.linenum FROM history h LEFT OUTER JOIN line l ON (h.lineid = l.lineid) %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset) + fmt.Printf("HISTORY QUERY %s\n", query) marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index de02764c6..d1978ddcf 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -368,6 +368,7 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { quickSetStr(&h.Remote.Name, m, "remotename") quickSetBool(&h.IsMetaCmd, m, "ismetacmd") quickSetStr(&h.HistoryNum, m, "historynum") + quickSetInt64(&h.LineNum, m, "linenum") quickSetBool(&h.Incognito, m, "incognito") return true } @@ -497,6 +498,7 @@ type HistoryItemType struct { // transient (string because of different history orderings) HistoryNum string `json:"historynum"` + LineNum int64 `json:"linenum"` } type HistoryQueryOpts struct { From e16c6534ad69b93f0400d1c3d53781be4c091bda Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 15 Mar 2023 18:12:55 -0700 Subject: [PATCH 300/397] add screenid to cmd, remove sessionid from screenlines --- db/migrations/000009_screenprimary.down.sql | 2 +- db/migrations/000010_removewindowid.down.sql | 2 ++ db/migrations/000011_cmdscreenid.down.sql | 2 ++ db/migrations/000011_cmdscreenid.up.sql | 5 +++++ pkg/cmdrunner/cmdrunner.go | 3 ++- pkg/remote/remote.go | 4 +++- pkg/sstore/dbops.go | 23 +++++++++++++++----- pkg/sstore/fileops.go | 6 ++++- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 10 +++++---- pkg/sstore/updatebus.go | 1 + 11 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 db/migrations/000011_cmdscreenid.down.sql create mode 100644 db/migrations/000011_cmdscreenid.up.sql diff --git a/db/migrations/000009_screenprimary.down.sql b/db/migrations/000009_screenprimary.down.sql index 1c2aaf962..a4b586776 100644 --- a/db/migrations/000009_screenprimary.down.sql +++ b/db/migrations/000009_screenprimary.down.sql @@ -1,3 +1,3 @@ --- invalid, will throw an error, cannot migrate down past 9 +-- invalid, will throw an error, cannot migrate down SELECT x; diff --git a/db/migrations/000010_removewindowid.down.sql b/db/migrations/000010_removewindowid.down.sql index e69de29bb..6332dc5ba 100644 --- a/db/migrations/000010_removewindowid.down.sql +++ b/db/migrations/000010_removewindowid.down.sql @@ -0,0 +1,2 @@ +-- invalid, will throw an error, cannot migrate down +SELECT x; diff --git a/db/migrations/000011_cmdscreenid.down.sql b/db/migrations/000011_cmdscreenid.down.sql new file mode 100644 index 000000000..62fbbdf00 --- /dev/null +++ b/db/migrations/000011_cmdscreenid.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE cmd DROP COLUMN screenid; + diff --git a/db/migrations/000011_cmdscreenid.up.sql b/db/migrations/000011_cmdscreenid.up.sql new file mode 100644 index 000000000..13ea21bd3 --- /dev/null +++ b/db/migrations/000011_cmdscreenid.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE cmd ADD COLUMN screenid varchar(36) NOT NULL DEFAULT ''; + +UPDATE cmd +SET screenid = (SELECT line.screenid FROM line WHERE line.cmdid = cmd.cmdid) +; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 332c87793..3ab5975de 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1164,6 +1164,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ SessionId: ids.SessionId, + ScreenId: ids.ScreenId, CmdId: scbase.GenPromptUUID(), CmdStr: cmdStr, Remote: ids.Remote.RemotePtr, @@ -1185,7 +1186,7 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) } // can ignore ptyupdate - _, err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, cmd.CmdId, cmdOutput, 0) + _, err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, ids.ScreenId, cmd.CmdId, cmdOutput, 0) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot append to local ptyout file for %s command: %v", metaCmd, err) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 23d3d9716..1d11c96e2 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1340,6 +1340,7 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt } cmd := &sstore.CmdType{ SessionId: runPacket.CK.GetSessionId(), + ScreenId: screenId, CmdId: runPacket.CK.GetCmdId(), CmdStr: runPacket.Command, Remote: remotePtr, @@ -1593,7 +1594,8 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa var ack *packet.DataAckPacketType if len(realData) > 0 { dataPos := dataPosMap[dataPk.CK] - update, err := sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), dataPk.CK.GetCmdId(), realData, dataPos) + rcmd := msh.GetRunningCmd(dataPk.CK) + update, err := sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index b61476e36..e7cdfa7ef 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -316,7 +316,6 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi whereClause += " AND NOT h.ismetacmd" } query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum, l.linenum FROM history h LEFT OUTER JOIN line l ON (h.lineid = l.lineid) %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset) - fmt.Printf("HISTORY QUERY %s\n", query) marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { @@ -420,15 +419,17 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenLinesType, error) { - query := `SELECT sessionid, screenid FROM screen WHERE screenid = ?` + query := `SELECT screenid FROM screen WHERE screenid = ?` screen := GetMappable[*ScreenLinesType](tx, query, screenId) if screen == nil { return nil, nil } + query = `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId := tx.GetString(query, screenId) query = `SELECT * FROM line WHERE sessionid = ? AND screenid = ? ORDER BY linenum` - tx.Select(&screen.Lines, query, screen.SessionId, screen.ScreenId) + tx.Select(&screen.Lines, query, sessionId, screen.ScreenId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND screenid = ?)` - screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.SessionId, screen.ScreenId) + screen.Cmds = SelectMapsGen[*CmdType](tx, query, sessionId, screen.ScreenId) return screen, nil }) } @@ -749,6 +750,9 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line.LineNum != 0 { return fmt.Errorf("line should not hage linenum set") } + if cmd.ScreenId == "" { + return fmt.Errorf("cmd should have screenid set") + } return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` if !tx.Exists(query, line.SessionId, line.ScreenId) { @@ -766,8 +770,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, cmdid, remoteownerid, remoteid, remotename, cmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) - VALUES (:sessionid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) +INSERT INTO cmd ( sessionid, screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:sessionid,:screenid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExec(query, cmdMap) } @@ -2358,3 +2362,10 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem return rtn, nil }) } + +func GetScreenIdFromCmd(ctx context.Context, sessionId string, cmdId string) (string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { + query := `SELECT screenid FROM cmd WHERE sessionid = ? AND cmdid = ?` + return tx.GetString(query, sessionId, cmdId), nil + }) +} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index bd654ee62..c27971a27 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -32,7 +32,10 @@ func StatCmdPtyFile(ctx context.Context, sessionId string, cmdId string) (*cirfi return cirfile.StatCirFile(ctx, ptyOutFileName) } -func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { +func AppendToCmdPtyBlob(ctx context.Context, sessionId string, screenId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { + if screenId == "" { + return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set") + } if pos < 0 { return nil, fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) } @@ -52,6 +55,7 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, cmdId string, dat data64 := base64.StdEncoding.EncodeToString(data) update := &PtyDataUpdate{ SessionId: sessionId, + ScreenId: screenId, CmdId: cmdId, PtyPos: pos, PtyData64: data64, diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 5e98e3e7a..64b52d607 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 10 +const MaxMigration = 11 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d1978ddcf..8a9c74035 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -379,10 +379,9 @@ type ScreenOptsType struct { } type ScreenLinesType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - Lines []*LineType `json:"lines" dbmap:"-"` - Cmds []*CmdType `json:"cmds" dbmap:"-"` + ScreenId string `json:"screenid"` + Lines []*LineType `json:"lines" dbmap:"-"` + Cmds []*CmdType `json:"cmds" dbmap:"-"` } func (ScreenLinesType) UseDBMap() {} @@ -834,6 +833,7 @@ type CmdDoneInfo struct { type CmdType struct { SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` CmdId string `json:"cmdid"` Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` @@ -894,6 +894,7 @@ func (r *RemoteType) FromMap(m map[string]interface{}) bool { func (cmd *CmdType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["sessionid"] = cmd.SessionId + rtn["screenid"] = cmd.ScreenId rtn["cmdid"] = cmd.CmdId rtn["remoteownerid"] = cmd.Remote.OwnerId rtn["remoteid"] = cmd.Remote.RemoteId @@ -916,6 +917,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetStr(&cmd.SessionId, m, "sessionid") + quickSetStr(&cmd.ScreenId, m, "screenid") quickSetStr(&cmd.CmdId, m, "cmdid") quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index fa1a067ee..416b6920c 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -18,6 +18,7 @@ type UpdatePacket interface { type PtyDataUpdate struct { SessionId string `json:"sessionid,omitempty"` + ScreenId string `json:"screenid,omitempty"` CmdId string `json:"cmdid,omitempty"` RemoteId string `json:"remoteid,omitempty"` PtyPos int64 `json:"ptypos"` From 5bc6f1fd6c8f60c956acd8b48bdc2f2b25741f73 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Mar 2023 21:46:10 -0700 Subject: [PATCH 301/397] set renderer from kwargs --- pkg/cmdrunner/cmdrunner.go | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3ab5975de..46491d4f3 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -40,6 +40,7 @@ func init() { const DefaultUserId = "sawka" const MaxNameLen = 50 +const MaxRendererLen = 50 const MaxRemoteAliasLen = 50 const PasswordUnchangedSentinel = "--unchanged--" const DefaultPTERM = "MxM" @@ -78,6 +79,7 @@ var hostNameRe = regexp.MustCompile("^[a-z][a-z0-9.-]*$") var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") +var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$") var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$") var wsRe = regexp.MustCompile("\\s+") var sigNameRe = regexp.MustCompile("^((SIG[A-Z0-9]+)|(\\d+))$") @@ -324,6 +326,10 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, fmt.Errorf("/run error: %w", err) } + if err = validateRenderer(pk.Kwargs["renderer"]); err != nil { + return nil, fmt.Errorf("/run error: %w", err) + } + renderer := pk.Kwargs["renderer"] cmdStr := firstArg(pk) isRtnStateCmd := IsReturnStateCommand(cmdStr) // runPacket.State is set in remote.RunCommand() @@ -345,7 +351,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } - update, err := addLineForCmd(ctx, "/run", true, ids, cmd) + update, err := addLineForCmd(ctx, "/run", true, ids, cmd, renderer) if err != nil { return nil, err } @@ -1018,7 +1024,7 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd) + update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd, "") if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1152,7 +1158,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd) + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "") if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1194,8 +1200,8 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr return cmd, nil } -func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType) (*sstore.ModelUpdate, error) { - rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, cmd, "") +func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType, renderer string) (*sstore.ModelUpdate, error) { + rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, cmd, renderer) if err != nil { return nil, err } @@ -1467,6 +1473,19 @@ func validateName(name string, typeStr string) error { return nil } +func validateRenderer(renderer string) error { + if renderer == "" { + return nil + } + if len(renderer) > MaxRendererLen { + return fmt.Errorf("renderer name too long, max length is %d", MaxRendererLen) + } + if !rendererRe.MatchString(renderer) { + return fmt.Errorf("invalid renderer format") + } + return nil +} + func validateColor(color string, typeStr string) error { for _, c := range ColorNames { if color == c { @@ -1740,7 +1759,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/reset", false, ids, cmd) + update, err := addLineForCmd(ctx, "/reset", false, ids, cmd, "") if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -2376,6 +2395,11 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if line.Ephemeral { buf.WriteString(fmt.Sprintf(" %-15s %v\n", "ephemeral", true)) } + if line.Renderer != "" { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "renderer", line.Renderer)) + } else { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "renderer", "terminal")) + } if cmd != nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef())) From 5b74117bcaaa9c6bd72713e1a694a5496635d7ef Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Mar 2023 14:47:30 -0700 Subject: [PATCH 302/397] bump to 0.1.7, add line:set --- pkg/cmdrunner/cmdrunner.go | 44 +++++++++++++++++++++++++++++++++++++- pkg/scbase/scbase.go | 2 +- pkg/sstore/dbops.go | 8 +++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 46491d4f3..43b2308f7 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -162,6 +162,7 @@ func init() { registerCmdFn("line:purge", LinePurgeCommand) registerCmdFn("line:setheight", LineSetHeightCommand) registerCmdFn("line:view", LineViewCommand) + registerCmdFn("line:set", LineSetCommand) registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) @@ -2041,7 +2042,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"}, "or", false)) + return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "purge", "setheight", "set"}, "or", false)) } func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -2072,6 +2073,47 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, nil } +func LineSetCommand(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) != 1 { + return nil, fmt.Errorf("/line:set requires 1 argument (linearg)") + } + lineArg := pk.Args[0] + lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + var varsUpdated []string + if renderer, found := pk.Kwargs["renderer"]; found { + if err = validateRenderer(renderer); err != nil { + return nil, fmt.Errorf("invalid renderer value: %w", err) + } + err = sstore.UpdateLineRenderer(ctx, lineId, renderer) + if err != nil { + return nil, fmt.Errorf("error changing line renderer: %v", err) + } + varsUpdated = append(varsUpdated, "renderer") + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{"renderer"}, "or", false)) + } + updatedLine, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) + if err != nil { + return nil, fmt.Errorf("/line:set cannot retrieve updated line: %v", err) + } + update := sstore.ModelUpdate{ + Line: updatedLine, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("line updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, + } + return update, nil +} + func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) != 3 { return nil, fmt.Errorf("usage /line:view [session] [screen] [line]") diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 6f97c6bd8..2f0cee3ee 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -31,7 +31,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.6" +const PromptVersion = "v0.1.7" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e7cdfa7ef..d3aa1c5b1 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1840,6 +1840,14 @@ func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { return nil } +func UpdateLineRenderer(ctx context.Context, lineId string, renderer string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET renderer = ? WHERE lineid = ?` + tx.Exec(query, renderer, lineId) + return nil + }) +} + // can return nil, nil if line is not found func GetLineById(ctx context.Context, sessionId string, screenId string, lineId string) (*LineType, error) { var rtn *LineType From e363ed0d0ee9a7f1ba6da32840c1ec127982781c Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Mar 2023 15:07:29 -0700 Subject: [PATCH 303/397] copy sshport to real sshopts. print port number in console --- pkg/remote/remote.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 1d11c96e2..c173345a8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -660,6 +660,7 @@ func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { SSHOptsStr: opts.SSHOptsStr, SSHIdentity: opts.SSHIdentity, SSHUser: opts.SSHUser, + SSHPort: opts.SSHPort, } } @@ -1050,7 +1051,11 @@ func (msh *MShellProc) Launch(interactive bool) { msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n") return } - msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) + if remoteCopy.SSHOpts.SSHPort != 0 && remoteCopy.SSHOpts.SSHPort != 22 { + msh.WriteToPtyBuffer("connecting to %s (port %d)...\n", remoteCopy.RemoteCanonicalName, remoteCopy.SSHOpts.SSHPort) + } else { + msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) + } sshOpts := convertSSHOpts(remoteCopy.SSHOpts) sshOpts.SSHErrorsToTty = true if remoteCopy.ConnectMode != sstore.ConnectModeManual && remoteCopy.SSHOpts.SSHPassword == "" && !interactive { From e01a0a7fa04f0299f4e15c80feeef12aa11d9ca9 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Mar 2023 15:57:01 -0700 Subject: [PATCH 304/397] write local-server-bin --- scripthaus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripthaus.md b/scripthaus.md index 8768504e2..2f9ec41c4 100644 --- a/scripthaus.md +++ b/scripthaus.md @@ -12,5 +12,5 @@ sqlite3 /Users/mike/prompt-dev/prompt.db ```bash # @scripthaus command build -go build -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M')" -o ~/prompt-dev/local-server ./cmd +go build -ldflags "-X main.BuildTime=$(date +'%Y%m%d%H%M')" -o bin/local-server ./cmd ``` From 2ba3a0928554f493d313b9d71df46e89f15b60d0 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Mar 2023 18:19:59 -0700 Subject: [PATCH 305/397] prevent nulls (problem with my DB) --- db/migrations/000010_removewindowid.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrations/000010_removewindowid.up.sql b/db/migrations/000010_removewindowid.up.sql index c131c7b51..10b1bbc0d 100644 --- a/db/migrations/000010_removewindowid.up.sql +++ b/db/migrations/000010_removewindowid.up.sql @@ -2,12 +2,12 @@ ALTER TABLE remote_instance RENAME COLUMN windowid TO screenid; ALTER TABLE line RENAME COLUMN windowid TO screenid; UPDATE remote_instance -SET screenid = (SELECT screen.screenid FROM screen WHERE screen.windowid = remote_instance.screenid) +SET screenid = COALESCE((SELECT screen.screenid FROM screen WHERE screen.windowid = remote_instance.screenid), '') WHERE screenid <> '' ; UPDATE line -SET screenid = (SELECT screen.screenid FROM screen WHERE screen.windowid = line.screenid) +SET screenid = COALESCE((SELECT screen.screenid FROM screen WHERE screen.windowid = line.screenid), '') WHERE screenid <> '' ; From 064e31eb2cc2bb6b9649449188143393c34224f2 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Mar 2023 21:36:49 -0700 Subject: [PATCH 306/397] fix history query time --- db/migrations/000012_historylinenum.down.sql | 1 + db/migrations/000012_historylinenum.up.sql | 6 ++++++ pkg/cmdrunner/cmdrunner.go | 3 +++ pkg/sstore/dbops.go | 8 ++++---- pkg/sstore/migrate.go | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 db/migrations/000012_historylinenum.down.sql create mode 100644 db/migrations/000012_historylinenum.up.sql diff --git a/db/migrations/000012_historylinenum.down.sql b/db/migrations/000012_historylinenum.down.sql new file mode 100644 index 000000000..89bde3e6f --- /dev/null +++ b/db/migrations/000012_historylinenum.down.sql @@ -0,0 +1 @@ +ALTER TABLE history DROP COLUMN linenum; diff --git a/db/migrations/000012_historylinenum.up.sql b/db/migrations/000012_historylinenum.up.sql new file mode 100644 index 000000000..d7dc63535 --- /dev/null +++ b/db/migrations/000012_historylinenum.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE history ADD COLUMN linenum int NOT NULL DEFAULT 0; + +UPDATE history +SET linenum = COALESCE((SELECT line.linenum FROM line WHERE line.lineid = history.lineid), 0) +; + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 43b2308f7..6cfb69c14 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -95,6 +95,7 @@ type SetVarScope struct { type historyContextType struct { LineId string + LineNum int64 CmdId string RemotePtr *sstore.RemotePtrType } @@ -378,6 +379,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history SessionId: ids.SessionId, ScreenId: ids.ScreenId, LineId: historyContext.LineId, + LineNum: historyContext.LineNum, HadError: hadError, CmdId: historyContext.CmdId, CmdStr: cmdStr, @@ -1240,6 +1242,7 @@ func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstor hctx := ctxVal.(*historyContextType) if line != nil { hctx.LineId = line.LineId + hctx.LineNum = line.LineNum } if cmd != nil { hctx.CmdId = cmd.CmdId diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index d3aa1c5b1..e10f6571d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -17,7 +17,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito" +const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito, h.linenum" const DefaultMaxHistoryItems = 1000 type SingleConnDBGetter struct { @@ -187,8 +187,8 @@ 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, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito)` + ( historyid, ts, userid, sessionid, screenid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito, linenum) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito,:linenum)` tx.NamedExec(query, hitem.ToMap()) return nil }) @@ -315,7 +315,7 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi if opts.NoMeta { whereClause += " AND NOT h.ismetacmd" } - query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum, l.linenum FROM history h LEFT OUTER JOIN line l ON (h.lineid = l.lineid) %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset) + query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum FROM history h %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset) marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 64b52d607..894e4769e 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 11 +const MaxMigration = 12 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { From 1742a3dbbc243e62918934ea49da0d3d40e3d1a5 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 18 Mar 2023 11:49:00 -0700 Subject: [PATCH 307/397] add linenum to map --- pkg/sstore/sstore.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 8a9c74035..b85b83eae 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -342,6 +342,7 @@ func (h *HistoryItemType) ToMap() map[string]interface{} { rtn["sessionid"] = h.SessionId rtn["screenid"] = h.ScreenId rtn["lineid"] = h.LineId + rtn["linenum"] = h.LineNum rtn["haderror"] = h.HadError rtn["cmdid"] = h.CmdId rtn["cmdstr"] = h.CmdStr From cec75c0d5b2cdb67a7b499c3a6e1a6e2e2da168a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Mar 2023 12:19:48 -0700 Subject: [PATCH 308/397] add simple history expansion --- db/schema.sql | 22 +++++----- pkg/cmdrunner/cmdrunner.go | 83 ++++++++++++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 16 ++++++++ 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 059edcc63..68a47ba99 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -95,7 +95,7 @@ CREATE TABLE cmd ( runout json NOT NULL, rtnstate boolean NOT NULL, rtnbasehash varchar(36) NOT NULL, - rtndiffhasharr json NOT NULL, + rtndiffhasharr json NOT NULL, screenid varchar(36) NOT NULL DEFAULT '', PRIMARY KEY (sessionid, cmdid) ); CREATE TABLE history ( @@ -113,7 +113,16 @@ CREATE TABLE history ( cmdstr text NOT NULL, ismetacmd boolean, incognito boolean -); +, linenum int NOT NULL DEFAULT 0); +CREATE TABLE activity ( + day varchar(20) PRIMARY KEY, + uploaded boolean NOT NULL, + tdata json NOT NULL, + tzname varchar(50) NOT NULL, + tzoffset int NOT NULL, + clientversion varchar(20) NOT NULL, + clientarch varchar(20) NOT NULL +, buildtime varchar(20) NOT NULL DEFAULT '-', osrelease varchar(20) NOT NULL DEFAULT '-'); CREATE TABLE bookmark ( bookmarkid varchar(36) PRIMARY KEY, createdts bigint NOT NULL, @@ -134,15 +143,6 @@ CREATE TABLE bookmark_cmd ( cmdid varchar(36) NOT NULL, PRIMARY KEY (bookmarkid, sessionid, cmdid) ); -CREATE TABLE activity ( - day varchar(20) PRIMARY KEY, - uploaded boolean NOT NULL, - tdata json NOT NULL, - tzname varchar(50) NOT NULL, - tzoffset int NOT NULL, - clientversion varchar(50) NOT NULL, - clientarch varchar(50) NOT NULL -, buildtime varchar(20) NOT NULL DEFAULT '-', osrelease varchar(20) NOT NULL DEFAULT '-'); CREATE TABLE playbook ( playbookid varchar(36) PRIMARY KEY, playbookname varchar(100) NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 6cfb69c14..e976be136 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -47,6 +47,7 @@ const DefaultPTERM = "MxM" const MaxCommandLen = 4096 const MaxSignalLen = 12 const MaxSignalNum = 64 +const MaxEvalDepth = 5 var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -87,6 +88,7 @@ var sigNameRe = regexp.MustCompile("^((SIG[A-Z0-9]+)|(\\d+))$") type contextType string var historyContextKey = contextType("history") +var depthContextKey = contextType("depth") type SetVarScope struct { ScopeName string @@ -323,6 +325,68 @@ func resolveNonNegInt(arg string, def int) (int, error) { return ival, nil } +var histExpansionRe = regexp.MustCompile(`^!(\d+)$`) + +func doCmdHistoryExpansion(ctx context.Context, ids resolvedIds, cmdStr string) (string, error) { + if !strings.HasPrefix(cmdStr, "!") { + return "", nil + } + if strings.HasPrefix(cmdStr, "! ") { + return "", nil + } + if cmdStr == "!!" { + return doHistoryExpansion(ctx, ids, -1) + } + if strings.HasPrefix(cmdStr, "!-") { + return "", fmt.Errorf("prompt does not support negative history offsets, use a stable positive history offset instead: '![linenum]'") + } + m := histExpansionRe.FindStringSubmatch(cmdStr) + if m == nil { + return "", fmt.Errorf("unsupported history substitution, can use '!!' or '![linenum]'") + } + ival, err := strconv.Atoi(m[1]) + if err != nil { + return "", fmt.Errorf("invalid history expansion") + } + return doHistoryExpansion(ctx, ids, ival) +} + +func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string, error) { + if hnum == 0 { + return "", fmt.Errorf("invalid history expansion, cannot expand line number '0'") + } + if hnum < -1 { + return "", fmt.Errorf("invalid history expansion, cannot expand negative history offsets") + } + foundHistoryNum := hnum + if hnum == -1 { + var err error + foundHistoryNum, err = sstore.GetLastHistoryLineNum(ctx, ids.ScreenId) + if err != nil { + return "", fmt.Errorf("cannot expand history, error finding last history item: %v", err) + } + if foundHistoryNum == 0 { + return "", fmt.Errorf("cannot expand history, no last history item") + } + } + hitem, err := sstore.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum) + if err != nil { + return "", fmt.Errorf("cannot get history item '%d': %v", foundHistoryNum, err) + } + if hitem == nil { + return "", fmt.Errorf("cannot expand history, history item '%d' not found", foundHistoryNum) + } + return hitem.CmdStr, nil +} + +func getEvalDepth(ctx context.Context) int { + depthVal := ctx.Value(depthContextKey) + if depthVal == nil { + return 0 + } + return depthVal.(int) +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { @@ -333,6 +397,22 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } renderer := pk.Kwargs["renderer"] cmdStr := firstArg(pk) + expandedCmdStr, err := doCmdHistoryExpansion(ctx, ids, cmdStr) + if err != nil { + return nil, err + } + if expandedCmdStr != "" { + newPk := scpacket.MakeFeCommandPacket() + newPk.MetaCmd = "eval" + newPk.Args = []string{expandedCmdStr} + newPk.Kwargs = pk.Kwargs + newPk.RawStr = pk.RawStr + newPk.UIContext = pk.UIContext + newPk.Interactive = pk.Interactive + evalDepth := getEvalDepth(ctx) + ctxWithDepth := context.WithValue(ctx, depthContextKey, evalDepth+1) + return EvalCommand(ctxWithDepth, newPk) + } isRtnStateCmd := IsReturnStateCommand(cmdStr) // runPacket.State is set in remote.RunCommand() runPacket := packet.MakeRunPacket() @@ -410,6 +490,9 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. // fall through (non-fatal error) } } + if getEvalDepth(ctx) > MaxEvalDepth { + return nil, fmt.Errorf("alias/history expansion max-depth exceeded") + } var historyContext historyContextType ctxWithHistory := context.WithValue(ctx, historyContextKey, &historyContext) var update sstore.UpdatePacket diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index e10f6571d..3af2c051c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -341,6 +341,22 @@ func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryR return rtn, nil } +func GetHistoryItemByLineNum(ctx context.Context, screenId string, lineNum int) (*HistoryItemType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*HistoryItemType, error) { + query := `SELECT * FROM history WHERE screenid = ? AND linenum = ?` + hitem := GetMapGen[*HistoryItemType](tx, query, screenId, lineNum) + return hitem, nil + }) +} + +func GetLastHistoryLineNum(ctx context.Context, screenId string) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := `SELECT COALESCE(max(linenum), 0) FROM history WHERE screenid = ?` + maxLineNum := tx.GetInt(query, screenId) + return maxLineNum, nil + }) +} + // includes archived sessions func GetBareSessions(ctx context.Context) ([]*SessionType, error) { var rtn []*SessionType From 02ae7ea972eab235e64c5815be3543cc4c85ee6b Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Mar 2023 19:20:57 -0700 Subject: [PATCH 309/397] checkpoint on big cmd screen migration --- .gitignore | 3 + cmd/main-server.go | 31 +- db/migrations/000013_cmdmigration.down.sql | 2 + db/migrations/000013_cmdmigration.up.sql | 123 +++++++ pkg/cmdrunner/cmdrunner.go | 81 +++-- pkg/cmdrunner/resolver.go | 2 +- pkg/remote/remote.go | 36 +- pkg/scbase/scbase.go | 59 ++-- pkg/scws/scws.go | 2 +- pkg/sstore/dbops.go | 370 +++++++++------------ pkg/sstore/fileops.go | 30 +- pkg/sstore/migrate.go | 4 +- pkg/sstore/sstore.go | 175 ++++++++-- pkg/sstore/updatebus.go | 40 ++- 14 files changed, 607 insertions(+), 351 deletions(-) create mode 100644 .gitignore create mode 100644 db/migrations/000013_cmdmigration.down.sql create mode 100644 db/migrations/000013_cmdmigration.up.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a89c46b43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +bin/ +*.out diff --git a/cmd/main-server.go b/cmd/main-server.go index 57f3a618d..5b782edf1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -143,6 +143,12 @@ func HandleGetClientData(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, err) return } + mdata, err := sstore.GetCmdMigrationInfo(r.Context()) + if err != nil { + WriteJsonError(w, err) + return + } + cdata.Migration = mdata WriteJsonSuccess(w, cdata) return } @@ -223,16 +229,16 @@ func HandleRtnState(w http.ResponseWriter, r *http.Request) { return }() qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") + screenId := qvals.Get("screenid") cmdId := qvals.Get("cmdid") - if sessionId == "" || cmdId == "" { + if screenId == "" || cmdId == "" { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid"))) + w.Write([]byte(fmt.Sprintf("must specify screenid and cmdid"))) return } - if _, err := uuid.Parse(sessionId); err != nil { + if _, err := uuid.Parse(screenId); err != nil { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("invalid sessionid: %v", err))) + w.Write([]byte(fmt.Sprintf("invalid screenid: %v", err))) return } if _, err := uuid.Parse(cmdId); err != nil { @@ -240,7 +246,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(), sessionId, cmdId) + data, err := cmdrunner.GetRtnStateDiff(r.Context(), screenId, cmdId) if err != nil { w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("cannot get rtnstate diff: %v", err))) @@ -278,16 +284,16 @@ func HandleRemotePty(w http.ResponseWriter, r *http.Request) { func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() - sessionId := qvals.Get("sessionid") + screenId := qvals.Get("screenid") cmdId := qvals.Get("cmdid") - if sessionId == "" || cmdId == "" { + if screenId == "" || cmdId == "" { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("must specify sessionid and cmdid"))) + w.Write([]byte(fmt.Sprintf("must specify screenid and cmdid"))) return } - if _, err := uuid.Parse(sessionId); err != nil { + if _, err := uuid.Parse(screenId); err != nil { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("invalid sessionid: %v", err))) + w.Write([]byte(fmt.Sprintf("invalid screenid: %v", err))) return } if _, err := uuid.Parse(cmdId); err != nil { @@ -295,7 +301,7 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) return } - realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), sessionId, cmdId) + realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), screenId, cmdId) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusOK) @@ -558,6 +564,7 @@ func main() { go telemetryLoop() go stdinReadWatch() go runWebSocketServer() + go sstore.RunCmdScreenMigration() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty)) diff --git a/db/migrations/000013_cmdmigration.down.sql b/db/migrations/000013_cmdmigration.down.sql new file mode 100644 index 000000000..6332dc5ba --- /dev/null +++ b/db/migrations/000013_cmdmigration.down.sql @@ -0,0 +1,2 @@ +-- invalid, will throw an error, cannot migrate down +SELECT x; diff --git a/db/migrations/000013_cmdmigration.up.sql b/db/migrations/000013_cmdmigration.up.sql new file mode 100644 index 000000000..eed358ee0 --- /dev/null +++ b/db/migrations/000013_cmdmigration.up.sql @@ -0,0 +1,123 @@ +DELETE FROM cmd +WHERE screenid = ''; + +DELETE FROM line +WHERE screenid = ''; + +DELETE FROM cmd +WHERE cmdid NOT IN (SELECT cmdid FROM line); + +DELETE FROM line +WHERE cmdid <> '' AND cmdid NOT IN (SELECT cmdid FROM cmd); + +CREATE TABLE new_bookmark_cmd ( + bookmarkid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (bookmarkid, screenid, cmdid) +); +INSERT INTO new_bookmark_cmd +SELECT + b.bookmarkid, + c.screenid, + c.cmdid +FROM bookmark_cmd b, cmd c +WHERE b.cmdid = c.cmdid; +DROP TABLE bookmark_cmd; +ALTER TABLE new_bookmark_cmd RENAME TO bookmark_cmd; + +ALTER TABLE client ADD COLUMN cmdstoretype varchar(20) DEFAULT 'session'; + +CREATE TABLE cmd_migrate ( + sessionid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL +); +INSERT INTO cmd_migrate +SELECT sessionid, screenid, cmdid +FROM cmd; + +-- update primary key for screen +CREATE TABLE new_screen ( + screenid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, + name varchar(50) NOT NULL, + screenidx int NOT NULL, + screenopts json NOT NULL, + ownerid varchar(36) NOT NULL, + sharemode varchar(12) NOT NULL, + curremoteownerid varchar(36) NOT NULL, + curremoteid varchar(36) NOT NULL, + curremotename varchar(50) NOT NULL, + nextlinenum int NOT NULL, + selectedline int NOT NULL, + anchor json NOT NULL, + focustype varchar(12) NOT NULL, + archived boolean NOT NULL, + archivedts bigint NOT NULL, + PRIMARY KEY (screenid) +); +INSERT INTO new_screen +SELECT screenid, sessionid, name, screenidx, screenopts, ownerid, sharemode, + curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, + anchor, focustype, archived, archivedts +FROM screen; +DROP TABLE screen; +ALTER TABLE new_screen RENAME TO screen; + +-- drop sessionid from line +CREATE TABLE new_line ( + screenid varchar(36) NOT NULL, + userid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + ts bigint NOT NULL, + linenum int NOT NULL, + linenumtemp boolean NOT NULL, + linetype varchar(10) NOT NULL, + linelocal boolean NOT NULL, + text text NOT NULL, + cmdid varchar(36) NOT NULL, + ephemeral boolean NOT NULL, + contentheight int NOT NULL, + star int NOT NULL, + archived boolean NOT NULL, + renderer varchar(50) NOT NULL, + bookmarked boolean NOT NULL, + PRIMARY KEY (screenid, lineid) +); +INSERT INTO new_line +SELECT screenid, userid, lineid, ts, linenum, linenumtemp, linetype, linelocal, + text, cmdid, ephemeral, contentheight, star, archived, renderer, bookmarked +FROM line; +DROP TABLE line; +ALTER TABLE new_line RENAME TO line; + +-- drop sessionid from cmd +CREATE TABLE new_cmd ( + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + cmdstr text NOT NULL, + rawcmdstr text NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, + termopts json NOT NULL, + origtermopts json NOT NULL, + status varchar(10) NOT NULL, + startpk json NOT NULL, + doneinfo json NOT NULL, + runout json NOT NULL, + rtnstate boolean NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, + PRIMARY KEY (screenid, cmdid) +); +INSERT INTO new_cmd +SELECT screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, cmdstr, + festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, runout, rtnstate, rtnbasehash, rtndiffhasharr +FROM cmd; +DROP TABLE cmd; +ALTER TABLE new_cmd RENAME TO cmd; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e976be136..cb22c3fd6 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -417,7 +417,7 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U // runPacket.State is set in remote.RunCommand() runPacket := packet.MakeRunPacket() runPacket.ReqId = uuid.New().String() - runPacket.CK = base.MakeCommandKey(ids.SessionId, scbase.GenPromptUUID()) + runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenPromptUUID()) runPacket.UsePty = true ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) @@ -433,12 +433,13 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, err } + cmd.RawCmdStr = pk.GetRawStr() update, err := addLineForCmd(ctx, "/run", true, ids, cmd, renderer) if err != nil { return nil, err } update.Interactive = pk.Interactive - sstore.MainBus.SendUpdate(ids.SessionId, update) + sstore.MainBus.SendScreenUpdate(ids.ScreenId, update) return nil, nil } @@ -559,7 +560,7 @@ func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("/screen:purge cannot purge screen: %w", err) } - update, err := sstore.DeleteScreen(ctx, ids.ScreenId) + update, err := sstore.PurgeScreen(ctx, ids.ScreenId, false) if err != nil { return nil, err } @@ -1255,10 +1256,10 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ - SessionId: ids.SessionId, ScreenId: ids.ScreenId, CmdId: scbase.GenPromptUUID(), CmdStr: cmdStr, + RawCmdStr: cmdStr, Remote: ids.Remote.RemotePtr, TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}, Status: sstore.CmdStatusDone, @@ -1272,13 +1273,13 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr if ids.Remote.FeState != nil { cmd.FeState = *ids.Remote.FeState } - err := sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) } // can ignore ptyupdate - _, err = sstore.AppendToCmdPtyBlob(ctx, cmd.SessionId, ids.ScreenId, cmd.CmdId, cmdOutput, 0) + _, err = sstore.AppendToCmdPtyBlob(ctx, ids.ScreenId, cmd.CmdId, cmdOutput, 0) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot append to local ptyout file for %s command: %v", metaCmd, err) @@ -1287,7 +1288,7 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr } func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType, renderer string) (*sstore.ModelUpdate, error) { - rtnLine, err := sstore.AddCmdLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, cmd, renderer) + rtnLine, err := sstore.AddCmdLine(ctx, ids.ScreenId, DefaultUserId, cmd, renderer) if err != nil { return nil, err } @@ -1495,7 +1496,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if strings.TrimSpace(text) == "" { return nil, fmt.Errorf("cannot post empty comment") } - rtnLine, err := sstore.AddCommentLine(ctx, ids.SessionId, ids.ScreenId, DefaultUserId, text) + rtnLine, err := sstore.AddCommentLine(ctx, ids.ScreenId, DefaultUserId, text) if err != nil { return nil, err } @@ -1637,7 +1638,7 @@ func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } - update, err := sstore.DeleteSession(ctx, ids.SessionId) + update, err := sstore.PurgeSession(ctx, ids.SessionId) if err != nil { return nil, fmt.Errorf("cannot delete session: %v", err) } @@ -1907,10 +1908,9 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) continue } lineObj := &sstore.LineType{ - SessionId: historyItem.SessionId, - ScreenId: historyItem.ScreenId, - LineId: historyItem.LineId, - Remove: true, + ScreenId: historyItem.ScreenId, + LineId: historyItem.LineId, + Remove: true, } update.Lines = append(update.Lines, lineObj) } @@ -2076,7 +2076,7 @@ func splitLinesForInfo(str string) []string { func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) error { siPk := packet.MakeSpecialInputPacket() - siPk.CK = base.MakeCommandKey(cmd.SessionId, cmd.CmdId) + siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) siPk.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols} msh := remote.GetRemoteById(cmd.Remote.RemoteId) if msh == nil { @@ -2088,7 +2088,7 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) } newTermOpts := cmd.TermOpts newTermOpts.Cols = int64(newCols) - err = sstore.UpdateCmdTermOpts(ctx, cmd.SessionId, cmd.CmdId, newTermOpts) + err = sstore.UpdateCmdTermOpts(ctx, cmd.ScreenId, cmd.CmdId, newTermOpts) if err != nil { return err } @@ -2112,7 +2112,7 @@ func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("/screen:resize invalid zero/negative 'cols' argument") } cols = base.BoundInt(cols, shexec.MinTermCols, shexec.MaxTermCols) - runningCmds, err := sstore.GetRunningScreenCmds(ctx, ids.SessionId, ids.ScreenId) + runningCmds, err := sstore.GetRunningScreenCmds(ctx, ids.ScreenId) if err != nil { return nil, fmt.Errorf("/screen:resize cannot get running commands: %v", err) } @@ -2140,7 +2140,7 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("/line:setheight requires 2 arguments (linearg and height)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2168,7 +2168,7 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, fmt.Errorf("/line:set requires 1 argument (linearg)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2186,7 +2186,7 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if len(varsUpdated) == 0 { return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{"renderer"}, "or", false)) } - updatedLine, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) + updatedLine, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:set cannot retrieve updated line: %v", err) } @@ -2332,21 +2332,21 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, fmt.Errorf("/line:bookmark requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) + lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) } if cmdObj == nil { return nil, fmt.Errorf("cannot bookmark non-cmd line") } - ck := base.MakeCommandKey(lineObj.SessionId, cmdObj.CmdId) + ck := base.MakeCommandKey(lineObj.ScreenId, cmdObj.CmdId) bm := &sstore.BookmarkType{ BookmarkId: uuid.New().String(), CreatedTs: time.Now().UnixMilli(), @@ -2360,7 +2360,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("cannot insert bookmark: %v", err) } - newLineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) + newLineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) } @@ -2387,7 +2387,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, fmt.Errorf("/line:star only takes up to 2 arguments (line-number and star-value)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2405,7 +2405,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:star error updating star value: %v", err) } - lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:star error getting line: %v", err) } @@ -2425,7 +2425,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, fmt.Errorf("/line:archive requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2440,7 +2440,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err) } - lineObj, err := sstore.GetLineById(ctx, ids.SessionId, ids.ScreenId, lineId) + lineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:archive error getting line: %v", err) } @@ -2461,7 +2461,7 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } var lineIds []string for _, lineArg := range pk.Args { - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } @@ -2470,17 +2470,16 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } lineIds = append(lineIds, lineId) } - err = sstore.PurgeLinesByIds(ctx, ids.SessionId, lineIds) + err = sstore.PurgeLinesByIds(ctx, ids.ScreenId, lineIds) if err != nil { return nil, fmt.Errorf("/line:purge error purging lines: %v", err) } update := sstore.ModelUpdate{} for _, lineId := range lineIds { lineObj := &sstore.LineType{ - SessionId: ids.SessionId, - ScreenId: ids.ScreenId, - LineId: lineId, - Remove: true, + ScreenId: ids.ScreenId, + LineId: lineId, + Remove: true, } update.Lines = append(update.Lines, lineObj) } @@ -2496,14 +2495,14 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, fmt.Errorf("/line:show requires an argument (line number or id)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("error getting line: %v", err) } @@ -2542,7 +2541,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if cmd.RtnState { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "rtnstate", "true")) } - stat, _ := sstore.StatCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId) + stat, _ := sstore.StatCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId) if stat == nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", "-")) } else { @@ -2599,11 +2598,11 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/signal requires a second argument (signal name)") } lineArg := pk.Args[0] - lineId, err := sstore.FindLineIdByArg(ctx, ids.SessionId, ids.ScreenId, lineArg) + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) if err != nil { return nil, fmt.Errorf("error looking up lineid: %v", err) } - line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.SessionId, ids.ScreenId, lineId) + line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("error getting line: %v", err) } @@ -2640,7 +2639,7 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("cannot send signal, remote is not connected") } siPk := packet.MakeSpecialInputPacket() - siPk.CK = base.MakeCommandKey(cmd.SessionId, cmd.CmdId) + siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) siPk.SigName = sigArg err = msh.SendSpecialInput(siPk) if err != nil { @@ -2950,8 +2949,8 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt } } -func GetRtnStateDiff(ctx context.Context, sessionId string, cmdId string) ([]byte, error) { - cmd, err := sstore.GetCmdById(ctx, sessionId, cmdId) +func GetRtnStateDiff(ctx context.Context, screenId string, cmdId string) ([]byte, error) { + cmd, err := sstore.GetCmdByScreenId(ctx, screenId, cmdId) if err != nil { return nil, err } diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 67f3ed6c0..1b5aaa6cc 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -298,7 +298,7 @@ func resolveSession(ctx context.Context, sessionArg string, curSessionArg string } func resolveLine(ctx context.Context, sessionId string, screenId string, lineArg string, curLineArg string) (*ResolveItem, error) { - lines, err := sstore.GetLineResolveItems(ctx, sessionId, screenId) + lines, err := sstore.GetLineResolveItems(ctx, screenId) if err != nil { return nil, fmt.Errorf("could not get lines: %v", err) } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c173345a8..a2352497c 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -578,7 +578,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { func (msh *MShellProc) NotifyRemoteUpdate() { rstate := msh.GetRemoteRuntimeState() update := &sstore.ModelUpdate{Remotes: []interface{}{rstate}} - sstore.MainBus.SendUpdate("", update) + sstore.MainBus.SendUpdate(update) } func GetAllRemoteRuntimeState() []RemoteRuntimeState { @@ -811,7 +811,7 @@ func sendRemotePtyUpdate(remoteId string, dataOffset int64, data []byte) { PtyData64: data64, PtyDataLen: int64(len(data)), } - sstore.MainBus.SendUpdate("", update) + sstore.MainBus.SendUpdate(update) } func (msh *MShellProc) isWaitingForPassword_nolock() bool { @@ -1265,8 +1265,8 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt if remotePtr.OwnerId != "" { return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef()) } - if sessionId != runPacket.CK.GetSessionId() { - return nil, nil, fmt.Errorf("run commands sessionids do not match") + if screenId != runPacket.CK.GetGroupId() { + return nil, nil, fmt.Errorf("run commands screenids do not match") } msh := GetRemoteById(remotePtr.RemoteId) if msh == nil { @@ -1284,7 +1284,7 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt } ok, existingPSC := msh.testAndSetPendingStateCmd(remotePtr.Name, newPSC) if !ok { - line, _, err := sstore.GetLineCmdByCmdId(ctx, sessionId, screenId, existingPSC.GetCmdId()) + line, _, err := sstore.GetLineCmdByCmdId(ctx, screenId, existingPSC.GetCmdId()) if err != nil { return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) } @@ -1344,10 +1344,10 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt status = sstore.CmdStatusDetached } cmd := &sstore.CmdType{ - SessionId: runPacket.CK.GetSessionId(), - ScreenId: screenId, + ScreenId: runPacket.CK.GetGroupId(), CmdId: runPacket.CK.GetCmdId(), CmdStr: runPacket.Command, + RawCmdStr: runPacket.Command, Remote: remotePtr, FeState: *sstore.FeStateFromShellState(currentState), StatePtr: *statePtr, @@ -1358,7 +1358,7 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt RunOut: nil, RtnState: runPacket.ReturnState, } - err = sstore.CreateCmdPtyFile(ctx, cmd.SessionId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO the cmd is running, so this is a tricky error to handle return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) @@ -1476,12 +1476,12 @@ func makeDataAckPacket(ck base.CommandKey, fdNum int, ackLen int, err error) *pa func (msh *MShellProc) notifyHangups_nolock() { for ck, _ := range msh.RunningCmds { - cmd, err := sstore.GetCmdById(context.Background(), ck.GetSessionId(), ck.GetCmdId()) + cmd, err := sstore.GetCmdByScreenId(context.Background(), ck.GetGroupId(), ck.GetCmdId()) if err != nil { continue } update := sstore.ModelUpdate{Cmd: cmd} - sstore.MainBus.SendUpdate(ck.GetSessionId(), update) + sstore.MainBus.SendScreenUpdate(ck.GetGroupId(), update) } msh.RunningCmds = make(map[base.CommandKey]RunCmdType) } @@ -1505,12 +1505,12 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return } - screens, err := sstore.UpdateScreensWithCmdFg(context.Background(), donePk.CK.GetSessionId(), donePk.CK.GetCmdId()) + screen, err := sstore.UpdateScreenWithCmdFg(context.Background(), donePk.CK.GetGroupId(), donePk.CK.GetCmdId()) if err != nil { msh.WriteToPtyBuffer("*error trying to update cmd-fg screens: %v\n", err) // fall-through (nothing to do) } - update.Screens = screens + update.Screens = []*sstore.ScreenType{screen} rct := msh.GetRunningCmd(donePk.CK) var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { @@ -1550,13 +1550,13 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { // fall-through (nothing to do) } } - sstore.MainBus.SendUpdate(donePk.CK.GetSessionId(), update) + sstore.MainBus.SendScreenUpdate(donePk.CK.GetGroupId(), update) return } func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) { defer msh.RemoveRunningCmd(finalPk.CK) - rtnCmd, err := sstore.GetCmdById(context.Background(), finalPk.CK.GetSessionId(), finalPk.CK.GetCmdId()) + rtnCmd, err := sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId()) if err != nil { log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err) return @@ -1566,7 +1566,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) } log.Printf("finalpk %s (hangup): %s\n", finalPk.CK, finalPk.Error) sstore.HangupCmd(context.Background(), finalPk.CK) - rtnCmd, err = sstore.GetCmdById(context.Background(), finalPk.CK.GetSessionId(), finalPk.CK.GetCmdId()) + rtnCmd, err = sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId()) if err != nil { log.Printf("error getting cmd(2) in handleCmdFinalPacket: %v\n", err) return @@ -1576,7 +1576,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) return } update := &sstore.ModelUpdate{Cmd: rtnCmd} - sstore.MainBus.SendUpdate(finalPk.CK.GetSessionId(), update) + sstore.MainBus.SendScreenUpdate(finalPk.CK.GetGroupId(), update) } // TODO notify FE about cmd errors @@ -1600,7 +1600,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa if len(realData) > 0 { dataPos := dataPosMap[dataPk.CK] rcmd := msh.GetRunningCmd(dataPk.CK) - update, err := sstore.AppendToCmdPtyBlob(context.Background(), dataPk.CK.GetSessionId(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) + update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos) if err != nil { ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err) } else { @@ -1608,7 +1608,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa } dataPosMap[dataPk.CK] += int64(len(realData)) if update != nil { - sstore.MainBus.SendUpdate(dataPk.CK.GetSessionId(), update) + sstore.MainBus.SendScreenUpdate(dataPk.CK.GetGroupId(), update) } } if ack != nil { diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 2f0cee3ee..e4e2dc13a 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -27,6 +27,7 @@ const HomeVarName = "HOME" const PromptHomeVarName = "PROMPT_HOME" const PromptDevVarName = "PROMPT_DEV" const SessionsDirBaseName = "sessions" +const ScreensDirBaseName = "screens" const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" @@ -36,6 +37,7 @@ const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" var SessionDirCache = make(map[string]string) +var ScreenDirCache = make(map[string]string) var BaseLock = &sync.Mutex{} var BuildTime = "-" @@ -164,6 +166,7 @@ func AcquirePromptLock() (*os.File, error) { return fd, nil } +// deprecated (v0.1.8) func EnsureSessionDir(sessionId string) (string, error) { if sessionId == "" { return "", fmt.Errorf("cannot get session dir for blank sessionid") @@ -186,12 +189,41 @@ func EnsureSessionDir(sessionId string) (string, error) { return sdir, nil } +// deprecated (v0.1.8) func GetSessionsDir() string { promptHome := GetPromptHomeDir() sdir := path.Join(promptHome, SessionsDirBaseName) return sdir } +func EnsureScreenDir(screenId string) (string, error) { + if screenId == "" { + return "", fmt.Errorf("cannot get screen dir for blank sessionid") + } + BaseLock.Lock() + sdir, ok := ScreenDirCache[screenId] + BaseLock.Unlock() + if ok { + return sdir, nil + } + scHome := GetPromptHomeDir() + sdir = path.Join(scHome, ScreensDirBaseName, screenId) + err := ensureDir(sdir) + if err != nil { + return "", err + } + BaseLock.Lock() + ScreenDirCache[screenId] = sdir + BaseLock.Unlock() + return sdir, nil +} + +func GetScreensDir() string { + promptHome := GetPromptHomeDir() + sdir := path.Join(promptHome, ScreensDirBaseName) + return sdir +} + func ensureDir(dirName string) error { info, err := os.Stat(dirName) if errors.Is(err, fs.ErrNotExist) { @@ -211,7 +243,8 @@ func ensureDir(dirName string) error { return nil } -func PtyOutFile(sessionId string, cmdId string) (string, error) { +// deprecated (v0.1.8) +func PtyOutFile_Sessions(sessionId string, cmdId string) (string, error) { sdir, err := EnsureSessionDir(sessionId) if err != nil { return "", err @@ -225,30 +258,18 @@ func PtyOutFile(sessionId string, cmdId string) (string, error) { return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } -func RunOutFile(sessionId string, cmdId string) (string, error) { - sdir, err := EnsureSessionDir(sessionId) +func PtyOutFile(screenId string, cmdId string) (string, error) { + sdir, err := EnsureScreenDir(screenId) if err != nil { return "", err } - if sessionId == "" { - return "", fmt.Errorf("cannot get runout file for blank sessionid") + if screenId == "" { + return "", fmt.Errorf("cannot get ptyout file for blank screenid") } if cmdId == "" { - return "", fmt.Errorf("cannot get runout file for blank cmdid") + return "", fmt.Errorf("cannot get ptyout file for blank cmdid") } - return fmt.Sprintf("%s/%s.runout", sdir, cmdId), nil -} - -type PromptFileNameGenerator struct { - PromptHome string -} - -func (g PromptFileNameGenerator) PtyOutFile(ck base.CommandKey) string { - return path.Join(g.PromptHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".ptyout") -} - -func (g PromptFileNameGenerator) RunOutFile(ck base.CommandKey) string { - return path.Join(g.PromptHome, SessionsDirBaseName, ck.GetSessionId(), ck.GetCmdId()+".runout") + return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } func GenPromptUUID() string { diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 50bb7388b..97694ddc9 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -99,7 +99,7 @@ func (ws *WSState) WatchScreen(sessionId string, screenId string) { } ws.SessionId = sessionId ws.ScreenId = screenId - ws.UpdateCh = sstore.MainBus.RegisterChannel(ws.ClientId, ws.SessionId) + ws.UpdateCh = sstore.MainBus.RegisterChannel(ws.ClientId, ws.ScreenId) go ws.RunUpdates(ws.UpdateCh) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3af2c051c..64f300686 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -440,12 +440,10 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, if screen == nil { return nil, nil } - query = `SELECT sessionid FROM screen WHERE screenid = ?` - sessionId := tx.GetString(query, screenId) - query = `SELECT * FROM line WHERE sessionid = ? AND screenid = ? ORDER BY linenum` - tx.Select(&screen.Lines, query, sessionId, screen.ScreenId) - query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND screenid = ?)` - screen.Cmds = SelectMapsGen[*CmdType](tx, query, sessionId, screen.ScreenId) + query = `SELECT * FROM line WHERE screenid = ? ORDER BY linenum` + tx.Select(&screen.Lines, query, screen.ScreenId) + query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?)` + screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.ScreenId) return screen, nil }) } @@ -700,22 +698,22 @@ func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { }) } -func FindLineIdByArg(ctx context.Context, sessionId string, screenId string, lineArg string) (string, error) { +func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (string, error) { var lineId string txErr := WithTx(ctx, func(tx *TxWrap) error { lineNum, err := strconv.Atoi(lineArg) if err == nil { // valid linenum - query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND linenum = ?` - lineId = tx.GetString(query, sessionId, screenId, lineNum) + query := `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?` + lineId = tx.GetString(query, screenId, lineNum) } else if len(lineArg) == 8 { // prefix id string match - query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND substr(lineid, 1, 8) = ?` - lineId = tx.GetString(query, sessionId, screenId, lineArg) + query := `SELECT lineid FROM line WHERE screenid = ? AND substr(lineid, 1, 8) = ?` + lineId = tx.GetString(query, screenId, lineArg) } else { // id match - query := `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` - lineId = tx.GetString(query, sessionId, screenId, lineArg) + query := `SELECT lineid FROM line WHERE screenid = ? AND lineid = ?` + lineId = tx.GetString(query, screenId, lineArg) } return nil }) @@ -725,33 +723,33 @@ func FindLineIdByArg(ctx context.Context, sessionId string, screenId string, lin return lineId, nil } -func GetLineCmdByLineId(ctx context.Context, sessionId string, screenId string, lineId string) (*LineType, *CmdType, error) { +func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` - found := tx.Get(&lineVal, query, sessionId, screenId, lineId) + query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` + found := tx.Get(&lineVal, query, screenId, lineId) if !found { return nil, nil, nil } var cmdRtn *CmdType if lineVal.CmdId != "" { - query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - cmdRtn = GetMapGen[*CmdType](tx, query, sessionId, lineVal.CmdId) + query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` + cmdRtn = GetMapGen[*CmdType](tx, query, screenId, lineVal.CmdId) } return &lineVal, cmdRtn, nil }) } -func GetLineCmdByCmdId(ctx context.Context, sessionId string, screenId string, cmdId string) (*LineType, *CmdType, error) { +func GetLineCmdByCmdId(ctx context.Context, screenId string, cmdId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { var lineVal LineType - query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND cmdid = ?` - found := tx.Get(&lineVal, query, sessionId, screenId, cmdId) + query := `SELECT * FROM line WHERE screenid = ? AND cmdid = ?` + found := tx.Get(&lineVal, query, screenId, cmdId) if !found { return nil, nil, nil } - query = `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - cmdRtn := GetMapGen[*CmdType](tx, query, sessionId, cmdId) + query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` + cmdRtn := GetMapGen[*CmdType](tx, query, screenId, cmdId) return &lineVal, cmdRtn, nil }) } @@ -770,24 +768,24 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { return fmt.Errorf("cmd should have screenid set") } return WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid FROM screen WHERE sessionid = ? AND screenid = ?` - if !tx.Exists(query, line.SessionId, line.ScreenId) { - return fmt.Errorf("screen not found, cannot insert line[%s/%s]", line.SessionId, line.ScreenId) + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, line.ScreenId) { + return fmt.Errorf("screen not found, cannot insert line[%s]", line.ScreenId) } - query = `SELECT nextlinenum FROM screen WHERE sessionid = ? AND screenid = ?` - nextLineNum := tx.GetInt(query, line.SessionId, line.ScreenId) + query = `SELECT nextlinenum FROM screen WHERE screenid = ?` + nextLineNum := tx.GetInt(query, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( sessionid, screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked, pinned) - VALUES (:sessionid,:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked,:pinned)` + query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked) + VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked)` tx.NamedExec(query, line) - query = `UPDATE screen SET nextlinenum = ? WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, nextLineNum+1, line.SessionId, line.ScreenId) + query = `UPDATE screen SET nextlinenum = ? WHERE screenid = ?` + tx.Exec(query, nextLineNum+1, line.ScreenId) if cmd != nil { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( sessionid, screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) - VALUES (:sessionid,:screenid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) +INSERT INTO cmd ( screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:screenid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExec(query, cmdMap) } @@ -795,11 +793,11 @@ INSERT INTO cmd ( sessionid, screenid, cmdid, remoteownerid, remoteid, remotena }) } -func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, error) { +func GetCmdByScreenId(ctx context.Context, screenId string, cmdId string) (*CmdType, error) { var cmd *CmdType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM cmd WHERE sessionid = ? AND cmdid = ?` - cmd = GetMapGen[*CmdType](tx, query, sessionId, cmdId) + query := `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` + cmd = GetMapGen[*CmdType](tx, query, screenId, cmdId) return nil }) if err != nil { @@ -808,18 +806,6 @@ func GetCmdById(ctx context.Context, sessionId string, cmdId string) (*CmdType, return cmd, nil } -func HasDoneInfo(ctx context.Context, ck base.CommandKey) (bool, error) { - var found bool - txErr := WithTx(ctx, func(tx *TxWrap) error { - found = tx.Exists(`SELECT sessionid FROM cmd WHERE sessionid = ? AND cmdid = ? AND doneinfo is NOT NULL`, ck.GetSessionId(), ck.GetCmdId()) - return nil - }) - if txErr != nil { - return false, txErr - } - return found, nil -} - func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDoneInfo) (*ModelUpdate, error) { if doneInfo == nil { return nil, fmt.Errorf("invalid cmddone packet") @@ -829,10 +815,10 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon } var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, CmdStatusDone, quickJson(doneInfo), ck.GetSessionId(), ck.GetCmdId()) + query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, CmdStatusDone, quickJson(doneInfo), ck.GetGroupId(), ck.GetCmdId()) var err error - rtnCmd, err = GetCmdById(tx.Context(), ck.GetSessionId(), ck.GetCmdId()) + rtnCmd, err = GetCmdByScreenId(tx.Context(), ck.GetGroupId(), ck.GetCmdId()) if err != nil { return err } @@ -852,8 +838,8 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt return fmt.Errorf("cannot update cmdrtnstate, empty ck") } txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetSessionId(), ck.GetCmdId()) + query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetGroupId(), ck.GetCmdId()) return nil }) if txErr != nil { @@ -867,8 +853,8 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err return fmt.Errorf("invalid cmderror packet (no ck)") } return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, quickJson(errPk), errPk.CK.GetSessionId(), errPk.CK.GetCmdId()) + query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, quickJson(errPk), errPk.CK.GetGroupId(), errPk.CK.GetCmdId()) return nil }) } @@ -899,8 +885,8 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) error { func HangupCmd(ctx context.Context, ck base.CommandKey) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ? WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, CmdStatusHangup, ck.GetSessionId(), ck.GetCmdId()) + query := `UPDATE cmd SET status = ? WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), ck.GetCmdId()) return nil }) } @@ -949,40 +935,27 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (* return &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}, nil } -func cleanSessionCmds(ctx context.Context, sessionId string) error { +// screen may not exist at this point (so don't query screen table) +func cleanScreenCmds(ctx context.Context, screenId string) error { var removedCmds []string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT cmdid FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - removedCmds = tx.SelectStrings(query, sessionId, sessionId) - query = `DELETE FROM cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE sessionid = ?)` - tx.Exec(query, sessionId, sessionId) - query = `DELETE FROM bookmark_cmd WHERE sessionid = ? AND cmdid NOT IN (SELECT cmdid FROM cmd WHERE sessionid = ?)` - tx.Exec(query, sessionId, sessionId) + query := `SELECT cmdid FROM cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE screenid = ?)` + removedCmds = tx.SelectStrings(query, screenId, screenId) + query = `DELETE FROM cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE screenid = ?)` + tx.Exec(query, screenId, screenId) + query = `DELETE FROM bookmark_cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM cmd WHERE screenid = ?)` + tx.Exec(query, screenId, screenId) return nil }) if txErr != nil { return txErr } for _, cmdId := range removedCmds { - DeletePtyOutFile(ctx, sessionId, cmdId) + DeletePtyOutFile(ctx, screenId, cmdId) } return nil } -func CleanScreen(sessionId string, screenId string) { - // NOTE: context.Background() here! (this could take a long time, and is async) - txErr := WithTx(context.Background(), func(tx *TxWrap) error { - query := `DELETE FROM history WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) - query = `DELETE FROM line WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) - return cleanSessionCmds(tx.Context(), sessionId) - }) - if txErr != nil { - fmt.Printf("ERROR cleaning session:%s screen:%s : %v\n", sessionId, screenId, txErr) - } -} - func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (UpdatePacket, error) { var isActive bool txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -1042,7 +1015,7 @@ func UnArchiveScreen(ctx context.Context, sessionId string, screenId string) err return txErr } -func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { +func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdatePacket, error) { var sessionId string var isActive bool txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -1050,30 +1023,39 @@ func DeleteScreen(ctx context.Context, screenId string) (UpdatePacket, error) { if !tx.Exists(query, screenId) { return fmt.Errorf("cannot purge screen (not found)") } - query = `SELECT sessionid FROM screen WHERE screenid = ?` - sessionId = tx.GetString(query, screenId) - if sessionId == "" { - return fmt.Errorf("cannot purge 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") - } - 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 := getNextId(screenIds, screenId) - tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + if !sessionDel { + query = `SELECT sessionid FROM screen WHERE screenid = ?` + sessionId = tx.GetString(query, screenId) + if sessionId == "" { + return fmt.Errorf("cannot purge 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") + } + 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 := getNextId(screenIds, screenId) + tx.Exec(`UPDATE session SET activescreenid = ? WHERE sessionid = ?`, nextId, sessionId) + } } 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) return nil }) if txErr != nil { return nil, txErr } - go CleanScreen(sessionId, screenId) + go cleanScreenCmds(context.Background(), screenId) + if sessionDel { + return nil, nil + } update := ModelUpdate{} update.Screens = []*ScreenType{&ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}} if isActive { @@ -1312,13 +1294,12 @@ func SetScreenName(ctx context.Context, sessionId string, screenId string, name func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM screen WHERE screenid = ?` - sessionId := tx.GetString(query, screenId) - if sessionId == "" { - return fmt.Errorf("screen sessionid does not exist") + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { + return fmt.Errorf("screen does not exist") } - query = `UPDATE line SET archived = 1 WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) + query = `UPDATE line SET archived = 1 WHERE screenid = ?` + tx.Exec(query, screenId) return nil }) if txErr != nil { @@ -1333,19 +1314,13 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) { var lineIds []string - var sessionId string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT sessionid FROM screen WHERE screenid = ?` - sessionId = tx.GetString(query, screenId) - if sessionId == "" { - return fmt.Errorf("screen sessionid does not exist") - } - query = `SELECT lineid FROM line WHERE sessionid = ? AND screenid = ?` - lineIds = tx.SelectStrings(query, sessionId, screenId) - query = `DELETE FROM line WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) - query = `DELETE FROM history WHERE sessionid = ? AND screenid = ?` - tx.Exec(query, sessionId, screenId) + 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 = ?` + tx.Exec(query, screenId) query = `UPDATE screen SET nextlinenum = 1 WHERE screenid = ?` tx.Exec(query, screenId) return nil @@ -1353,7 +1328,7 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error if txErr != nil { return nil, txErr } - go cleanSessionCmds(context.Background(), sessionId) + go cleanScreenCmds(context.Background(), screenId) screen, err := GetScreenById(ctx, screenId) if err != nil { return nil, err @@ -1364,21 +1339,20 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error } for _, lineId := range lineIds { line := &LineType{ - SessionId: sessionId, - ScreenId: screenId, - LineId: lineId, - Remove: true, + ScreenId: screenId, + LineId: lineId, + Remove: true, } screenLines.Lines = append(screenLines.Lines, line) } return &ModelUpdate{Screens: []*ScreenType{screen}, ScreenLines: screenLines}, nil } -func GetRunningScreenCmds(ctx context.Context, sessionId string, screenId string) ([]*CmdType, error) { +func GetRunningScreenCmds(ctx context.Context, screenId string) ([]*CmdType, error) { var rtn []*CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE sessionid = ? AND screenid = ?) AND status = ?` - rtn = SelectMapsGen[*CmdType](tx, query, sessionId, screenId, CmdStatusRunning) + query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?) AND status = ?` + rtn = SelectMapsGen[*CmdType](tx, query, screenId, CmdStatusRunning) return nil }) if txErr != nil { @@ -1387,10 +1361,10 @@ func GetRunningScreenCmds(ctx context.Context, sessionId string, screenId string return rtn, nil } -func UpdateCmdTermOpts(ctx context.Context, sessionId string, cmdId string, termOpts TermOpts) error { +func UpdateCmdTermOpts(ctx context.Context, screenId string, cmdId string, termOpts TermOpts) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET termopts = ? WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, termOpts, sessionId, cmdId) + query := `UPDATE cmd SET termopts = ? WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, termOpts, screenId, cmdId) return nil }) return txErr @@ -1417,25 +1391,24 @@ func ScreenReset(ctx context.Context, screenId string) ([]*RemoteInstance, error }) } -func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) { +func PurgeSession(ctx context.Context, sessionId string) (UpdatePacket, error) { var newActiveSessionId string + var screenIds []string 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") } + query = `SELECT screenid FROM screen WHERE sessionid = ?` + screenIds = tx.SelectStrings(query, sessionId) + for _, screenId := range screenIds { + _, err := PurgeScreen(ctx, screenId, true) + if err != nil { + return fmt.Errorf("error purging screen[%s]: %v", screenId, err) + } + } query = `DELETE FROM session WHERE sessionid = ?` tx.Exec(query, sessionId) - query = `DELETE FROM screen WHERE sessionid = ?` - tx.Exec(query, sessionId) - query = `DELETE FROM history WHERE sessionid = ?` - tx.Exec(query, sessionId) - query = `DELETE FROM line WHERE sessionid = ?` - tx.Exec(query, sessionId) - query = `DELETE FROM cmd WHERE sessionid = ?` - tx.Exec(query, sessionId) - query = `DELETE FROM bookmark_cmd WHERE sessionid = ?` - tx.Exec(query, sessionId) newActiveSessionId, _ = fixActiveSessionId(tx.Context()) return nil }) @@ -1453,6 +1426,9 @@ func DeleteSession(ctx context.Context, sessionId string) (UpdatePacket, error) } } update.Sessions = append(update.Sessions, &SessionType{SessionId: sessionId, Remove: true}) + for _, screenId := range screenIds { + update.Screens = append(update.Screens, &ScreenType{ScreenId: screenId, Remove: true}) + } return update, nil } @@ -1561,7 +1537,7 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, rtn.NumArchivedScreens = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM line WHERE sessionid = ?` rtn.NumLines = tx.GetInt(query, sessionId) - query = `SELECT count(*) FROM cmd WHERE sessionid = ?` + query = `SELECT count(*) FROM cmd WHERE screenid IN (select screenid FROM screen WHERE sessionid = ?)` rtn.NumCmds = tx.GetInt(query, sessionId) return nil }) @@ -1686,11 +1662,11 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter return GetScreenById(ctx, screenId) } -func GetLineResolveItems(ctx context.Context, sessionId string, screenId string) ([]ResolveItem, error) { +func GetLineResolveItems(ctx context.Context, screenId string) ([]ResolveItem, error) { var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT lineid as id, linenum as num FROM line WHERE sessionid = ? AND screenid = ? ORDER BY linenum` - tx.Select(&rtn, query, sessionId, screenId) + query := `SELECT lineid as id, linenum as num FROM line WHERE screenid = ? ORDER BY linenum` + tx.Select(&rtn, query, screenId) return nil }) if txErr != nil { @@ -1699,39 +1675,24 @@ func GetLineResolveItems(ctx context.Context, sessionId string, screenId string) return rtn, nil } -func UpdateScreensWithCmdFg(ctx context.Context, sessionId string, cmdId string) ([]*ScreenType, error) { - var rtn []*ScreenType - txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT screenid - FROM screen s - WHERE - s.sessionid = ? - AND s.focustype = 'cmd-fg' - AND s.selectedline IN (SELECT linenum - FROM line l - WHERE l.sessionid = s.sessionid - AND l.screenid = s.screenid - AND l.cmdid = ? - )` - screenIds := tx.SelectStrings(query, sessionId, cmdId) - if len(screenIds) == 0 { - return nil +func UpdateScreenWithCmdFg(ctx context.Context, screenId string, cmdId string) (*ScreenType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { + query := `SELECT screenid + FROM screen s + WHERE s.screenid = ? AND s.focustype = 'cmd-fg' + AND s.selectedline IN (SELECT linenum FROM line l WHERE l.screenid = s.screenid AND l.cmdid = ?) + ` + if !tx.Exists(query, screenId, cmdId) { + return nil, nil } - for _, screenId := range screenIds { - editMap := make(map[string]interface{}) - editMap[ScreenField_Focus] = ScreenFocusInput - screen, err := UpdateScreen(tx.Context(), screenId, editMap) - if err != nil { - return err - } - rtn = append(rtn, screen) + editMap := make(map[string]interface{}) + editMap[ScreenField_Focus] = ScreenFocusInput + screen, err := UpdateScreen(tx.Context(), screenId, editMap) + if err != nil { + return nil, err } - return nil + return screen, nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil } func StoreStateBase(ctx context.Context, state *packet.ShellState) error { @@ -1865,12 +1826,12 @@ func UpdateLineRenderer(ctx context.Context, lineId string, renderer string) err } // can return nil, nil if line is not found -func GetLineById(ctx context.Context, sessionId string, screenId string, lineId string) (*LineType, error) { +func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType, error) { var rtn *LineType txErr := WithTx(ctx, func(tx *TxWrap) error { var line LineType - query := `SELECT * FROM line WHERE sessionid = ? AND screenid = ? AND lineid = ?` - found := tx.Get(&line, query, sessionId, screenId, lineId) + query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` + found := tx.Get(&line, query, screenId, lineId) if found { rtn = &line } @@ -1891,32 +1852,28 @@ func SetLineArchivedById(ctx context.Context, lineId string, archived bool) erro return txErr } -func purgeCmdById(ctx context.Context, sessionId string, cmdId string) error { +func purgeCmdByScreenId(ctx context.Context, screenId string, cmdId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `DELETE FROM cmd WHERE sessionid = ? AND cmdid = ?` - tx.Exec(query, sessionId, cmdId) - return DeletePtyOutFile(tx.Context(), sessionId, cmdId) + query := `DELETE FROM cmd WHERE screenid = ? AND cmdid = ?` + tx.Exec(query, screenId, cmdId) + return DeletePtyOutFile(tx.Context(), screenId, cmdId) }) return txErr } -func PurgeLinesByIds(ctx context.Context, sessionId string, lineIds []string) error { +func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { for _, lineId := range lineIds { - query := `SELECT cmdid FROM line WHERE sessionid = ? AND lineid = ?` - cmdId := tx.GetString(query, sessionId, lineId) - query = `DELETE FROM line WHERE sessionid = ? AND lineid = ?` - tx.Exec(query, sessionId, lineId) - query = `DELETE FROM history WHERE sessionid = ? AND lineid = ?` - tx.Exec(query, sessionId, lineId) + query := `SELECT cmdid FROM line WHERE screenid = ? AND lineid = ?` + cmdId := tx.GetString(query, screenId, lineId) + 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) if cmdId != "" { - query = `SELECT count(*) FROM line WHERE sessionid = ? AND cmdid = ?` - cmdRefCount := tx.GetInt(query, sessionId, cmdId) - if cmdRefCount == 0 { - err := purgeCmdById(tx.Context(), sessionId, cmdId) - if err != nil { - return err - } + err := purgeCmdByScreenId(tx.Context(), screenId, cmdId) + if err != nil { + return err } } } @@ -2084,7 +2041,7 @@ type bookmarkOrderType struct { type bookmarkCmdType struct { BookmarkId string - SessionId string + ScreenId string CmdId string } @@ -2110,12 +2067,12 @@ func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { } } var cmds []bookmarkCmdType - query = `SELECT bookmarkid, sessionid, cmdid FROM bookmark_cmd` + query = `SELECT bookmarkid, screenid, cmdid FROM bookmark_cmd` tx.Select(&cmds, query) for _, cmd := range cmds { bm := bmMap[cmd.BookmarkId] if bm != nil { - bm.Cmds = append(bm.Cmds, base.MakeCommandKey(cmd.SessionId, cmd.CmdId)) + bm.Cmds = append(bm.Cmds, base.MakeCommandKey(cmd.ScreenId, cmd.CmdId)) } } return nil @@ -2137,11 +2094,11 @@ func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*Bookm query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?` orderIdx := tx.GetInt(query, bookmarkId, tag) rtn.OrderIdx = int64(orderIdx) - query = `SELECT bookmarkid, sessionid, cmdid FROM bookmark_cmd WHERE bookmarkid = ?` + query = `SELECT bookmarkid, screenid, cmdid FROM bookmark_cmd WHERE bookmarkid = ?` var cmds []bookmarkCmdType tx.Select(&cmds, query, bookmarkId) for _, cmd := range cmds { - rtn.Cmds = append(rtn.Cmds, base.MakeCommandKey(cmd.SessionId, cmd.CmdId)) + rtn.Cmds = append(rtn.Cmds, base.MakeCommandKey(cmd.ScreenId, cmd.CmdId)) } return nil }) @@ -2188,13 +2145,13 @@ func InsertBookmark(ctx context.Context, bm *BookmarkType) error { query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)` tx.Exec(query, tag, bm.BookmarkId, maxOrder+1) } - query = `INSERT INTO bookmark_cmd (bookmarkid, sessionid, cmdid) VALUES (?, ?, ?)` + query = `INSERT INTO bookmark_cmd (bookmarkid, screenid, cmdid) VALUES (?, ?, ?)` for _, ck := range bm.Cmds { - tx.Exec(query, bm.BookmarkId, ck.GetSessionId(), ck.GetCmdId()) + tx.Exec(query, bm.BookmarkId, ck.GetGroupId(), ck.GetCmdId()) } - query = `UPDATE line SET bookmarked = 1 WHERE sessionid = ? AND cmdid = ?` + query = `UPDATE line SET bookmarked = 1 WHERE screenid = ? AND cmdid = ?` for _, ck := range bm.Cmds { - tx.Exec(query, ck.GetSessionId(), ck.GetCmdId()) + tx.Exec(query, ck.GetGroupId(), ck.GetCmdId()) } return nil }) @@ -2248,7 +2205,7 @@ func DeleteBookmark(ctx context.Context, bookmarkId string) error { tx.Exec(query, bookmarkId) query = `DELETE FROM bookmark_order WHERE bookmarkid = ?` tx.Exec(query, bookmarkId) - query = `UPDATE line SET bookmarked = 0 WHERE bookmarked AND cmdid <> '' AND (sessionid||cmdid) IN (SELECT sessionid||cmdid FROM bookmark_cmd WHERE bookmarkid = ?) ` + query = `UPDATE line SET bookmarked = 0 WHERE bookmarked AND cmdid <> '' AND (screenid||cmdid) IN (SELECT screenid||cmdid FROM bookmark_cmd WHERE bookmarkid = ?) ` tx.Exec(query, bookmarkId) query = `DELETE FROM bookmark_cmd WHERE bookmarkid = ?` tx.Exec(query, bookmarkId) @@ -2377,7 +2334,7 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem tx.Exec(query, quickJsonArr(historyIds)) for _, hitem := range rtn { if hitem.LineId != "" { - err := PurgeLinesByIds(tx.Context(), hitem.SessionId, []string{hitem.LineId}) + err := PurgeLinesByIds(tx.Context(), hitem.ScreenId, []string{hitem.LineId}) if err != nil { return nil, err } @@ -2386,10 +2343,3 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem return rtn, nil }) } - -func GetScreenIdFromCmd(ctx context.Context, sessionId string, cmdId string) (string, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { - query := `SELECT screenid FROM cmd WHERE sessionid = ? AND cmdid = ?` - return tx.GetString(query, sessionId, cmdId), nil - }) -} diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index c27971a27..2585c50df 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -12,8 +12,8 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) -func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSize int64) error { - ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) +func CreateCmdPtyFile(ctx context.Context, screenId string, cmdId string, maxSize int64) error { + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { return err } @@ -24,22 +24,22 @@ func CreateCmdPtyFile(ctx context.Context, sessionId string, cmdId string, maxSi return f.Close() } -func StatCmdPtyFile(ctx context.Context, sessionId string, cmdId string) (*cirfile.Stat, error) { - ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) +func StatCmdPtyFile(ctx context.Context, screenId string, cmdId string) (*cirfile.Stat, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { return nil, err } return cirfile.StatCirFile(ctx, ptyOutFileName) } -func AppendToCmdPtyBlob(ctx context.Context, sessionId string, screenId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { +func AppendToCmdPtyBlob(ctx context.Context, screenId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { if screenId == "" { return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set") } if pos < 0 { return nil, fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) } - ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { return nil, err } @@ -54,7 +54,6 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, screenId string, } data64 := base64.StdEncoding.EncodeToString(data) update := &PtyDataUpdate{ - SessionId: sessionId, ScreenId: screenId, CmdId: cmdId, PtyPos: pos, @@ -65,8 +64,8 @@ func AppendToCmdPtyBlob(ctx context.Context, sessionId string, screenId string, } // returns (offset, data, err) -func ReadFullPtyOutFile(ctx context.Context, sessionId string, cmdId string) (int64, []byte, error) { - ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) +func ReadFullPtyOutFile(ctx context.Context, screenId string, cmdId string) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { return 0, nil, err } @@ -141,8 +140,8 @@ func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) { return rtn, nil } -func DeletePtyOutFile(ctx context.Context, sessionId string, cmdId string) error { - ptyOutFileName, err := scbase.PtyOutFile(sessionId, cmdId) +func DeletePtyOutFile(ctx context.Context, screenId string, cmdId string) error { + ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) if err != nil { return err } @@ -157,3 +156,12 @@ func DeleteSessionDir(ctx context.Context, sessionId string) error { fmt.Printf("remove-all %s\n", sessionDir) return os.RemoveAll(sessionDir) } + +func DeleteScreenDir(ctx context.Context, screenId string) error { + screenDir, err := scbase.EnsureScreenDir(screenId) + if err != nil { + return fmt.Errorf("error getting screendir: %w", err) + } + fmt.Printf("remove-all %s\n", screenDir) + return os.RemoveAll(screenDir) +} diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 894e4769e..613006922 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 12 +const MaxMigration = 13 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { @@ -81,7 +81,9 @@ func MigrateUp() error { if err != nil { return fmt.Errorf("error creating database backup: %v", err) } + startTime := time.Now() err = m.Migrate(MaxMigration) + log.Printf("[db] migration took %v\n", time.Since(startTime)) if err != nil { return err } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b85b83eae..6d52f8150 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -76,6 +76,11 @@ const ( ScreenFocusCmdFg = "cmd-fg" ) +const ( + CmdStoreTypeSession = "session" + CmdStoreTypeScreen = "screen" +) + const MaxTzNameLen = 50 var globalDBLock = &sync.Mutex{} @@ -187,17 +192,26 @@ type FeOptsType struct { TermFontSize int `json:"termfontsize,omitempty"` } +type ClientMigrationData struct { + MigrationType string `json:"migrationtype"` + MigrationPos int `json:"migrationpos"` + MigrationTotal int `json:"migrationtotal"` + MigrationDone bool `json:"migrationdone"` +} + type ClientData struct { - ClientId string `json:"clientid"` - UserId string `json:"userid"` - UserPrivateKeyBytes []byte `json:"-"` - UserPublicKeyBytes []byte `json:"-"` - UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` - UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` - ActiveSessionId string `json:"activesessionid"` - WinSize ClientWinSizeType `json:"winsize"` - ClientOpts ClientOptsType `json:"clientopts"` - FeOpts FeOptsType `json:"feopts"` + ClientId string `json:"clientid"` + UserId string `json:"userid"` + UserPrivateKeyBytes []byte `json:"-"` + UserPublicKeyBytes []byte `json:"-"` + UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` + UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` + ActiveSessionId string `json:"activesessionid"` + WinSize ClientWinSizeType `json:"winsize"` + ClientOpts ClientOptsType `json:"clientopts"` + FeOpts FeOptsType `json:"feopts"` + CmdStoreType string `json:"cmdstoretype"` + Migration *ClientMigrationData `json:"migration,omitempty" dbmap:"-"` } func (ClientData) UseDBMap() {} @@ -641,7 +655,6 @@ func (ri *RemoteInstance) ToMap() map[string]interface{} { } type LineType struct { - SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` UserId string `json:"userid"` LineId string `json:"lineid"` @@ -657,7 +670,6 @@ type LineType struct { ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` Bookmarked bool `json:"bookmarked,omitempty"` - Pinned bool `json:"pinned,omitempty"` Archived bool `json:"archived,omitempty"` Remove bool `json:"remove,omitempty"` } @@ -832,12 +844,18 @@ type CmdDoneInfo struct { DurationMs int64 `json:"durationms"` } +type CmdMapType struct { + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + CmdId string `json:"cmdid"` +} + type CmdType struct { - SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` CmdId string `json:"cmdid"` Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` + RawCmdStr string `json:"rawcmdstr"` FeState FeStateType `json:"festate"` StatePtr ShellStatePtr `json:"state"` TermOpts TermOpts `json:"termopts"` @@ -894,13 +912,13 @@ func (r *RemoteType) FromMap(m map[string]interface{}) bool { func (cmd *CmdType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) - rtn["sessionid"] = cmd.SessionId rtn["screenid"] = cmd.ScreenId rtn["cmdid"] = cmd.CmdId rtn["remoteownerid"] = cmd.Remote.OwnerId rtn["remoteid"] = cmd.Remote.RemoteId rtn["remotename"] = cmd.Remote.Name rtn["cmdstr"] = cmd.CmdStr + rtn["rawcmdstr"] = cmd.RawCmdStr rtn["festate"] = quickJson(cmd.FeState) rtn["statebasehash"] = cmd.StatePtr.BaseHash rtn["statediffhasharr"] = quickJsonArr(cmd.StatePtr.DiffHashArr) @@ -917,13 +935,13 @@ func (cmd *CmdType) ToMap() map[string]interface{} { } func (cmd *CmdType) FromMap(m map[string]interface{}) bool { - quickSetStr(&cmd.SessionId, m, "sessionid") quickSetStr(&cmd.ScreenId, m, "screenid") quickSetStr(&cmd.CmdId, m, "cmdid") quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") quickSetStr(&cmd.Remote.Name, m, "remotename") quickSetStr(&cmd.CmdStr, m, "cmdstr") + quickSetStr(&cmd.RawCmdStr, m, "rawcmdstr") quickSetJson(&cmd.FeState, m, "festate") quickSetStr(&cmd.StatePtr.BaseHash, m, "statebasehash") quickSetJsonArr(&cmd.StatePtr.DiffHashArr, m, "statediffhasharr") @@ -939,9 +957,8 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool { return true } -func makeNewLineCmd(sessionId string, screenId string, userId string, cmdId string, renderer string) *LineType { +func makeNewLineCmd(screenId string, userId string, cmdId string, renderer string) *LineType { rtn := &LineType{} - rtn.SessionId = sessionId rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() @@ -954,9 +971,8 @@ func makeNewLineCmd(sessionId string, screenId string, userId string, cmdId stri return rtn } -func makeNewLineText(sessionId string, screenId string, userId string, text string) *LineType { +func makeNewLineText(screenId string, userId string, text string) *LineType { rtn := &LineType{} - rtn.SessionId = sessionId rtn.ScreenId = screenId rtn.UserId = userId rtn.LineId = scbase.GenPromptUUID() @@ -968,8 +984,8 @@ func makeNewLineText(sessionId string, screenId string, userId string, text stri return rtn } -func AddCommentLine(ctx context.Context, sessionId string, screenId string, userId string, commentText string) (*LineType, error) { - rtnLine := makeNewLineText(sessionId, screenId, userId, commentText) +func AddCommentLine(ctx context.Context, screenId string, userId string, commentText string) (*LineType, error) { + rtnLine := makeNewLineText(screenId, userId, commentText) err := InsertLine(ctx, rtnLine, nil) if err != nil { return nil, err @@ -977,8 +993,8 @@ func AddCommentLine(ctx context.Context, sessionId string, screenId string, user return rtnLine, nil } -func AddCmdLine(ctx context.Context, sessionId string, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { - rtnLine := makeNewLineCmd(sessionId, screenId, userId, cmd.CmdId, renderer) +func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { + rtnLine := makeNewLineCmd(screenId, userId, cmd.CmdId, renderer) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err @@ -1085,6 +1101,7 @@ func createClientData(tx *TxWrap) error { UserPublicKeyBytes: pubBytes, ActiveSessionId: "", WinSize: ClientWinSizeType{}, + CmdStoreType: CmdStoreTypeScreen, } query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` @@ -1137,6 +1154,28 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { return rtn, nil } +func GetCmdMigrationInfo(ctx context.Context) (*ClientMigrationData, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*ClientMigrationData, error) { + cdata := GetMappable[*ClientData](tx, `SELECT * FROM client`) + if cdata == nil { + return nil, fmt.Errorf("no client data found") + } + if cdata.CmdStoreType == "session" { + total := tx.GetInt(`SELECT count(*) FROM cmd`) + posInv := tx.GetInt(`SELECT count(*) FROM cmd_migrate`) + mdata := &ClientMigrationData{ + MigrationType: "cmdscreen", + MigrationPos: total - posInv, + MigrationTotal: total, + MigrationDone: false, + } + return mdata, nil + } + // no migration info + return nil, nil + }) +} + func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET clientopts = ?` @@ -1145,3 +1184,93 @@ func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { }) return txErr } + +type cmdMigrationType struct { + SessionId string + ScreenId string + CmdId string +} + +func getSliceChunk[T any](slice []T, chunkSize int) ([]T, []T) { + if chunkSize >= len(slice) { + return slice, nil + } + return slice[0:chunkSize], slice[chunkSize:] +} + +func processChunk(ctx context.Context, mchunk []cmdMigrationType) error { + for _, mig := range mchunk { + newFile, err := scbase.PtyOutFile(mig.ScreenId, mig.CmdId) + if err != nil { + log.Printf("ptyoutfile error: %v\n", err) + continue + } + oldFile, err := scbase.PtyOutFile_Sessions(mig.SessionId, mig.CmdId) + if err != nil { + log.Printf("ptyoutfile_sessions error: %v\n", err) + continue + } + err = os.Rename(oldFile, newFile) + if err != nil { + log.Printf("error renaming %s => %s: %v\n", oldFile, newFile, err) + continue + } + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + for _, mig := range mchunk { + query := `DELETE FROM cmd_migrate WHERE cmdid = ?` + tx.Exec(query, mig.CmdId) + } + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +func RunCmdScreenMigration() { + ctx := context.Background() + startTime := time.Now() + mdata, err := GetCmdMigrationInfo(ctx) + if err != nil { + log.Printf("[prompt] error trying to run cmd migration: %v\n", err) + return + } + if mdata == nil || mdata.MigrationType != "cmdscreen" { + return + } + var migrations []cmdMigrationType + txErr := WithTx(ctx, func(tx *TxWrap) error { + tx.Select(&migrations, `SELECT * FROM cmd_migrate`) + return nil + }) + if txErr != nil { + log.Printf("[prompt] error trying to get cmd migrations: %v\n", txErr) + return + } + log.Printf("[db] got %d cmd migrations\n", len(migrations)) + for len(migrations) > 0 { + var mchunk []cmdMigrationType + mchunk, migrations = getSliceChunk(migrations, 5) + err = processChunk(ctx, mchunk) + if err != nil { + log.Printf("[prompt] cmd migration failed on chunk: %v\n%#v\n", err, mchunk) + return + } + } + err = os.RemoveAll(scbase.GetSessionsDir()) + if err != nil { + log.Printf("[db] cannot remove old sessions dir %s: %v\n", scbase.GetSessionsDir(), err) + } + txErr = WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET cmdstoretype = 'screen'` + tx.Exec(query) + return nil + }) + if txErr != nil { + log.Printf("[db] cannot change client cmdstoretype: %v\n", err) + } + log.Printf("[db] cmd screen migration done: %v\n", time.Since(startTime)) + return +} diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 416b6920c..657c87509 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -17,7 +17,6 @@ type UpdatePacket interface { } type PtyDataUpdate struct { - SessionId string `json:"sessionid,omitempty"` ScreenId string `json:"screenid,omitempty"` CmdId string `json:"cmdid,omitempty"` RemoteId string `json:"remoteid,omitempty"` @@ -123,16 +122,16 @@ type CmdLineType struct { } type UpdateChannel struct { - SessionId string - ClientId string - Ch chan interface{} + ScreenId string + ClientId string + Ch chan interface{} } -func (uch UpdateChannel) Match(sessionId string) bool { - if sessionId == "" { +func (uch UpdateChannel) Match(screenId string) bool { + if screenId == "" { return true } - return sessionId == uch.SessionId + return screenId == uch.ScreenId } type UpdateBus struct { @@ -148,19 +147,19 @@ func MakeUpdateBus() *UpdateBus { } // always returns a new channel -func (bus *UpdateBus) RegisterChannel(clientId string, sessionId string) chan interface{} { +func (bus *UpdateBus) RegisterChannel(clientId string, screenId string) chan interface{} { bus.Lock.Lock() defer bus.Lock.Unlock() uch, found := bus.Channels[clientId] if found { close(uch.Ch) - uch.SessionId = sessionId + uch.ScreenId = screenId uch.Ch = make(chan interface{}, UpdateChSize) } else { uch = UpdateChannel{ - ClientId: clientId, - SessionId: sessionId, - Ch: make(chan interface{}, UpdateChSize), + ClientId: clientId, + ScreenId: screenId, + Ch: make(chan interface{}, UpdateChSize), } } bus.Channels[clientId] = uch @@ -177,11 +176,24 @@ func (bus *UpdateBus) UnregisterChannel(clientId string) { } } -func (bus *UpdateBus) SendUpdate(sessionId string, update interface{}) { +func (bus *UpdateBus) SendUpdate(update interface{}) { bus.Lock.Lock() defer bus.Lock.Unlock() for _, uch := range bus.Channels { - if uch.Match(sessionId) { + select { + case uch.Ch <- update: + + default: + log.Printf("[error] dropped update on updatebus uch clientid=%s\n", uch.ClientId) + } + } +} + +func (bus *UpdateBus) SendScreenUpdate(screenId string, update interface{}) { + bus.Lock.Lock() + defer bus.Lock.Unlock() + for _, uch := range bus.Channels { + if uch.Match(screenId) { select { case uch.Ch <- update: From bd0cffb2c3f1381c2e6be1794642acf9330db277 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Mar 2023 21:38:39 -0700 Subject: [PATCH 310/397] update schema --- db/schema.sql | 103 ++++++++++++++++++++----------------- pkg/cmdrunner/cmdrunner.go | 3 +- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 68a47ba99..9ebfb32d5 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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 '{}'); +, clientopts json NOT NULL DEFAULT '', feopts json NOT NULL DEFAULT '{}', cmdstoretype varchar(20) DEFAULT 'session'); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, @@ -41,24 +41,6 @@ CREATE TABLE state_diff ( diffhasharr json NOT NULL, data blob NOT NULL ); -CREATE TABLE line ( - sessionid varchar(36) NOT NULL, - screenid varchar(36) NOT NULL, - userid varchar(36) NOT NULL, - lineid varchar(36) NOT NULL, - ts bigint NOT NULL, - linenum int NOT NULL, - linenumtemp boolean NOT NULL, - linetype varchar(10) NOT NULL, - linelocal boolean NOT NULL, - text text NOT NULL, - cmdid varchar(36) NOT NULL, - ephemeral boolean NOT NULL, - contentheight int NOT NULL, - star int NOT NULL, - archived boolean NOT NULL, renderer varchar(50) NOT NULL DEFAULT '', bookmarked boolean NOT NULL DEFAULT 0, pinned boolean NOT NULL DEFAULT 0, - PRIMARY KEY (sessionid, screenid, lineid) -); CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, physicalid varchar(36) NOT NULL, @@ -77,27 +59,6 @@ CREATE TABLE remote ( archived boolean NOT NULL, remoteidx int NOT NULL ); -CREATE TABLE cmd ( - sessionid varchar(36) NOT NULL, - cmdid varchar(36) NOT NULL, - remoteownerid varchar(36) NOT NULL, - remoteid varchar(36) NOT NULL, - remotename varchar(50) NOT NULL, - cmdstr text NOT NULL, - festate json NOT NULL, - statebasehash varchar(36) NOT NULL, - statediffhasharr json NOT NULL, - termopts json NOT NULL, - origtermopts json NOT NULL, - status varchar(10) NOT NULL, - startpk json NOT NULL, - doneinfo json NOT NULL, - runout json NOT NULL, - rtnstate boolean NOT NULL, - rtnbasehash varchar(36) NOT NULL, - rtndiffhasharr json NOT NULL, screenid varchar(36) NOT NULL DEFAULT '', - PRIMARY KEY (sessionid, cmdid) -); CREATE TABLE history ( historyid varchar(36) PRIMARY KEY, ts bigint NOT NULL, @@ -137,12 +98,6 @@ CREATE TABLE bookmark_order ( orderidx int NOT NULL, PRIMARY KEY (tag, bookmarkid) ); -CREATE TABLE bookmark_cmd ( - bookmarkid varchar(36) NOT NULL, - sessionid varchar(36) NOT NULL, - cmdid varchar(36) NOT NULL, - PRIMARY KEY (bookmarkid, sessionid, cmdid) -); CREATE TABLE playbook ( playbookid varchar(36) PRIMARY KEY, playbookname varchar(100) NOT NULL, @@ -173,9 +128,20 @@ CREATE TABLE cloud_update ( updatetype varchar(50) NOT NULL, updatekeys json NOT NULL ); -CREATE TABLE IF NOT EXISTS "screen" ( +CREATE TABLE IF NOT EXISTS "bookmark_cmd" ( + bookmarkid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (bookmarkid, screenid, cmdid) +); +CREATE TABLE cmd_migrate ( sessionid varchar(36) NOT NULL, screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL +); +CREATE TABLE IF NOT EXISTS "screen" ( + screenid varchar(36) NOT NULL, + sessionid varchar(36) NOT NULL, name varchar(50) NOT NULL, screenidx int NOT NULL, screenopts json NOT NULL, @@ -190,5 +156,46 @@ CREATE TABLE IF NOT EXISTS "screen" ( focustype varchar(12) NOT NULL, archived boolean NOT NULL, archivedts bigint NOT NULL, - PRIMARY KEY (sessionid, screenid) + PRIMARY KEY (screenid) +); +CREATE TABLE IF NOT EXISTS "line" ( + screenid varchar(36) NOT NULL, + userid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + ts bigint NOT NULL, + linenum int NOT NULL, + linenumtemp boolean NOT NULL, + linetype varchar(10) NOT NULL, + linelocal boolean NOT NULL, + text text NOT NULL, + cmdid varchar(36) NOT NULL, + ephemeral boolean NOT NULL, + contentheight int NOT NULL, + star int NOT NULL, + archived boolean NOT NULL, + renderer varchar(50) NOT NULL, + bookmarked boolean NOT NULL, + PRIMARY KEY (screenid, lineid) +); +CREATE TABLE IF NOT EXISTS "cmd" ( + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + cmdstr text NOT NULL, + rawcmdstr text NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, + termopts json NOT NULL, + origtermopts json NOT NULL, + status varchar(10) NOT NULL, + startpk json NOT NULL, + doneinfo json NOT NULL, + runout json NOT NULL, + rtnstate boolean NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, + PRIMARY KEY (screenid, cmdid) ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index cb22c3fd6..34b794b61 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -539,7 +539,7 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } return update, nil } else { - fmt.Printf("unarchive screen %s\n", screenId) + log.Printf("unarchive screen %s\n", screenId) err = sstore.UnArchiveScreen(ctx, ids.SessionId, screenId) if err != nil { return nil, fmt.Errorf("/screen:archive cannot re-open screen: %v", err) @@ -2582,7 +2582,6 @@ func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U } setMap[scopeName][varName] = argVal } - fmt.Printf("setmap: %#v\n", setMap) return nil, nil } From 8299d3ff54d8a57d0dcbb0341879bb363293f897 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 23 Mar 2023 12:08:03 -0700 Subject: [PATCH 311/397] send updated clientdata when telemetry is updated --- pkg/cmdrunner/cmdrunner.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 34b794b61..16eaeb568 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2727,7 +2727,6 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "backend", scbase.PromptVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) buf.WriteString(fmt.Sprintf(" %-15s %d\n", "db-version", dbVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "client-version", clientVersion)) @@ -2780,7 +2779,13 @@ func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // ignore error, but log log.Printf("[error] sending telemetry update (in /telemetry:on): %v\n", err) } - return sstore.InfoMsgUpdate("telemetry is now on"), nil + clientData, err = sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) + } + update := sstore.InfoMsgUpdate("telemetry is now on") + update.ClientData = clientData + return update, nil } func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -2795,7 +2800,13 @@ func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, err } - return sstore.InfoMsgUpdate("telemetry is now off"), nil + clientData, err = sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) + } + update := sstore.InfoMsgUpdate("telemetry is now off") + update.ClientData = clientData + return update, nil } func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { From bfe8ee1c02ee1e809ac45c6178a2e81b383a772c Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 24 Mar 2023 10:34:07 -0700 Subject: [PATCH 312/397] starting on screen:webshare feature. simplify bookmarks (removed bookmarked from line) --- .../000014_simplifybookmarks.down.sql | 9 ++ db/migrations/000014_simplifybookmarks.up.sql | 3 + db/migrations/000015_lineupdates.down.sql | 4 + db/migrations/000015_lineupdates.up.sql | 10 ++ db/schema.sql | 16 +-- pkg/cmdrunner/cmdrunner.go | 127 +++++++++++++----- pkg/pcloud/pcloud.go | 3 + pkg/sstore/dbops.go | 94 +++++++------ pkg/sstore/migrate.go | 2 +- pkg/sstore/quick.go | 23 ++++ pkg/sstore/sstore.go | 86 ++++++++---- pkg/sstore/updatebus.go | 37 ++--- 12 files changed, 284 insertions(+), 130 deletions(-) create mode 100644 db/migrations/000014_simplifybookmarks.down.sql create mode 100644 db/migrations/000014_simplifybookmarks.up.sql create mode 100644 db/migrations/000015_lineupdates.down.sql create mode 100644 db/migrations/000015_lineupdates.up.sql diff --git a/db/migrations/000014_simplifybookmarks.down.sql b/db/migrations/000014_simplifybookmarks.down.sql new file mode 100644 index 000000000..131195767 --- /dev/null +++ b/db/migrations/000014_simplifybookmarks.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS "bookmark_cmd" ( + bookmarkid varchar(36) NOT NULL, + screenid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (bookmarkid, screenid, cmdid) +); + +ALTER TABLE line ADD COLUMN bookmarked boolean NOT NULL DEFAULT 0; + diff --git a/db/migrations/000014_simplifybookmarks.up.sql b/db/migrations/000014_simplifybookmarks.up.sql new file mode 100644 index 000000000..fa2e43ebb --- /dev/null +++ b/db/migrations/000014_simplifybookmarks.up.sql @@ -0,0 +1,3 @@ +DROP TABLE bookmark_cmd; +ALTER TABLE line DROP COLUMN bookmarked; + diff --git a/db/migrations/000015_lineupdates.down.sql b/db/migrations/000015_lineupdates.down.sql new file mode 100644 index 000000000..2a4c19c8a --- /dev/null +++ b/db/migrations/000015_lineupdates.down.sql @@ -0,0 +1,4 @@ +DROP TABLE screenupdates; + +ALTER TABLE screen DROP COLUMN webshareopts; + diff --git a/db/migrations/000015_lineupdates.up.sql b/db/migrations/000015_lineupdates.up.sql new file mode 100644 index 000000000..619d55db8 --- /dev/null +++ b/db/migrations/000015_lineupdates.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE screenupdates ( + updateid integer PRIMARY KEY, + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + updatetype varchar(50) NOT NULL, + updatets bigint NOT NULL +); + +ALTER TABLE screen ADD COLUMN webshareopts json NOT NULL DEFAULT 'null'; + diff --git a/db/schema.sql b/db/schema.sql index 9ebfb32d5..0b9ba237a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -128,12 +128,6 @@ CREATE TABLE cloud_update ( updatetype varchar(50) NOT NULL, updatekeys json NOT NULL ); -CREATE TABLE IF NOT EXISTS "bookmark_cmd" ( - bookmarkid varchar(36) NOT NULL, - screenid varchar(36) NOT NULL, - cmdid varchar(36) NOT NULL, - PRIMARY KEY (bookmarkid, screenid, cmdid) -); CREATE TABLE cmd_migrate ( sessionid varchar(36) NOT NULL, screenid varchar(36) NOT NULL, @@ -155,7 +149,7 @@ CREATE TABLE IF NOT EXISTS "screen" ( anchor json NOT NULL, focustype varchar(12) NOT NULL, archived boolean NOT NULL, - archivedts bigint NOT NULL, + archivedts bigint NOT NULL, webshareopts json NOT NULL DEFAULT 'null', PRIMARY KEY (screenid) ); CREATE TABLE IF NOT EXISTS "line" ( @@ -174,7 +168,6 @@ CREATE TABLE IF NOT EXISTS "line" ( star int NOT NULL, archived boolean NOT NULL, renderer varchar(50) NOT NULL, - bookmarked boolean NOT NULL, PRIMARY KEY (screenid, lineid) ); CREATE TABLE IF NOT EXISTS "cmd" ( @@ -199,3 +192,10 @@ CREATE TABLE IF NOT EXISTS "cmd" ( rtndiffhasharr json NOT NULL, PRIMARY KEY (screenid, cmdid) ); +CREATE TABLE screenupdates ( + updateid integer PRIMARY KEY, + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + updatetype varchar(50) NOT NULL, + updatets bigint NOT NULL +); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 16eaeb568..7bbbe3ffc 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3,6 +3,8 @@ package cmdrunner import ( "bytes" "context" + "crypto/rand" + "encoding/base64" "fmt" "log" "os" @@ -12,6 +14,7 @@ import ( "strings" "syscall" "time" + "unicode" "github.com/alessio/shellescape" "github.com/google/uuid" @@ -40,6 +43,7 @@ func init() { const DefaultUserId = "sawka" const MaxNameLen = 50 +const MaxShareNameLen = 150 const MaxRendererLen = 50 const MaxRemoteAliasLen = 50 const PasswordUnchangedSentinel = "--unchanged--" @@ -131,7 +135,6 @@ func init() { registerCmdFn("session:showall", SessionShowAllCommand) registerCmdFn("session:show", SessionShowCommand) registerCmdFn("session:openshared", SessionOpenSharedCommand) - registerCmdFn("session:opencloud", SessionOpenCloudCommand) registerCmdFn("screen", ScreenCommand) registerCmdFn("screen:archive", ScreenArchiveCommand) @@ -141,6 +144,7 @@ func init() { registerCmdFn("screen:set", ScreenSetCommand) registerCmdFn("screen:showall", ScreenShowAllCommand) registerCmdFn("screen:reset", ScreenResetCommand) + registerCmdFn("screen:webshare", ScreenWebShareCommand) registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) @@ -1561,6 +1565,18 @@ func validateName(name string, typeStr string) error { return nil } +func validateShareName(name string) error { + if len(name) > MaxShareNameLen { + return fmt.Errorf("share name too long, max length is %d", MaxShareNameLen) + } + for _, ch := range name { + if !unicode.IsPrint(ch) { + return fmt.Errorf("invalid character %q in share name", string(ch)) + } + } + return nil +} + func validateRenderer(renderer string) error { if renderer == "" { return nil @@ -1601,22 +1617,6 @@ func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketT return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)") } -func SessionOpenCloudCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - activate := resolveBool(pk.Kwargs["activate"], true) - newName := pk.Kwargs["name"] - if newName != "" { - err := validateName(newName, "session") - if err != nil { - return nil, err - } - } - update, err := sstore.InsertSessionWithName(ctx, newName, sstore.ShareModeShared, activate) - if err != nil { - return nil, err - } - return update, nil -} - func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { activate := resolveBool(pk.Kwargs["activate"], true) newName := pk.Kwargs["name"] @@ -1633,6 +1633,53 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return update, nil } +func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Screen) + if err != nil { + return nil, err + } + shouldShare := true + if len(pk.Args) > 0 { + shouldShare = resolveBool(pk.Args[0], true) + } + shareName := pk.Kwargs["sharename"] + if err := validateShareName(shareName); err != nil { + return nil, err + } + var infoMsg string + if shouldShare { + viewKeyBytes := make([]byte, 9) + _, err = rand.Read(viewKeyBytes) + if err != nil { + return nil, fmt.Errorf("cannot create viewkey: %v", err) + } + webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: base64.RawURLEncoding.EncodeToString(viewKeyBytes)} + err = sstore.ScreenWebShareStart(ctx, ids.ScreenId, webShareOpts) + if err != nil { + return nil, fmt.Errorf("cannot web-share screen: %v", err) + } + pcloud.NotifyUpdateWriter() + infoMsg = fmt.Sprintf("screen is now shared to the web at %s", "[screen-share-url]") + } else { + err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("cannot stop web-sharing screen: %v", err) + } + infoMsg = fmt.Sprintf("screen is no longer web shared") + } + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("cannot get updated screen: %v", err) + } + update := sstore.ModelUpdate{ + Screens: []*sstore.ScreenType{screen}, + Info: &sstore.InfoMsgType{ + InfoMsg: infoMsg, + }, + } + return update, nil +} + func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session) if err != nil { @@ -2339,36 +2386,42 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if lineId == "" { return nil, fmt.Errorf("line %q not found", lineArg) } - lineObj, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) + _, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId) if err != nil { return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) } if cmdObj == nil { return nil, fmt.Errorf("cannot bookmark non-cmd line") } - ck := base.MakeCommandKey(lineObj.ScreenId, cmdObj.CmdId) - bm := &sstore.BookmarkType{ - BookmarkId: uuid.New().String(), - CreatedTs: time.Now().UnixMilli(), - CmdStr: cmdObj.CmdStr, - Alias: "", - Tags: nil, - Description: "", - Cmds: []base.CommandKey{ck}, - } - err = sstore.InsertBookmark(ctx, bm) + existingBmIds, err := sstore.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr) if err != nil { - return nil, fmt.Errorf("cannot insert bookmark: %v", err) + return nil, fmt.Errorf("error trying to retrieve current boookmarks: %v", err) } - newLineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) - if err != nil { - return nil, fmt.Errorf("/line:bookmark error getting line: %v", err) + var newBmId string + if len(existingBmIds) > 0 { + newBmId = existingBmIds[0] + } else { + newBm := &sstore.BookmarkType{ + BookmarkId: uuid.New().String(), + CreatedTs: time.Now().UnixMilli(), + CmdStr: cmdObj.CmdStr, + Alias: "", + Tags: nil, + Description: "", + } + err = sstore.InsertBookmark(ctx, newBm) + if err != nil { + return nil, fmt.Errorf("cannot insert bookmark: %v", err) + } + newBmId = newBm.BookmarkId } - if newLineObj == nil { - // no line (which is strange given we checked for it above). just return a nop. - return nil, nil + bms, err := sstore.GetBookmarks(ctx, "") + update := sstore.ModelUpdate{ + MainView: sstore.MainViewBookmarks, + Bookmarks: bms, + SelectedBookmark: newBmId, } - return sstore.ModelUpdate{Line: newLineObj}, nil + return update, nil } func LinePinCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 388433bd2..915892c94 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -209,3 +209,6 @@ func CreateCloudSession(ctx context.Context) error { } return nil } + +func NotifyUpdateWriter() { +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 64f300686..fcc509ed1 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -663,7 +663,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, Archived: false, ArchivedTs: 0, } - query = `INSERT INTO screen (sessionid, screenid, name, screenidx, screenopts, ownerid, sharemode, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,:ownerid,:sharemode,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)` + query = `INSERT INTO screen ( sessionid, screenid, name, screenidx, screenopts, ownerid, sharemode, webshareopts, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) + VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,: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 = ?` @@ -775,8 +776,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM screen WHERE screenid = ?` nextLineNum := tx.GetInt(query, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived, bookmarked) - VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived,:bookmarked)` + query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived) + VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived)` tx.NamedExec(query, line) query = `UPDATE screen SET nextlinenum = ? WHERE screenid = ?` tx.Exec(query, nextLineNum+1, line.ScreenId) @@ -943,8 +944,6 @@ func cleanScreenCmds(ctx context.Context, screenId string) error { removedCmds = tx.SelectStrings(query, screenId, screenId) query = `DELETE FROM cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE screenid = ?)` tx.Exec(query, screenId, screenId) - query = `DELETE FROM bookmark_cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM cmd WHERE screenid = ?)` - tx.Exec(query, screenId, screenId) return nil }) if txErr != nil { @@ -2039,12 +2038,6 @@ type bookmarkOrderType struct { OrderIdx int64 } -type bookmarkCmdType struct { - BookmarkId string - ScreenId string - CmdId string -} - func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { var bms []*BookmarkType txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -2066,15 +2059,6 @@ func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { bm.OrderIdx = bmOrder.OrderIdx } } - var cmds []bookmarkCmdType - query = `SELECT bookmarkid, screenid, cmdid FROM bookmark_cmd` - tx.Select(&cmds, query) - for _, cmd := range cmds { - bm := bmMap[cmd.BookmarkId] - if bm != nil { - bm.Cmds = append(bm.Cmds, base.MakeCommandKey(cmd.ScreenId, cmd.CmdId)) - } - } return nil }) if txErr != nil { @@ -2094,12 +2078,6 @@ func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*Bookm query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?` orderIdx := tx.GetInt(query, bookmarkId, tag) rtn.OrderIdx = int64(orderIdx) - query = `SELECT bookmarkid, screenid, cmdid FROM bookmark_cmd WHERE bookmarkid = ?` - var cmds []bookmarkCmdType - tx.Select(&cmds, query, bookmarkId) - for _, cmd := range cmds { - rtn.Cmds = append(rtn.Cmds, base.MakeCommandKey(cmd.ScreenId, cmd.CmdId)) - } return nil }) if txErr != nil { @@ -2126,6 +2104,14 @@ func GetBookmarkIdByArg(ctx context.Context, bookmarkArg string) (string, error) return rtnId, nil } +func GetBookmarkIdsByCmdStr(ctx context.Context, cmdStr string) ([]string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]string, error) { + query := `SELECT bookmarkid FROM bookmark WHERE cmdstr = ?` + bmIds := tx.SelectStrings(query, cmdStr) + return bmIds, nil + }) +} + // ignores OrderIdx field func InsertBookmark(ctx context.Context, bm *BookmarkType) error { if bm == nil || bm.BookmarkId == "" { @@ -2145,14 +2131,6 @@ func InsertBookmark(ctx context.Context, bm *BookmarkType) error { query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)` tx.Exec(query, tag, bm.BookmarkId, maxOrder+1) } - query = `INSERT INTO bookmark_cmd (bookmarkid, screenid, cmdid) VALUES (?, ?, ?)` - for _, ck := range bm.Cmds { - tx.Exec(query, bm.BookmarkId, ck.GetGroupId(), ck.GetCmdId()) - } - query = `UPDATE line SET bookmarked = 1 WHERE screenid = ? AND cmdid = ?` - for _, ck := range bm.Cmds { - tx.Exec(query, ck.GetGroupId(), ck.GetCmdId()) - } return nil }) return txErr @@ -2205,10 +2183,6 @@ func DeleteBookmark(ctx context.Context, bookmarkId string) error { tx.Exec(query, bookmarkId) query = `DELETE FROM bookmark_order WHERE bookmarkid = ?` tx.Exec(query, bookmarkId) - query = `UPDATE line SET bookmarked = 0 WHERE bookmarked AND cmdid <> '' AND (screenid||cmdid) IN (SELECT screenid||cmdid FROM bookmark_cmd WHERE bookmarkid = ?) ` - tx.Exec(query, bookmarkId) - query = `DELETE FROM bookmark_cmd WHERE bookmarkid = ?` - tx.Exec(query, bookmarkId) fixupBookmarkOrder(tx) return nil }) @@ -2343,3 +2317,47 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem return rtn, nil }) } + +func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenWebShareOpts) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { + return fmt.Errorf("screen does not exist") + } + shareMode := tx.GetString(`SELECT sharemode FROM screen WHERE screenid = ?`, screenId) + if shareMode == ShareModeWeb { + return fmt.Errorf("screen is already shared to web") + } + if shareMode != ShareModeLocal { + return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", shareMode) + } + query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` + tx.Exec(query, ShareModeWeb, quickJson(shareOpts), screenId) + query = `INSERT INTO screenupdates (screenid, lineid, updatetype, updatets) + SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? ORDER BY linenum` + tx.Exec(query, UpdateType_LineNew, time.Now().UnixMilli(), screenId) + return nil + }) +} + +func ScreenWebShareStop(ctx context.Context, screenId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `SELECT screenid FROM screen WHERE screenid = ?` + if !tx.Exists(query, screenId) { + return fmt.Errorf("screen does not exist") + } + shareMode := tx.GetString(`SELECT sharemode FROM screen WHERE screenid = ?`, screenId) + if shareMode != ShareModeWeb { + return fmt.Errorf("screen is not currently shared to the web") + } + query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` + tx.Exec(query, ShareModeLocal, "null", screenId) + query = `DELETE FROM screenupdates WHERE screenid = ?` + tx.Exec(query, screenId) + return nil + }) +} + +func isWebShare(tx *TxWrap, screenId string) bool { + return tx.Exists(`SELECT screenid FROM screen WHERE screenid = ? AND sharemode = ?`, screenId, ShareModeWeb) +} diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 613006922..2eaf8ace6 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 13 +const MaxMigration = 15 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index e92c81f53..116b24991 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -80,6 +80,21 @@ func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { json.Unmarshal([]byte(str), ptr) } +func quickSetNullableJson(ptr interface{}, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + str, ok := v.(string) + if !ok { + return + } + if str == "" { + str = "null" + } + json.Unmarshal([]byte(str), ptr) +} + func quickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { v, ok := m[name] if !ok { @@ -95,6 +110,14 @@ func quickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { json.Unmarshal([]byte(str), ptr) } +func quickNullableJson(v interface{}) string { + if v == nil { + return "null" + } + barr, _ := json.Marshal(v) + return string(barr) +} + func quickJson(v interface{}) string { if v == nil { return "{}" diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 6d52f8150..28fb3b84e 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -54,10 +54,8 @@ const ( ) const ( - ShareModeLocal = "local" - ShareModePrivate = "private" - ShareModeView = "view" - ShareModeShared = "shared" + ShareModeLocal = "local" + ShareModeWeb = "web" ) const ( @@ -81,6 +79,19 @@ const ( CmdStoreTypeScreen = "screen" ) +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" +) + const MaxTzNameLen = 50 var globalDBLock = &sync.Mutex{} @@ -212,6 +223,7 @@ type ClientData struct { FeOpts FeOptsType `json:"feopts"` CmdStoreType string `json:"cmdstoretype"` Migration *ClientMigrationData `json:"migration,omitempty" dbmap:"-"` + DBVersion int `json:"dbversion" dbmap:"-"` } func (ClientData) UseDBMap() {} @@ -401,21 +413,27 @@ type ScreenLinesType struct { func (ScreenLinesType) UseDBMap() {} +type ScreenWebShareOpts struct { + ShareName string `json:"sharename"` + ViewKey string `json:"viewkey"` +} + type ScreenType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - Name string `json:"name"` - ScreenIdx int64 `json:"screenidx"` - ScreenOpts ScreenOptsType `json:"screenopts"` - OwnerId string `json:"ownerid"` - ShareMode string `json:"sharemode"` - CurRemote RemotePtrType `json:"curremote"` - NextLineNum int64 `json:"nextlinenum"` - SelectedLine int64 `json:"selectedline"` - Anchor ScreenAnchorType `json:"anchor"` - FocusType string `json:"focustype"` - Archived bool `json:"archived,omitempty"` - ArchivedTs int64 `json:"archivedts,omitempty"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + Name string `json:"name"` + ScreenIdx int64 `json:"screenidx"` + ScreenOpts ScreenOptsType `json:"screenopts"` + OwnerId string `json:"ownerid"` + ShareMode string `json:"sharemode"` + WebShareOpts *ScreenWebShareOpts `json:"webshareopts,omitempty"` + CurRemote RemotePtrType `json:"curremote"` + NextLineNum int64 `json:"nextlinenum"` + SelectedLine int64 `json:"selectedline"` + Anchor ScreenAnchorType `json:"anchor"` + FocusType string `json:"focustype"` + Archived bool `json:"archived,omitempty"` + ArchivedTs int64 `json:"archivedts,omitempty"` // only for updates Full bool `json:"full,omitempty"` @@ -431,6 +449,7 @@ func (s *ScreenType) ToMap() map[string]interface{} { rtn["screenopts"] = quickJson(s.ScreenOpts) rtn["ownerid"] = s.OwnerId rtn["sharemode"] = s.ShareMode + rtn["webshareopts"] = quickNullableJson(s.WebShareOpts) rtn["curremoteownerid"] = s.CurRemote.OwnerId rtn["curremoteid"] = s.CurRemote.RemoteId rtn["curremotename"] = s.CurRemote.Name @@ -451,6 +470,7 @@ func (s *ScreenType) FromMap(m map[string]interface{}) bool { quickSetJson(&s.ScreenOpts, m, "screenopts") quickSetStr(&s.OwnerId, m, "ownerid") quickSetStr(&s.ShareMode, m, "sharemode") + quickSetNullableJson(&s.WebShareOpts, m, "webshareopts") quickSetStr(&s.CurRemote.OwnerId, m, "curremoteownerid") quickSetStr(&s.CurRemote.RemoteId, m, "curremoteid") quickSetStr(&s.CurRemote.Name, m, "curremotename") @@ -654,6 +674,16 @@ func (ri *RemoteInstance) ToMap() map[string]interface{} { return rtn } +type ScreenUpdateType struct { + UpdateId int64 `json:"updateid"` + ScreenId string `json:"screenid"` + LineId string `json:"lineid"` + UpdateType string `json:"updatetype"` + UpdateTs int64 `json:"updatets"` +} + +func (ScreenUpdateType) UseDBMap() {} + type LineType struct { ScreenId string `json:"screenid"` UserId string `json:"userid"` @@ -669,7 +699,6 @@ type LineType struct { Ephemeral bool `json:"ephemeral,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` - Bookmarked bool `json:"bookmarked,omitempty"` Archived bool `json:"archived,omitempty"` Remove bool `json:"remove,omitempty"` } @@ -747,15 +776,14 @@ type PlaybookEntry struct { } type BookmarkType struct { - BookmarkId string `json:"bookmarkid"` - CreatedTs int64 `json:"createdts"` - CmdStr string `json:"cmdstr"` - Alias string `json:"alias,omitempty"` - Tags []string `json:"tags"` - Description string `json:"description"` - Cmds []base.CommandKey `json:"cmds"` - OrderIdx int64 `json:"orderidx"` - Remove bool `json:"remove,omitempty"` + BookmarkId string `json:"bookmarkid"` + CreatedTs int64 `json:"createdts"` + CmdStr string `json:"cmdstr"` + Alias string `json:"alias,omitempty"` + Tags []string `json:"tags"` + Description string `json:"description"` + OrderIdx int64 `json:"orderidx"` + Remove bool `json:"remove,omitempty"` } func (bm *BookmarkType) GetSimpleKey() string { @@ -1127,6 +1155,8 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { if cdata == nil { return nil, fmt.Errorf("no client data found") } + dbVersion := tx.GetInt(`SELECT version FROM schema_migrations`) + cdata.DBVersion = dbVersion return cdata, nil }) if err != nil { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 657c87509..add29e740 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -30,24 +30,25 @@ func (PtyDataUpdate) UpdateType() string { } 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 *CmdLineType `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"` - HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` - ClientData *ClientData `json:"clientdata,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 *CmdLineType `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"` } func (ModelUpdate) UpdateType() string { From 4aa2bab06ecbce7afa3bf151289fa9ee9339ea9a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 24 Mar 2023 18:35:29 -0700 Subject: [PATCH 313/397] fix null screen bug --- pkg/remote/remote.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a2352497c..d95ad095f 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1510,7 +1510,9 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error trying to update cmd-fg screens: %v\n", err) // fall-through (nothing to do) } - update.Screens = []*sstore.ScreenType{screen} + if screen != nil { + update.Screens = []*sstore.ScreenType{screen} + } rct := msh.GetRunningCmd(donePk.CK) var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { From 9ffb6720626376b9c4d7faedc928ec26da079788 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 25 Mar 2023 12:54:56 -0700 Subject: [PATCH 314/397] add screenupdates --- db/migrations/000015_lineupdates.down.sql | 2 +- db/migrations/000015_lineupdates.up.sql | 2 +- pkg/cmdrunner/cmdrunner.go | 4 +- pkg/sstore/dbops.go | 72 ++++++++++++++++++++--- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/db/migrations/000015_lineupdates.down.sql b/db/migrations/000015_lineupdates.down.sql index 2a4c19c8a..302819b23 100644 --- a/db/migrations/000015_lineupdates.down.sql +++ b/db/migrations/000015_lineupdates.down.sql @@ -1,4 +1,4 @@ -DROP TABLE screenupdates; +DROP TABLE screenupdate; ALTER TABLE screen DROP COLUMN webshareopts; diff --git a/db/migrations/000015_lineupdates.up.sql b/db/migrations/000015_lineupdates.up.sql index 619d55db8..93aad43f9 100644 --- a/db/migrations/000015_lineupdates.up.sql +++ b/db/migrations/000015_lineupdates.up.sql @@ -1,4 +1,4 @@ -CREATE TABLE screenupdates ( +CREATE TABLE screenupdate ( updateid integer PRIMARY KEY, screenid varchar(36) NOT NULL, lineid varchar(36) NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 7bbbe3ffc..a9e93bdae 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2224,7 +2224,7 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err = validateRenderer(renderer); err != nil { return nil, fmt.Errorf("invalid renderer value: %w", err) } - err = sstore.UpdateLineRenderer(ctx, lineId, renderer) + err = sstore.UpdateLineRenderer(ctx, ids.ScreenId, lineId, renderer) if err != nil { return nil, fmt.Errorf("error changing line renderer: %v", err) } @@ -2489,7 +2489,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if len(pk.Args) >= 2 { shouldArchive = resolveBool(pk.Args[1], true) } - err = sstore.SetLineArchivedById(ctx, lineId, shouldArchive) + err = sstore.SetLineArchivedById(ctx, ids.ScreenId, lineId, shouldArchive) if err != nil { return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index fcc509ed1..ffed93c93 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2,6 +2,7 @@ package sstore import ( "context" + "errors" "fmt" "strconv" "strings" @@ -790,6 +791,9 @@ 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) + } return nil }) } @@ -814,15 +818,20 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon if ck.IsEmpty() { return nil, fmt.Errorf("cannot update cmddoneinfo, empty ck") } + screenId := ck.GetGroupId() var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, CmdStatusDone, quickJson(doneInfo), ck.GetGroupId(), ck.GetCmdId()) + tx.Exec(query, CmdStatusDone, quickJson(doneInfo), screenId, ck.GetCmdId()) var err error - rtnCmd, err = GetCmdByScreenId(tx.Context(), ck.GetGroupId(), ck.GetCmdId()) + rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, ck.GetCmdId()) if err != nil { return err } + if isWebShare(tx, screenId) { + insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdDoneInfo) + insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdStatus) + } return nil }) if txErr != nil { @@ -838,9 +847,13 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt if ck.IsEmpty() { return fmt.Errorf("cannot update cmdrtnstate, empty ck") } + screenId := ck.GetGroupId() txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), ck.GetGroupId(), ck.GetCmdId()) + tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), screenId, ck.GetCmdId()) + if isWebShare(tx, screenId) { + insertScreenUpdateByCmdId(tx, screenId, ck.GetCmdId(), UpdateType_CmdRtnState) + } return nil }) if txErr != nil { @@ -853,9 +866,13 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err if errPk == nil || errPk.CK.IsEmpty() { return fmt.Errorf("invalid cmderror packet (no ck)") } + screenId := errPk.CK.GetGroupId() return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, quickJson(errPk), errPk.CK.GetGroupId(), errPk.CK.GetCmdId()) + tx.Exec(query, quickJson(errPk), screenId, errPk.CK.GetCmdId()) + if isWebShare(tx, screenId) { + insertScreenUpdateByCmdId(tx, screenId, errPk.CK.GetCmdId(), UpdateType_CmdRunOut) + } return nil }) } @@ -1816,10 +1833,13 @@ func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { return nil } -func UpdateLineRenderer(ctx context.Context, lineId string, renderer string) error { +func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, renderer string) error { return WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET renderer = ? WHERE lineid = ?` tx.Exec(query, renderer, lineId) + if isWebShare(tx, screenId) { + insertScreenUpdate(tx, screenId, lineId, UpdateType_LineRenderer) + } return nil }) } @@ -1842,10 +1862,13 @@ func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType return rtn, nil } -func SetLineArchivedById(ctx context.Context, lineId string, archived bool) error { +func SetLineArchivedById(ctx context.Context, screenId string, lineId string, archived bool) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET archived = ? WHERE lineid = ?` tx.Exec(query, archived, lineId) + if isWebShare(tx, screenId) { + insertScreenUpdate(tx, screenId, lineId, UpdateType_LineArchived) + } return nil }) return txErr @@ -1862,6 +1885,7 @@ func purgeCmdByScreenId(ctx context.Context, screenId string, cmdId string) erro func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { + isWS := isWebShare(tx, screenId) for _, lineId := range lineIds { query := `SELECT cmdid FROM line WHERE screenid = ? AND lineid = ?` cmdId := tx.GetString(query, screenId, lineId) @@ -1875,6 +1899,9 @@ func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) err return err } } + if isWS { + insertScreenUpdate(tx, screenId, lineId, UpdateType_LineDel) + } } return nil }) @@ -2333,7 +2360,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) - query = `INSERT INTO screenupdates (screenid, lineid, updatetype, updatets) + 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) return nil @@ -2352,7 +2379,7 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeLocal, "null", screenId) - query = `DELETE FROM screenupdates WHERE screenid = ?` + query = `DELETE FROM screenupdate WHERE screenid = ?` tx.Exec(query, screenId) return nil }) @@ -2361,3 +2388,32 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { 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) { + if screenId == "" { + tx.SetErr(errors.New("invalid screen-update, screenid is empty")) + return + } + if lineId == "" { + tx.SetErr(errors.New("invalid screen-update, lineid is empty")) + return + } + query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` + tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) +} + +func insertScreenUpdateByCmdId(tx *TxWrap, screenId string, cmdId string, updateType string) { + if screenId == "" { + tx.SetErr(errors.New("invalid screen-update, screenid is empty")) + return + } + if cmdId == "" { + tx.SetErr(errors.New("invalid screen-update, cmdid is empty")) + return + } + query := `SELECT lineid FROM line WHERE screenid = ? AND cmdid = ?` + lineId := tx.GetString(query, screenId, cmdId) + if lineId != "" { + insertScreenUpdate(tx, screenId, lineId, updateType) + } +} From cf3644f0fdb3fecfbb055617acdef2169cc108b6 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Mar 2023 13:21:58 -0700 Subject: [PATCH 315/397] working on webshareupdates --- cmd/main-server.go | 3 +- db/migrations/000016_webptypos.down.sql | 3 + db/migrations/000016_webptypos.up.sql | 8 + pkg/cmdrunner/cmdrunner.go | 89 ----------- pkg/cmdrunner/shparse.go | 84 ----------- pkg/pcloud/pcloud.go | 182 +++++++++++++++++++---- pkg/pcloud/pclouddata.go | 100 +++++++++++++ pkg/rtnstate/rtnstate.go | 188 ++++++++++++++++++++++++ pkg/sstore/dbops.go | 104 +++++++++++-- pkg/sstore/fileops.go | 22 ++- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 71 ++------- 12 files changed, 580 insertions(+), 276 deletions(-) create mode 100644 db/migrations/000016_webptypos.down.sql create mode 100644 db/migrations/000016_webptypos.up.sql create mode 100644 pkg/pcloud/pclouddata.go create mode 100644 pkg/rtnstate/rtnstate.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 5b782edf1..378a34e7e 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 000000000..1d5c5243a --- /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 000000000..69ee74865 --- /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 a9e93bdae..9ee78210b 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 8d5aea0ee..70b62d71e 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 915892c94..a5a83f756 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 000000000..694cd527c --- /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 000000000..42e2314c8 --- /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 ffed93c93..bc7b0daab 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 2585c50df..9361f4459 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 2eaf8ace6..b6f461567 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 28fb3b84e..385d05815 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"` From aed5a4db1dfab85aff0c118a4a55008be7042854 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Mar 2023 13:36:33 -0700 Subject: [PATCH 316/397] multiple updates in request --- db/migrations/000016_webptypos.up.sql | 2 +- pkg/pcloud/pcloud.go | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/db/migrations/000016_webptypos.up.sql b/db/migrations/000016_webptypos.up.sql index 69ee74865..e21618b0e 100644 --- a/db/migrations/000016_webptypos.up.sql +++ b/db/migrations/000016_webptypos.up.sql @@ -2,7 +2,7 @@ CREATE TABLE webptypos ( screenid varchar(36) NOT NULL, lineid varchar(36) NOT NULL, ptypos bigint NOT NULL, - PRIMARY KEY (screenid, cmdid) + PRIMARY KEY (screenid, lineid) ); CREATE INDEX idx_screenupdate_ids ON screenupdate (screenid, lineid); diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index a5a83f756..d457fd51b 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -22,10 +22,12 @@ const PCloudEndpoint = "https://api.getprompt.dev/central" const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 1 const MaxPtyUpdateSize = (128 * 1024) + 1 +const MaxUpdatesPerReq = 10 const TelemetryUrl = "/telemetry" const NoTelemetryUrl = "/no-telemetry" const CreateWebScreenUrl = "/auth/create-web-screen" +const WebShareUpdateUrl = "/auth/web-share-update" type AuthInfo struct { UserId string `json:"userid"` @@ -177,7 +179,7 @@ func defaultError(err error, estr string) error { return errors.New(estr) } -func makeWebScreenUpdate(ctx context.Context, update sstore.ScreenUpdateType) (*WebShareUpdateType, error) { +func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (*WebShareUpdateType, error) { rtn := &WebShareUpdateType{ ScreenId: update.ScreenId, LineId: update.LineId, @@ -286,7 +288,7 @@ func makeWebScreenUpdate(ctx context.Context, update sstore.ScreenUpdateType) (* return rtn, nil } -func finalizeWebScreenUpdate(ctx context.Context, screenUpdate sstore.ScreenUpdateType, webUpdate *WebShareUpdateType) error { +func finalizeWebScreenUpdate(ctx context.Context, screenUpdate *sstore.ScreenUpdateType, webUpdate *WebShareUpdateType) error { switch screenUpdate.UpdateType { case sstore.UpdateType_PtyPos: dataEof := len(webUpdate.PtyData.Data) < MaxPtyUpdateSize @@ -312,7 +314,26 @@ func finalizeWebScreenUpdate(ctx context.Context, screenUpdate sstore.ScreenUpda return nil } -func DoWebScreenUpdate(ctx context.Context, update sstore.ScreenUpdateType) error { +func DoWebScreenUpdates(ctx context.Context, authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) error { + var webUpdates []*WebShareUpdateType + for _, update := range updateArr { + webUpdate, err := makeWebShareUpdate(ctx, update) + if err != nil { + return fmt.Errorf("error create web-share update updateid:%d: %v", update.UpdateId, err) + } + if webUpdate == nil { + continue + } + webUpdates = append(webUpdates, webUpdate) + } + req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, webUpdates) + if err != nil { + return fmt.Errorf("cannot create auth-post-req for %s: %v", WebShareUpdateUrl, err) + } + _, err = doRequest(req, nil) + if err != nil { + return err + } return nil } From 150c8cfaeeb57508721abc6028f2cc96d6e228c0 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Mar 2023 18:48:43 -0700 Subject: [PATCH 317/397] now calling promptcentral web updates --- cmd/main-server.go | 4 ++ pkg/pcloud/pcloud.go | 147 ++++++++++++++++++++++++++++++++++++--- pkg/pcloud/pclouddata.go | 54 ++++++++++++-- pkg/sstore/dbops.go | 12 +++- pkg/sstore/sstore.go | 1 + 5 files changed, 203 insertions(+), 15 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 378a34e7e..0aeeb2e46 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 d457fd51b..2830feba0 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 NotifyUpdateWriter() { +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 694cd527c..8cfc34115 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 bc7b0daab..bb4b43449 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 385d05815..46c90ed13 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" From 582cd807a8d415d18c6855685e461f0baa89819c Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Mar 2023 23:07:30 -0700 Subject: [PATCH 318/397] pcloud web-share working with stubbed out promptcentral --- pkg/cmdrunner/cmdrunner.go | 1 - pkg/pcloud/pcloud.go | 60 +++++++++----------------------------- pkg/sstore/dbops.go | 36 +++++++++++++++++++++-- 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 9ee78210b..af5029f4f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1657,7 +1657,6 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot web-share screen: %v", err) } - pcloud.NotifyUpdateWriter() infoMsg = fmt.Sprintf("screen is now shared to the web at %s", "[screen-share-url]") } else { err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 2830feba0..dca98837f 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -30,11 +30,9 @@ 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 updateWriterLock = &sync.Mutex{} var updateWriterRunning = false type AuthInfo struct { @@ -101,7 +99,7 @@ func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*htt func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) { apiUrl := req.Header.Get("X-PromptAPIUrl") - log.Printf("sending request %v\n", req.URL) + log.Printf("[pcloud] sending request %v\n", req.URL) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("error contacting pcloud %q service: %v", apiUrl, err) @@ -138,7 +136,7 @@ func SendTelemetry(ctx context.Context, force bool) error { if len(activity) == 0 { return nil } - log.Printf("sending telemetry data\n") + log.Printf("[pcloud] sending telemetry data\n") dayStr := sstore.GetCurDayStr() input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, CurDay: dayStr, Activity: activity} req, err := makeAnonPostReq(ctx, TelemetryUrl, input) @@ -369,49 +367,21 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) return nil } -func CreateWebScreen(ctx context.Context, screen *WebShareScreenType) error { - authInfo, err := getAuthInfo(ctx) - if err != nil { - return err - } - req, err := makeAuthPostReq(ctx, CreateWebScreenUrl, authInfo, screen) - if err != nil { - return err - } - _, err = doRequest(req, nil) - if err != nil { - return err - } - 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() + updateWriterLock.Lock() + defer updateWriterLock.Unlock() updateWriterRunning = running } func GetUpdateWriterRunning() bool { - updateWriterCVar.L.Lock() - defer updateWriterCVar.L.Unlock() + updateWriterLock.Lock() + defer updateWriterLock.Unlock() return updateWriterRunning } func StartUpdateWriter() { - updateWriterCVar.L.Lock() - defer updateWriterCVar.L.Unlock() + updateWriterLock.Lock() + defer updateWriterLock.Unlock() if updateWriterRunning { return } @@ -449,6 +419,7 @@ func runWebShareUpdateWriter() { numErrors := 0 numSendErrors := 0 for { + time.Sleep(100 * time.Millisecond) updateArr, err := sstore.GetScreenUpdates(context.Background(), MaxUpdatesPerReq) if err != nil { log.Printf("[pcloud] error retrieving updates: %v", err) @@ -460,7 +431,8 @@ func runWebShareUpdateWriter() { } } if len(updateArr) == 0 { - updateWriterCheckMoreData() + sstore.UpdateWriterCheckMoreData() + continue } numErrors = 0 authInfo, err := getAuthInfo(context.Background()) @@ -472,13 +444,7 @@ func runWebShareUpdateWriter() { time.Sleep(backoffTime) continue } + log.Printf("[pcloud] sent %d web-updates\n", len(updateArr)) numSendErrors = 0 } } - -func NotifyUpdateWriter() { - updateWriterCVar.L.Lock() - defer updateWriterCVar.L.Unlock() - updateWriterMoreData = true - updateWriterCVar.Signal() -} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index bb4b43449..852a6ef0f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -21,6 +21,9 @@ import ( const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito, h.linenum" const DefaultMaxHistoryItems = 1000 +var updateWriterCVar = sync.NewCond(&sync.Mutex{}) +var updateWriterMoreData = false + type SingleConnDBGetter struct { SingleConnLock *sync.Mutex } @@ -50,6 +53,25 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { return txwrap.DBGWithTx(ctx, dbWrap, fn) } +func NotifyUpdateWriter() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + updateWriterMoreData = true + updateWriterCVar.Signal() +} + +func UpdateWriterCheckMoreData() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + for { + if updateWriterMoreData { + updateWriterMoreData = false + break + } + updateWriterCVar.Wait() + } +} + func NumSessions(ctx context.Context) (int, error) { var numSessions int txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -2383,6 +2405,7 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW 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) + NotifyUpdateWriter() return nil }) } @@ -2417,6 +2440,7 @@ func insertScreenUpdate(tx *TxWrap, screenId string, updateType string) { } query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` tx.Exec(query, screenId, "", updateType, time.Now().UnixMilli()) + NotifyUpdateWriter() } func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateType string) { @@ -2428,8 +2452,12 @@ func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateTy tx.SetErr(errors.New("invalid screen-update, lineid is empty")) return } - query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` - tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) + query := `SELECT updateid FROM screenupdate WHERE screenid = ? AND lineid = ? AND (updatetype = ? OR updatetype = ?)` + if !tx.Exists(query, screenId, lineId, updateType, UpdateType_LineNew) { + query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` + tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) + NotifyUpdateWriter() + } } func insertScreenCmdUpdate(tx *TxWrap, screenId string, cmdId string, updateType string) { @@ -2474,7 +2502,9 @@ func InsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) erro } 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) + query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` + tx.Exec(query, screenId, lineId, UpdateType_PtyPos, time.Now().UnixMilli()) + NotifyUpdateWriter() } return nil }) From 29205ba586381dcf7e68f8bca655a54ca8b94ade Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 Mar 2023 23:31:22 -0700 Subject: [PATCH 319/397] for non-relative resolves, return nil if the number is out of range --- pkg/cmdrunner/resolver.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 1b5aaa6cc..cce62160b 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -162,6 +162,8 @@ func resolveByPosition(isNumeric bool, allItems []ResolveItem, curId string, pos } } finalPos = curIdx + posArg.Pos + finalPos = boundInt(finalPos, len(items), posArg.IsWrap) + return &items[finalPos-1] } else if isNumeric { // these resolve items have a "Num" set that should be used to look up non-relative positions // use allItems for numeric resolve @@ -174,9 +176,11 @@ func resolveByPosition(isNumeric bool, allItems []ResolveItem, curId string, pos } else { // non-numeric means position is just the index finalPos = posArg.Pos + if finalPos <= 0 || finalPos > len(items) { + return nil + } + return &items[finalPos-1] } - finalPos = boundInt(finalPos, len(items), posArg.IsWrap) - return &items[finalPos-1] } func resolveRemoteArg(remoteArg string) (*sstore.RemotePtrType, error) { From b0269d5228c97cb6bee6db0bb47ae2f88350cb3a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 Mar 2023 14:11:02 -0700 Subject: [PATCH 320/397] move generic db functions to dbutil (for use with PromptCentral) --- pkg/dbutil/dbutil.go | 175 +++++++++++++++++++++++++++++++++++++++++ pkg/dbutil/map.go | 183 +++++++++++++++++++++++++++++++++++++++++++ pkg/sstore/dbops.go | 59 +++++++------- pkg/sstore/map.go | 169 --------------------------------------- pkg/sstore/quick.go | 169 +++------------------------------------ pkg/sstore/sstore.go | 7 +- 6 files changed, 405 insertions(+), 357 deletions(-) create mode 100644 pkg/dbutil/dbutil.go create mode 100644 pkg/dbutil/map.go diff --git a/pkg/dbutil/dbutil.go b/pkg/dbutil/dbutil.go new file mode 100644 index 000000000..872a09110 --- /dev/null +++ b/pkg/dbutil/dbutil.go @@ -0,0 +1,175 @@ +package dbutil + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "strconv" +) + +func QuickSetStr(strVal *string, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + ival, ok := v.(int64) + if ok { + *strVal = strconv.FormatInt(ival, 10) + return + } + str, ok := v.(string) + if !ok { + return + } + *strVal = str +} + +func QuickSetInt64(ival *int64, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlInt, ok := v.(int64) + if !ok { + return + } + *ival = sqlInt +} + +func QuickSetBool(bval *bool, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlInt, ok := v.(int64) + if ok { + if sqlInt > 0 { + *bval = true + } + return + } + sqlBool, ok := v.(bool) + if ok { + *bval = sqlBool + } +} + +func QuickSetBytes(bval *[]byte, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlBytes, ok := v.([]byte) + if ok { + *bval = sqlBytes + } +} + +func getByteArr(m map[string]any, name string, def string) ([]byte, bool) { + v, ok := m[name] + if !ok { + return nil, false + } + barr, ok := v.([]byte) + if !ok { + str, ok := v.(string) + if !ok { + return nil, false + } + barr = []byte(str) + } + if len(barr) == 0 { + barr = []byte(def) + } + return barr, true +} + +func QuickSetJson(ptr interface{}, m map[string]interface{}, name string) { + barr, ok := getByteArr(m, name, "{}") + if !ok { + return + } + json.Unmarshal(barr, ptr) +} + +func QuickSetNullableJson(ptr interface{}, m map[string]interface{}, name string) { + barr, ok := getByteArr(m, name, "null") + if !ok { + return + } + json.Unmarshal(barr, ptr) +} + +func QuickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { + barr, ok := getByteArr(m, name, "[]") + if !ok { + return + } + json.Unmarshal(barr, ptr) +} + +func QuickNullableJson(v interface{}) string { + if v == nil { + return "null" + } + barr, _ := json.Marshal(v) + return string(barr) +} + +func QuickJson(v interface{}) string { + if v == nil { + return "{}" + } + barr, _ := json.Marshal(v) + return string(barr) +} + +func QuickJsonBytes(v interface{}) []byte { + if v == nil { + return []byte("{}") + } + barr, _ := json.Marshal(v) + return barr +} + +func QuickJsonArr(v interface{}) string { + if v == nil { + return "[]" + } + barr, _ := json.Marshal(v) + return string(barr) +} + +func QuickJsonArrBytes(v interface{}) []byte { + if v == nil { + return []byte("[]") + } + barr, _ := json.Marshal(v) + return barr +} + +func QuickScanJson(ptr interface{}, val interface{}) error { + barrVal, ok := val.([]byte) + if !ok { + strVal, ok := val.(string) + if !ok { + return fmt.Errorf("cannot scan '%T' into '%T'", val, ptr) + } + barrVal = []byte(strVal) + } + if len(barrVal) == 0 { + barrVal = []byte("{}") + } + return json.Unmarshal(barrVal, ptr) +} + +func QuickValueJson(v interface{}) (driver.Value, error) { + if v == nil { + return "{}", nil + } + barr, err := json.Marshal(v) + if err != nil { + return nil, err + } + return string(barr), nil +} diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go new file mode 100644 index 000000000..7e67516bb --- /dev/null +++ b/pkg/dbutil/map.go @@ -0,0 +1,183 @@ +package dbutil + +import ( + "fmt" + "reflect" + "strings" + + "github.com/sawka/txwrap" +) + +type DBMappable interface { + UseDBMap() +} + +type MapConverter interface { + ToMap() map[string]interface{} + FromMap(map[string]interface{}) bool +} + +type HasSimpleKey interface { + GetSimpleKey() string +} + +type MapConverterPtr[T any] interface { + MapConverter + *T +} + +type DBMappablePtr[T any] interface { + DBMappable + *T +} + +func FromMap[PT MapConverterPtr[T], T any](m map[string]any) PT { + if len(m) == 0 { + return nil + } + rtn := PT(new(T)) + ok := rtn.FromMap(m) + if !ok { + return nil + } + return rtn +} + +func GetMapGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) PT { + m := tx.GetMap(query, args...) + return FromMap[PT](m) +} + +func GetMappable[PT DBMappablePtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) PT { + rtn := PT(new(T)) + m := tx.GetMap(query, args...) + if len(m) == 0 { + return nil + } + FromDBMap(rtn, m) + return rtn +} + +func SelectMapsGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) []PT { + var rtn []PT + marr := tx.SelectMaps(query, args...) + for _, m := range marr { + val := FromMap[PT](m) + if val != nil { + rtn = append(rtn, val) + } + } + return rtn +} + +func MakeGenMap[T HasSimpleKey](arr []T) map[string]T { + rtn := make(map[string]T) + for _, val := range arr { + rtn[val.GetSimpleKey()] = val + } + return rtn +} + +func isStructType(rt reflect.Type) bool { + if rt.Kind() == reflect.Struct { + return true + } + if rt.Kind() == reflect.Pointer && rt.Elem().Kind() == reflect.Struct { + return true + } + return false +} + +func isByteArrayType(t reflect.Type) bool { + return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 +} + +func ToDBMap(v DBMappable, useBytes bool) map[string]interface{} { + if v == nil { + return nil + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructToDBMap", v)) + } + rt := rv.Type() + m := make(map[string]interface{}) + numFields := rt.NumField() + for i := 0; i < numFields; i++ { + field := rt.Field(i) + fieldVal := rv.FieldByIndex(field.Index) + dbName := field.Tag.Get("dbmap") + if dbName == "" { + dbName = strings.ToLower(field.Name) + } + if dbName == "-" { + continue + } + if isByteArrayType(field.Type) { + m[dbName] = fieldVal.Interface() + } else if field.Type.Kind() == reflect.Slice { + if useBytes { + m[dbName] = QuickJsonArrBytes(fieldVal.Interface()) + } else { + m[dbName] = QuickJsonArr(fieldVal.Interface()) + } + } else if isStructType(field.Type) { + if useBytes { + m[dbName] = QuickJsonBytes(fieldVal.Interface()) + } else { + m[dbName] = QuickJson(fieldVal.Interface()) + } + } else { + m[dbName] = fieldVal.Interface() + } + } + return m +} + +func FromDBMap(v DBMappable, m map[string]interface{}) { + if v == nil { + panic("StructFromDBMap, v cannot be nil") + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructFromDBMap", v)) + } + rt := rv.Type() + numFields := rt.NumField() + for i := 0; i < numFields; i++ { + field := rt.Field(i) + fieldVal := rv.FieldByIndex(field.Index) + dbName := field.Tag.Get("dbmap") + if dbName == "" { + dbName = strings.ToLower(field.Name) + } + if dbName == "-" { + continue + } + if isByteArrayType(field.Type) { + barrVal := fieldVal.Addr().Interface() + QuickSetBytes(barrVal.(*[]byte), m, dbName) + } else if field.Type.Kind() == reflect.Slice { + QuickSetJsonArr(fieldVal.Addr().Interface(), m, dbName) + } else if isStructType(field.Type) { + QuickSetJson(fieldVal.Addr().Interface(), m, dbName) + } else if field.Type.Kind() == reflect.String { + strVal := fieldVal.Addr().Interface() + QuickSetStr(strVal.(*string), m, dbName) + } else if field.Type.Kind() == reflect.Int64 { + intVal := fieldVal.Addr().Interface() + QuickSetInt64(intVal.(*int64), m, dbName) + } else if field.Type.Kind() == reflect.Bool { + boolVal := fieldVal.Addr().Interface() + QuickSetBool(boolVal.(*bool), m, dbName) + } else { + panic(fmt.Sprintf("StructFromDBMap invalid field type %v in %T", fieldVal.Type(), v)) + } + } +} diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 852a6ef0f..647dc282a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -15,6 +15,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" + "github.com/scripthaus-dev/sh2-server/pkg/dbutil" "github.com/scripthaus-dev/sh2-server/pkg/scbase" ) @@ -88,7 +89,7 @@ func GetAllRemotes(ctx context.Context) ([]*RemoteType, error) { query := `SELECT * FROM remote ORDER BY remoteidx` marr := tx.SelectMaps(query) for _, m := range marr { - rtn = append(rtn, FromMap[*RemoteType](m)) + rtn = append(rtn, dbutil.FromMap[*RemoteType](m)) } return nil }) @@ -103,7 +104,7 @@ func GetRemoteByAlias(ctx context.Context, alias string) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remotealias = ?` m := tx.GetMap(query, alias) - remote = FromMap[*RemoteType](m) + remote = dbutil.FromMap[*RemoteType](m) return nil }) if err != nil { @@ -117,7 +118,7 @@ func GetRemoteById(ctx context.Context, remoteId string) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remoteid = ?` m := tx.GetMap(query, remoteId) - remote = FromMap[*RemoteType](m) + remote = dbutil.FromMap[*RemoteType](m) return nil }) if err != nil { @@ -131,7 +132,7 @@ func GetLocalRemote(ctx context.Context) (*RemoteType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE local` m := tx.GetMap(query) - remote = FromMap[*RemoteType](m) + remote = dbutil.FromMap[*RemoteType](m) return nil }) if err != nil { @@ -144,7 +145,7 @@ func GetRemoteByCanonicalName(ctx context.Context, cname string) (*RemoteType, e var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE remotecanonicalname = ?` - remote = GetMapGen[*RemoteType](tx, query, cname) + remote = dbutil.GetMapGen[*RemoteType](tx, query, cname) return nil }) if err != nil { @@ -157,7 +158,7 @@ func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, var remote *RemoteType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote WHERE physicalid = ?` - remote = GetMapGen[*RemoteType](tx, query, physicalId) + remote = dbutil.GetMapGen[*RemoteType](tx, query, physicalId) return nil }) if err != nil { @@ -342,7 +343,7 @@ func runHistoryQuery(tx *TxWrap, opts HistoryQueryOpts, realOffset int, itemLimi marr := tx.SelectMaps(query, queryArgs...) rtn := make([]*HistoryItemType, len(marr)) for idx, m := range marr { - hitem := FromMap[*HistoryItemType](m) + hitem := dbutil.FromMap[*HistoryItemType](m) rtn[idx] = hitem } return rtn, nil @@ -367,7 +368,7 @@ func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryR func GetHistoryItemByLineNum(ctx context.Context, screenId string, lineNum int) (*HistoryItemType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*HistoryItemType, error) { query := `SELECT * FROM history WHERE screenid = ? AND linenum = ?` - hitem := GetMapGen[*HistoryItemType](tx, query, screenId, lineNum) + hitem := dbutil.GetMapGen[*HistoryItemType](tx, query, screenId, lineNum) return hitem, nil }) } @@ -438,12 +439,12 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { session.Full = true } query = `SELECT * FROM screen ORDER BY archived, screenidx, archivedts` - update.Screens = SelectMapsGen[*ScreenType](tx, query) + update.Screens = dbutil.SelectMapsGen[*ScreenType](tx, query) for _, screen := range update.Screens { screen.Full = true } query = `SELECT * FROM remote_instance` - riArr := SelectMapsGen[*RemoteInstance](tx, query) + riArr := dbutil.SelectMapsGen[*RemoteInstance](tx, query) for _, ri := range riArr { s := sessionMap[ri.SessionId] if s != nil { @@ -459,14 +460,14 @@ func GetAllSessions(ctx context.Context) (*ModelUpdate, error) { func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenLinesType, error) { query := `SELECT screenid FROM screen WHERE screenid = ?` - screen := GetMappable[*ScreenLinesType](tx, query, screenId) + screen := dbutil.GetMappable[*ScreenLinesType](tx, query, screenId) if screen == nil { return nil, nil } query = `SELECT * FROM line WHERE screenid = ? ORDER BY linenum` tx.Select(&screen.Lines, query, screen.ScreenId) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?)` - screen.Cmds = SelectMapsGen[*CmdType](tx, query, screen.ScreenId) + screen.Cmds = dbutil.SelectMapsGen[*CmdType](tx, query, screen.ScreenId) return screen, nil }) } @@ -475,7 +476,7 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, func GetSessionScreens(ctx context.Context, sessionId string) ([]*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenType, error) { query := `SELECT * FROM screen WHERE sessionid = ? ORDER BY archived, screenidx, archivedts` - rtn := SelectMapsGen[*ScreenType](tx, query, sessionId) + rtn := dbutil.SelectMapsGen[*ScreenType](tx, query, sessionId) for _, screen := range rtn { screen.Full = true } @@ -716,7 +717,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { query := `SELECT * FROM screen WHERE screenid = ?` - screen := GetMapGen[*ScreenType](tx, query, screenId) + screen := dbutil.GetMapGen[*ScreenType](tx, query, screenId) screen.Full = true return screen, nil }) @@ -766,7 +767,7 @@ func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*L var cmdRtn *CmdType if lineVal.CmdId != "" { query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmdRtn = GetMapGen[*CmdType](tx, query, screenId, lineVal.CmdId) + cmdRtn = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineVal.CmdId) } return &lineVal, cmdRtn, nil }) @@ -781,7 +782,7 @@ func GetLineCmdByCmdId(ctx context.Context, screenId string, cmdId string) (*Lin return nil, nil, nil } query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmdRtn := GetMapGen[*CmdType](tx, query, screenId, cmdId) + cmdRtn := dbutil.GetMapGen[*CmdType](tx, query, screenId, cmdId) return &lineVal, cmdRtn, nil }) } @@ -832,7 +833,7 @@ func GetCmdByScreenId(ctx context.Context, screenId string, cmdId string) (*CmdT var cmd *CmdType err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmd = GetMapGen[*CmdType](tx, query, screenId, cmdId) + cmd = dbutil.GetMapGen[*CmdType](tx, query, screenId, cmdId) return nil }) if err != nil { @@ -1181,7 +1182,7 @@ func GetRemoteInstance(ctx context.Context, sessionId string, screenId string, r var ri *RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote_instance WHERE sessionid = ? AND screenid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - ri = GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + ri = dbutil.GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) return nil }) if txErr != nil { @@ -1227,7 +1228,7 @@ func UpdateRemoteState(ctx context.Context, sessionId string, screenId string, r return fmt.Errorf("cannot update remote instance state: %w", err) } query := `SELECT * FROM remote_instance WHERE sessionid = ? AND screenid = ? AND remoteownerid = ? AND remoteid = ? AND name = ?` - ri = GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) + ri = dbutil.GetMapGen[*RemoteInstance](tx, query, sessionId, screenId, remotePtr.OwnerId, remotePtr.RemoteId, remotePtr.Name) if ri == nil { ri = &RemoteInstance{ RIId: scbase.GenPromptUUID(), @@ -1408,7 +1409,7 @@ func GetRunningScreenCmds(ctx context.Context, screenId string) ([]*CmdType, err var rtn []*CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?) AND status = ?` - rtn = SelectMapsGen[*CmdType](tx, query, screenId, CmdStatusRunning) + rtn = dbutil.SelectMapsGen[*CmdType](tx, query, screenId, CmdStatusRunning) return nil }) if txErr != nil { @@ -1824,7 +1825,7 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, } for idx, diffHash := range ssPtr.DiffHashArr { query = `SELECT * FROM state_diff WHERE diffhash = ?` - stateDiff := GetMapGen[*StateDiff](tx, query, diffHash) + stateDiff := dbutil.GetMapGen[*StateDiff](tx, query, diffHash) if stateDiff == nil { return fmt.Errorf("ShellStateDiff %s not found", diffHash) } @@ -1953,7 +1954,7 @@ func GetRIsForScreen(ctx context.Context, sessionId string, screenId string) ([] var rtn []*RemoteInstance txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM remote_instance WHERE sessionid = ? AND (screenid = '' OR screenid = ?)` - rtn = SelectMapsGen[*RemoteInstance](tx, query, sessionId, screenId) + rtn = dbutil.SelectMapsGen[*RemoteInstance](tx, query, sessionId, screenId) return nil }) if txErr != nil { @@ -2112,12 +2113,12 @@ func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) { var query string if tag == "" { query = `SELECT * FROM bookmark` - bms = SelectMapsGen[*BookmarkType](tx, query) + bms = dbutil.SelectMapsGen[*BookmarkType](tx, query) } else { query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)` - bms = SelectMapsGen[*BookmarkType](tx, query, tag) + bms = dbutil.SelectMapsGen[*BookmarkType](tx, query, tag) } - bmMap := MakeGenMap(bms) + bmMap := dbutil.MakeGenMap(bms) var orders []bookmarkOrderType query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?` tx.Select(&orders, query, tag) @@ -2139,7 +2140,7 @@ func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*Bookm var rtn *BookmarkType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM bookmark WHERE bookmarkid = ?` - rtn = GetMapGen[*BookmarkType](tx, query, bookmarkId) + rtn = dbutil.GetMapGen[*BookmarkType](tx, query, bookmarkId) if rtn == nil { return nil } @@ -2275,7 +2276,7 @@ func CreatePlaybook(ctx context.Context, name string) (*PlaybookType, error) { func selectPlaybook(tx *TxWrap, playbookId string) *PlaybookType { query := `SELECT * FROM playbook where playbookid = ?` - playbook := GetMapGen[*PlaybookType](tx, query, playbookId) + playbook := dbutil.GetMapGen[*PlaybookType](tx, query, playbookId) return playbook } @@ -2363,7 +2364,7 @@ func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryIte query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` tx.Select(&lineArr, query, quickJsonArr(getLineIdsFromHistoryItems(historyItems))) query = `SELECT * FROM cmd WHERE cmdid IN (SELECT value FROM json_each(?))` - cmdArr := SelectMapsGen[*CmdType](tx, query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) + cmdArr := dbutil.SelectMapsGen[*CmdType](tx, query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) return lineArr, cmdArr, nil }) } @@ -2371,7 +2372,7 @@ 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 := SelectMapsGen[*HistoryItemType](tx, query, quickJsonArr(historyIds)) + rtn := dbutil.SelectMapsGen[*HistoryItemType](tx, query, quickJsonArr(historyIds)) query = `DELETE FROM history WHERE historyid IN (SELECT value FROM json_each(?))` tx.Exec(query, quickJsonArr(historyIds)) for _, hitem := range rtn { diff --git a/pkg/sstore/map.go b/pkg/sstore/map.go index 6f97600e2..c24a5345b 100644 --- a/pkg/sstore/map.go +++ b/pkg/sstore/map.go @@ -2,81 +2,8 @@ package sstore import ( "context" - "fmt" - "reflect" - "strings" ) -type DBMappable interface { - UseDBMap() -} - -type MapConverter interface { - ToMap() map[string]interface{} - FromMap(map[string]interface{}) bool -} - -type HasSimpleKey interface { - GetSimpleKey() string -} - -type MapConverterPtr[T any] interface { - MapConverter - *T -} - -type DBMappablePtr[T any] interface { - DBMappable - *T -} - -func FromMap[PT MapConverterPtr[T], T any](m map[string]any) PT { - if len(m) == 0 { - return nil - } - rtn := PT(new(T)) - ok := rtn.FromMap(m) - if !ok { - return nil - } - return rtn -} - -func GetMapGen[PT MapConverterPtr[T], T any](tx *TxWrap, query string, args ...interface{}) PT { - m := tx.GetMap(query, args...) - return FromMap[PT](m) -} - -func GetMappable[PT DBMappablePtr[T], T any](tx *TxWrap, query string, args ...interface{}) PT { - rtn := PT(new(T)) - m := tx.GetMap(query, args...) - if len(m) == 0 { - return nil - } - FromDBMap(rtn, m) - return rtn -} - -func SelectMapsGen[PT MapConverterPtr[T], T any](tx *TxWrap, query string, args ...interface{}) []PT { - var rtn []PT - marr := tx.SelectMaps(query, args...) - for _, m := range marr { - val := FromMap[PT](m) - if val != nil { - rtn = append(rtn, val) - } - } - return rtn -} - -func MakeGenMap[T HasSimpleKey](arr []T) map[string]T { - rtn := make(map[string]T) - for _, val := range arr { - rtn[val.GetSimpleKey()] = val - } - return rtn -} - func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { var rtn RT txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -104,99 +31,3 @@ func WithTxRtn3[RT1 any, RT2 any](ctx context.Context, fn func(tx *TxWrap) (RT1, }) return rtn1, rtn2, txErr } - -func isStructType(rt reflect.Type) bool { - if rt.Kind() == reflect.Struct { - return true - } - if rt.Kind() == reflect.Pointer && rt.Elem().Kind() == reflect.Struct { - return true - } - return false -} - -func isByteArrayType(t reflect.Type) bool { - return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 -} - -func ToDBMap(v DBMappable) map[string]interface{} { - if v == nil { - return nil - } - rv := reflect.ValueOf(v) - if rv.Kind() == reflect.Pointer { - rv = rv.Elem() - } - if rv.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructToDBMap", v)) - } - rt := rv.Type() - m := make(map[string]interface{}) - numFields := rt.NumField() - for i := 0; i < numFields; i++ { - field := rt.Field(i) - fieldVal := rv.FieldByIndex(field.Index) - dbName := field.Tag.Get("dbmap") - if dbName == "" { - dbName = strings.ToLower(field.Name) - } - if dbName == "-" { - continue - } - if isByteArrayType(field.Type) { - m[dbName] = fieldVal.Interface() - } else if field.Type.Kind() == reflect.Slice { - m[dbName] = quickJsonArr(fieldVal.Interface()) - } else if isStructType(field.Type) { - m[dbName] = quickJson(fieldVal.Interface()) - } else { - m[dbName] = fieldVal.Interface() - } - } - return m -} - -func FromDBMap(v DBMappable, m map[string]interface{}) { - if v == nil { - panic("StructFromDBMap, v cannot be nil") - } - rv := reflect.ValueOf(v) - if rv.Kind() == reflect.Pointer { - rv = rv.Elem() - } - if rv.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid type %T (non-struct) passed to StructFromDBMap", v)) - } - rt := rv.Type() - numFields := rt.NumField() - for i := 0; i < numFields; i++ { - field := rt.Field(i) - fieldVal := rv.FieldByIndex(field.Index) - dbName := field.Tag.Get("dbmap") - if dbName == "" { - dbName = strings.ToLower(field.Name) - } - if dbName == "-" { - continue - } - if isByteArrayType(field.Type) { - barrVal := fieldVal.Addr().Interface() - quickSetBytes(barrVal.(*[]byte), m, dbName) - } else if field.Type.Kind() == reflect.Slice { - quickSetJsonArr(fieldVal.Addr().Interface(), m, dbName) - } else if isStructType(field.Type) { - quickSetJson(fieldVal.Addr().Interface(), m, dbName) - } else if field.Type.Kind() == reflect.String { - strVal := fieldVal.Addr().Interface() - quickSetStr(strVal.(*string), m, dbName) - } else if field.Type.Kind() == reflect.Int64 { - intVal := fieldVal.Addr().Interface() - quickSetInt64(intVal.(*int64), m, dbName) - } else if field.Type.Kind() == reflect.Bool { - boolVal := fieldVal.Addr().Interface() - quickSetBool(boolVal.(*bool), m, dbName) - } else { - panic(fmt.Sprintf("StructFromDBMap invalid field type %v in %T", fieldVal.Type(), v)) - } - } -} diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index 116b24991..788c404dd 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -1,161 +1,18 @@ package sstore import ( - "database/sql/driver" - "encoding/json" - "fmt" - "strconv" + "github.com/scripthaus-dev/sh2-server/pkg/dbutil" ) -func quickSetStr(strVal *string, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - ival, ok := v.(int64) - if ok { - *strVal = strconv.FormatInt(ival, 10) - return - } - str, ok := v.(string) - if !ok { - return - } - *strVal = str -} - -func quickSetInt64(ival *int64, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - sqlInt, ok := v.(int64) - if !ok { - return - } - *ival = sqlInt -} - -func quickSetBool(bval *bool, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - sqlInt, ok := v.(int64) - if ok { - if sqlInt > 0 { - *bval = true - } - return - } - sqlBool, ok := v.(bool) - if ok { - *bval = sqlBool - } -} - -func quickSetBytes(bval *[]byte, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - sqlBytes, ok := v.([]byte) - if ok { - *bval = sqlBytes - } -} - -func quickSetJson(ptr interface{}, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - str, ok := v.(string) - if !ok { - return - } - if str == "" { - str = "{}" - } - json.Unmarshal([]byte(str), ptr) -} - -func quickSetNullableJson(ptr interface{}, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - str, ok := v.(string) - if !ok { - return - } - if str == "" { - str = "null" - } - json.Unmarshal([]byte(str), ptr) -} - -func quickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { - v, ok := m[name] - if !ok { - return - } - str, ok := v.(string) - if !ok { - return - } - if str == "" { - str = "[]" - } - json.Unmarshal([]byte(str), ptr) -} - -func quickNullableJson(v interface{}) string { - if v == nil { - return "null" - } - barr, _ := json.Marshal(v) - return string(barr) -} - -func quickJson(v interface{}) string { - if v == nil { - return "{}" - } - barr, _ := json.Marshal(v) - return string(barr) -} - -func quickJsonArr(v interface{}) string { - if v == nil { - return "[]" - } - barr, _ := json.Marshal(v) - return string(barr) -} - -func quickScanJson(ptr interface{}, val interface{}) error { - barrVal, ok := val.([]byte) - if !ok { - strVal, ok := val.(string) - if !ok { - return fmt.Errorf("cannot scan '%T' into '%T'", val, ptr) - } - barrVal = []byte(strVal) - } - if len(barrVal) == 0 { - barrVal = []byte("{}") - } - return json.Unmarshal(barrVal, ptr) -} - -func quickValueJson(v interface{}) (driver.Value, error) { - if v == nil { - return "{}", nil - } - barr, err := json.Marshal(v) - if err != nil { - return nil, err - } - return string(barr), nil -} +var quickSetStr = dbutil.QuickSetStr +var quickSetInt64 = dbutil.QuickSetInt64 +var quickSetBool = dbutil.QuickSetBool +var quickSetBytes = dbutil.QuickSetBytes +var quickSetJson = dbutil.QuickSetJson +var quickSetNullableJson = dbutil.QuickSetNullableJson +var quickSetJsonArr = dbutil.QuickSetJsonArr +var quickNullableJson = dbutil.QuickNullableJson +var quickJson = dbutil.QuickJson +var quickJsonArr = dbutil.QuickJsonArr +var quickScanJson = dbutil.QuickScanJson +var quickValueJson = dbutil.QuickValueJson diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 46c90ed13..d5440984c 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -22,6 +22,7 @@ import ( "github.com/sawka/txwrap" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/dbutil" "github.com/scripthaus-dev/sh2-server/pkg/scbase" _ "github.com/mattn/go-sqlite3" @@ -1095,7 +1096,7 @@ func createClientData(tx *TxWrap) error { } query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` - tx.NamedExec(query, ToDBMap(c)) + tx.NamedExec(query, dbutil.ToDBMap(c, false)) log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil } @@ -1113,7 +1114,7 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { return nil, createErr } } - cdata := GetMappable[*ClientData](tx, `SELECT * FROM client`) + cdata := dbutil.GetMappable[*ClientData](tx, `SELECT * FROM client`) if cdata == nil { return nil, fmt.Errorf("no client data found") } @@ -1148,7 +1149,7 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { func GetCmdMigrationInfo(ctx context.Context) (*ClientMigrationData, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ClientMigrationData, error) { - cdata := GetMappable[*ClientData](tx, `SELECT * FROM client`) + cdata := dbutil.GetMappable[*ClientData](tx, `SELECT * FROM client`) if cdata == nil { return nil, fmt.Errorf("no client data found") } From 1dbdb3d50b71c4a0c4b3d8e75f27dc8c2c4cd1d7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 Mar 2023 18:19:17 -0700 Subject: [PATCH 321/397] got screen:new and screen:del implemented --- pkg/dbutil/map.go | 12 ++++++++++++ pkg/pcloud/pcloud.go | 18 ++++++++++++++---- pkg/pcloud/pclouddata.go | 21 +++++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 7e67516bb..c4fd61a11 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -21,6 +21,10 @@ type HasSimpleKey interface { GetSimpleKey() string } +type HasSimpleInt64Key interface { + GetSimpleKey() int64 +} + type MapConverterPtr[T any] interface { MapConverter *T @@ -78,6 +82,14 @@ func MakeGenMap[T HasSimpleKey](arr []T) map[string]T { return rtn } +func MakeGenMapInt64[T HasSimpleInt64Key](arr []T) map[int64]T { + rtn := make(map[int64]T) + for _, val := range arr { + rtn[val.GetSimpleKey()] = val + } + return rtn +} + func isStructType(rt reflect.Type) bool { if rt.Kind() == reflect.Struct { return true diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index dca98837f..1026eab17 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/scripthaus-dev/sh2-server/pkg/dbutil" "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -328,6 +329,11 @@ func finalizeWebScreenUpdate(ctx context.Context, webUpdate *WebShareUpdateType) return nil } +type webShareResponseType struct { + Success bool `json:"success"` + Data []*WebShareUpdateResponseType `json:"data"` +} + func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) error { var webUpdates []*WebShareUpdateType for _, update := range updateArr { @@ -353,16 +359,23 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) if err != nil { return fmt.Errorf("cannot create auth-post-req for %s: %v", WebShareUpdateUrl, err) } - _, err = doRequest(req, nil) + var resp webShareResponseType + _, err = doRequest(req, &resp) if err != nil { return err } + respMap := dbutil.MakeGenMapInt64(resp.Data) 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) } + resp := respMap[update.UpdateId] + if resp == nil { + resp = &WebShareUpdateResponseType{Success: false, Error: "resp not found"} + } + log.Printf("[pcloud] updateid:%d, type:%s %s/%s success:%v err:%v\n", update.UpdateId, update.UpdateType, update.ScreenId, update.LineId, resp.Success, resp.Error) } return nil } @@ -390,9 +403,6 @@ func StartUpdateWriter() { } func computeBackoff(numFailures int) time.Duration { - // TODO remove once API implemented - return time.Hour - switch numFailures { case 1: return 100 * time.Millisecond diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 8cfc34115..7f56edeec 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -25,7 +25,7 @@ type TelemetryInputType struct { type WebShareUpdateType struct { ScreenId string `json:"screenid"` LineId string `json:"lineid"` - UpdateId int64 `json:"-"` // just for internal use + UpdateId int64 `json:"updateid"` UpdateType string `json:"updatetype"` Screen *WebShareScreenType `json:"screen,omitempty"` @@ -38,6 +38,16 @@ type WebShareUpdateType struct { TermOpts *sstore.TermOpts `json:"termopts,omitempty"` } +type WebShareUpdateResponseType struct { + UpdateId int64 `json:"updateid"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +func (ur *WebShareUpdateResponseType) GetSimpleKey() int64 { + return ur.UpdateId +} + type WebShareRemotePtr struct { Alias string `json:"remotealias,omitempty"` CanonicalName string `json:"remotecanonicalname"` @@ -72,10 +82,13 @@ func webScreenFromScreen(s *sstore.ScreenType) (*WebShareScreenType, error) { if s.WebShareOpts.ViewKey == "" { return nil, fmt.Errorf("invalid screen, no ViewKey") } - if s.WebShareOpts.ShareName == "" { - return nil, fmt.Errorf("invalid screen, no ShareName") + var shareName string + if s.WebShareOpts.ShareName != "" { + shareName = s.WebShareOpts.ShareName + } else { + shareName = s.Name } - return &WebShareScreenType{ScreenId: s.ScreenId, ShareName: s.WebShareOpts.ShareName, ViewKey: s.WebShareOpts.ViewKey}, nil + return &WebShareScreenType{ScreenId: s.ScreenId, ShareName: shareName, ViewKey: s.WebShareOpts.ViewKey}, nil } type WebShareLineType struct { From e674333621f19d316848a1b7f3572ddc9dacef8c Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Mar 2023 00:23:31 -0700 Subject: [PATCH 322/397] support map types in DBMappable --- pkg/dbutil/map.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index c4fd61a11..59bde8f6d 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -104,6 +104,10 @@ func isByteArrayType(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 } +func isStringMapType(t reflect.Type) bool { + return t.Kind() == reflect.Map && t.Key().Kind() == reflect.String +} + func ToDBMap(v DBMappable, useBytes bool) map[string]interface{} { if v == nil { return nil @@ -136,7 +140,7 @@ func ToDBMap(v DBMappable, useBytes bool) map[string]interface{} { } else { m[dbName] = QuickJsonArr(fieldVal.Interface()) } - } else if isStructType(field.Type) { + } else if isStructType(field.Type) || isStringMapType(field.Type) { if useBytes { m[dbName] = QuickJsonBytes(fieldVal.Interface()) } else { @@ -177,7 +181,7 @@ func FromDBMap(v DBMappable, m map[string]interface{}) { QuickSetBytes(barrVal.(*[]byte), m, dbName) } else if field.Type.Kind() == reflect.Slice { QuickSetJsonArr(fieldVal.Addr().Interface(), m, dbName) - } else if isStructType(field.Type) { + } else if isStructType(field.Type) || isStringMapType(field.Type) { QuickSetJson(fieldVal.Addr().Interface(), m, dbName) } else if field.Type.Kind() == reflect.String { strVal := fieldVal.Addr().Interface() From dde547351c52195d90e76dd7c3dcbddaa0f8cc83 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Mar 2023 00:24:37 -0700 Subject: [PATCH 323/397] add some synchronization magic for ptypos updates --- pkg/pcloud/pcloud.go | 40 +++++++++++++++++++++++++++----------- pkg/pcloud/pclouddata.go | 1 + pkg/sstore/dbops.go | 42 ++++++++++++++++++++++++++++++++++++++-- pkg/sstore/fileops.go | 2 +- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 1026eab17..9c26a18d3 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -24,7 +24,7 @@ import ( const PCloudEndpoint = "https://api.getprompt.dev/central" const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 1 -const MaxPtyUpdateSize = (128 * 1024) + 1 +const MaxPtyUpdateSize = (128 * 1024) const MaxUpdatesPerReq = 10 const MaxUpdateWriterErrors = 3 const PCloudDefaultTimeout = 5 * time.Second @@ -291,11 +291,19 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* if err != nil { return nil, fmt.Errorf("error getting ptypos: %v", err) } - realOffset, data, err := sstore.ReadPtyOutFile(ctx, update.ScreenId, cmdId, ptyPos, MaxPtyUpdateSize) + sstore.SetWebScreenPtyPosDelIntent(update.ScreenId, update.LineId) + realOffset, data, err := sstore.ReadPtyOutFile(ctx, update.ScreenId, cmdId, ptyPos, MaxPtyUpdateSize+1) if err != nil { return nil, fmt.Errorf("error getting ptydata: %v", err) } - rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data} + if len(data) == 0 { + return nil, nil + } + if len(data) > MaxPtyUpdateSize { + rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data[0:MaxPtyUpdateSize], Eof: false} + } else { + rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data, Eof: true} + } default: return nil, fmt.Errorf("unsupported update type (pcloud/makeWebScreenUpdate): %s\n", update.UpdateType) @@ -306,7 +314,7 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* func finalizeWebScreenUpdate(ctx context.Context, webUpdate *WebShareUpdateType) error { switch webUpdate.UpdateType { case sstore.UpdateType_PtyPos: - dataEof := len(webUpdate.PtyData.Data) < MaxPtyUpdateSize + dataEof := webUpdate.PtyData.Eof newPos := webUpdate.PtyData.PtyPos + int64(len(webUpdate.PtyData.Data)) if dataEof { err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) @@ -318,6 +326,10 @@ func finalizeWebScreenUpdate(ctx context.Context, webUpdate *WebShareUpdateType) if err != nil { return err } + err = sstore.MaybeRemovePtyPosUpdate(ctx, webUpdate.ScreenId, webUpdate.LineId, webUpdate.UpdateId) + if err != nil { + return err + } default: err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) @@ -338,21 +350,27 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) var webUpdates []*WebShareUpdateType for _, update := range updateArr { webUpdate, err := makeWebShareUpdate(context.Background(), update) - if err != nil { - // 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 || webUpdate == nil { + // log error (if there is one), remove update, and continue + if err != nil { + log.Printf("[pcloud] error create web-share update updateid:%d: %v", update.UpdateId, err) + } + if update.UpdateType == sstore.UpdateType_PtyPos { + err = sstore.MaybeRemovePtyPosUpdate(context.Background(), update.ScreenId, update.LineId, update.UpdateId) + } else { + 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) } + if len(webUpdates) == 0 { + return nil + } ctx, cancelFn := context.WithTimeout(context.Background(), PCloudDefaultTimeout) defer cancelFn() req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, webUpdates) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 7f56edeec..102fe76de 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -156,4 +156,5 @@ func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) type WebSharePtyData struct { PtyPos int64 `json:"ptypos,omitempty"` Data []byte `json:"data,omitempty"` + Eof bool `json:"-"` // internal use } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 647dc282a..f6fd32a59 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -24,6 +24,8 @@ const DefaultMaxHistoryItems = 1000 var updateWriterCVar = sync.NewCond(&sync.Mutex{}) var updateWriterMoreData = false +var WebScreenPtyPosLock = &sync.Mutex{} +var WebScreenPtyPosDelIntent = make(map[string]bool) // map[screenid + ":" + lineid] -> bool type SingleConnDBGetter struct { SingleConnLock *sync.Mutex @@ -2400,12 +2402,16 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW if shareMode != ShareModeLocal { return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", shareMode) } + nowTs := time.Now().UnixMilli() 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) + tx.Exec(query, UpdateType_LineNew, nowTs, screenId) + query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) + SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid` + tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) NotifyUpdateWriter() return nil }) @@ -2425,6 +2431,8 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { tx.Exec(query, ShareModeLocal, "null", screenId) query = `DELETE FROM screenupdate WHERE screenid = ?` tx.Exec(query, screenId) + query = `DELETE FROM webptypos WHERE screenid = ?` + tx.Exec(query, screenId) insertScreenUpdate(tx, screenId, UpdateType_ScreenDel) return nil }) @@ -2494,13 +2502,31 @@ func RemoveScreenUpdate(ctx context.Context, updateId int64) error { }) } -func InsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { +func SetWebScreenPtyPosDelIntent(screenId string, lineId string) { + WebScreenPtyPosLock.Lock() + defer WebScreenPtyPosLock.Unlock() + WebScreenPtyPosDelIntent[screenId+":"+lineId] = true +} + +func ClearWebScreenPtyPosDelIntent(screenId string, lineId string) bool { + WebScreenPtyPosLock.Lock() + defer WebScreenPtyPosLock.Unlock() + rtn := WebScreenPtyPosDelIntent[screenId+":"+lineId] + delete(WebScreenPtyPosDelIntent, screenId+":"+lineId) + return rtn +} + +func MaybeInsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { return WithTx(ctx, func(tx *TxWrap) error { + if !isWebShare(tx, screenId) { + return nil + } 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) } + ClearWebScreenPtyPosDelIntent(screenId, lineId) // clear delete intention because we have a new update query = `SELECT updateid FROM screenupdate WHERE screenid = ? AND lineid = ? AND updatetype = ?` if !tx.Exists(query, screenId, lineId, UpdateType_PtyPos) { query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` @@ -2511,6 +2537,18 @@ func InsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) erro }) } +func MaybeRemovePtyPosUpdate(ctx context.Context, screenId string, lineId string, updateId int64) error { + return WithTx(ctx, func(tx *TxWrap) error { + intent := ClearWebScreenPtyPosDelIntent(screenId, lineId) // check for intention before deleting + if !intent { + return nil + } + query := `DELETE FROM screenupdate WHERE updateid = ?` + tx.Exec(query, updateId) + 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 = ?` diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 9361f4459..c2f8e0506 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -61,7 +61,7 @@ func AppendToCmdPtyBlob(ctx context.Context, screenId string, cmdId string, data PtyData64: data64, PtyDataLen: int64(len(data)), } - err = InsertPtyPosUpdate(ctx, screenId, cmdId) + err = MaybeInsertPtyPosUpdate(ctx, screenId, cmdId) if err != nil { // just log log.Printf("error inserting ptypos update %s/%s: %v\n", screenId, cmdId, err) From 50caeee5c2868b0861144b3ca459d74ae8f023a0 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Mar 2023 01:04:34 -0700 Subject: [PATCH 324/397] support int fields in DBMappable --- pkg/dbutil/dbutil.go | 28 +++++++++++++++++++++++++--- pkg/dbutil/map.go | 3 +++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/dbutil/dbutil.go b/pkg/dbutil/dbutil.go index 872a09110..e3dfbbf40 100644 --- a/pkg/dbutil/dbutil.go +++ b/pkg/dbutil/dbutil.go @@ -24,16 +24,38 @@ func QuickSetStr(strVal *string, m map[string]interface{}, name string) { *strVal = str } +func QuickSetInt(ival *int, m map[string]interface{}, name string) { + v, ok := m[name] + if !ok { + return + } + sqlInt, ok := v.(int) + if ok { + *ival = sqlInt + return + } + sqlInt64, ok := v.(int64) + if ok { + *ival = int(sqlInt64) + return + } +} + func QuickSetInt64(ival *int64, m map[string]interface{}, name string) { v, ok := m[name] if !ok { return } - sqlInt, ok := v.(int64) - if !ok { + sqlInt64, ok := v.(int64) + if ok { + *ival = sqlInt64 + return + } + sqlInt, ok := v.(int) + if ok { + *ival = int64(sqlInt) return } - *ival = sqlInt } func QuickSetBool(bval *bool, m map[string]interface{}, name string) { diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 59bde8f6d..0614e3683 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -189,6 +189,9 @@ func FromDBMap(v DBMappable, m map[string]interface{}) { } else if field.Type.Kind() == reflect.Int64 { intVal := fieldVal.Addr().Interface() QuickSetInt64(intVal.(*int64), m, dbName) + } else if field.Type.Kind() == reflect.Int { + intVal := fieldVal.Addr().Interface() + QuickSetInt(intVal.(*int), m, dbName) } else if field.Type.Kind() == reflect.Bool { boolVal := fieldVal.Addr().Interface() QuickSetBool(boolVal.(*bool), m, dbName) From 6753f5eaf4dcf969aedb4b92406506d5b23d40fd Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Mar 2023 01:05:01 -0700 Subject: [PATCH 325/397] add updatets --- pkg/pcloud/pcloud.go | 1 + pkg/pcloud/pclouddata.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 9c26a18d3..c31d95f57 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -192,6 +192,7 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* LineId: update.LineId, UpdateId: update.UpdateId, UpdateType: update.UpdateType, + UpdateTs: update.UpdateTs, } switch update.UpdateType { case sstore.UpdateType_ScreenNew: diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 102fe76de..e9d925509 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -27,6 +27,7 @@ type WebShareUpdateType struct { LineId string `json:"lineid"` UpdateId int64 `json:"updateid"` UpdateType string `json:"updatetype"` + UpdateTs int64 `json:"updatets"` Screen *WebShareScreenType `json:"screen,omitempty"` Line *WebShareLineType `json:"line,omitempty"` From 2277e167498c57bd7f83faac6c26f17008167768 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 28 Mar 2023 22:53:59 -0700 Subject: [PATCH 326/397] bug fixes for webshare --- pkg/cmdrunner/cmdrunner.go | 5 +++-- pkg/dbutil/map.go | 14 ++++++++++++++ pkg/pcloud/pclouddata.go | 4 ++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index af5029f4f..e8973501f 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1652,12 +1652,13 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot create viewkey: %v", err) } - webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: base64.RawURLEncoding.EncodeToString(viewKeyBytes)} + viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) + webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} err = sstore.ScreenWebShareStart(ctx, ids.ScreenId, webShareOpts) if err != nil { return nil, fmt.Errorf("cannot web-share screen: %v", err) } - infoMsg = fmt.Sprintf("screen is now shared to the web at %s", "[screen-share-url]") + infoMsg = fmt.Sprintf("screen is now shared to the web at %s", fmt.Sprintf("screenid=%s viewkey=%s", ids.ScreenId, viewKey)) } else { err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) if err != nil { diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 0614e3683..25ca99d74 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -62,6 +62,20 @@ func GetMappable[PT DBMappablePtr[T], T any](tx *txwrap.TxWrap, query string, ar return rtn } +func SelectMappable[PT DBMappablePtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) []PT { + var rtn []PT + marr := tx.SelectMaps(query, args...) + for _, m := range marr { + if len(m) == 0 { + continue + } + val := PT(new(T)) + FromDBMap(val, m) + rtn = append(rtn, val) + } + return rtn +} + func SelectMapsGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) []PT { var rtn []PT marr := tx.SelectMaps(query, args...) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index e9d925509..da85cccd0 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -50,8 +50,8 @@ func (ur *WebShareUpdateResponseType) GetSimpleKey() int64 { } type WebShareRemotePtr struct { - Alias string `json:"remotealias,omitempty"` - CanonicalName string `json:"remotecanonicalname"` + Alias string `json:"alias,omitempty"` + CanonicalName string `json:"canonicalname"` Name string `json:"name,omitempty"` } From 5480d5d7f60125bbb3b24ac44c6cd10cfd52720a Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 29 Mar 2023 12:40:53 -0700 Subject: [PATCH 327/397] fix nil checking in dbutil (typed nils) --- db/migrations/000017_remotevars.down.sql | 2 ++ db/migrations/000017_remotevars.up.sql | 2 ++ db/schema.sql | 11 ++++++++-- pkg/dbutil/dbutil.go | 27 ++++++++++++++++++------ pkg/dbutil/map.go | 4 ++-- 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 db/migrations/000017_remotevars.down.sql create mode 100644 db/migrations/000017_remotevars.up.sql diff --git a/db/migrations/000017_remotevars.down.sql b/db/migrations/000017_remotevars.down.sql new file mode 100644 index 000000000..442c1b8bc --- /dev/null +++ b/db/migrations/000017_remotevars.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE remote DROP COLUMN statevars; + diff --git a/db/migrations/000017_remotevars.up.sql b/db/migrations/000017_remotevars.up.sql new file mode 100644 index 000000000..f91360538 --- /dev/null +++ b/db/migrations/000017_remotevars.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE remote ADD COLUMN statevars json NOT NULL DEFAULT '{}'; + diff --git a/db/schema.sql b/db/schema.sql index 0b9ba237a..018e84425 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -58,7 +58,7 @@ CREATE TABLE remote ( local boolean NOT NULL, archived boolean NOT NULL, remoteidx int NOT NULL -); +, statevars json NOT NULL DEFAULT '{}'); CREATE TABLE history ( historyid varchar(36) PRIMARY KEY, ts bigint NOT NULL, @@ -192,10 +192,17 @@ CREATE TABLE IF NOT EXISTS "cmd" ( rtndiffhasharr json NOT NULL, PRIMARY KEY (screenid, cmdid) ); -CREATE TABLE screenupdates ( +CREATE TABLE screenupdate ( updateid integer PRIMARY KEY, screenid varchar(36) NOT NULL, lineid varchar(36) NOT NULL, updatetype varchar(50) NOT NULL, updatets bigint NOT NULL ); +CREATE TABLE webptypos ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + ptypos bigint NOT NULL, + PRIMARY KEY (screenid, lineid) +); +CREATE INDEX idx_screenupdate_ids ON screenupdate (screenid, lineid); diff --git a/pkg/dbutil/dbutil.go b/pkg/dbutil/dbutil.go index e3dfbbf40..363873272 100644 --- a/pkg/dbutil/dbutil.go +++ b/pkg/dbutil/dbutil.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "reflect" "strconv" ) @@ -130,8 +131,22 @@ func QuickSetJsonArr(ptr interface{}, m map[string]interface{}, name string) { json.Unmarshal(barr, ptr) } +func CheckNil(v interface{}) bool { + rv := reflect.ValueOf(v) + if !rv.IsValid() { + return true + } + switch rv.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return rv.IsNil() + + default: + return false + } +} + func QuickNullableJson(v interface{}) string { - if v == nil { + if CheckNil(v) { return "null" } barr, _ := json.Marshal(v) @@ -139,7 +154,7 @@ func QuickNullableJson(v interface{}) string { } func QuickJson(v interface{}) string { - if v == nil { + if CheckNil(v) { return "{}" } barr, _ := json.Marshal(v) @@ -147,7 +162,7 @@ func QuickJson(v interface{}) string { } func QuickJsonBytes(v interface{}) []byte { - if v == nil { + if CheckNil(v) { return []byte("{}") } barr, _ := json.Marshal(v) @@ -155,7 +170,7 @@ func QuickJsonBytes(v interface{}) []byte { } func QuickJsonArr(v interface{}) string { - if v == nil { + if CheckNil(v) { return "[]" } barr, _ := json.Marshal(v) @@ -163,7 +178,7 @@ func QuickJsonArr(v interface{}) string { } func QuickJsonArrBytes(v interface{}) []byte { - if v == nil { + if CheckNil(v) { return []byte("[]") } barr, _ := json.Marshal(v) @@ -186,7 +201,7 @@ func QuickScanJson(ptr interface{}, val interface{}) error { } func QuickValueJson(v interface{}) (driver.Value, error) { - if v == nil { + if CheckNil(v) { return "{}", nil } barr, err := json.Marshal(v) diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 25ca99d74..491f70372 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -123,7 +123,7 @@ func isStringMapType(t reflect.Type) bool { } func ToDBMap(v DBMappable, useBytes bool) map[string]interface{} { - if v == nil { + if CheckNil(v) { return nil } rv := reflect.ValueOf(v) @@ -168,7 +168,7 @@ func ToDBMap(v DBMappable, useBytes bool) map[string]interface{} { } func FromDBMap(v DBMappable, m map[string]interface{}) { - if v == nil { + if CheckNil(v) { panic("StructFromDBMap, v cannot be nil") } rv := reflect.ValueOf(v) From a340f93c2086a1c73523055d2c3f2ddd8bb5686b Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 29 Mar 2023 12:42:04 -0700 Subject: [PATCH 328/397] add statevars to remote, add vars into webremote --- pkg/pcloud/pclouddata.go | 40 ++++++++++++++++++++++-------------- pkg/remote/remote.go | 44 +++++++++++++++++++++++++++++++++++++++- pkg/sstore/dbops.go | 12 +++++++++-- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 40 ++++++++++++++++++++---------------- 5 files changed, 101 insertions(+), 37 deletions(-) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index da85cccd0..f77660d5b 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -49,22 +49,13 @@ func (ur *WebShareUpdateResponseType) GetSimpleKey() int64 { return ur.UpdateId } -type WebShareRemotePtr struct { +type WebShareRemote struct { + RemoteId string `json:"remoteid"` Alias string `json:"alias,omitempty"` CanonicalName string `json:"canonicalname"` 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, - } + HomeDir string `json:"homedir,omitempty"` + IsRoot bool `json:"isroot,omitempty"` } type WebShareScreenType struct { @@ -73,6 +64,17 @@ type WebShareScreenType struct { ViewKey string `json:"viewkey"` } +func webRemoteFromRemote(rptr sstore.RemotePtrType, r *sstore.RemoteType) *WebShareRemote { + return &WebShareRemote{ + RemoteId: r.RemoteId, + Alias: r.RemoteAlias, + CanonicalName: r.RemoteCanonicalName, + Name: rptr.Name, + HomeDir: r.StateVars["home"], + IsRoot: r.StateVars["remoteuser"] == "root", + } +} + func webScreenFromScreen(s *sstore.ScreenType) (*WebShareScreenType, error) { if s == nil || s.ScreenId == "" { return nil, fmt.Errorf("invalid nil screen") @@ -121,7 +123,7 @@ type WebShareCmdType struct { LineId string `json:"lineid"` CmdStr string `json:"cmdstr"` RawCmdStr string `json:"rawcmdstr"` - Remote *WebShareRemotePtr `json:"remote"` + Remote *WebShareRemote `json:"remote"` FeState sstore.FeStateType `json:"festate"` TermOpts sstore.TermOpts `json:"termopts"` Status string `json:"status"` @@ -132,11 +134,19 @@ type WebShareCmdType struct { } func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) { + if cmd.Remote.RemoteId == "" { + return nil, fmt.Errorf("invalid cmd, remoteptr has no remoteid") + } + remote := remote.GetRemoteCopyById(cmd.Remote.RemoteId) + if remote == nil { + return nil, fmt.Errorf("invalid cmd, cannot retrieve remote:%s", cmd.Remote.RemoteId) + } + webRemote := webRemoteFromRemote(cmd.Remote, remote) rtn := &WebShareCmdType{ LineId: lineId, CmdStr: cmd.CmdStr, RawCmdStr: cmd.RawCmdStr, - Remote: webRemoteFromRemotePtr(cmd.Remote), + Remote: webRemote, FeState: cmd.FeState, TermOpts: cmd.TermOpts, Status: cmd.Status, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d95ad095f..1129b9495 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -387,6 +387,15 @@ func GetRemoteById(remoteId string) *MShellProc { return GlobalStore.Map[remoteId] } +func GetRemoteCopyById(remoteId string) *sstore.RemoteType { + msh := GetRemoteById(remoteId) + if msh == nil { + return nil + } + rcopy := msh.GetRemoteCopy() + return &rcopy +} + func GetRemoteMap() map[string]*MShellProc { GlobalStore.Lock.Lock() defer GlobalStore.Lock.Unlock() @@ -518,7 +527,10 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { if msh.Status == StatusConnecting { state.WaitingForPassword = msh.isWaitingForPassword_nolock() } - vars := make(map[string]string) + vars := msh.Remote.StateVars + if vars == nil { + vars = make(map[string]string) + } vars["user"] = msh.Remote.RemoteUser vars["bestuser"] = vars["user"] vars["host"] = msh.Remote.RemoteHost @@ -963,6 +975,33 @@ func (msh *MShellProc) RunInstall() { return } +func (msh *MShellProc) updateRemoteStateVars(ctx context.Context, remoteId string, initPk *packet.InitPacketType) { + msh.Lock.Lock() + defer msh.Lock.Unlock() + stateVars := getStateVarsFromInitPk(initPk) + if stateVars == nil { + return + } + msh.Remote.StateVars = stateVars + err := sstore.UpdateRemoteStateVars(ctx, remoteId, stateVars) + if err != nil { + // ignore error, nothing to do + log.Printf("error updating remote statevars: %v\n", err) + } +} + +func getStateVarsFromInitPk(initPk *packet.InitPacketType) map[string]string { + if initPk == nil || initPk.NotFound { + return nil + } + rtn := make(map[string]string) + rtn["home"] = initPk.HomeDir + rtn["remoteuser"] = initPk.User + rtn["remotehost"] = initPk.HostName + rtn["remoteuname"] = initPk.UName + return rtn +} + func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, error) { reinitPk := packet.MakeReInitPacket() reinitPk.ReqId = uuid.New().String() @@ -986,6 +1025,8 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro msh.CurrentState = hval msh.StateMap[hval] = initPk.State }) + msh.updateRemoteStateVars(ctx, msh.RemoteId, initPk) + return initPk, nil } @@ -1139,6 +1180,7 @@ func (msh *MShellProc) Launch(interactive bool) { msh.WriteToPtyBuffer("*error connecting to remote: %v\n", err) return } + msh.updateRemoteStateVars(context.Background(), msh.RemoteId, initPk) msh.WriteToPtyBuffer("connected state:%s\n", stateBaseHash) msh.WithLock(func() { msh.ServerProc = cproc diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f6fd32a59..dc8265337 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -199,14 +199,22 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local)` + ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local, statevars) VALUES + (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local,:statevars)` tx.NamedExec(query, r.ToMap()) return nil }) return txErr } +func UpdateRemoteStateVars(ctx context.Context, remoteId string, stateVars map[string]string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE remote SET statevars = ? WHERE remoteid = ?` + tx.Exec(query, quickJson(stateVars), remoteId) + return nil + }) +} + func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if hitem == nil { return fmt.Errorf("cannot insert nil history item") diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index b6f461567..fb1bfb7f7 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 16 +const MaxMigration = 17 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d5440984c..3357ec939 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -377,8 +377,9 @@ type ScreenLinesType struct { func (ScreenLinesType) UseDBMap() {} type ScreenWebShareOpts struct { - ShareName string `json:"sharename"` - ViewKey string `json:"viewkey"` + ShareName string `json:"sharename"` + ViewKey string `json:"viewkey"` + SharedRemotes []string `json:"sharedremotes"` } type ScreenType struct { @@ -804,22 +805,23 @@ func (opts RemoteOptsType) Value() (driver.Value, error) { } type RemoteType struct { - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalid"` - RemoteType string `json:"remotetype"` - RemoteAlias string `json:"remotealias"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteSudo bool `json:"remotesudo"` - RemoteUser string `json:"remoteuser"` - RemoteHost string `json:"remotehost"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - SSHOpts *SSHOpts `json:"sshopts"` - RemoteOpts *RemoteOptsType `json:"remoteopts"` - LastConnectTs int64 `json:"lastconnectts"` - Archived bool `json:"archived"` - RemoteIdx int64 `json:"remoteidx"` - Local bool `json:"local"` + RemoteId string `json:"remoteid"` + PhysicalId string `json:"physicalid"` + RemoteType string `json:"remotetype"` + RemoteAlias string `json:"remotealias"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteSudo bool `json:"remotesudo"` + RemoteUser string `json:"remoteuser"` + RemoteHost string `json:"remotehost"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + SSHOpts *SSHOpts `json:"sshopts"` + RemoteOpts *RemoteOptsType `json:"remoteopts"` + LastConnectTs int64 `json:"lastconnectts"` + Archived bool `json:"archived"` + RemoteIdx int64 `json:"remoteidx"` + Local bool `json:"local"` + StateVars map[string]string `json:"statevars"` } func (r *RemoteType) GetName() string { @@ -878,6 +880,7 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["archived"] = r.Archived rtn["remoteidx"] = r.RemoteIdx rtn["local"] = r.Local + rtn["statevars"] = quickJson(r.StateVars) return rtn } @@ -898,6 +901,7 @@ func (r *RemoteType) FromMap(m map[string]interface{}) bool { quickSetBool(&r.Archived, m, "archived") quickSetInt64(&r.RemoteIdx, m, "remoteidx") quickSetBool(&r.Local, m, "local") + quickSetJson(&r.StateVars, m, "statevars") return true } From 3709381034a8e0c6a91c9fe6294f57951438e409 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Mar 2023 00:07:25 -0700 Subject: [PATCH 329/397] never omit ptypos or data from WebSharePtyData --- pkg/pcloud/pclouddata.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index f77660d5b..685ef7d1b 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -165,7 +165,7 @@ func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) } type WebSharePtyData struct { - PtyPos int64 `json:"ptypos,omitempty"` - Data []byte `json:"data,omitempty"` + PtyPos int64 `json:"ptypos"` + Data []byte `json:"data"` Eof bool `json:"-"` // internal use } From 83ce367ace60743f84f67a3dc8b8c84a884b49ee Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Mar 2023 00:59:05 -0700 Subject: [PATCH 330/397] set archived update for archived lines --- pkg/sstore/dbops.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index dc8265337..f2637829d 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1365,7 +1365,12 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err if !tx.Exists(query, screenId) { return fmt.Errorf("screen does not exist") } - query = `UPDATE line SET archived = 1 WHERE screenid = ?` + if isWebShare(tx, screenId) { + query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) + SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND archived = 0` + tx.Exec(query, UpdateType_LineArchived, time.Now().UnixMilli(), screenId) + } + query = `UPDATE line SET archived = 1 WHERE screenid = ? AND archived = 0` tx.Exec(query, screenId) return nil }) From 652c8208449e72418a72e906614c2bddc2ddab2e Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Mar 2023 12:59:58 -0700 Subject: [PATCH 331/397] simplify updates, remove line:archived (now just line:del) --- pkg/cmdrunner/cmdrunner.go | 11 +++++- pkg/pcloud/pcloud.go | 31 ++++----------- pkg/sstore/dbops.go | 79 +++++++++++++++----------------------- pkg/sstore/sstore.go | 1 - 4 files changed, 49 insertions(+), 73 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e8973501f..fbc682461 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -51,6 +51,8 @@ const MaxCommandLen = 4096 const MaxSignalLen = 12 const MaxSignalNum = 64 const MaxEvalDepth = 5 +const DevWebScreenUrlFmt = "http://devtest.getprompt.com:9001/static/index.html?screenid=%s&viewkey=%s" +const ProdWebScreenUrlFmt = "https://share.getprompt.dev/s/%s?viewkey=%s" var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -220,6 +222,13 @@ func GetCmdStr(pk *scpacket.FeCommandPacketType) string { return pk.MetaCmd + ":" + pk.MetaSubCmd } +func GetWebShareUrl(screenId string, viewKey string) string { + if scbase.IsDevMode() { + return fmt.Sprintf(DevWebScreenUrlFmt, screenId, viewKey) + } + return fmt.Sprintf(ProdWebScreenUrlFmt, screenId, viewKey) +} + func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { metaCmd := SubMetaCmd(pk.MetaCmd) var cmdName string @@ -1658,7 +1667,7 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot web-share screen: %v", err) } - infoMsg = fmt.Sprintf("screen is now shared to the web at %s", fmt.Sprintf("screenid=%s viewkey=%s", ids.ScreenId, viewKey)) + infoMsg = fmt.Sprintf("screen is now shared to the web at %s", GetWebShareUrl(ids.ScreenId, viewKey)) } else { err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) if err != nil { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index c31d95f57..9f3465ffc 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -237,13 +237,6 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* 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 { @@ -292,7 +285,6 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* if err != nil { return nil, fmt.Errorf("error getting ptypos: %v", err) } - sstore.SetWebScreenPtyPosDelIntent(update.ScreenId, update.LineId) realOffset, data, err := sstore.ReadPtyOutFile(ctx, update.ScreenId, cmdId, ptyPos, MaxPtyUpdateSize+1) if err != nil { return nil, fmt.Errorf("error getting ptydata: %v", err) @@ -315,30 +307,23 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* func finalizeWebScreenUpdate(ctx context.Context, webUpdate *WebShareUpdateType) error { switch webUpdate.UpdateType { case sstore.UpdateType_PtyPos: - dataEof := webUpdate.PtyData.Eof newPos := webUpdate.PtyData.PtyPos + int64(len(webUpdate.PtyData.Data)) - if dataEof { - err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) - if err != nil { - return err - } - } err := sstore.SetWebPtyPos(ctx, webUpdate.ScreenId, webUpdate.LineId, newPos) if err != nil { return err } - err = sstore.MaybeRemovePtyPosUpdate(ctx, webUpdate.ScreenId, webUpdate.LineId, webUpdate.UpdateId) - if err != nil { - return err - } - default: - err := sstore.RemoveScreenUpdate(ctx, webUpdate.UpdateId) + case sstore.UpdateType_LineDel: + err := sstore.DeleteWebPtyPos(ctx, webUpdate.ScreenId, webUpdate.LineId) if err != nil { - // this is not great, this *should* never fail and is not easy to recover from return err } } + 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 + } return nil } @@ -357,8 +342,6 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) log.Printf("[pcloud] error create web-share update updateid:%d: %v", update.UpdateId, err) } if update.UpdateType == sstore.UpdateType_PtyPos { - err = sstore.MaybeRemovePtyPosUpdate(context.Background(), update.ScreenId, update.LineId, update.UpdateId) - } else { err = sstore.RemoveScreenUpdate(context.Background(), update.UpdateId) } if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index f2637829d..2c1f2fd41 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1365,10 +1365,15 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err if !tx.Exists(query, screenId) { return fmt.Errorf("screen does not exist") } + fmt.Printf("** archive-screen-lines: %s\n", screenId) if isWebShare(tx, screenId) { query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND archived = 0` - tx.Exec(query, UpdateType_LineArchived, time.Now().UnixMilli(), screenId) + tx.Exec(query, UpdateType_LineDel, time.Now().UnixMilli(), screenId) + NotifyUpdateWriter() + query = `SELECT count(*) FROM line WHERE screenid = ? AND archived = 0` + count := tx.GetInt(query, screenId) + fmt.Printf("** archive-screen-lines: wrote into screenupdate: %d\n", count) } query = `UPDATE line SET archived = 1 WHERE screenid = ? AND archived = 0` tx.Exec(query, screenId) @@ -1924,7 +1929,11 @@ 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) { - insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineArchived) + if archived { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel) + } else { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineNew) + } } return nil }) @@ -2420,10 +2429,10 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW 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` + SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum` tx.Exec(query, UpdateType_LineNew, nowTs, screenId) query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid` + SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived` tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) NotifyUpdateWriter() return nil @@ -2474,12 +2483,16 @@ func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateTy tx.SetErr(errors.New("invalid screen-update, lineid is empty")) return } - query := `SELECT updateid FROM screenupdate WHERE screenid = ? AND lineid = ? AND (updatetype = ? OR updatetype = ?)` - if !tx.Exists(query, screenId, lineId, updateType, UpdateType_LineNew) { - query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` - tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) - NotifyUpdateWriter() + if updateType == UpdateType_LineNew || updateType == UpdateType_LineDel { + query := `DELETE FROM screenupdate WHERE screenid = ? AND lineid = ?` + tx.Exec(query, screenId, lineId) } + query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` + tx.Exec(query, screenId, lineId, updateType, time.Now().UnixMilli()) + if updateType == UpdateType_LineNew { + tx.Exec(query, screenId, lineId, UpdateType_PtyPos, time.Now().UnixMilli()) + } + NotifyUpdateWriter() } func insertScreenCmdUpdate(tx *TxWrap, screenId string, cmdId string, updateType string) { @@ -2515,49 +2528,12 @@ func RemoveScreenUpdate(ctx context.Context, updateId int64) error { }) } -func SetWebScreenPtyPosDelIntent(screenId string, lineId string) { - WebScreenPtyPosLock.Lock() - defer WebScreenPtyPosLock.Unlock() - WebScreenPtyPosDelIntent[screenId+":"+lineId] = true -} - -func ClearWebScreenPtyPosDelIntent(screenId string, lineId string) bool { - WebScreenPtyPosLock.Lock() - defer WebScreenPtyPosLock.Unlock() - rtn := WebScreenPtyPosDelIntent[screenId+":"+lineId] - delete(WebScreenPtyPosDelIntent, screenId+":"+lineId) - return rtn -} - func MaybeInsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { return WithTx(ctx, func(tx *TxWrap) error { if !isWebShare(tx, screenId) { return nil } - 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) - } - ClearWebScreenPtyPosDelIntent(screenId, lineId) // clear delete intention because we have a new update - query = `SELECT updateid FROM screenupdate WHERE screenid = ? AND lineid = ? AND updatetype = ?` - if !tx.Exists(query, screenId, lineId, UpdateType_PtyPos) { - query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` - tx.Exec(query, screenId, lineId, UpdateType_PtyPos, time.Now().UnixMilli()) - NotifyUpdateWriter() - } - return nil - }) -} - -func MaybeRemovePtyPosUpdate(ctx context.Context, screenId string, lineId string, updateId int64) error { - return WithTx(ctx, func(tx *TxWrap) error { - intent := ClearWebScreenPtyPosDelIntent(screenId, lineId) // check for intention before deleting - if !intent { - return nil - } - query := `DELETE FROM screenupdate WHERE updateid = ?` - tx.Exec(query, updateId) + insertScreenCmdUpdate(tx, screenId, cmdId, UpdateType_PtyPos) return nil }) } @@ -2570,6 +2546,15 @@ func GetWebPtyPos(ctx context.Context, screenId string, lineId string) (int64, e }) } +func DeleteWebPtyPos(ctx context.Context, screenId string, lineId string) error { + fmt.Printf("del webptypos %s:%s\n", screenId, lineId) + return WithTx(ctx, func(tx *TxWrap) error { + query := `DELETE FROM webptypos WHERE screenid = ? AND lineid = ?` + tx.Exec(query, screenId, lineId) + return 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 = ?` diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 3357ec939..072dd8af5 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -86,7 +86,6 @@ const ( 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_CmdTermOpts = "cmd:termopts" From 274697039ae5e703253249882de0fb53ed243c2c Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Mar 2023 18:08:35 -0700 Subject: [PATCH 332/397] webshare selected line, sync webupdate for screen:new, fix line resolver for hidden items --- pkg/cmdrunner/cmdrunner.go | 26 ++++++++++++-- pkg/pcloud/pcloud.go | 72 +++++++++++++++++++++++++++++++++----- pkg/pcloud/pclouddata.go | 10 +++--- pkg/sstore/dbops.go | 60 ++++++++++++++++++++++--------- pkg/sstore/sstore.go | 23 ++++++------ pkg/sstore/updatebus.go | 2 ++ 6 files changed, 151 insertions(+), 42 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index fbc682461..e6adcc74c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "fmt" "log" + "net/url" "os" "regexp" "sort" @@ -1641,6 +1642,10 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return update, nil } +func makeExternLink(urlStr string) string { + return fmt.Sprintf(`https://extern?%s`, url.QueryEscape(urlStr)) +} + func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Screen) if err != nil { @@ -1655,6 +1660,7 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType return nil, err } var infoMsg string + var infoWebShareLink bool if shouldShare { viewKeyBytes := make([]byte, 9) _, err = rand.Read(viewKeyBytes) @@ -1663,11 +1669,24 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("cannot get screen: %v", err) + } + err = sstore.CanScreenWebShare(screen) + if err != nil { + return nil, err + } + webUpdate := pcloud.MakeScreenNewUpdate(screen, webShareOpts) + err = pcloud.DoSyncWebUpdate(webUpdate) + if err != nil { + return nil, fmt.Errorf("error starting webshare, error contacting prompt cloud server: %v", err) + } err = sstore.ScreenWebShareStart(ctx, ids.ScreenId, webShareOpts) if err != nil { - return nil, fmt.Errorf("cannot web-share screen: %v", err) + return nil, fmt.Errorf("cannot webshare screen, error updating: %v", err) } - infoMsg = fmt.Sprintf("screen is now shared to the web at %s", GetWebShareUrl(ids.ScreenId, viewKey)) + infoWebShareLink = true } else { err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) if err != nil { @@ -1682,7 +1701,8 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType update := sstore.ModelUpdate{ Screens: []*sstore.ScreenType{screen}, Info: &sstore.InfoMsgType{ - InfoMsg: infoMsg, + InfoMsg: infoMsg, + WebShareLink: infoWebShareLink, }, } return update, nil diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 9f3465ffc..eb07022ea 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -186,6 +186,22 @@ func defaultError(err error, estr string) error { return errors.New(estr) } +func MakeScreenNewUpdate(screen *sstore.ScreenType, webShareOpts sstore.ScreenWebShareOpts) *WebShareUpdateType { + rtn := &WebShareUpdateType{ + ScreenId: screen.ScreenId, + UpdateId: -1, + UpdateType: sstore.UpdateType_ScreenNew, + UpdateTs: time.Now().UnixMilli(), + } + rtn.Screen = &WebShareScreenType{ + ScreenId: screen.ScreenId, + SelectedLine: int(screen.SelectedLine), + ShareName: webShareOpts.ShareName, + ViewKey: webShareOpts.ViewKey, + } + return rtn +} + func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (*WebShareUpdateType, error) { rtn := &WebShareUpdateType{ ScreenId: update.ScreenId, @@ -208,15 +224,19 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* case sstore.UpdateType_ScreenDel: break - case sstore.UpdateType_ScreenName: + case sstore.UpdateType_ScreenName, sstore.UpdateType_ScreenSelectedLine: 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)") + if screen == nil || screen.WebShareOpts == nil { + return nil, fmt.Errorf("invalid screen, not webshared (makeWebScreenUpdate)") + } + if update.UpdateType == sstore.UpdateType_ScreenName { + rtn.SVal = screen.WebShareOpts.ShareName + } else if update.UpdateType == sstore.UpdateType_ScreenSelectedLine { + rtn.IVal = int64(screen.SelectedLine) } - rtn.SVal = screen.WebShareOpts.ShareName case sstore.UpdateType_LineNew: line, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) @@ -332,7 +352,7 @@ type webShareResponseType struct { Data []*WebShareUpdateResponseType `json:"data"` } -func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) error { +func convertUpdates(updateArr []*sstore.ScreenUpdateType) []*WebShareUpdateType { var webUpdates []*WebShareUpdateType for _, update := range updateArr { webUpdate, err := makeWebShareUpdate(context.Background(), update) @@ -352,9 +372,43 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) } webUpdates = append(webUpdates, webUpdate) } + return webUpdates +} + +func DoSyncWebUpdate(webUpdate *WebShareUpdateType) error { + authInfo, err := getAuthInfo(context.Background()) + if err != nil { + return fmt.Errorf("could not get authinfo for request: %v", err) + } + ctx, cancelFn := context.WithTimeout(context.Background(), PCloudDefaultTimeout) + defer cancelFn() + req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, []*WebShareUpdateType{webUpdate}) + if err != nil { + return fmt.Errorf("cannot create auth-post-req for %s: %v", WebShareUpdateUrl, err) + } + var resp webShareResponseType + _, err = doRequest(req, &resp) + if err != nil { + return err + } + if len(resp.Data) == 0 { + return fmt.Errorf("invalid response received from server") + } + urt := resp.Data[0] + if urt.Error != "" { + return errors.New(urt.Error) + } + return nil +} + +func DoWebUpdates(webUpdates []*WebShareUpdateType) error { if len(webUpdates) == 0 { return nil } + authInfo, err := getAuthInfo(context.Background()) + if err != nil { + return fmt.Errorf("could not get authinfo for request: %v", err) + } ctx, cancelFn := context.WithTimeout(context.Background(), PCloudDefaultTimeout) defer cancelFn() req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, webUpdates) @@ -377,7 +431,9 @@ func DoWebScreenUpdates(authInfo AuthInfo, updateArr []*sstore.ScreenUpdateType) if resp == nil { resp = &WebShareUpdateResponseType{Success: false, Error: "resp not found"} } - log.Printf("[pcloud] updateid:%d, type:%s %s/%s success:%v err:%v\n", update.UpdateId, update.UpdateType, update.ScreenId, update.LineId, resp.Success, resp.Error) + if resp.Error != "" { + log.Printf("[pcloud] error updateid:%d, type:%s %s/%s err:%v\n", update.UpdateId, update.UpdateType, update.ScreenId, update.LineId, resp.Error) + } } return nil } @@ -447,8 +503,8 @@ func runWebShareUpdateWriter() { continue } numErrors = 0 - authInfo, err := getAuthInfo(context.Background()) - err = DoWebScreenUpdates(authInfo, updateArr) + webUpdates := convertUpdates(updateArr) + err = DoWebUpdates(webUpdates) if err != nil { numSendErrors++ backoffTime := computeBackoff(numSendErrors) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 685ef7d1b..456a14692 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -34,6 +34,7 @@ type WebShareUpdateType struct { Cmd *WebShareCmdType `json:"cmd,omitempty"` PtyData *WebSharePtyData `json:"ptydata,omitempty"` SVal string `json:"sval,omitempty"` + IVal int64 `json:"ival,omitempty"` BVal bool `json:"bval,omitempty"` DoneInfo *sstore.CmdDoneInfo `json:"doneinfo,omitempty"` TermOpts *sstore.TermOpts `json:"termopts,omitempty"` @@ -59,9 +60,10 @@ type WebShareRemote struct { } type WebShareScreenType struct { - ScreenId string `json:"screenid"` - ShareName string `json:"sharename"` - ViewKey string `json:"viewkey"` + ScreenId string `json:"screenid"` + ShareName string `json:"sharename"` + ViewKey string `json:"viewkey"` + SelectedLine int `json:"selectedline"` } func webRemoteFromRemote(rptr sstore.RemotePtrType, r *sstore.RemoteType) *WebShareRemote { @@ -91,7 +93,7 @@ func webScreenFromScreen(s *sstore.ScreenType) (*WebShareScreenType, error) { } else { shareName = s.Name } - return &WebShareScreenType{ScreenId: s.ScreenId, ShareName: shareName, ViewKey: s.WebShareOpts.ViewKey}, nil + return &WebShareScreenType{ScreenId: s.ScreenId, ShareName: shareName, ViewKey: s.WebShareOpts.ViewKey, SelectedLine: int(s.SelectedLine)}, nil } type WebShareLineType struct { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 2c1f2fd41..761ba31e6 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1114,6 +1114,7 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP tx.Exec(query, screenId) query = `DELETE FROM line WHERE screenid = ?` tx.Exec(query, screenId) + insertScreenDelUpdate(tx, screenId) return nil }) if txErr != nil { @@ -1715,6 +1716,9 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter if sline, found := editMap[ScreenField_SelectedLine]; found { query = `UPDATE screen SET selectedline = ? WHERE screenid = ?` tx.Exec(query, sline, screenId) + if isWebShare(tx, screenId) { + insertScreenUpdate(tx, screenId, UpdateType_ScreenSelectedLine) + } } if focusType, found := editMap[ScreenField_Focus]; found { query = `UPDATE screen SET focustype = ? WHERE screenid = ?` @@ -1743,7 +1747,7 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter func GetLineResolveItems(ctx context.Context, screenId string) ([]ResolveItem, error) { var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT lineid as id, linenum as num FROM line WHERE screenid = ? ORDER BY linenum` + query := `SELECT lineid as id, linenum as num, archived as hidden FROM line WHERE screenid = ? ORDER BY linenum` tx.Select(&rtn, query, screenId) return nil }) @@ -2411,6 +2415,19 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem }) } +func CanScreenWebShare(screen *ScreenType) error { + if screen == nil { + return fmt.Errorf("cannot share screen, not found") + } + if screen.ShareMode == ShareModeWeb { + return fmt.Errorf("screen is already shared to web") + } + if screen.ShareMode != ShareModeLocal { + return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", screen.ShareMode) + } + return nil +} + func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenWebShareOpts) error { return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE screenid = ?` @@ -2424,17 +2441,9 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW if shareMode != ShareModeLocal { return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", shareMode) } - nowTs := time.Now().UnixMilli() 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 = ? AND NOT archived ORDER BY linenum` - tx.Exec(query, UpdateType_LineNew, nowTs, screenId) - query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived` - tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) - NotifyUpdateWriter() + insertScreenNewUpdate(tx, screenId) return nil }) } @@ -2451,11 +2460,7 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeLocal, "null", screenId) - query = `DELETE FROM screenupdate WHERE screenid = ?` - tx.Exec(query, screenId) - query = `DELETE FROM webptypos WHERE screenid = ?` - tx.Exec(query, screenId) - insertScreenUpdate(tx, screenId, UpdateType_ScreenDel) + insertScreenDelUpdate(tx, screenId) return nil }) } @@ -2469,11 +2474,31 @@ func insertScreenUpdate(tx *TxWrap, screenId string, updateType string) { tx.SetErr(errors.New("invalid screen-update, screenid is empty")) return } + nowTs := time.Now().UnixMilli() query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) VALUES (?, ?, ?, ?)` - tx.Exec(query, screenId, "", updateType, time.Now().UnixMilli()) + tx.Exec(query, screenId, "", updateType, nowTs) NotifyUpdateWriter() } +func insertScreenNewUpdate(tx *TxWrap, screenId string) { + nowTs := time.Now().UnixMilli() + query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) + SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum` + tx.Exec(query, UpdateType_LineNew, nowTs, screenId) + query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) + SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived` + tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) + NotifyUpdateWriter() +} + +func insertScreenDelUpdate(tx *TxWrap, screenId string) { + query := `DELETE FROM screenupdate WHERE screenid = ?` + tx.Exec(query, screenId) + query = `DELETE FROM webptypos WHERE screenid = ?` + tx.Exec(query, screenId) + insertScreenUpdate(tx, screenId, UpdateType_ScreenDel) +} + func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateType string) { if screenId == "" { tx.SetErr(errors.New("invalid screen-update, screenid is empty")) @@ -2521,6 +2546,9 @@ func GetScreenUpdates(ctx context.Context, maxNum int) ([]*ScreenUpdateType, err } func RemoveScreenUpdate(ctx context.Context, updateId int64) error { + if updateId < 0 { + return nil // in-memory updates (not from DB) + } 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 072dd8af5..ae48f9ff5 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -81,17 +81,18 @@ const ( ) const ( - UpdateType_ScreenNew = "screen:new" - UpdateType_ScreenDel = "screen:del" - UpdateType_ScreenName = "screen:sharename" - UpdateType_LineNew = "line:new" - UpdateType_LineDel = "line:del" - UpdateType_LineRenderer = "line:renderer" - UpdateType_CmdStatus = "cmd:status" - UpdateType_CmdTermOpts = "cmd:termopts" - UpdateType_CmdDoneInfo = "cmd:doneinfo" - UpdateType_CmdRtnState = "cmd:rtnstate" - UpdateType_PtyPos = "pty:pos" + UpdateType_ScreenNew = "screen:new" + UpdateType_ScreenDel = "screen:del" + UpdateType_ScreenSelectedLine = "screen:selectedline" + UpdateType_ScreenName = "screen:sharename" + UpdateType_LineNew = "line:new" + UpdateType_LineDel = "line:del" + UpdateType_LineRenderer = "line:renderer" + UpdateType_CmdStatus = "cmd:status" + UpdateType_CmdTermOpts = "cmd:termopts" + UpdateType_CmdDoneInfo = "cmd:doneinfo" + UpdateType_CmdRtnState = "cmd:rtnstate" + UpdateType_PtyPos = "pty:pos" ) const MaxTzNameLen = 50 diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index add29e740..5cdd22e6d 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -100,6 +100,8 @@ type InfoMsgType struct { InfoTitle string `json:"infotitle"` InfoError string `json:"infoerror,omitempty"` InfoMsg string `json:"infomsg,omitempty"` + InfoMsgHtml bool `json:"infomsghtml,omitempty"` + WebShareLink bool `json:"websharelink,omitempty"` InfoComps []string `json:"infocomps,omitempty"` InfoCompsMore bool `json:"infocompssmore,omitempty"` InfoLines []string `json:"infolines,omitempty"` From 4c7d3f20796d9e486a71acc213d5d5fbc450b2e0 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 30 Mar 2023 20:55:27 -0700 Subject: [PATCH 333/397] add contentheight --- pkg/pcloud/pclouddata.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 456a14692..5c481e516 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -97,26 +97,28 @@ func webScreenFromScreen(s *sstore.ScreenType) (*WebShareScreenType, error) { } 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"` + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineType string `json:"linetype"` + ContentHeight int64 `json:"contentheight"` + 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) { 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, + LineId: line.LineId, + Ts: line.Ts, + LineNum: line.LineNum, + LineType: line.LineType, + ContentHeight: line.ContentHeight, + Renderer: line.Renderer, + Text: line.Text, + CmdId: line.CmdId, + Archived: line.Archived, } return rtn, nil } From a7faf2040f4992477c965c5407fb94fea6f33860 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Mar 2023 00:32:38 -0700 Subject: [PATCH 334/397] get contentheigh updates working --- pkg/cmdrunner/cmdrunner.go | 9 ++++++--- pkg/pcloud/pcloud.go | 8 ++++++-- pkg/sstore/dbops.go | 5 ++++- pkg/sstore/sstore.go | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e6adcc74c..c837b31bb 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1667,12 +1667,15 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot create viewkey: %v", err) } - viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) - webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} screen, err := sstore.GetScreenById(ctx, ids.ScreenId) if err != nil { return nil, fmt.Errorf("cannot get screen: %v", err) } + if shareName == "" { + shareName = screen.Name + } + viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) + webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} err = sstore.CanScreenWebShare(screen) if err != nil { return nil, err @@ -2226,7 +2229,7 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if heightVal > 10000 { return nil, fmt.Errorf("/line:setheight invalid height val (too large): %d", heightVal) } - err = sstore.UpdateLineHeight(ctx, lineId, heightVal) + err = sstore.UpdateLineHeight(ctx, ids.ScreenId, lineId, heightVal) if err != nil { return nil, fmt.Errorf("/line:setheight error updating height: %v", err) } diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index eb07022ea..9e42bfa77 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -257,12 +257,16 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* case sstore.UpdateType_LineDel: break - case sstore.UpdateType_LineRenderer: + case sstore.UpdateType_LineRenderer, sstore.UpdateType_LineContentHeight: 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 + if update.UpdateType == sstore.UpdateType_LineRenderer { + rtn.SVal = line.Renderer + } else if update.UpdateType == sstore.UpdateType_LineContentHeight { + rtn.IVal = line.ContentHeight + } case sstore.UpdateType_CmdStatus: _, cmd, err := sstore.GetLineCmdByLineId(ctx, update.ScreenId, update.LineId) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 761ba31e6..4b2b9058e 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1887,10 +1887,13 @@ func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { return nil } -func UpdateLineHeight(ctx context.Context, lineId string, heightVal int) error { +func UpdateLineHeight(ctx context.Context, screenId string, lineId string, heightVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE line SET contentheight = ? WHERE lineid = ?` tx.Exec(query, heightVal, lineId) + if isWebShare(tx, screenId) { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineContentHeight) + } return nil }) if txErr != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index ae48f9ff5..7963ae313 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -88,6 +88,7 @@ const ( UpdateType_LineNew = "line:new" UpdateType_LineDel = "line:del" UpdateType_LineRenderer = "line:renderer" + UpdateType_LineContentHeight = "line:contentheight" UpdateType_CmdStatus = "cmd:status" UpdateType_CmdTermOpts = "cmd:termopts" UpdateType_CmdDoneInfo = "cmd:doneinfo" From 5e5a6aa53f859fc4fc8ff0c567392483b8d22bd8 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Mar 2023 13:25:57 -0700 Subject: [PATCH 335/397] do synchronous webscreen delete --- pkg/cmdrunner/cmdrunner.go | 28 +++++++++++++++++++++++++++- pkg/pcloud/pcloud.go | 10 ++++++++++ pkg/sstore/dbops.go | 10 ++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index c837b31bb..8b2bd359e 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -176,6 +176,7 @@ func init() { registerCmdFn("client", ClientCommand) registerCmdFn("client:show", ClientShowCommand) registerCmdFn("client:set", ClientSetCommand) + registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) registerCmdFn("telemetry", TelemetryCommand) registerCmdFn("telemetry:on", TelemetryOnCommand) @@ -1691,11 +1692,26 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } infoWebShareLink = true } else { + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("cannot get screen: %v", err) + } + if screen == nil { + return nil, fmt.Errorf("screen not found") + } + if screen.ShareMode != sstore.ShareModeWeb { + return nil, fmt.Errorf("screen is not currently shared") + } + webUpdate := pcloud.MakeScreenDelUpdate(screen, ids.ScreenId) + err = pcloud.DoSyncWebUpdate(webUpdate) + if err != nil { + return nil, fmt.Errorf("error stopping webshare, error contacting prompt cloud server: %v", err) + } err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) if err != nil { return nil, fmt.Errorf("cannot stop web-sharing screen: %v", err) } - infoMsg = fmt.Sprintf("screen is no longer web shared") + infoMsg = fmt.Sprintf("screen websharing stopped") } screen, err := sstore.GetScreenById(ctx, ids.ScreenId) if err != nil { @@ -2749,6 +2765,16 @@ func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"show", "set"}, "or", false)) } +func ClientNotifyUpdateWriterCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + sstore.NotifyUpdateWriter() + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("notified update writer"), + }, + } + return update, nil +} + func boolToStr(v bool, trueStr string, falseStr string) string { if v { return trueStr diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 9e42bfa77..f75758e0d 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -202,6 +202,16 @@ func MakeScreenNewUpdate(screen *sstore.ScreenType, webShareOpts sstore.ScreenWe return rtn } +func MakeScreenDelUpdate(screen *sstore.ScreenType, screenId string) *WebShareUpdateType { + rtn := &WebShareUpdateType{ + ScreenId: screenId, + UpdateId: -1, + UpdateType: sstore.UpdateType_ScreenDel, + UpdateTs: time.Now().UnixMilli(), + } + return rtn +} + func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (*WebShareUpdateType, error) { rtn := &WebShareUpdateType{ ScreenId: update.ScreenId, diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 4b2b9058e..37caa7ebf 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2463,7 +2463,7 @@ func ScreenWebShareStop(ctx context.Context, screenId string) error { } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeLocal, "null", screenId) - insertScreenDelUpdate(tx, screenId) + handleScreenDelUpdate(tx, screenId) return nil }) } @@ -2494,12 +2494,18 @@ func insertScreenNewUpdate(tx *TxWrap, screenId string) { NotifyUpdateWriter() } -func insertScreenDelUpdate(tx *TxWrap, screenId string) { +func handleScreenDelUpdate(tx *TxWrap, screenId string) { query := `DELETE FROM screenupdate WHERE screenid = ?` tx.Exec(query, screenId) query = `DELETE FROM webptypos WHERE screenid = ?` tx.Exec(query, screenId) + // don't insert UpdateType_ScreenDel (we already processed it in cmdrunner) +} + +func insertScreenDelUpdate(tx *TxWrap, screenId string) { + handleScreenDelUpdate(tx, screenId) insertScreenUpdate(tx, screenId, UpdateType_ScreenDel) + // don't insert UpdateType_ScreenDel (we already processed it in cmdrunner) } func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateType string) { From f808c7b3628827a738e675c4cf346df18903e01b Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Mar 2023 18:15:51 -0700 Subject: [PATCH 336/397] better way to handle updates -- use a dedup across all updates, pack as many updates as possible into the packet --- pkg/dbutil/map.go | 18 ++++++ pkg/pcloud/pcloud.go | 119 ++++++++++++++++++++++++++++----------- pkg/pcloud/pclouddata.go | 23 +++++++- pkg/sstore/dbops.go | 36 +++++++++--- 4 files changed, 154 insertions(+), 42 deletions(-) diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 491f70372..76d1a09cf 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -12,6 +12,11 @@ type DBMappable interface { UseDBMap() } +type MapEntry[T any] struct { + Key string + Val T +} + type MapConverter interface { ToMap() map[string]interface{} FromMap(map[string]interface{}) bool @@ -88,6 +93,19 @@ func SelectMapsGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string return rtn } +func SelectSimpleMap[T any](tx *txwrap.TxWrap, query string, args ...interface{}) map[string]T { + var rtn []MapEntry[T] + tx.Select(&rtn, query, args...) + if len(rtn) == 0 { + return nil + } + rtnMap := make(map[string]T) + for _, entry := range rtn { + rtnMap[entry.Key] = entry.Val + } + return rtnMap +} + func MakeGenMap[T HasSimpleKey](arr []T) map[string]T { rtn := make(map[string]T) for _, val := range arr { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index f75758e0d..5ca5bbc1d 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -26,8 +26,14 @@ const PCloudEndpointVarName = "PCLOUD_ENDPOINT" const APIVersion = 1 const MaxPtyUpdateSize = (128 * 1024) const MaxUpdatesPerReq = 10 +const MaxUpdatesToDeDup = 1000 const MaxUpdateWriterErrors = 3 const PCloudDefaultTimeout = 5 * time.Second +const PCloudWebShareUpdateTimeout = 15 * time.Second + +// setting to 1M to be safe (max is 6M for API-GW + Lambda, but there is base64 encoding and upload time) +// we allow one extra update past this estimated size +const MaxUpdatePayloadSize = 1 * (1024 * 1024) const TelemetryUrl = "/telemetry" const NoTelemetryUrl = "/no-telemetry" @@ -366,27 +372,20 @@ type webShareResponseType struct { Data []*WebShareUpdateResponseType `json:"data"` } -func convertUpdates(updateArr []*sstore.ScreenUpdateType) []*WebShareUpdateType { - var webUpdates []*WebShareUpdateType - for _, update := range updateArr { - webUpdate, err := makeWebShareUpdate(context.Background(), update) - if err != nil || webUpdate == nil { - // log error (if there is one), remove update, and continue - if err != nil { - log.Printf("[pcloud] error create web-share update updateid:%d: %v", update.UpdateId, err) - } - if update.UpdateType == sstore.UpdateType_PtyPos { - 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 +func convertUpdate(update *sstore.ScreenUpdateType) *WebShareUpdateType { + webUpdate, err := makeWebShareUpdate(context.Background(), update) + if err != nil || webUpdate == nil { + if err != nil { + log.Printf("[pcloud] error create web-share update updateid:%d: %v", update.UpdateId, err) + } + // if err, or no web update created, remove the screenupdate + removeErr := sstore.RemoveScreenUpdate(context.Background(), update.UpdateId) + if removeErr != 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, removeErr) } - webUpdates = append(webUpdates, webUpdate) } - return webUpdates + return webUpdate } func DoSyncWebUpdate(webUpdate *WebShareUpdateType) error { @@ -423,7 +422,7 @@ func DoWebUpdates(webUpdates []*WebShareUpdateType) error { if err != nil { return fmt.Errorf("could not get authinfo for request: %v", err) } - ctx, cancelFn := context.WithTimeout(context.Background(), PCloudDefaultTimeout) + ctx, cancelFn := context.WithTimeout(context.Background(), PCloudWebShareUpdateTimeout) defer cancelFn() req, err := makeAuthPostReq(ctx, WebShareUpdateUrl, authInfo, webUpdates) if err != nil { @@ -477,9 +476,9 @@ func StartUpdateWriter() { func computeBackoff(numFailures int) time.Duration { switch numFailures { case 1: - return 100 * time.Millisecond + return 500 * time.Millisecond case 2: - return 1 * time.Second + return 2 * time.Second case 3: return 5 * time.Second case 4: @@ -493,6 +492,34 @@ func computeBackoff(numFailures int) time.Duration { } } +type updateKey struct { + ScreenId string + LineId string + UpdateType string +} + +func DeDupUpdates(ctx context.Context, updateArr []*sstore.ScreenUpdateType) ([]*sstore.ScreenUpdateType, error) { + var rtn []*sstore.ScreenUpdateType + var idsToDelete []int64 + umap := make(map[updateKey]bool) + for _, update := range updateArr { + key := updateKey{ScreenId: update.ScreenId, LineId: update.LineId, UpdateType: update.UpdateType} + if umap[key] { + idsToDelete = append(idsToDelete, update.UpdateId) + continue + } + umap[key] = true + rtn = append(rtn, update) + } + if len(idsToDelete) > 0 { + err := sstore.RemoveScreenUpdates(ctx, idsToDelete) + if err != nil { + return nil, fmt.Errorf("error trying to delete screenupdates: %v\n", err) + } + } + return rtn, nil +} + func runWebShareUpdateWriter() { defer func() { setUpdateWriterRunning(false) @@ -501,32 +528,58 @@ func runWebShareUpdateWriter() { numErrors := 0 numSendErrors := 0 for { + if numErrors > MaxUpdateWriterErrors { + log.Printf("[pcloud] update-writer, too many errors, exiting\n") + break + } time.Sleep(100 * time.Millisecond) - updateArr, err := sstore.GetScreenUpdates(context.Background(), MaxUpdatesPerReq) + fullUpdateArr, err := sstore.GetScreenUpdates(context.Background(), MaxUpdatesToDeDup) 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 - } + continue } - if len(updateArr) == 0 { - sstore.UpdateWriterCheckMoreData() + updateArr, err := DeDupUpdates(context.Background(), fullUpdateArr) + if err != nil { + log.Printf("[pcloud] error deduping screenupdates: %v", err) + time.Sleep(1 * time.Second) + numErrors++ continue } numErrors = 0 - webUpdates := convertUpdates(updateArr) - err = DoWebUpdates(webUpdates) + + var webUpdateArr []*WebShareUpdateType + totalSize := 0 + for _, update := range updateArr { + webUpdate := convertUpdate(update) + if webUpdate == nil { + continue + } + webUpdateArr = append(webUpdateArr, webUpdate) + totalSize += webUpdate.GetEstimatedSize() + if totalSize > MaxUpdatePayloadSize { + break + } + } + if len(webUpdateArr) == 0 { + sstore.UpdateWriterCheckMoreData() + continue + } + err = DoWebUpdates(webUpdateArr) if err != nil { numSendErrors++ backoffTime := computeBackoff(numSendErrors) - log.Printf("[pcloud] error processing web-updates (backoff=%v): %v\n", backoffTime, err) + log.Printf("[pcloud] error processing %d web-updates (backoff=%v): %v\n", len(webUpdateArr), backoffTime, err) time.Sleep(backoffTime) continue } - log.Printf("[pcloud] sent %d web-updates\n", len(updateArr)) + log.Printf("[pcloud] sent %d web-updates\n", len(webUpdateArr)) + var debugStrs []string + for _, webUpdate := range webUpdateArr { + debugStrs = append(debugStrs, webUpdate.String()) + } + log.Printf("[pcloud] updates: %s\n", strings.Join(debugStrs, " ")) numSendErrors = 0 } } diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 5c481e516..4fd023c9d 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -2,6 +2,7 @@ package pcloud import ( "context" + "encoding/json" "fmt" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -40,6 +41,26 @@ type WebShareUpdateType struct { TermOpts *sstore.TermOpts `json:"termopts,omitempty"` } +const EstimatedSizePadding = 100 + +func (update *WebShareUpdateType) GetEstimatedSize() int { + barr, _ := json.Marshal(update) + return len(barr) + 100 +} + +func (update *WebShareUpdateType) String() string { + var idStr string + if update.LineId != "" && update.ScreenId != "" { + idStr = fmt.Sprintf("%s:%s", update.ScreenId[0:8], update.LineId[0:8]) + } else if update.ScreenId != "" { + idStr = update.ScreenId[0:8] + } + if update.UpdateType == sstore.UpdateType_PtyPos && update.PtyData != nil { + return fmt.Sprintf("ptydata[%s][%d:%d]", idStr, update.PtyData.PtyPos, len(update.PtyData.Data)) + } + return fmt.Sprintf("%s[%s]", update.UpdateType, idStr) +} + type WebShareUpdateResponseType struct { UpdateId int64 `json:"updateid"` Success bool `json:"success"` @@ -105,7 +126,6 @@ type WebShareLineType struct { 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) { @@ -118,7 +138,6 @@ func webLineFromLine(line *sstore.LineType) (*WebShareLineType, error) { Renderer: line.Renderer, Text: line.Text, CmdId: line.CmdId, - Archived: line.Archived, } return rtn, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 37caa7ebf..634be10b8 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "strconv" "strings" "sync" @@ -23,7 +24,6 @@ const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lin const DefaultMaxHistoryItems = 1000 var updateWriterCVar = sync.NewCond(&sync.Mutex{}) -var updateWriterMoreData = false var WebScreenPtyPosLock = &sync.Mutex{} var WebScreenPtyPosDelIntent = make(map[string]bool) // map[screenid + ":" + lineid] -> bool @@ -57,18 +57,25 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { } func NotifyUpdateWriter() { - updateWriterCVar.L.Lock() - defer updateWriterCVar.L.Unlock() - updateWriterMoreData = true - updateWriterCVar.Signal() + // must happen in a goroutine to prevent deadlock. + // update-writer holds this lock while reading from the DB. we can't be holding the DB lock while calling this! + go func() { + updateWriterCVar.L.Lock() + defer updateWriterCVar.L.Unlock() + updateWriterCVar.Signal() + }() } func UpdateWriterCheckMoreData() { updateWriterCVar.L.Lock() defer updateWriterCVar.L.Unlock() for { - if updateWriterMoreData { - updateWriterMoreData = false + updateCount, err := CountScreenUpdates(context.Background()) + if err != nil { + log.Printf("ERROR getting screen update count (sleeping): %v", err) + // will just lead to a Wait() + } + if updateCount > 0 { break } updateWriterCVar.Wait() @@ -2565,6 +2572,21 @@ func RemoveScreenUpdate(ctx context.Context, updateId int64) error { }) } +func CountScreenUpdates(ctx context.Context) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := `SELECT count(*) FROM screenupdate` + return tx.GetInt(query), nil + }) +} + +func RemoveScreenUpdates(ctx context.Context, updateIds []int64) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `DELETE FROM screenupdate WHERE updateid IN (SELECT value FROM json_each(?))` + tx.Exec(query, quickJsonArr(updateIds)) + return nil + }) +} + func MaybeInsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { return WithTx(ctx, func(tx *TxWrap) error { if !isWebShare(tx, screenId) { From 59fd6299208ab6f8546b5b59fdfb01a8a6a7baab Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 2 Apr 2023 00:20:05 -0700 Subject: [PATCH 337/397] small updates webshare/archive interaction. --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/sstore/dbops.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 8b2bd359e..035d90ee6 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -556,7 +556,7 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) log.Printf("unarchive screen %s\n", screenId) err = sstore.UnArchiveScreen(ctx, ids.SessionId, screenId) if err != nil { - return nil, fmt.Errorf("/screen:archive cannot re-open screen: %v", err) + return nil, fmt.Errorf("/screen:archive cannot un-archive screen: %v", err) } screen, err := sstore.GetScreenById(ctx, screenId) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 634be10b8..3ddd7c94a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1037,6 +1037,9 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda if !tx.Exists(query, sessionId, screenId) { return fmt.Errorf("cannot close screen (not found)") } + if 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 { @@ -2435,6 +2438,9 @@ func CanScreenWebShare(screen *ScreenType) error { if screen.ShareMode != ShareModeLocal { return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", screen.ShareMode) } + if screen.Archived { + return fmt.Errorf("screen cannot be shared, must un-archive before sharing") + } return nil } From 0a16bb21819a36c2554616da64d64752d20e49c4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 01:37:36 -0700 Subject: [PATCH 338/397] add two new fields to remotetype for better messages on client --- pkg/remote/remote.go | 12 ++++++++++++ pkg/sstore/sstore.go | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 1129b9495..225c5f72d 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -97,6 +97,7 @@ type MShellProc struct { ServerProc *shexec.ClientProc UName string Err error + ErrNoInitPk bool ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc @@ -135,6 +136,8 @@ type RemoteRuntimeState struct { InstallStatus string `json:"installstatus"` InstallErrorStr string `json:"installerrorstr,omitempty"` NeedsMShellUpgrade bool `json:"needsmshellupgrade,omitempty"` + NoInitPk bool `json:"noinitpk,omitempty"` + AuthType string `json:"authtype,omitempty"` ConnectMode string `json:"connectmode"` AutoInstall bool `json:"autoinstall"` Archived bool `json:"archived,omitempty"` @@ -513,6 +516,11 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { InstallStatus: msh.InstallStatus, NeedsMShellUpgrade: msh.NeedsMShellUpgrade, Local: msh.Remote.Local, + NoInitPk: msh.ErrNoInitPk, + AuthType: sstore.RemoteAuthTypeNone, + } + if msh.Remote.SSHOpts != nil { + state.AuthType = msh.Remote.SSHOpts.GetAuthType() } if msh.Remote.RemoteOpts != nil { optsCopy := *msh.Remote.RemoteOpts @@ -1135,6 +1143,7 @@ func (msh *MShellProc) Launch(interactive bool) { defer makeClientCancelFn() msh.WithLock(func() { msh.Err = nil + msh.ErrNoInitPk = false msh.Status = StatusConnecting msh.MakeClientCancelFn = makeClientCancelFn go msh.NotifyRemoteUpdate() @@ -1145,6 +1154,9 @@ func (msh *MShellProc) Launch(interactive bool) { var stateBaseHash string msh.WithLock(func() { msh.MakeClientCancelFn = nil + if initPk == nil { + msh.ErrNoInitPk = true + } if initPk != nil { msh.UName = initPk.UName mshellVersion = initPk.Version diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 7963ae313..6715ce39b 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -54,6 +54,13 @@ const ( CmdStatusWaiting = "waiting" ) +const ( + RemoteAuthTypeNone = "none" + RemoteAuthTypePassword = "password" + RemoteAuthTypeKey = "key" + RemoteAuthTypeKeyPassword = "key+password" +) + const ( ShareModeLocal = "local" ShareModeWeb = "web" @@ -793,6 +800,19 @@ type SSHOpts struct { SSHPassword string `json:"sshpassword,omitempty"` } +func (opts SSHOpts) GetAuthType() string { + if opts.SSHPassword != "" && opts.SSHIdentity != "" { + return RemoteAuthTypeKeyPassword + } + if opts.SSHIdentity != "" { + return RemoteAuthTypeKey + } + if opts.SSHPassword != "" { + return RemoteAuthTypePassword + } + return RemoteAuthTypeNone +} + type RemoteOptsType struct { Color string `json:"color"` } From faadfdffaa3bd4679ffa63694d09a7744476184e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 15:55:36 -0700 Subject: [PATCH 339/397] new remoteview type to hook into remoteview window --- pkg/cmdrunner/cmdrunner.go | 40 ++++++++++++++++++++++++++------------ pkg/sstore/updatebus.go | 28 ++++++++++++++------------ 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 035d90ee6..60f3336b6 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -727,7 +727,7 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) mshell := ids.Remote.MShell go mshell.RunInstall() return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -741,7 +741,7 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke mshell := ids.Remote.MShell go mshell.CancelInstall() return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -754,7 +754,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } go ids.Remote.MShell.Launch(true) return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -768,7 +768,7 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy force := resolveBool(pk.Kwargs["force"], false) go ids.Remote.MShell.Disconnect(force) return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, }, nil @@ -782,7 +782,7 @@ func makeRemoteEditUpdate_new(err error) sstore.UpdatePacket { redit.ErrorStr = err.Error() } update := sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ RemoteEdit: redit, }, } @@ -809,7 +809,7 @@ func makeRemoteEditUpdate_edit(ids resolvedIds, err error) sstore.UpdatePacket { redit.ErrorStr = err.Error() } update := sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ RemoteEdit: redit, }, } @@ -836,7 +836,7 @@ type RemoteEditArgs struct { EditMap map[string]interface{} } -func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteEditArgs, error) { +func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal bool) (*RemoteEditArgs, error) { var canonicalName string var sshOpts *sstore.SSHOpts var isSudo bool @@ -943,18 +943,27 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType) (*RemoteE editMap[sstore.RemoteField_Alias] = alias } if connectMode != "" { + if isLocal { + return nil, fmt.Errorf("Cannot edit connect mode for 'local' remote") + } editMap[sstore.RemoteField_ConnectMode] = connectMode } if _, found := pk.Kwargs[sstore.RemoteField_AutoInstall]; found { editMap[sstore.RemoteField_AutoInstall] = autoInstall } if _, found := pk.Kwargs["key"]; found { + if isLocal { + return nil, fmt.Errorf("Cannot edit ssh key file for 'local' remote") + } editMap[sstore.RemoteField_SSHKey] = keyFile } if _, found := pk.Kwargs[sstore.RemoteField_Color]; found { editMap[sstore.RemoteField_Color] = color } if _, found := pk.Kwargs["password"]; found && pk.Kwargs["password"] != PasswordUnchangedSentinel { + if isLocal { + return nil, fmt.Errorf("Cannot edit ssh password for 'local' remote") + } editMap[sstore.RemoteField_SSHPassword] = sshPassword } @@ -978,7 +987,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if visualEdit && !isSubmitted && len(pk.Args) == 0 { return makeRemoteEditUpdate_new(nil), nil } - editArgs, err := parseRemoteEditArgs(true, pk) + editArgs, err := parseRemoteEditArgs(true, pk, false) if err != nil { return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("/remote:new %v", err)) } @@ -1004,7 +1013,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } // SUCCESS return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: r.RemoteId, }, }, nil @@ -1017,7 +1026,7 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } visualEdit := resolveBool(pk.Kwargs["visual"], false) isSubmitted := resolveBool(pk.Kwargs["submit"], false) - editArgs, err := parseRemoteEditArgs(false, pk) + editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal()) if err != nil { return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new %v", err)) } @@ -1031,6 +1040,13 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if err != nil { return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err)) } + if visualEdit { + return sstore.ModelUpdate{ + RemoteView: &sstore.RemoteViewType{ + PtyRemoteId: ids.Remote.RemoteCopy.RemoteId, + }, + }, nil + } update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("remote %q updated", ids.Remote.DisplayName), @@ -1047,7 +1063,7 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } state := ids.Remote.RState return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ PtyRemoteId: state.RemoteId, }, }, nil @@ -1066,7 +1082,7 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) buf.WriteString(fmt.Sprintf("%-12s %-5s %8s %s\n", rstate.Status, rstate.RemoteType, rstate.RemoteId[0:8], name)) } return sstore.ModelUpdate{ - Info: &sstore.InfoMsgType{ + RemoteView: &sstore.RemoteViewType{ RemoteShowAll: true, }, }, nil diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 5cdd22e6d..ae505aab6 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -49,12 +49,19 @@ type ModelUpdate struct { SelectedBookmark string `json:"selectedbookmark,omitempty"` HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` ClientData *ClientData `json:"clientdata,omitempty"` + RemoteView *RemoteViewType `json:"remoteview,omitempty"` } func (ModelUpdate) UpdateType() string { return ModelUpdateStr } +type RemoteViewType struct { + RemoteShowAll bool `json:"remoteshowall,omitempty"` + PtyRemoteId string `json:"ptyremoteid,omitempty"` + RemoteEdit *RemoteEditType `json:"remoteedit,omitempty"` +} + func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { modelUpdate, ok := update.(ModelUpdate) if !ok { @@ -97,18 +104,15 @@ type RemoteEditType struct { } type InfoMsgType struct { - InfoTitle string `json:"infotitle"` - InfoError string `json:"infoerror,omitempty"` - InfoMsg string `json:"infomsg,omitempty"` - InfoMsgHtml bool `json:"infomsghtml,omitempty"` - WebShareLink bool `json:"websharelink,omitempty"` - InfoComps []string `json:"infocomps,omitempty"` - InfoCompsMore bool `json:"infocompssmore,omitempty"` - InfoLines []string `json:"infolines,omitempty"` - TimeoutMs int64 `json:"timeoutms,omitempty"` - PtyRemoteId string `json:"ptyremoteid,omitempty"` - RemoteShowAll bool `json:"remoteshowall,omitempty"` - RemoteEdit *RemoteEditType `json:"remoteedit,omitempty"` + InfoTitle string `json:"infotitle"` + InfoError string `json:"infoerror,omitempty"` + InfoMsg string `json:"infomsg,omitempty"` + InfoMsgHtml bool `json:"infomsghtml,omitempty"` + WebShareLink bool `json:"websharelink,omitempty"` + InfoComps []string `json:"infocomps,omitempty"` + InfoCompsMore bool `json:"infocompssmore,omitempty"` + InfoLines []string `json:"infolines,omitempty"` + TimeoutMs int64 `json:"timeoutms,omitempty"` } type HistoryInfoType struct { From 936500a18e7b3b9de71aa77468afb8322f5a5036 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 18:57:38 -0700 Subject: [PATCH 340/397] add client deadline and update remoteruntimestate --- pkg/remote/remote.go | 74 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 225c5f72d..9336836f5 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -38,6 +38,7 @@ const CircBufSize = 64 * 1024 const RemoteTermRows = 8 const RemoteTermCols = 80 const PtyReadBufSize = 100 +const RemoteConnectTimeout = 15 * time.Second const MShellServerCommandFmt = ` PATH=$PATH:~/.mshell; @@ -101,6 +102,7 @@ type MShellProc struct { ControllingPty *os.File PtyBuffer *circbuf.Buffer MakeClientCancelFn context.CancelFunc + MakeClientDeadline *time.Time StateMap map[string]*packet.ShellState // sha1->state CurrentState string // sha1 NumTryConnect int @@ -132,6 +134,7 @@ type RemoteRuntimeState struct { RemoteVars map[string]string `json:"remotevars"` DefaultFeState *sstore.FeStateType `json:"defaultfestate"` Status string `json:"status"` + ConnectTimeout int `json:"connecttimeout,omitempty"` ErrorStr string `json:"errorstr,omitempty"` InstallStatus string `json:"installstatus"` InstallErrorStr string `json:"installerrorstr,omitempty"` @@ -534,6 +537,12 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { } if msh.Status == StatusConnecting { state.WaitingForPassword = msh.isWaitingForPassword_nolock() + if msh.MakeClientDeadline != nil { + state.ConnectTimeout = int((*msh.MakeClientDeadline).Sub(time.Now()) / time.Second) + if state.ConnectTimeout < 0 { + state.ConnectTimeout = 0 + } + } } vars := msh.Remote.StateVars if vars == nil { @@ -668,9 +677,49 @@ func SendRemoteInput(pk *scpacket.RemoteInputPacketType) error { if err != nil { return fmt.Errorf("writing to pty: %v", err) } + msh.resetClientDeadline() return nil } +func (msh *MShellProc) getClientDeadline() *time.Time { + msh.Lock.Lock() + defer msh.Lock.Unlock() + return msh.MakeClientDeadline +} + +func (msh *MShellProc) resetClientDeadline() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + if msh.Status != StatusConnecting { + return + } + deadline := msh.MakeClientDeadline + if deadline == nil { + return + } + newDeadline := time.Now().Add(RemoteConnectTimeout) + msh.MakeClientDeadline = &newDeadline +} + +func (msh *MShellProc) watchClientDeadlineTime() { + for { + time.Sleep(1 * time.Second) + status := msh.GetStatus() + if status != StatusConnecting { + break + } + deadline := msh.getClientDeadline() + if deadline == nil { + break + } + if time.Now().After(*deadline) { + msh.Disconnect(false) + break + } + go msh.NotifyRemoteUpdate() + } +} + func convertSSHOpts(opts *sstore.SSHOpts) shexec.SSHOpts { if opts == nil || opts.Local { opts = &sstore.SSHOpts{} @@ -977,6 +1026,8 @@ func (msh *MShellProc) RunInstall() { msh.InstallStatus = StatusDisconnected msh.InstallCancelFn = nil msh.NeedsMShellUpgrade = false + msh.Status = StatusDisconnected + msh.Err = nil }) msh.WriteToPtyBuffer("successfully installed mshell %s to ~/.mshell\n", scbase.MShellVersion) go msh.NotifyRemoteUpdate() @@ -1146,14 +1197,22 @@ func (msh *MShellProc) Launch(interactive bool) { msh.ErrNoInitPk = false msh.Status = StatusConnecting msh.MakeClientCancelFn = makeClientCancelFn + deadlineTime := time.Now().Add(RemoteConnectTimeout) + msh.MakeClientDeadline = &deadlineTime go msh.NotifyRemoteUpdate() }) + go msh.watchClientDeadlineTime() cproc, initPk, err := shexec.MakeClientProc(makeClientCtx, ecmd) // TODO check if initPk.State is not nil var mshellVersion string var stateBaseHash string + var hitDeadline bool msh.WithLock(func() { msh.MakeClientCancelFn = nil + if time.Now().After(*msh.MakeClientDeadline) { + hitDeadline = true + } + msh.MakeClientDeadline = nil if initPk == nil { msh.ErrNoInitPk = true } @@ -1177,11 +1236,16 @@ func (msh *MShellProc) Launch(interactive bool) { // no notify here, because we'll call notify in either case below }) if err == context.Canceled { - msh.WriteToPtyBuffer("*forced disconnection\n") - msh.WithLock(func() { - msh.Status = StatusDisconnected - go msh.NotifyRemoteUpdate() - }) + if hitDeadline { + msh.WriteToPtyBuffer("*connect timeout\n") + msh.setErrorStatus(errors.New("connect timeout")) + } else { + msh.WriteToPtyBuffer("*forced disconnection\n") + msh.WithLock(func() { + msh.Status = StatusDisconnected + go msh.NotifyRemoteUpdate() + }) + } return } if err == nil && semver.MajorMinor(mshellVersion) != semver.MajorMinor(scbase.MShellVersion) { From c7024b0ef5a28c73f82ad50818861a7920510018 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 21:31:40 -0700 Subject: [PATCH 341/397] switch to 'connection' --- pkg/remote/remote.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 9336836f5..c827da494 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -653,7 +653,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc { PendingStateCmds: make(map[string]base.CommandKey), StateMap: make(map[string]*packet.ShellState), } - rtn.WriteToPtyBuffer("console for remote [%s]\n", r.GetName()) + rtn.WriteToPtyBuffer("console for connection [%s]\n", r.GetName()) return rtn } From 6e6fe4d4bf15f15a86c2e2c5ff6ef1a8ecf9ef76 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 22:15:43 -0700 Subject: [PATCH 342/397] auto-install and reconnect flow working --- pkg/remote/remote.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index c827da494..007ab4bb5 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -501,6 +501,16 @@ func (msh *MShellProc) IsSudo() bool { return msh.Remote.RemoteSudo } +func (msh *MShellProc) tryAutoInstall() { + msh.Lock.Lock() + defer msh.Lock.Unlock() + if !msh.Remote.AutoInstall || !msh.NeedsMShellUpgrade || msh.InstallErr != nil { + return + } + msh.writeToPtyBuffer_nolock("trying auto-install\n") + go msh.RunInstall() +} + func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -1022,15 +1032,21 @@ func (msh *MShellProc) RunInstall() { msh.setInstallErrorStatus(statusErr) return } + var connectMode string msh.WithLock(func() { msh.InstallStatus = StatusDisconnected msh.InstallCancelFn = nil msh.NeedsMShellUpgrade = false msh.Status = StatusDisconnected msh.Err = nil + connectMode = msh.Remote.ConnectMode }) msh.WriteToPtyBuffer("successfully installed mshell %s to ~/.mshell\n", scbase.MShellVersion) go msh.NotifyRemoteUpdate() + if connectMode == sstore.ConnectModeStartup || connectMode == sstore.ConnectModeAuto { + // the install was successful, and we don't have a manual connect mode, try to connect + go msh.Launch(true) + } return } @@ -1254,6 +1270,7 @@ func (msh *MShellProc) Launch(interactive bool) { if err != nil { msh.setErrorStatus(err) msh.WriteToPtyBuffer("*error connecting to remote: %v\n", err) + go msh.tryAutoInstall() return } msh.updateRemoteStateVars(context.Background(), msh.RemoteId, initPk) From c9e52b01485a093f7a1a0b0bdd6091b72ff5e3f1 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 3 Apr 2023 22:33:10 -0700 Subject: [PATCH 343/397] fix dev url to be index-dev.html --- pkg/cmdrunner/cmdrunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 60f3336b6..a48e3eb9d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -52,7 +52,7 @@ const MaxCommandLen = 4096 const MaxSignalLen = 12 const MaxSignalNum = 64 const MaxEvalDepth = 5 -const DevWebScreenUrlFmt = "http://devtest.getprompt.com:9001/static/index.html?screenid=%s&viewkey=%s" +const DevWebScreenUrlFmt = "http://devtest.getprompt.com:9001/static/index-dev.html?screenid=%s&viewkey=%s" const ProdWebScreenUrlFmt = "https://share.getprompt.dev/s/%s?viewkey=%s" var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} From 1d5fe3c5beb54e0d173eb531ea53a049c5d8bfc9 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 09:15:31 -0700 Subject: [PATCH 344/397] bump version --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index e4e2dc13a..2684121c8 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.1.7" +const PromptVersion = "v0.2.0" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From f6c6a40aba3acbfb663c912c6efe83a3fac631f0 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 21:52:20 -0700 Subject: [PATCH 345/397] add client:accepttos command --- pkg/cmdrunner/cmdrunner.go | 22 ++++++++++++++++++++++ pkg/cmdrunner/shparse.go | 2 ++ pkg/sstore/dbops.go | 4 ++-- pkg/sstore/sstore.go | 3 ++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a48e3eb9d..e2728cffe 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -177,6 +177,7 @@ func init() { registerCmdFn("client:show", ClientShowCommand) registerCmdFn("client:set", ClientSetCommand) registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) + registerCmdFn("client:accepttos", ClientAcceptTosCommand) registerCmdFn("telemetry", TelemetryCommand) registerCmdFn("telemetry:on", TelemetryOnCommand) @@ -2798,6 +2799,27 @@ func boolToStr(v bool, trueStr string, falseStr string) string { return falseStr } +func ClientAcceptTosCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v", err) + } + clientOpts := clientData.ClientOpts + clientOpts.AcceptedTos = time.Now().UnixMilli() + err = sstore.SetClientOpts(ctx, clientOpts) + if err != nil { + return nil, fmt.Errorf("error updating client data: %v", err) + } + clientData, err = sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) + } + update := sstore.ModelUpdate{ + ClientData: clientData, + } + return update, nil +} + func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 70b62d71e..fcd9c293c 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -77,6 +77,8 @@ func SubMetaCmd(cmd string) string { return "eval" case "export": return "setenv" + case "connection": + return "remote" default: return cmd } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3ddd7c94a..8397b9a39 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2499,10 +2499,10 @@ func insertScreenUpdate(tx *TxWrap, screenId string, updateType string) { func insertScreenNewUpdate(tx *TxWrap, screenId string) { nowTs := time.Now().UnixMilli() query := `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum` + SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum DESC` tx.Exec(query, UpdateType_LineNew, nowTs, screenId) query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived` + SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived ORDER BY l.linenum DESC` tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) NotifyUpdateWriter() } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 6715ce39b..6b706c148 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -212,7 +212,8 @@ func (tdata *TelemetryData) Scan(val interface{}) error { } type ClientOptsType struct { - NoTelemetry bool `json:"notelemetry,omitempty"` + NoTelemetry bool `json:"notelemetry,omitempty"` + AcceptedTos int64 `json:"acceptedtos,omitempty"` } type FeOptsType struct { From 0ee3af71bdf79ee6e43eca0382aabf9049da9440 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 22:28:52 -0700 Subject: [PATCH 346/397] update sharename --- pkg/cmdrunner/cmdrunner.go | 12 +++++++++++- pkg/sstore/dbops.go | 9 +++++++++ pkg/sstore/sstore.go | 5 ++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e2728cffe..1b97fd2c5 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -620,6 +620,16 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss varsUpdated = append(varsUpdated, "name") setNonAnchor = true } + if pk.Kwargs["sharename"] != "" { + shareName := pk.Kwargs["sharename"] + err = validateShareName(shareName) + if err != nil { + return nil, err + } + updateMap[sstore.ScreenField_ShareName] = shareName + varsUpdated = append(varsUpdated, "sharename") + setNonAnchor = true + } if pk.Kwargs["tabcolor"] != "" { color := pk.Kwargs["tabcolor"] err = validateColor(color, "screen tabcolor") @@ -679,7 +689,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line"}, "or", false)) + return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line", "sharename"}, "or", false)) } screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8397b9a39..517a94625 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1707,6 +1707,7 @@ const ( ScreenField_TabColor = "tabcolor" // string ScreenField_PTerm = "pterm" // string ScreenField_Name = "name" // string + ScreenField_ShareName = "sharename" // string ) func UpdateScreen(ctx context.Context, screenId string, editMap map[string]interface{}) (*ScreenType, error) { @@ -1746,6 +1747,14 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter query = `UPDATE screen SET name = ? WHERE screenid = ?` tx.Exec(query, name, screenId) } + if shareName, found := editMap[ScreenField_ShareName]; found { + if !isWebShare(tx, screenId) { + return fmt.Errorf("cannot set sharename, screen is not web-shared") + } + query = `UPDATE screen SET webshareopts = json_set(webshareopts, '$.sharename', ?) WHERE screenid = ?` + tx.Exec(query, shareName, screenId) + insertScreenUpdate(tx, screenId, UpdateType_ScreenName) + } return nil }) if txErr != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 6b706c148..825ccc15d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -386,9 +386,8 @@ type ScreenLinesType struct { func (ScreenLinesType) UseDBMap() {} type ScreenWebShareOpts struct { - ShareName string `json:"sharename"` - ViewKey string `json:"viewkey"` - SharedRemotes []string `json:"sharedremotes"` + ShareName string `json:"sharename"` + ViewKey string `json:"viewkey"` } type ScreenType struct { From fa8db1d80ef259c294d36fd41447c8f101d92872 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 23:24:08 -0700 Subject: [PATCH 347/397] limit screen sharing to 50 commands --- pkg/sstore/dbops.go | 22 ++++++---------------- pkg/sstore/sstore.go | 3 +++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 517a94625..3d6f9833f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2437,22 +2437,6 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem }) } -func CanScreenWebShare(screen *ScreenType) error { - if screen == nil { - return fmt.Errorf("cannot share screen, not found") - } - if screen.ShareMode == ShareModeWeb { - return fmt.Errorf("screen is already shared to web") - } - if screen.ShareMode != ShareModeLocal { - return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", screen.ShareMode) - } - if screen.Archived { - return fmt.Errorf("screen cannot be shared, must un-archive before sharing") - } - return nil -} - func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenWebShareOpts) error { return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE screenid = ?` @@ -2466,6 +2450,12 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW if shareMode != ShareModeLocal { return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", shareMode) } + query = `SELECT count(*) FROM line WHERE screenid = ? AND NOT archived` + lineCount := tx.GetInt(query, screenId) + if lineCount > MaxWebShareLineCount { + return fmt.Errorf("screen cannot be shared, limited to a maximum of %d lines", MaxWebShareLineCount) + go UpdateCurrentActivity(context.Background(), ActivityUpdate{WebShareLimit: 1}) + } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeWeb, quickJson(shareOpts), screenId) insertScreenNewUpdate(tx, screenId) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 825ccc15d..70df63582 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -33,6 +33,7 @@ const LineTypeText = "text" const LineNoHeight = -1 const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" +const MaxWebShareLineCount = 50 const DefaultSessionName = "default" const LocalRemoteAlias = "local" @@ -177,6 +178,7 @@ type ActivityUpdate struct { HistoryView int BookmarksView int NumConns int + WebShareLimit int BuildTime string } @@ -201,6 +203,7 @@ type TelemetryData struct { HistoryView int `json:"historyview,omitempty"` BookmarksView int `json:"bookmarksview,omitempty"` NumConns int `json:"numconns"` + WebShareLimit int `json:"websharelimit,omitempty"` } func (tdata TelemetryData) Value() (driver.Value, error) { From 174a4ef8fd4171bd5a08e255a13b9ef1b2445d90 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 23:38:34 -0700 Subject: [PATCH 348/397] add limits to websharing --- pkg/cmdrunner/cmdrunner.go | 2 +- pkg/sstore/dbops.go | 54 +++++++++++++++++++++++++++++++++----- pkg/sstore/sstore.go | 1 + 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1b97fd2c5..1b1a72b0d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1704,7 +1704,7 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} - err = sstore.CanScreenWebShare(screen) + err = sstore.CanScreenWebShare(ctx, screen) if err != nil { return nil, err } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3d6f9833f..c4ca7cc9c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -2437,6 +2437,54 @@ func PurgeHistoryByIds(ctx context.Context, historyIds []string) ([]*HistoryItem }) } +func CountScreenWebShares(ctx context.Context) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := `SELECT count(*) FROM screen WHERE sharemode = ?` + count := tx.GetInt(query, ShareModeWeb) + return count, nil + }) +} + +func CountScreenLines(ctx context.Context, screenId string) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := `SELECT count(*) FROM line WHERE screenid = ? AND NOT archived` + lineCount := tx.GetInt(query, screenId) + return lineCount, nil + }) +} + +func CanScreenWebShare(ctx context.Context, screen *ScreenType) error { + if screen == nil { + return fmt.Errorf("cannot share screen, not found") + } + if screen.ShareMode == ShareModeWeb { + return fmt.Errorf("screen is already shared to web") + } + if screen.ShareMode != ShareModeLocal { + return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", screen.ShareMode) + } + if screen.Archived { + return fmt.Errorf("screen cannot be shared, must un-archive before sharing") + } + webShareCount, err := CountScreenWebShares(ctx) + if err != nil { + return fmt.Errorf("screen cannot be share: error getting webshare count: %v", err) + } + if webShareCount >= MaxWebShareScreenCount { + go UpdateCurrentActivity(context.Background(), ActivityUpdate{WebShareLimit: 1}) + return fmt.Errorf("screen cannot be shared, limited to a maximum of %d shared screen(s)", MaxWebShareScreenCount) + } + lineCount, err := CountScreenLines(ctx, screen.ScreenId) + if err != nil { + return fmt.Errorf("screen cannot be share: error getting screen line count: %v", err) + } + if lineCount > MaxWebShareLineCount { + go UpdateCurrentActivity(context.Background(), ActivityUpdate{WebShareLimit: 1}) + return fmt.Errorf("screen cannot be shared, limited to a maximum of %d lines", MaxWebShareLineCount) + } + return nil +} + func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenWebShareOpts) error { return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE screenid = ?` @@ -2450,12 +2498,6 @@ func ScreenWebShareStart(ctx context.Context, screenId string, shareOpts ScreenW if shareMode != ShareModeLocal { return fmt.Errorf("screen cannot be shared, invalid current share mode %q (must be local)", shareMode) } - query = `SELECT count(*) FROM line WHERE screenid = ? AND NOT archived` - lineCount := tx.GetInt(query, screenId) - if lineCount > MaxWebShareLineCount { - return fmt.Errorf("screen cannot be shared, limited to a maximum of %d lines", MaxWebShareLineCount) - go UpdateCurrentActivity(context.Background(), ActivityUpdate{WebShareLimit: 1}) - } query = `UPDATE screen SET sharemode = ?, webshareopts = ? WHERE screenid = ?` tx.Exec(query, ShareModeWeb, quickJson(shareOpts), screenId) insertScreenNewUpdate(tx, screenId) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 70df63582..3bbad9e3a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -34,6 +34,7 @@ const LineNoHeight = -1 const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const MaxWebShareLineCount = 50 +const MaxWebShareScreenCount = 5 const DefaultSessionName = "default" const LocalRemoteAlias = "local" From 6c4a8e01df1e4efadd8dc25d5ea24ab2db4287e1 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 4 Apr 2023 23:44:47 -0700 Subject: [PATCH 349/397] dont log start of new ws conn --- pkg/scws/scws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index 97694ddc9..b61fabc81 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -190,7 +190,7 @@ func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error log.Printf("[ws %s] watchscreen %s/%s\n", ws.ClientId, wsPk.SessionId, wsPk.ScreenId) } if wsPk.Connect { - log.Printf("[ws %s] watchscreen connect\n", ws.ClientId) + // log.Printf("[ws %s] watchscreen connect\n", ws.ClientId) err := ws.handleConnection() if err != nil { return fmt.Errorf("connect: %w", err) From bc89309cfe3d16edc61f1e556cdd295d6025b39b Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 5 Apr 2023 00:25:22 -0700 Subject: [PATCH 350/397] fix nil ptr in /line:view --- pkg/cmdrunner/cmdrunner.go | 24 ++++++++++++++++-------- pkg/sstore/sstore.go | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 1b1a72b0d..180f2dec7 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -2332,10 +2332,16 @@ func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, fmt.Errorf("/line:view invalid session arg: %v", err) } + if sessionId == "" { + return nil, fmt.Errorf("/line:view no session found") + } screenRItem, err := resolveSessionScreen(ctx, sessionId, screenArg, "") if err != nil { return nil, fmt.Errorf("/line:view invalid screen arg: %v", err) } + if screenRItem == nil { + return nil, fmt.Errorf("/line:view no screen found") + } screen, err := sstore.GetScreenById(ctx, screenRItem.Id) if err != nil { return nil, fmt.Errorf("/line:view could not get screen: %v", err) @@ -2348,15 +2354,17 @@ func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if err != nil { return nil, err } - updateMap := make(map[string]interface{}) - updateMap[sstore.ScreenField_SelectedLine] = lineRItem.Num - updateMap[sstore.ScreenField_AnchorLine] = lineRItem.Num - updateMap[sstore.ScreenField_AnchorOffset] = 0 - screen, err = sstore.UpdateScreen(ctx, screenRItem.Id, updateMap) - if err != nil { - return nil, err + if lineRItem != nil { + updateMap := make(map[string]interface{}) + updateMap[sstore.ScreenField_SelectedLine] = lineRItem.Num + updateMap[sstore.ScreenField_AnchorLine] = lineRItem.Num + updateMap[sstore.ScreenField_AnchorOffset] = 0 + screen, err = sstore.UpdateScreen(ctx, screenRItem.Id, updateMap) + if err != nil { + return nil, err + } + update.Screens = []*sstore.ScreenType{screen} } - update.Screens = []*sstore.ScreenType{screen} return update, nil } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 3bbad9e3a..e3105f26d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -34,7 +34,7 @@ const LineNoHeight = -1 const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const MaxWebShareLineCount = 50 -const MaxWebShareScreenCount = 5 +const MaxWebShareScreenCount = 3 const DefaultSessionName = "default" const LocalRemoteAlias = "local" From 000d86b7af27d607cd42a6de8f9fe4e43d677465 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 5 Apr 2023 00:46:47 -0700 Subject: [PATCH 351/397] fix session stats --- pkg/sstore/dbops.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c4ca7cc9c..9d4737881 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1624,9 +1624,9 @@ func GetSessionStats(ctx context.Context, sessionId string) (*SessionStatsType, rtn.NumScreens = tx.GetInt(query, sessionId) query = `SELECT count(*) FROM screen WHERE sessionid = ? AND archived` rtn.NumArchivedScreens = tx.GetInt(query, sessionId) - query = `SELECT count(*) FROM line WHERE sessionid = ?` + query = `SELECT count(*) FROM line WHERE screenid IN (SELECT screenid FROM screen WHERE sessionid = ?)` rtn.NumLines = tx.GetInt(query, sessionId) - query = `SELECT count(*) FROM cmd WHERE screenid IN (select screenid FROM screen WHERE sessionid = ?)` + query = `SELECT count(*) FROM cmd WHERE screenid IN (SELECT screenid FROM screen WHERE sessionid = ?)` rtn.NumCmds = tx.GetInt(query, sessionId) return nil }) From c7289556e5ff589fa5d52cf748b5538a5e66e30a Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 5 Apr 2023 15:09:51 -0700 Subject: [PATCH 352/397] bug fixes for prod version of webshare -- allow notify to reset backoff time --- pkg/cmdrunner/cmdrunner.go | 10 +------ pkg/pcloud/pcloud.go | 56 ++++++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 180f2dec7..a7cc9b7cd 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -52,8 +52,6 @@ const MaxCommandLen = 4096 const MaxSignalLen = 12 const MaxSignalNum = 64 const MaxEvalDepth = 5 -const DevWebScreenUrlFmt = "http://devtest.getprompt.com:9001/static/index-dev.html?screenid=%s&viewkey=%s" -const ProdWebScreenUrlFmt = "https://share.getprompt.dev/s/%s?viewkey=%s" var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -225,13 +223,6 @@ func GetCmdStr(pk *scpacket.FeCommandPacketType) string { return pk.MetaCmd + ":" + pk.MetaSubCmd } -func GetWebShareUrl(screenId string, viewKey string) string { - if scbase.IsDevMode() { - return fmt.Sprintf(DevWebScreenUrlFmt, screenId, viewKey) - } - return fmt.Sprintf(ProdWebScreenUrlFmt, screenId, viewKey) -} - func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { metaCmd := SubMetaCmd(pk.MetaCmd) var cmdName string @@ -2801,6 +2792,7 @@ func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } func ClientNotifyUpdateWriterCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + pcloud.ResetUpdateWriterNumFailures() sstore.NotifyUpdateWriter() update := sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 5ca5bbc1d..40a5fe08e 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -41,6 +41,7 @@ const WebShareUpdateUrl = "/auth/web-share-update" var updateWriterLock = &sync.Mutex{} var updateWriterRunning = false +var updateWriterNumFailures = 0 type AuthInfo struct { UserId string `json:"userid"` @@ -106,7 +107,7 @@ func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*htt func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) { apiUrl := req.Header.Get("X-PromptAPIUrl") - log.Printf("[pcloud] sending request %v\n", req.URL) + log.Printf("[pcloud] sending request %s %v\n", req.Method, req.URL) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("error contacting pcloud %q service: %v", apiUrl, err) @@ -473,10 +474,15 @@ func StartUpdateWriter() { go runWebShareUpdateWriter() } -func computeBackoff(numFailures int) time.Duration { +func computeUpdateWriterBackoff() time.Duration { + updateWriterLock.Lock() + numFailures := updateWriterNumFailures + updateWriterLock.Unlock() switch numFailures { + case 0: + return 0 case 1: - return 500 * time.Millisecond + return 1 * time.Second case 2: return 2 * time.Second case 3: @@ -492,6 +498,24 @@ func computeBackoff(numFailures int) time.Duration { } } +func incrementUpdateWriterNumFailures() { + updateWriterLock.Lock() + defer updateWriterLock.Unlock() + updateWriterNumFailures++ +} + +func ResetUpdateWriterNumFailures() { + updateWriterLock.Lock() + defer updateWriterLock.Unlock() + updateWriterNumFailures = 0 +} + +func GetUpdateWriterNumFailures() int { + updateWriterLock.Lock() + defer updateWriterLock.Unlock() + return updateWriterNumFailures +} + type updateKey struct { ScreenId string LineId string @@ -526,7 +550,6 @@ func runWebShareUpdateWriter() { }() log.Printf("[pcloud] starting update writer\n") numErrors := 0 - numSendErrors := 0 for { if numErrors > MaxUpdateWriterErrors { log.Printf("[pcloud] update-writer, too many errors, exiting\n") @@ -568,10 +591,10 @@ func runWebShareUpdateWriter() { } err = DoWebUpdates(webUpdateArr) if err != nil { - numSendErrors++ - backoffTime := computeBackoff(numSendErrors) + incrementUpdateWriterNumFailures() + backoffTime := computeUpdateWriterBackoff() log.Printf("[pcloud] error processing %d web-updates (backoff=%v): %v\n", len(webUpdateArr), backoffTime, err) - time.Sleep(backoffTime) + updateBackoffSleep(backoffTime) continue } log.Printf("[pcloud] sent %d web-updates\n", len(webUpdateArr)) @@ -580,6 +603,23 @@ func runWebShareUpdateWriter() { debugStrs = append(debugStrs, webUpdate.String()) } log.Printf("[pcloud] updates: %s\n", strings.Join(debugStrs, " ")) - numSendErrors = 0 + ResetUpdateWriterNumFailures() + } +} + +// todo fix this, set deadline, check with condition variable, backoff then just needs to notify +func updateBackoffSleep(backoffTime time.Duration) { + var totalSleep time.Duration + for { + sleepTime := time.Second + totalSleep += sleepTime + time.Sleep(sleepTime) + if totalSleep >= backoffTime { + break + } + numFailures := GetUpdateWriterNumFailures() + if numFailures == 0 { + break + } } } From b92c160333b878e3b11e1ac6c97c3a44acc48e79 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 7 Apr 2023 15:48:44 -0700 Subject: [PATCH 353/397] fix session:purge --- pkg/sstore/dbops.go | 15 +++++++-------- pkg/sstore/fileops.go | 9 --------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 9d4737881..0748a310a 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1124,13 +1124,18 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP tx.Exec(query, screenId) query = `DELETE FROM line WHERE screenid = ?` tx.Exec(query, screenId) + query = `DELETE FROM cmd WHERE screenid = ?` + tx.Exec(query, screenId) insertScreenDelUpdate(tx, screenId) return nil }) if txErr != nil { return nil, txErr } - go cleanScreenCmds(context.Background(), screenId) + delErr := DeleteScreenDir(ctx, screenId) + if delErr != nil { + log.Printf("error removing screendir") + } if sessionDel { return nil, nil } @@ -1491,7 +1496,7 @@ func PurgeSession(ctx context.Context, sessionId string) (UpdatePacket, error) { query = `SELECT screenid FROM screen WHERE sessionid = ?` screenIds = tx.SelectStrings(query, sessionId) for _, screenId := range screenIds { - _, err := PurgeScreen(ctx, screenId, true) + _, err := PurgeScreen(tx.Context(), screenId, true) if err != nil { return fmt.Errorf("error purging screen[%s]: %v", screenId, err) } @@ -1504,16 +1509,10 @@ func PurgeSession(ctx context.Context, sessionId string) (UpdatePacket, error) { if txErr != nil { return nil, txErr } - delErr := DeleteSessionDir(ctx, sessionId) update := ModelUpdate{} if newActiveSessionId != "" { update.ActiveSessionId = newActiveSessionId } - if delErr != nil { - update.Info = &InfoMsgType{ - InfoMsg: fmt.Sprintf("error removing session files: %v", delErr), - } - } update.Sessions = append(update.Sessions, &SessionType{SessionId: sessionId, Remove: true}) for _, screenId := range screenIds { update.Screens = append(update.Screens, &ScreenType{ScreenId: screenId, Remove: true}) diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index c2f8e0506..0c521f87c 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -168,15 +168,6 @@ func DeletePtyOutFile(ctx context.Context, screenId string, cmdId string) error return os.Remove(ptyOutFileName) } -func DeleteSessionDir(ctx context.Context, sessionId string) error { - sessionDir, err := scbase.EnsureSessionDir(sessionId) - if err != nil { - return fmt.Errorf("error getting sessiondir: %w", err) - } - fmt.Printf("remove-all %s\n", sessionDir) - return os.RemoveAll(sessionDir) -} - func DeleteScreenDir(ctx context.Context, screenId string) error { screenDir, err := scbase.EnsureScreenDir(screenId) if err != nil { From 4e93eff86a8be22dce239497268e507334534e66 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 11 Apr 2023 23:54:18 -0700 Subject: [PATCH 354/397] updates to FeState to store variables -- implement python venv and gitbranch in festate for prompt --- pkg/cmdrunner/cmdrunner.go | 20 +++++++++---------- pkg/cmdrunner/resolver.go | 4 ++-- pkg/cmdrunner/shparse.go | 5 ++++- pkg/remote/remote.go | 12 +++++------ pkg/sstore/dbops.go | 2 ++ pkg/sstore/sstore.go | 41 ++++++++++++++++++++++---------------- 6 files changed, 48 insertions(+), 36 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a7cc9b7cd..a56bca090 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -1197,8 +1197,8 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re baseDisplayName := msh.GetDisplayName() displayName := rptr.GetDisplayName(baseDisplayName) cwdStr := "-" - if ri.FeState.Cwd != "" { - cwdStr = ri.FeState.Cwd + if ri.FeState["cwd"] != "" { + cwdStr = ri.FeState["cwd"] } buf.WriteString(fmt.Sprintf("%-30s %-50s\n", displayName, cwdStr)) } @@ -1217,8 +1217,8 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re continue } cwdStr := "-" - if feState.Cwd != "" { - cwdStr = feState.Cwd + if feState["cwd"] != "" { + cwdStr = feState["cwd"] } buf.WriteString(fmt.Sprintf("%-30s %-50s (default)\n", msh.GetDisplayName(), cwdStr)) } @@ -1303,7 +1303,7 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr cmd.StatePtr = *ids.Remote.StatePtr } if ids.Remote.FeState != nil { - cmd.FeState = *ids.Remote.FeState + cmd.FeState = ids.Remote.FeState } err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) if err != nil { @@ -1455,7 +1455,7 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix - cgPacket.Cwd = ids.Remote.FeState.Cwd + cgPacket.Cwd = ids.Remote.FeState["cwd"] resp, err := ids.Remote.MShell.PacketRpc(ctx, cgPacket) if err != nil { return nil, false, err @@ -1495,7 +1495,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto rptr := ids.Remote.RemotePtr compCtx.RemotePtr = &rptr if ids.Remote.FeState != nil { - compCtx.Cwd = ids.Remote.FeState.Cwd + compCtx.Cwd = ids.Remote.FeState["cwd"] } } compCtx.ForDisplay = showComps @@ -1949,7 +1949,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, fmt.Errorf("invalid initpk received from remote (no remote state)") } feState := sstore.FeStateFromShellState(initPk.State) - remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, *feState, initPk.State, nil) + remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, feState, initPk.State, nil) if err != nil { return nil, err } @@ -2657,8 +2657,8 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef())) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", cmd.Status)) - if cmd.FeState.Cwd != "" { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.FeState.Cwd)) + if cmd.FeState["cwd"] != "" { + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.FeState["cwd"])) } buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", formatTermOpts(cmd.TermOpts))) if cmd.TermOpts != cmd.OrigTermOpts { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index cce62160b..ea83da113 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -34,7 +34,7 @@ type ResolvedRemote struct { RState remote.RemoteRuntimeState RemoteCopy *sstore.RemoteType StatePtr *sstore.ShellStatePtr - FeState *sstore.FeStateType + FeState map[string]string } type ResolveItem = sstore.ResolveItem @@ -481,7 +481,7 @@ func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi rtn.FeState = msh.GetDefaultFeState() } else { rtn.StatePtr = &sstore.ShellStatePtr{BaseHash: ri.StateBaseHash, DiffHashArr: ri.StateDiffHashArr} - rtn.FeState = &ri.FeState + rtn.FeState = ri.FeState } } } diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index fcd9c293c..4e6868af6 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -23,6 +23,7 @@ type BareMetaCmdDecl struct { var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"cr", "cr"}, + BareMetaCmdDecl{"connect", "cr"}, BareMetaCmdDecl{"clear", "clear"}, BareMetaCmdDecl{"reset", "reset"}, } @@ -155,6 +156,8 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { return nil } +var literalRtnStateCommands = []string{".", "source", "unset", "cd", "alias", "unalias", "deactivate"} + // detects: export, declare, ., source, X=1, unset func IsReturnStateCommand(cmdStr string) bool { cmdReader := strings.NewReader(cmdStr) @@ -171,7 +174,7 @@ func IsReturnStateCommand(cmdStr string) bool { if len(callExpr.Args) > 0 && len(callExpr.Args[0].Parts) > 0 { lit, ok := callExpr.Args[0].Parts[0].(*syntax.Lit) if ok { - if lit.Value == "." || lit.Value == "source" || lit.Value == "unset" || lit.Value == "cd" || lit.Value == "alias" || lit.Value == "unalias" { + if utilfn.ContainsStr(literalRtnStateCommands, lit.Value) { return true } } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 007ab4bb5..699f4e85b 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -132,7 +132,7 @@ type RemoteRuntimeState struct { RemoteAlias string `json:"remotealias,omitempty"` RemoteCanonicalName string `json:"remotecanonicalname"` RemoteVars map[string]string `json:"remotevars"` - DefaultFeState *sstore.FeStateType `json:"defaultfestate"` + DefaultFeState map[string]string `json:"defaultfestate"` Status string `json:"status"` ConnectTimeout int `json:"connecttimeout,omitempty"` ErrorStr string `json:"errorstr,omitempty"` @@ -177,7 +177,7 @@ func (msh *MShellProc) GetDefaultStatePtr() *sstore.ShellStatePtr { return &sstore.ShellStatePtr{BaseHash: msh.CurrentState} } -func (msh *MShellProc) GetDefaultFeState() *sstore.FeStateType { +func (msh *MShellProc) GetDefaultFeState() map[string]string { state := msh.GetDefaultState() return sstore.FeStateFromShellState(state) } @@ -1484,7 +1484,7 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt CmdStr: runPacket.Command, RawCmdStr: runPacket.Command, Remote: remotePtr, - FeState: *sstore.FeStateFromShellState(currentState), + FeState: sstore.FeStateFromShellState(currentState), StatePtr: *statePtr, TermOpts: makeTermOpts(runPacket), Status: status, @@ -1652,7 +1652,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { var statePtr *sstore.ShellStatePtr if donePk.FinalState != nil && rct != nil { feState := sstore.FeStateFromShellState(donePk.FinalState) - remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, *feState, donePk.FinalState, nil) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, donePk.FinalState, nil) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) @@ -1667,7 +1667,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) } else { - remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, *feState, nil, donePk.FinalStateDiff) + remoteInst, err := sstore.UpdateRemoteState(context.Background(), rct.SessionId, rct.ScreenId, rct.RemotePtr, feState, nil, donePk.FinalStateDiff) if err != nil { msh.WriteToPtyBuffer("*error trying to update remotestate: %v\n", err) // fall-through (nothing to do) @@ -1995,7 +1995,7 @@ func (msh *MShellProc) getFullState(stateDiff *packet.ShellStateDiff) (*packet.S } // internal func, first tries the StateMap, otherwise will fallback on sstore.GetFullState -func (msh *MShellProc) getFeStateFromDiff(stateDiff *packet.ShellStateDiff) (*sstore.FeStateType, error) { +func (msh *MShellProc) getFeStateFromDiff(stateDiff *packet.ShellStateDiff) (map[string]string, error) { baseState := msh.GetStateByHash(stateDiff.BaseHash) if baseState != nil && len(stateDiff.DiffHashArr) == 0 { newState, err := shexec.ApplyShellStateDiff(*baseState, *stateDiff) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 0748a310a..03b0e3b61 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -31,6 +31,8 @@ type SingleConnDBGetter struct { SingleConnLock *sync.Mutex } +type FeStateType map[string]string + type TxWrap = txwrap.TxWrap var dbWrap *SingleConnDBGetter diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index e3105f26d..d18ac79ee 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -22,6 +22,7 @@ import ( "github.com/sawka/txwrap" "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/dbutil" "github.com/scripthaus-dev/sh2-server/pkg/scbase" @@ -563,15 +564,15 @@ func (ssptr *ShellStatePtr) IsEmpty() bool { } type RemoteInstance struct { - RIId string `json:"riid"` - Name string `json:"name"` - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - RemoteOwnerId string `json:"remoteownerid"` - RemoteId string `json:"remoteid"` - FeState FeStateType `json:"festate"` - StateBaseHash string `json:"-"` - StateDiffHashArr []string `json:"-"` + RIId string `json:"riid"` + Name string `json:"name"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + RemoteOwnerId string `json:"remoteownerid"` + RemoteId string `json:"remoteid"` + FeState map[string]string `json:"festate"` + StateBaseHash string `json:"-"` + StateDiffHashArr []string `json:"-"` // only for updates Remove bool `json:"remove,omitempty"` @@ -611,16 +612,22 @@ func (sd *StateDiff) ToMap() map[string]interface{} { return rtn } -type FeStateType struct { - Cwd string `json:"cwd"` - // maybe later we can add some vars -} - -func FeStateFromShellState(state *packet.ShellState) *FeStateType { +func FeStateFromShellState(state *packet.ShellState) map[string]string { if state == nil { return nil } - return &FeStateType{Cwd: state.Cwd} + rtn := make(map[string]string) + rtn["cwd"] = state.Cwd + envMap := shexec.EnvMapFromState(state) + if envMap["VIRTUAL_ENV"] != "" { + rtn["VIRTUAL_ENV"] = envMap["VIRTUAL_ENV"] + } + for key, val := range envMap { + if strings.HasPrefix(key, "PROMPTVAR_") { + rtn[key] = val + } + } + return rtn } func (ri *RemoteInstance) FromMap(m map[string]interface{}) bool { @@ -874,7 +881,7 @@ type CmdType struct { Remote RemotePtrType `json:"remote"` CmdStr string `json:"cmdstr"` RawCmdStr string `json:"rawcmdstr"` - FeState FeStateType `json:"festate"` + FeState map[string]string `json:"festate"` StatePtr ShellStatePtr `json:"state"` TermOpts TermOpts `json:"termopts"` OrigTermOpts TermOpts `json:"origtermopts"` From 7bc8f41c0fb4b97cf1d4c13c91fca2517fb860e2 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Apr 2023 14:43:11 -0700 Subject: [PATCH 355/397] now that we're tracking git branches, git switch and git checkout are rtnstate commands --- pkg/cmdrunner/shparse.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 4e6868af6..25c384d10 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -158,6 +158,21 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error { var literalRtnStateCommands = []string{".", "source", "unset", "cd", "alias", "unalias", "deactivate"} +func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string { + if len(callExpr.Args) <= argNum { + return "" + } + arg := callExpr.Args[argNum] + if len(arg.Parts) == 0 { + return "" + } + lit, ok := arg.Parts[0].(*syntax.Lit) + if !ok { + return "" + } + return lit.Value +} + // detects: export, declare, ., source, X=1, unset func IsReturnStateCommand(cmdStr string) bool { cmdReader := strings.NewReader(cmdStr) @@ -171,14 +186,15 @@ func IsReturnStateCommand(cmdStr string) bool { if len(callExpr.Assigns) > 0 && len(callExpr.Args) == 0 { return true } - if len(callExpr.Args) > 0 && len(callExpr.Args[0].Parts) > 0 { - lit, ok := callExpr.Args[0].Parts[0].(*syntax.Lit) - if ok { - if utilfn.ContainsStr(literalRtnStateCommands, lit.Value) { - return true - } + arg0 := getCallExprLitArg(callExpr, 0) + if arg0 != "" && utilfn.ContainsStr(literalRtnStateCommands, arg0) { + return true + } + if arg0 == "git" { + arg1 := getCallExprLitArg(callExpr, 1) + if arg1 == "checkout" || arg1 == "switch" { + return true } - } } else if _, ok := stmt.Cmd.(*syntax.DeclClause); ok { return true From 31d871920035ec64f692842706a10a33b4c7e883 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Apr 2023 21:42:16 -0700 Subject: [PATCH 356/397] add buildtime to remote state mshell version --- pkg/cmdrunner/cmdrunner.go | 34 ++++++++++++++++++++++++++++++++++ pkg/remote/remote.go | 15 ++++++++++----- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a56bca090..a20bec268 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -124,6 +124,7 @@ func init() { registerCmdFn("clear", ClearCommand) registerCmdFn("reset", RemoteResetCommand) registerCmdFn("signal", SignalCommand) + registerCmdFn("sync", SyncCommand) registerCmdFn("session", SessionCommand) registerCmdFn("session:open", SessionOpenCommand) @@ -393,6 +394,39 @@ func getEvalDepth(ctx context.Context) int { return depthVal.(int) } +func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, fmt.Errorf("/run error: %w", err) + } + runPacket := packet.MakeRunPacket() + runPacket.ReqId = uuid.New().String() + runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenPromptUUID()) + runPacket.UsePty = true + ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal) + if err != nil { + return nil, fmt.Errorf("/sync error, invalid 'pterm' value %q: %v", ptermVal, err) + } + runPacket.Command = ":" + runPacket.ReturnState = true + cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket) + if callback != nil { + defer callback() + } + if err != nil { + return nil, err + } + cmd.RawCmdStr = pk.GetRawStr() + update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal") + if err != nil { + return nil, err + } + update.Interactive = pk.Interactive + sstore.MainBus.SendScreenUpdate(ids.ScreenId, update) + return nil, nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 699f4e85b..cfd786dc8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -584,12 +584,17 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["color"] = msh.Remote.RemoteOpts.Color } if msh.ServerProc != nil && msh.ServerProc.InitPk != nil { - state.MShellVersion = msh.ServerProc.InitPk.Version - vars["home"] = msh.ServerProc.InitPk.HomeDir - vars["remoteuser"] = msh.ServerProc.InitPk.User + initPk := msh.ServerProc.InitPk + if initPk.BuildTime == "" || initPk.BuildTime == "0" { + state.MShellVersion = initPk.Version + } else { + state.MShellVersion = fmt.Sprintf("%s+%s", initPk.Version, initPk.BuildTime) + } + vars["home"] = initPk.HomeDir + vars["remoteuser"] = initPk.User vars["bestuser"] = vars["remoteuser"] - vars["remotehost"] = msh.ServerProc.InitPk.HostName - vars["remoteshorthost"] = makeShortHost(msh.ServerProc.InitPk.HostName) + vars["remotehost"] = initPk.HostName + vars["remoteshorthost"] = makeShortHost(initPk.HostName) vars["besthost"] = vars["remotehost"] vars["bestshorthost"] = vars["remoteshorthost"] } From 1ac1051205bceb7675570f84b7af5c85a625fa23 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Apr 2023 22:08:10 -0700 Subject: [PATCH 357/397] bump version to v0.2.1 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 2684121c8..8c5a75058 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.2.0" +const PromptVersion = "v0.2.1" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 398be033244d972ff23a853e659a0ba222d84cb9 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Apr 2023 12:53:15 -0700 Subject: [PATCH 358/397] simplify screen focus types (remove cmd-fg). send all screen focus updates. fix display issue with rtnstate (prompt vars) --- pkg/cmdrunner/cmdrunner.go | 6 ++--- pkg/remote/remote.go | 30 +++++++++++++++++++------ pkg/rtnstate/rtnstate.go | 8 +++++++ pkg/sstore/dbops.go | 45 ++++++++++++++++++++++++++++---------- pkg/sstore/fileops.go | 2 +- pkg/sstore/sstore.go | 1 - 6 files changed, 68 insertions(+), 24 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index a20bec268..fcf89b9a1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -671,8 +671,8 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } if pk.Kwargs["focus"] != "" { focusVal := pk.Kwargs["focus"] - if focusVal != sstore.ScreenFocusInput && focusVal != sstore.ScreenFocusCmd && focusVal != sstore.ScreenFocusCmdFg { - return nil, fmt.Errorf("/screen:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.ScreenFocusInput, sstore.ScreenFocusCmd, sstore.ScreenFocusCmdFg}, "or", false)) + if focusVal != sstore.ScreenFocusInput && focusVal != sstore.ScreenFocusCmd { + return nil, fmt.Errorf("/screen:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.ScreenFocusInput, sstore.ScreenFocusCmd}, "or", false)) } varsUpdated = append(varsUpdated, "focus") updateMap[sstore.ScreenField_Focus] = focusVal @@ -1367,7 +1367,7 @@ func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids re updateMap := make(map[string]interface{}) updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum if shouldFocus { - updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmdFg + updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmd } screen, err = sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index cfd786dc8..592c298b8 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1106,7 +1106,6 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro msh.StateMap[hval] = initPk.State }) msh.updateRemoteStateVars(ctx, msh.RemoteId, initPk) - return initPk, nil } @@ -1144,6 +1143,7 @@ func stripScVarsFromStateDiff(stateDiff *packet.ShellStateDiff) *packet.ShellSta var mapDiff statediff.MapDiffType err := mapDiff.Decode(stateDiff.VarsDiff) if err != nil { + log.Printf("error decoding statediff in stripScVarsFromStateDiff: %v\n", err) return stateDiff } delete(mapDiff.ToAdd, "PROMPT") @@ -1645,9 +1645,9 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return } - screen, err := sstore.UpdateScreenWithCmdFg(context.Background(), donePk.CK.GetGroupId(), donePk.CK.GetCmdId()) + screen, err := sstore.UpdateScreenFocusForDoneCmd(context.Background(), donePk.CK.GetGroupId(), donePk.CK.GetCmdId()) if err != nil { - msh.WriteToPtyBuffer("*error trying to update cmd-fg screens: %v\n", err) + msh.WriteToPtyBuffer("*error trying to update screen focus type: %v\n", err) // fall-through (nothing to do) } if screen != nil { @@ -1692,7 +1692,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { // fall-through (nothing to do) } } - sstore.MainBus.SendScreenUpdate(donePk.CK.GetGroupId(), update) + sstore.MainBus.SendUpdate(update) return } @@ -1707,7 +1707,11 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) return } log.Printf("finalpk %s (hangup): %s\n", finalPk.CK, finalPk.Error) - sstore.HangupCmd(context.Background(), finalPk.CK) + screen, err := sstore.HangupCmd(context.Background(), finalPk.CK) + if err != nil { + log.Printf("error in hangup-cmd in handleCmdFinalPacket: %v\n", err) + return + } rtnCmd, err = sstore.GetCmdByScreenId(context.Background(), finalPk.CK.GetGroupId(), finalPk.CK.GetCmdId()) if err != nil { log.Printf("error getting cmd(2) in handleCmdFinalPacket: %v\n", err) @@ -1718,7 +1722,10 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) return } update := &sstore.ModelUpdate{Cmd: rtnCmd} - sstore.MainBus.SendScreenUpdate(finalPk.CK.GetGroupId(), update) + if screen != nil { + update.Screens = []*sstore.ScreenType{screen} + } + sstore.MainBus.SendUpdate(update) } // TODO notify FE about cmd errors @@ -1777,17 +1784,26 @@ func (msh *MShellProc) makeHandleCmdFinalPacketClosure(finalPk *packet.CmdFinalP } } +func sendScreenUpdates(screens []*sstore.ScreenType) { + for _, screen := range screens { + sstore.MainBus.SendUpdate(&sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}) + } +} + func (msh *MShellProc) ProcessPackets() { defer msh.WithLock(func() { if msh.Status == StatusConnected { msh.Status = StatusDisconnected } - err := sstore.HangupRunningCmdsByRemoteId(context.Background(), msh.Remote.RemoteId) + screens, err := sstore.HangupRunningCmdsByRemoteId(context.Background(), msh.Remote.RemoteId) if err != nil { msh.writeToPtyBuffer_nolock("error calling HUP on cmds %v\n", err) } msh.notifyHangups_nolock() go msh.NotifyRemoteUpdate() + if len(screens) > 0 { + go sendScreenUpdates(screens) + } }) dataPosMap := make(map[base.CommandKey]int64) for pk := range msh.ServerProc.Output.MainCh { diff --git a/pkg/rtnstate/rtnstate.go b/pkg/rtnstate/rtnstate.go index 42e2314c8..e882cab5e 100644 --- a/pkg/rtnstate/rtnstate.go +++ b/pkg/rtnstate/rtnstate.go @@ -102,6 +102,8 @@ func ParseFuncs(funcs string) (map[string]string, error) { const MaxDiffKeyLen = 40 const MaxDiffValLen = 50 +var IgnoreVars = map[string]bool{"PROMPT": true, "PROMPT_VERSION": true, "MSHELL": true} + 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)) @@ -110,6 +112,9 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt newEnvMap := shexec.DeclMapFromState(&newState) oldEnvMap := shexec.DeclMapFromState(&oldState) for key, newVal := range newEnvMap { + if IgnoreVars[key] { + continue + } oldVal, found := oldEnvMap[key] if !found || !shexec.DeclsEqual(false, oldVal, newVal) { var exportStr string @@ -120,6 +125,9 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt } } for key, _ := range oldEnvMap { + if IgnoreVars[key] { + continue + } _, found := newEnvMap[key] if !found { buf.WriteString(fmt.Sprintf("unset %s\n", utilfn.EllipsisStr(key, MaxDiffKeyLen))) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 03b0e3b61..c1883dad7 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -940,32 +940,52 @@ func HangupAllRunningCmds(ctx context.Context) error { query = `UPDATE cmd SET status = ? WHERE status = ?` tx.Exec(query, CmdStatusHangup, CmdStatusRunning) for _, cmdPtr := range cmdPtrs { - insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + if isWebShare(tx, cmdPtr.ScreenId) { + 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 { +// TODO send update +func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) ([]*ScreenType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenType, error) { 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) + var rtn []*ScreenType for _, cmdPtr := range cmdPtrs { - insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + if isWebShare(tx, cmdPtr.ScreenId) { + insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + } + screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), cmdPtr.ScreenId, cmdPtr.CmdId) + if err != nil { + return nil, err + } + if screen != nil { + rtn = append(rtn, screen) + } } - return nil + return rtn, nil }) } -func HangupCmd(ctx context.Context, ck base.CommandKey) error { - return WithTx(ctx, func(tx *TxWrap) error { +// TODO send update +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 cmdid = ?` tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), ck.GetCmdId()) - insertScreenCmdUpdate(tx, ck.GetGroupId(), ck.GetCmdId(), UpdateType_CmdStatus) - return nil + if isWebShare(tx, ck.GetGroupId()) { + insertScreenCmdUpdate(tx, ck.GetGroupId(), ck.GetCmdId(), UpdateType_CmdStatus) + } + screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), ck.GetGroupId(), ck.GetCmdId()) + if err != nil { + return nil, err + } + return screen, nil }) } @@ -1777,14 +1797,14 @@ func GetLineResolveItems(ctx context.Context, screenId string) ([]ResolveItem, e return rtn, nil } -func UpdateScreenWithCmdFg(ctx context.Context, screenId string, cmdId string) (*ScreenType, error) { +func UpdateScreenFocusForDoneCmd(ctx context.Context, screenId string, cmdId string) (*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { query := `SELECT screenid FROM screen s - WHERE s.screenid = ? AND s.focustype = 'cmd-fg' + WHERE s.screenid = ? AND s.focustype = ? AND s.selectedline IN (SELECT linenum FROM line l WHERE l.screenid = s.screenid AND l.cmdid = ?) ` - if !tx.Exists(query, screenId, cmdId) { + if !tx.Exists(query, screenId, ScreenFocusCmd, cmdId) { return nil, nil } editMap := make(map[string]interface{}) @@ -1803,6 +1823,7 @@ func StoreStateBase(ctx context.Context, state *packet.ShellState) error { Ts: time.Now().UnixMilli(), } stateBase.BaseHash, stateBase.Data = state.EncodeAndHash() + // envMap := shexec.DeclMapFromState(state) txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT basehash FROM state_base WHERE basehash = ?` if tx.Exists(query, stateBase.BaseHash) { diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 0c521f87c..c6d11828e 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -173,6 +173,6 @@ func DeleteScreenDir(ctx context.Context, screenId string) error { if err != nil { return fmt.Errorf("error getting screendir: %w", err) } - fmt.Printf("remove-all %s\n", screenDir) + log.Printf("remove-all %s\n", screenDir) return os.RemoveAll(screenDir) } diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index d18ac79ee..c969093d0 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -82,7 +82,6 @@ const ( const ( ScreenFocusInput = "input" ScreenFocusCmd = "cmd" - ScreenFocusCmdFg = "cmd-fg" ) const ( From 51a7000de3d6889c53ac0582bb90f3192f7b134b Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 17 Apr 2023 15:22:30 -0700 Subject: [PATCH 359/397] only add screendel if websharing --- pkg/sstore/dbops.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c1883dad7..71debe688 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -1122,6 +1122,7 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP if !tx.Exists(query, screenId) { return fmt.Errorf("cannot purge screen (not found)") } + webSharing := isWebShare(tx, screenId) if !sessionDel { query = `SELECT sessionid FROM screen WHERE screenid = ?` sessionId = tx.GetString(query, screenId) @@ -1148,7 +1149,9 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP tx.Exec(query, screenId) query = `DELETE FROM cmd WHERE screenid = ?` tx.Exec(query, screenId) - insertScreenDelUpdate(tx, screenId) + if webSharing { + insertScreenDelUpdate(tx, screenId) + } return nil }) if txErr != nil { From 0cd3f8ac04582673bc3993edb8fa85a7281762cc Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 17 Apr 2023 15:23:58 -0700 Subject: [PATCH 360/397] can use view= instead of renderer= for setting renderer --- pkg/cmdrunner/cmdrunner.go | 77 +++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index fcf89b9a1..2ec52098b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -53,6 +53,11 @@ const MaxSignalLen = 12 const MaxSignalNum = 64 const MaxEvalDepth = 5 +const ( + KwArgRenderer = "renderer" + KwArgView = "view" +) + var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} @@ -427,15 +432,30 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return nil, nil } +func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) { + rval := pk.Kwargs[KwArgView] + if rval == "" { + rval = pk.Kwargs[KwArgRenderer] + } + if rval == "" { + return "", nil + } + err := validateRenderer(rval) + if err != nil { + return "", err + } + return rval, nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { return nil, fmt.Errorf("/run error: %w", err) } - if err = validateRenderer(pk.Kwargs["renderer"]); err != nil { - return nil, fmt.Errorf("/run error: %w", err) + renderer, err := getRendererArg(pk) + if err != nil { + return nil, fmt.Errorf("/run error, invalid view/renderer: %w", err) } - renderer := pk.Kwargs["renderer"] cmdStr := firstArg(pk) expandedCmdStr, err := doCmdHistoryExpansion(ctx, ids, cmdStr) if err != nil { @@ -596,11 +616,22 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) + 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) } - update, err := sstore.PurgeScreen(ctx, ids.ScreenId, false) + 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) + } + screenId = ri.Id + } + if screenId == "" { + return nil, fmt.Errorf("/screen:purge no active screen or screen arg passed") + } + update, err := sstore.PurgeScreen(ctx, screenId, false) if err != nil { return nil, err } @@ -1780,11 +1811,27 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Session) + ids, err := resolveUiIds(ctx, pk, 0) // don't force R_Session if err != nil { return nil, err } - update, err := sstore.PurgeSession(ctx, ids.SessionId) + sessionId := "" + 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) + } + if ritem == nil { + return nil, fmt.Errorf("/session:purge 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") + } + update, err := sstore.PurgeSession(ctx, sessionId) if err != nil { return nil, fmt.Errorf("cannot delete session: %v", err) } @@ -2319,7 +2366,7 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto return nil, fmt.Errorf("error looking up lineid: %v", err) } var varsUpdated []string - if renderer, found := pk.Kwargs["renderer"]; found { + if renderer, found := pk.Kwargs[KwArgRenderer]; found { if err = validateRenderer(renderer); err != nil { return nil, fmt.Errorf("invalid renderer value: %w", err) } @@ -2327,10 +2374,20 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, fmt.Errorf("error changing line renderer: %v", err) } - varsUpdated = append(varsUpdated, "renderer") + varsUpdated = append(varsUpdated, KwArgRenderer) + } + if view, found := pk.Kwargs[KwArgView]; found { + if err = validateRenderer(view); err != nil { + return nil, fmt.Errorf("invalid view value: %w", err) + } + err = sstore.UpdateLineRenderer(ctx, ids.ScreenId, lineId, view) + if err != nil { + return nil, fmt.Errorf("error changing line view: %v", err) + } + varsUpdated = append(varsUpdated, KwArgView) } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{"renderer"}, "or", false)) + return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView}, "or", false)) } updatedLine, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { From 3e295228e7773b089bb779497e70bc89e13fdcd1 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 17 Apr 2023 17:31:30 -0700 Subject: [PATCH 361/397] update txwrap dependency --- go.mod | 3 +-- go.sum | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 937d43daf..7803c8bf8 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.14 - github.com/sawka/txwrap v0.1.0 + github.com/sawka/txwrap v0.1.2 github.com/scripthaus-dev/mshell v0.0.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/mod v0.5.1 @@ -26,4 +26,3 @@ require ( go.uber.org/atomic v1.7.0 // indirect ) - diff --git a/go.sum b/go.sum index cdbe4311c..71eb2da2a 100644 --- a/go.sum +++ b/go.sum @@ -968,6 +968,8 @@ github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiB github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sawka/txwrap v0.1.0 h1:uWGplmEJUEd9WGYZy9fU+hoC2Z6Yal4NMH5DbKsUTdo= github.com/sawka/txwrap v0.1.0/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= +github.com/sawka/txwrap v0.1.2 h1:v8xS0Z1LE7/6vMZA81PYihI+0TSR6Zm1MalzzBIuXKc= +github.com/sawka/txwrap v0.1.2/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= From 97fd46eda00923eafa3b6b88c43517eb720b1100 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 17 Apr 2023 17:36:47 -0700 Subject: [PATCH 362/397] v0.2.2 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 8c5a75058..376775a20 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.2.1" +const PromptVersion = "v0.2.2" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From ab5deafdb6fb8d8305b2a65c86000c9a84ba4f44 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 2 May 2023 12:43:54 -0700 Subject: [PATCH 363/397] updates to remote table, working on openai remote type --- .gitignore | 3 ++ db/migrations/000018_modremote.down.sql | 14 +++++ db/migrations/000018_modremote.up.sql | 11 ++++ go.mod | 2 + go.sum | 2 + pkg/cmdrunner/cmdrunner.go | 11 ++-- pkg/remote/remote.go | 25 +++++---- pkg/sstore/dbops.go | 19 ++----- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 69 +++++++++++-------------- 10 files changed, 88 insertions(+), 70 deletions(-) create mode 100644 db/migrations/000018_modremote.down.sql create mode 100644 db/migrations/000018_modremote.up.sql diff --git a/.gitignore b/.gitignore index a89c46b43..7c9c1bf40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *~ bin/ *.out +.idea/ +test/ +temp.sql diff --git a/db/migrations/000018_modremote.down.sql b/db/migrations/000018_modremote.down.sql new file mode 100644 index 000000000..d3d735349 --- /dev/null +++ b/db/migrations/000018_modremote.down.sql @@ -0,0 +1,14 @@ +ALTER TABLE remote ADD COLUMN remotesudo; + +UPDATE remote +SET remotesudo = 1 +WHERE json_extract(sshopts, '$.issudo') +; + +UPDATE remote +SET sshopts = json_remove(sshopts, '$.issudo') +; + +ALTER TABLE remote ADD COLUMN physicalid varchar(36) NOT NULL DEFAULT ''; + +ALTER TABLE remote DROP COLUMN openaiopts; diff --git a/db/migrations/000018_modremote.up.sql b/db/migrations/000018_modremote.up.sql new file mode 100644 index 000000000..4cad763fe --- /dev/null +++ b/db/migrations/000018_modremote.up.sql @@ -0,0 +1,11 @@ +UPDATE remote +SET sshopts = json_set(sshopts, '$.issudo', json('true')) +WHERE remotesudo +; + +ALTER TABLE remote DROP COLUMN remotesudo; + +ALTER TABLE remote DROP COLUMN physicalid; + +ALTER TABLE remote ADD COLUMN openaiopts json NOT NULL DEFAULT '{}'; + diff --git a/go.mod b/go.mod index 7803c8bf8..5c2dade3b 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/sashabaranov/go-openai v1.9.0 // indirect go.uber.org/atomic v1.7.0 // indirect ) + diff --git a/go.sum b/go.sum index 71eb2da2a..e7586676f 100644 --- a/go.sum +++ b/go.sum @@ -965,6 +965,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sashabaranov/go-openai v1.9.0 h1:NoiO++IISxxJ1pRc0n7uZvMGMake0G+FJ1XPwXtprsA= +github.com/sashabaranov/go-openai v1.9.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sawka/txwrap v0.1.0 h1:uWGplmEJUEd9WGYZy9fU+hoC2Z6Yal4NMH5DbKsUTdo= github.com/sawka/txwrap v0.1.0/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 2ec52098b..5b159e05b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -544,14 +544,16 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if len(pk.Args[0]) > MaxCommandLen { return nil, fmt.Errorf("command length too long len:%d, max:%d", len(pk.Args[0]), MaxCommandLen) } - if pk.Interactive { + evalDepth := getEvalDepth(ctx) + if pk.Interactive && evalDepth == 0 { err := sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{NumCommands: 1}) if err != nil { log.Printf("[error] incrementing activity numcommands: %v\n", err) // fall through (non-fatal error) } + log.Printf("inc numcommands\n") } - if getEvalDepth(ctx) > MaxEvalDepth { + if evalDepth > MaxEvalDepth { return nil, fmt.Errorf("alias/history expansion max-depth exceeded") } var historyContext historyContextType @@ -893,7 +895,6 @@ func makeRemoteEditErrorReturn_edit(ids resolvedIds, visual bool, err error) (ss type RemoteEditArgs struct { CanonicalName string SSHOpts *sstore.SSHOpts - Sudo bool ConnectMode string Alias string AutoInstall bool @@ -942,6 +943,7 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b Local: false, SSHHost: remoteHost, SSHUser: remoteUser, + IsSudo: isSudo, } portVal, err := resolvePosInt(pk.Kwargs["port"], 0) if err != nil { @@ -1036,7 +1038,6 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b return &RemoteEditArgs{ SSHOpts: sshOpts, - Sudo: isSudo, ConnectMode: connectMode, Alias: alias, AutoInstall: autoInstall, @@ -1060,11 +1061,9 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } r := &sstore.RemoteType{ RemoteId: scbase.GenPromptUUID(), - PhysicalId: "", RemoteType: sstore.RemoteTypeSsh, RemoteAlias: editArgs.Alias, RemoteCanonicalName: editArgs.CanonicalName, - RemoteSudo: editArgs.Sudo, RemoteUser: editArgs.SSHOpts.SSHUser, RemoteHost: editArgs.SSHOpts.SSHHost, ConnectMode: editArgs.ConnectMode, diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 592c298b8..d0948be91 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -128,7 +128,6 @@ type RunCmdType struct { type RemoteRuntimeState struct { RemoteType string `json:"remotetype"` RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalremoteid"` RemoteAlias string `json:"remotealias,omitempty"` RemoteCanonicalName string `json:"remotecanonicalname"` RemoteVars map[string]string `json:"remotevars"` @@ -150,12 +149,22 @@ type RemoteRuntimeState struct { WaitingForPassword bool `json:"waitingforpassword,omitempty"` Local bool `json:"local,omitempty"` RemoteOpts *sstore.RemoteOptsType `json:"remoteopts,omitempty"` + CanComplete bool `json:"cancomplete,omitempty"` } func (state RemoteRuntimeState) IsConnected() bool { return state.Status == StatusConnected } +func CanComplete(remoteType string) bool { + switch remoteType { + case sstore.RemoteTypeSsh: + return true + default: + return false + } +} + func (msh *MShellProc) GetStatus() string { msh.Lock.Lock() defer msh.Lock.Unlock() @@ -234,7 +243,7 @@ func LoadRemotes(ctx context.Context) error { go msh.Launch(false) } if remote.Local { - if remote.RemoteSudo { + if remote.IsSudo() { numSudoLocal++ } else { numLocal++ @@ -498,7 +507,7 @@ func (msh *MShellProc) IsLocal() bool { func (msh *MShellProc) IsSudo() bool { msh.Lock.Lock() defer msh.Lock.Unlock() - return msh.Remote.RemoteSudo + return msh.Remote.IsSudo() } func (msh *MShellProc) tryAutoInstall() { @@ -519,7 +528,6 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { RemoteId: msh.Remote.RemoteId, RemoteAlias: msh.Remote.RemoteAlias, RemoteCanonicalName: msh.Remote.RemoteCanonicalName, - PhysicalId: msh.Remote.PhysicalId, Status: msh.Status, ConnectMode: msh.Remote.ConnectMode, AutoInstall: msh.Remote.AutoInstall, @@ -564,11 +572,10 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["shorthost"] = makeShortHost(msh.Remote.RemoteHost) vars["alias"] = msh.Remote.RemoteAlias vars["cname"] = msh.Remote.RemoteCanonicalName - vars["physicalid"] = msh.Remote.PhysicalId vars["remoteid"] = msh.Remote.RemoteId vars["status"] = msh.Status vars["type"] = msh.Remote.RemoteType - if msh.Remote.RemoteSudo { + if msh.Remote.IsSudo() { vars["sudo"] = "1" } if msh.Remote.Local { @@ -603,9 +610,9 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { state.DefaultFeState = sstore.FeStateFromShellState(curState) vars["cwd"] = curState.Cwd } - if msh.Remote.Local && msh.Remote.RemoteSudo { + if msh.Remote.Local && msh.Remote.IsSudo() { vars["bestuser"] = "sudo" - } else if msh.Remote.RemoteSudo { + } else if msh.Remote.IsSudo() { vars["bestuser"] = "sudo@" + vars["bestuser"] } if msh.Remote.Local { @@ -1185,7 +1192,7 @@ func (msh *MShellProc) Launch(interactive bool) { var cmdStr string if sshOpts.SSHHost == "" && remoteCopy.Local { var err error - cmdStr, err = MakeLocalMShellCommandStr(remoteCopy.RemoteSudo) + cmdStr, err = MakeLocalMShellCommandStr(remoteCopy.IsSudo()) if err != nil { msh.WriteToPtyBuffer("*error, cannot find local mshell binary: %v\n", err) return diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 71debe688..38977c5c5 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -165,19 +165,6 @@ func GetRemoteByCanonicalName(ctx context.Context, cname string) (*RemoteType, e return remote, nil } -func GetRemoteByPhysicalId(ctx context.Context, physicalId string) (*RemoteType, error) { - var remote *RemoteType - err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM remote WHERE physicalid = ?` - remote = dbutil.GetMapGen[*RemoteType](tx, query, physicalId) - return nil - }) - if err != nil { - return nil, err - } - return remote, nil -} - func UpsertRemote(ctx context.Context, r *RemoteType) error { if r == nil { return fmt.Errorf("cannot insert nil remote") @@ -208,8 +195,8 @@ func UpsertRemote(ctx context.Context, r *RemoteType) error { maxRemoteIdx := tx.GetInt(query) r.RemoteIdx = int64(maxRemoteIdx + 1) query = `INSERT INTO remote - ( remoteid, physicalid, remotetype, remotealias, remotecanonicalname, remotesudo, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local, statevars) VALUES - (:remoteid,:physicalid,:remotetype,:remotealias,:remotecanonicalname,:remotesudo,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local,:statevars)` + ( remoteid, remotetype, remotealias, remotecanonicalname, remoteuser, remotehost, connectmode, autoinstall, sshopts, remoteopts, lastconnectts, archived, remoteidx, local, statevars, openaiopts) VALUES + (:remoteid,:remotetype,:remotealias,:remotecanonicalname,:remoteuser,:remotehost,:connectmode,:autoinstall,:sshopts,:remoteopts,:lastconnectts,:archived,:remoteidx,:local,:statevars,:openaiopts)` tx.NamedExec(query, r.ToMap()) return nil }) @@ -816,7 +803,7 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if line.LineNum != 0 { return fmt.Errorf("line should not hage linenum set") } - if cmd.ScreenId == "" { + if cmd != nil && cmd.ScreenId == "" { return fmt.Errorf("cmd should have screenid set") } return WithTx(ctx, func(tx *TxWrap) error { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index fb1bfb7f7..f1a7b665c 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 17 +const MaxMigration = 18 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c969093d0..026588958 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -20,7 +20,6 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" - "github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/dbutil" @@ -76,7 +75,8 @@ const ( ) const ( - RemoteTypeSsh = "ssh" + RemoteTypeSsh = "ssh" + RemoteTypeOpenAI = "openai" ) const ( @@ -802,6 +802,7 @@ type ResolveItem struct { type SSHOpts struct { Local bool `json:"local,omitempty"` + IsSudo bool `json:"issudo,omitempty"` SSHHost string `json:"sshhost"` SSHUser string `json:"sshuser"` SSHOptsStr string `json:"sshopts,omitempty"` @@ -827,32 +828,34 @@ type RemoteOptsType struct { Color string `json:"color"` } -func (opts *RemoteOptsType) Scan(val interface{}) error { - return quickScanJson(opts, val) -} - -func (opts RemoteOptsType) Value() (driver.Value, error) { - return quickValueJson(opts) +type OpenAIOptsType struct { } type RemoteType struct { - RemoteId string `json:"remoteid"` - PhysicalId string `json:"physicalid"` - RemoteType string `json:"remotetype"` - RemoteAlias string `json:"remotealias"` - RemoteCanonicalName string `json:"remotecanonicalname"` - RemoteSudo bool `json:"remotesudo"` - RemoteUser string `json:"remoteuser"` - RemoteHost string `json:"remotehost"` - ConnectMode string `json:"connectmode"` - AutoInstall bool `json:"autoinstall"` - SSHOpts *SSHOpts `json:"sshopts"` - RemoteOpts *RemoteOptsType `json:"remoteopts"` - LastConnectTs int64 `json:"lastconnectts"` - Archived bool `json:"archived"` - RemoteIdx int64 `json:"remoteidx"` - Local bool `json:"local"` - StateVars map[string]string `json:"statevars"` + RemoteId string `json:"remoteid"` + RemoteType string `json:"remotetype"` + RemoteAlias string `json:"remotealias"` + RemoteCanonicalName string `json:"remotecanonicalname"` + RemoteOpts *RemoteOptsType `json:"remoteopts"` + LastConnectTs int64 `json:"lastconnectts"` + RemoteIdx int64 `json:"remoteidx"` + Archived bool `json:"archived"` + + // SSH fields + Local bool `json:"local"` + RemoteUser string `json:"remoteuser"` + RemoteHost string `json:"remotehost"` + ConnectMode string `json:"connectmode"` + AutoInstall bool `json:"autoinstall"` + SSHOpts *SSHOpts `json:"sshopts"` + StateVars map[string]string `json:"statevars"` + + // OpenAI fields + OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty"` +} + +func (r *RemoteType) IsSudo() bool { + return r.SSHOpts != nil && r.SSHOpts.IsSudo } func (r *RemoteType) GetName() string { @@ -896,11 +899,9 @@ type CmdType struct { func (r *RemoteType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["remoteid"] = r.RemoteId - rtn["physicalid"] = r.PhysicalId rtn["remotetype"] = r.RemoteType rtn["remotealias"] = r.RemoteAlias rtn["remotecanonicalname"] = r.RemoteCanonicalName - rtn["remotesudo"] = r.RemoteSudo rtn["remoteuser"] = r.RemoteUser rtn["remotehost"] = r.RemoteHost rtn["connectmode"] = r.ConnectMode @@ -912,16 +913,15 @@ func (r *RemoteType) ToMap() map[string]interface{} { rtn["remoteidx"] = r.RemoteIdx rtn["local"] = r.Local rtn["statevars"] = quickJson(r.StateVars) + rtn["openaiopts"] = quickJson(r.OpenAIOpts) return rtn } func (r *RemoteType) FromMap(m map[string]interface{}) bool { quickSetStr(&r.RemoteId, m, "remoteid") - quickSetStr(&r.PhysicalId, m, "physicalid") quickSetStr(&r.RemoteType, m, "remotetype") quickSetStr(&r.RemoteAlias, m, "remotealias") quickSetStr(&r.RemoteCanonicalName, m, "remotecanonicalname") - quickSetBool(&r.RemoteSudo, m, "remotesudo") quickSetStr(&r.RemoteUser, m, "remoteuser") quickSetStr(&r.RemoteHost, m, "remotehost") quickSetStr(&r.ConnectMode, m, "connectmode") @@ -933,6 +933,7 @@ func (r *RemoteType) FromMap(m map[string]interface{}) bool { quickSetInt64(&r.RemoteIdx, m, "remoteidx") quickSetBool(&r.Local, m, "local") quickSetJson(&r.StateVars, m, "statevars") + quickSetJson(&r.OpenAIOpts, m, "openaiopts") return true } @@ -1029,10 +1030,6 @@ func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdTyp } func EnsureLocalRemote(ctx context.Context) error { - physicalId, err := base.GetRemoteId() - if err != nil { - return fmt.Errorf("getting local physical remoteid: %w", err) - } remote, err := GetLocalRemote(ctx) if err != nil { return fmt.Errorf("getting local remote from db: %w", err) @@ -1051,11 +1048,9 @@ func EnsureLocalRemote(ctx context.Context) error { // create the local remote localRemote := &RemoteType{ RemoteId: scbase.GenPromptUUID(), - PhysicalId: physicalId, RemoteType: RemoteTypeSsh, RemoteAlias: LocalRemoteAlias, RemoteCanonicalName: fmt.Sprintf("%s@%s", user.Username, hostName), - RemoteSudo: false, RemoteUser: user.Username, RemoteHost: hostName, ConnectMode: ConnectModeStartup, @@ -1070,16 +1065,14 @@ func EnsureLocalRemote(ctx context.Context) error { log.Printf("[db] added local remote '%s', id=%s\n", localRemote.RemoteCanonicalName, localRemote.RemoteId) sudoRemote := &RemoteType{ RemoteId: scbase.GenPromptUUID(), - PhysicalId: "", RemoteType: RemoteTypeSsh, RemoteAlias: "sudo", RemoteCanonicalName: fmt.Sprintf("sudo@%s@%s", user.Username, hostName), - RemoteSudo: true, RemoteUser: "root", RemoteHost: hostName, ConnectMode: ConnectModeManual, AutoInstall: true, - SSHOpts: &SSHOpts{Local: true}, + SSHOpts: &SSHOpts{Local: true, IsSudo: true}, RemoteOpts: &RemoteOptsType{Color: "red"}, Local: true, } From 8302ca1fcb495ea9ad54f7f3a1c21db7ae9adea5 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 4 May 2023 01:01:13 -0700 Subject: [PATCH 364/397] openai api integration 'working' --- pkg/cmdrunner/cmdrunner.go | 213 +++++++++++++++++++++++++++++++++++- pkg/cmdrunner/shparse.go | 18 ++- pkg/cmdrunner/termopts.go | 10 ++ pkg/remote/openai/openai.go | 147 +++++++++++++++++++++++++ pkg/remote/remote.go | 2 +- pkg/sstore/dbops.go | 4 +- pkg/sstore/sstore.go | 70 +++++++++++- 7 files changed, 455 insertions(+), 9 deletions(-) create mode 100644 pkg/remote/openai/openai.go diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 5b159e05b..debea3027 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -24,6 +24,7 @@ import ( "github.com/scripthaus-dev/sh2-server/pkg/comp" "github.com/scripthaus-dev/sh2-server/pkg/pcloud" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/remote/openai" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/sstore" @@ -41,7 +42,7 @@ func init() { comp.RegisterSimpleCompFn(comp.CGTypeCommandMeta, simpleCompCommandMeta) } -const DefaultUserId = "sawka" +const DefaultUserId = "user" const MaxNameLen = 50 const MaxShareNameLen = 150 const MaxRendererLen = 50 @@ -198,6 +199,9 @@ func init() { registerCmdFn("bookmark:set", BookmarkSetCommand) registerCmdFn("bookmark:delete", BookmarkDeleteCommand) + registerCmdFn("openai", OpenAICommand) + registerCmdFn("openai:stream", OpenAICommand) + registerCmdFn("_killserver", KillServerCommand) registerCmdFn("set", SetCommand) @@ -551,7 +555,6 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. log.Printf("[error] incrementing activity numcommands: %v\n", err) // fall through (non-fatal error) } - log.Printf("inc numcommands\n") } if evalDepth > MaxEvalDepth { return nil, fmt.Errorf("alias/history expansion max-depth exceeded") @@ -1312,6 +1315,185 @@ func GetFullRemoteDisplayName(rptr *sstore.RemotePtrType, rstate *remote.RemoteR } } +func writeErrorToPty(cmd *sstore.CmdType, errStr string, outputPos int64) { + errPk := openai.CreateErrorPacket(errStr) + errBytes, err := packet.MarshalPacket(errPk) + if err != nil { + log.Printf("error writing error packet to openai response: %v\n", err) + return + } + errCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + update, err := sstore.AppendToCmdPtyBlob(errCtx, cmd.ScreenId, cmd.CmdId, errBytes, outputPos) + if err != nil { + log.Printf("error writing ptyupdate for openai response: %v\n", err) + return + } + sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update) + return +} + +func writePacketToPty(ctx context.Context, cmd *sstore.CmdType, pk packet.PacketType, outputPos *int64) error { + outBytes, err := packet.MarshalPacket(pk) + if err != nil { + return err + } + update, err := sstore.AppendToCmdPtyBlob(ctx, cmd.ScreenId, cmd.CmdId, outBytes, *outputPos) + if err != nil { + return err + } + *outputPos += int64(len(outBytes)) + sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update) + return nil +} + +func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) { + var outputPos int64 + var hadError bool + startTime := time.Now() + ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelFn() + defer func() { + r := recover() + if r != nil { + panicMsg := fmt.Sprintf("panic: %v", r) + log.Printf("panic in doOpenAICompletion: %s\n", panicMsg) + writeErrorToPty(cmd, panicMsg, outputPos) + hadError = true + } + duration := time.Since(startTime) + cmdStatus := sstore.CmdStatusDone + var exitCode int64 + if hadError { + cmdStatus = sstore.CmdStatusError + exitCode = 1 + } + doneInfo := &sstore.CmdDoneInfo{ + Ts: time.Now().UnixMilli(), + ExitCode: exitCode, + DurationMs: duration.Milliseconds(), + } + ck := base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) + update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, doneInfo, cmdStatus) + if err != nil { + // nothing to do + log.Printf("error updating cmddoneinfo (in openai): %v\n", err) + return + } + sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update) + }() + respPks, err := openai.RunCompletion(ctx, opts, prompt) + if err != nil { + writeErrorToPty(cmd, fmt.Sprintf("error calling OpenAI API: %v", err), outputPos) + return + } + for _, pk := range respPks { + err = writePacketToPty(ctx, cmd, pk, &outputPos) + if err != nil { + writeErrorToPty(cmd, fmt.Sprintf("error writing response to ptybuffer: %v", err), outputPos) + return + } + } + return +} + +func doOpenAIStreamCompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) { + var outputPos int64 + var hadError bool + startTime := time.Now() + ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelFn() + defer func() { + r := recover() + if r != nil { + panicMsg := fmt.Sprintf("panic: %v", r) + log.Printf("panic in doOpenAICompletion: %s\n", panicMsg) + writeErrorToPty(cmd, panicMsg, outputPos) + hadError = true + } + duration := time.Since(startTime) + cmdStatus := sstore.CmdStatusDone + var exitCode int64 + if hadError { + cmdStatus = sstore.CmdStatusError + exitCode = 1 + } + doneInfo := &sstore.CmdDoneInfo{ + Ts: time.Now().UnixMilli(), + ExitCode: exitCode, + DurationMs: duration.Milliseconds(), + } + ck := base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) + update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, doneInfo, cmdStatus) + if err != nil { + // nothing to do + log.Printf("error updating cmddoneinfo (in openai): %v\n", err) + return + } + sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update) + }() + ch, err := openai.RunCompletionStream(ctx, opts, prompt) + if err != nil { + writeErrorToPty(cmd, fmt.Sprintf("error calling OpenAI API: %v", err), outputPos) + return + } + for pk := range ch { + err = writePacketToPty(ctx, cmd, pk, &outputPos) + if err != nil { + writeErrorToPty(cmd, fmt.Sprintf("error writing response to ptybuffer: %v", err), outputPos) + return + } + } + return +} + +func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) + if err != nil { + return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err) + } + opts := &sstore.OpenAIOptsType{ + Model: "gpt-3.5-turbo", + APIToken: OpenAIKey, + MaxTokens: 1000, + } + promptStr := firstArg(pk) + if promptStr == "" { + return nil, fmt.Errorf("/openai error, prompt string is blank") + } + ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) + pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal) + if err != nil { + return nil, fmt.Errorf("/openai error, invalid 'pterm' value %q: %v", ptermVal, err) + } + termOpts := convertTermOpts(pkTermOpts) + cmd, err := makeDynCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), *termOpts) + if err != nil { + return nil, fmt.Errorf("/openai error, cannot make dyn cmd") + } + line, err := sstore.AddOpenAILine(ctx, ids.ScreenId, DefaultUserId, cmd) + if err != nil { + return nil, fmt.Errorf("cannot add new line: %v", err) + } + prompt := []sstore.OpenAIPromptMessageType{{Role: sstore.OpenAIRoleUser, Content: promptStr}} + if pk.MetaSubCmd == "stream" { + go doOpenAIStreamCompletion(cmd, opts, prompt) + } else { + go doOpenAICompletion(cmd, opts, prompt) + } + updateHistoryContext(ctx, line, cmd) + updateMap := make(map[string]interface{}) + updateMap[sstore.ScreenField_SelectedLine] = line.LineNum + updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput + screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) + if err != nil { + // ignore error again (nothing to do) + log.Printf("/openai error updating screen selected line: %v\n", err) + } + update := sstore.ModelUpdate{Line: line, Cmd: cmd, Screens: []*sstore.ScreenType{screen}} + return update, nil +} + func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { @@ -1350,6 +1532,33 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up return update, nil } +func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, termOpts sstore.TermOpts) (*sstore.CmdType, error) { + cmd := &sstore.CmdType{ + ScreenId: ids.ScreenId, + CmdId: scbase.GenPromptUUID(), + CmdStr: cmdStr, + RawCmdStr: cmdStr, + Remote: ids.Remote.RemotePtr, + TermOpts: termOpts, + Status: sstore.CmdStatusRunning, + StartPk: nil, + DoneInfo: nil, + RunOut: nil, + } + if ids.Remote.StatePtr != nil { + cmd.StatePtr = *ids.Remote.StatePtr + } + if ids.Remote.FeState != nil { + cmd.FeState = ids.Remote.FeState + } + err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) + } + return cmd, nil +} + func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ ScreenId: ids.ScreenId, diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 25c384d10..d1b48c7f3 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -28,6 +28,20 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"reset", "reset"}, } +const ( + CmdParseTypePositional = "pos" + CmdParseTypeRaw = "raw" +) + +var CmdParseOverrides map[string]string = map[string]string{ + "setenv": CmdParseTypePositional, + "unset": CmdParseTypePositional, + "set": CmdParseTypePositional, + "run": CmdParseTypeRaw, + "comment": CmdParseTypeRaw, + "openai": CmdParseTypeRaw, +} + func DumpPacket(pk *scpacket.FeCommandPacketType) { if pk == nil || pk.MetaCmd == "" { fmt.Printf("[no metacmd]\n") @@ -111,11 +125,11 @@ func parseMetaCmd(origCommandStr string) (string, string, string) { } func onlyPositionalArgs(metaCmd string, metaSubCmd string) bool { - return (metaCmd == "setenv" || metaCmd == "unset" || metaCmd == "set") && metaSubCmd == "" + return (CmdParseOverrides[metaCmd] == CmdParseTypePositional) && metaSubCmd == "" } func onlyRawArgs(metaCmd string, metaSubCmd string) bool { - return metaCmd == "run" || metaCmd == "comment" + return CmdParseOverrides[metaCmd] == CmdParseTypeRaw } func setBracketArgs(argMap map[string]string, bracketStr string) error { diff --git a/pkg/cmdrunner/termopts.go b/pkg/cmdrunner/termopts.go index e5c87a855..5b34dfa0a 100644 --- a/pkg/cmdrunner/termopts.go +++ b/pkg/cmdrunner/termopts.go @@ -9,6 +9,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/remote" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" ) // PTERM=MxM,Mx25 @@ -108,3 +109,12 @@ func GetUITermOpts(winSize *packet.WinSize, ptermStr string) (*packet.TermOpts, termOpts.Rows = base.BoundInt(termOpts.Rows, shexec.MinTermRows, shexec.MaxTermRows) return termOpts, nil } + +func convertTermOpts(pkto *packet.TermOpts) *sstore.TermOpts { + return &sstore.TermOpts{ + Rows: int64(pkto.Rows), + Cols: int64(pkto.Cols), + FlexRows: true, + MaxPtySize: pkto.MaxPtySize, + } +} diff --git a/pkg/remote/openai/openai.go b/pkg/remote/openai/openai.go new file mode 100644 index 000000000..04eb05d79 --- /dev/null +++ b/pkg/remote/openai/openai.go @@ -0,0 +1,147 @@ +package openai + +import ( + "context" + "fmt" + "io" + "log" + + openaiapi "github.com/sashabaranov/go-openai" + "github.com/scripthaus-dev/mshell/pkg/packet" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +// https://github.com/tiktoken-go/tokenizer + +const DefaultStreamChanSize = 10 + +func convertUsage(resp openaiapi.ChatCompletionResponse) *packet.OpenAIUsageType { + if resp.Usage.TotalTokens == 0 { + return nil + } + return &packet.OpenAIUsageType{ + PromptTokens: resp.Usage.PromptTokens, + CompletionTokens: resp.Usage.CompletionTokens, + TotalTokens: resp.Usage.TotalTokens, + } +} + +func convertPrompt(prompt []sstore.OpenAIPromptMessageType) []openaiapi.ChatCompletionMessage { + var rtn []openaiapi.ChatCompletionMessage + for _, p := range prompt { + msg := openaiapi.ChatCompletionMessage{Role: p.Role, Content: p.Content, Name: p.Name} + rtn = append(rtn, msg) + } + return rtn +} + +func RunCompletion(ctx context.Context, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) ([]*packet.OpenAIPacketType, error) { + if opts == nil { + return nil, fmt.Errorf("no openai opts found") + } + if opts.Model == "" { + return nil, fmt.Errorf("no openai model specified") + } + if opts.APIToken == "" { + return nil, fmt.Errorf("no api token") + } + client := openaiapi.NewClient(opts.APIToken) + req := openaiapi.ChatCompletionRequest{ + Model: opts.Model, + Messages: convertPrompt(prompt), + MaxTokens: opts.MaxTokens, + } + if opts.MaxChoices > 1 { + req.N = opts.MaxChoices + } + apiResp, err := client.CreateChatCompletion(ctx, req) + if err != nil { + return nil, fmt.Errorf("error calling openai API: %v", err) + } + if len(apiResp.Choices) == 0 { + return nil, fmt.Errorf("no response received") + } + return marshalResponse(apiResp), nil +} + +func RunCompletionStream(ctx context.Context, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) (chan *packet.OpenAIPacketType, error) { + if opts == nil { + return nil, fmt.Errorf("no openai opts found") + } + if opts.Model == "" { + return nil, fmt.Errorf("no openai model specified") + } + if opts.APIToken == "" { + return nil, fmt.Errorf("no api token") + } + client := openaiapi.NewClient(opts.APIToken) + req := openaiapi.ChatCompletionRequest{ + Model: opts.Model, + Messages: convertPrompt(prompt), + MaxTokens: opts.MaxTokens, + Stream: true, + } + if opts.MaxChoices > 1 { + req.N = opts.MaxChoices + } + apiResp, err := client.CreateChatCompletionStream(ctx, req) + if err != nil { + return nil, fmt.Errorf("error calling openai API: %v", err) + } + rtn := make(chan *packet.OpenAIPacketType, DefaultStreamChanSize) + go func() { + sentHeader := false + defer close(rtn) + for { + streamResp, err := apiResp.Recv() + if err == io.EOF { + break + } + if err != nil { + errPk := CreateErrorPacket(fmt.Sprintf("error in recv of streaming data: %v", err)) + rtn <- errPk + break + } + log.Printf("stream-resp: %#v\n", streamResp) + if streamResp.Model != "" && !sentHeader { + pk := packet.MakeOpenAIPacket() + pk.Model = streamResp.Model + pk.Created = streamResp.Created + rtn <- pk + sentHeader = true + } + for _, choice := range streamResp.Choices { + pk := packet.MakeOpenAIPacket() + pk.Index = choice.Index + pk.Text = choice.Delta.Content + pk.FinishReason = choice.FinishReason + rtn <- pk + } + } + }() + return rtn, err +} + +func marshalResponse(resp openaiapi.ChatCompletionResponse) []*packet.OpenAIPacketType { + var rtn []*packet.OpenAIPacketType + headerPk := packet.MakeOpenAIPacket() + headerPk.Model = resp.Model + headerPk.Created = resp.Created + headerPk.Usage = convertUsage(resp) + rtn = append(rtn, headerPk) + for _, choice := range resp.Choices { + choicePk := packet.MakeOpenAIPacket() + choicePk.Index = choice.Index + choicePk.Text = choice.Message.Content + choicePk.FinishReason = choice.FinishReason + rtn = append(rtn, choicePk) + } + return rtn +} + +func CreateErrorPacket(errStr string) *packet.OpenAIPacketType { + errPk := packet.MakeOpenAIPacket() + errPk.Text = errStr + errPk.FinishReason = "stop" + return errPk +} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index d0948be91..b14465580 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1647,7 +1647,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { ExitCode: int64(donePk.ExitCode), DurationMs: donePk.DurationMs, } - update, err := sstore.UpdateCmdDoneInfo(context.Background(), donePk.CK, doneInfo) + update, err := sstore.UpdateCmdDoneInfo(context.Background(), donePk.CK, doneInfo, sstore.CmdStatusDone) if err != nil { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 38977c5c5..a5eb11814 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -848,7 +848,7 @@ func GetCmdByScreenId(ctx context.Context, screenId string, cmdId string) (*CmdT return cmd, nil } -func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDoneInfo) (*ModelUpdate, error) { +func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDoneInfo, status string) (*ModelUpdate, error) { if doneInfo == nil { return nil, fmt.Errorf("invalid cmddone packet") } @@ -859,7 +859,7 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, CmdStatusDone, quickJson(doneInfo), screenId, ck.GetCmdId()) + tx.Exec(query, status, quickJson(doneInfo), screenId, ck.GetCmdId()) var err error rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, ck.GetCmdId()) if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 026588958..89a8bf6a6 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -28,8 +28,6 @@ import ( _ "github.com/mattn/go-sqlite3" ) -const LineTypeCmd = "cmd" -const LineTypeText = "text" const LineNoHeight = -1 const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" @@ -41,6 +39,12 @@ const LocalRemoteAlias = "local" const DefaultCwd = "~" +const ( + LineTypeCmd = "cmd" + LineTypeText = "text" + LineTypeOpenAI = "openai" +) + const ( MainViewSession = "session" MainViewBookmarks = "bookmarks" @@ -56,6 +60,16 @@ const ( CmdStatusWaiting = "waiting" ) +const ( + CmdRendererOpenAI = "openai" +) + +const ( + OpenAIRoleSystem = "system" + OpenAIRoleUser = "user" + OpenAIRoleAssistant = "assistant" +) + const ( RemoteAuthTypeNone = "none" RemoteAuthTypePassword = "password" @@ -685,6 +699,31 @@ type LineType struct { Remove bool `json:"remove,omitempty"` } +type OpenAIUsage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} + +type OpenAIChoiceType struct { + Text string `json:"text"` + Index int `json:"index"` + FinishReason string `json:"finish_reason"` +} + +type OpenAIResponse struct { + Model string `json:"model"` + Created int64 `json:"created"` + Usage *OpenAIUsage `json:"usage,omitempty"` + Choices []OpenAIChoiceType `json:"choices,omitempty"` +} + +type OpenAIPromptMessageType struct { + Role string `json:"role"` + Content string `json:"content"` + Name string `json:"name,omitempty"` +} + type PlaybookType struct { PlaybookId string `json:"playbookid"` PlaybookName string `json:"playbookname"` @@ -829,6 +868,10 @@ type RemoteOptsType struct { } type OpenAIOptsType struct { + Model string `json:"model"` + APIToken string `json:"apitoken"` + MaxTokens int `json:"maxtokens,omitempty"` + MaxChoices int `json:"maxchoices,omitempty"` } type RemoteType struct { @@ -1011,6 +1054,20 @@ func makeNewLineText(screenId string, userId string, text string) *LineType { return rtn } +func makeNewLineOpenAI(screenId string, userId string, cmdId string) *LineType { + rtn := &LineType{} + rtn.ScreenId = screenId + rtn.UserId = userId + rtn.LineId = scbase.GenPromptUUID() + rtn.CmdId = cmdId + rtn.Ts = time.Now().UnixMilli() + rtn.LineLocal = true + rtn.LineType = LineTypeOpenAI + rtn.ContentHeight = LineNoHeight + rtn.Renderer = CmdRendererOpenAI + return rtn +} + func AddCommentLine(ctx context.Context, screenId string, userId string, commentText string) (*LineType, error) { rtnLine := makeNewLineText(screenId, userId, commentText) err := InsertLine(ctx, rtnLine, nil) @@ -1020,6 +1077,15 @@ func AddCommentLine(ctx context.Context, screenId string, userId string, comment return rtnLine, nil } +func AddOpenAILine(ctx context.Context, screenId string, userId string, cmd *CmdType) (*LineType, error) { + rtnLine := makeNewLineOpenAI(screenId, userId, cmd.CmdId) + err := InsertLine(ctx, rtnLine, cmd) + if err != nil { + return nil, err + } + return rtnLine, nil +} + func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { rtnLine := makeNewLineCmd(screenId, userId, cmd.CmdId, renderer) err := InsertLine(ctx, rtnLine, cmd) From 59ff522cd1c0afd8d08923ea88ee62ba880e1677 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 May 2023 16:06:51 -0700 Subject: [PATCH 365/397] updates to read openai token from clientdata --- cmd/main-server.go | 4 + db/migrations/000019_clientopenai.down.sql | 1 + db/migrations/000019_clientopenai.up.sql | 2 + pkg/cmdrunner/cmdrunner.go | 212 ++++++++++++++++----- pkg/remote/openai/openai.go | 6 +- pkg/remote/remote.go | 5 +- pkg/sstore/dbops.go | 15 +- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 21 ++ pkg/sstore/updatebus.go | 28 ++- 10 files changed, 235 insertions(+), 61 deletions(-) create mode 100644 db/migrations/000019_clientopenai.down.sql create mode 100644 db/migrations/000019_clientopenai.up.sql diff --git a/cmd/main-server.go b/cmd/main-server.go index 0aeeb2e46..171bb5279 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -144,6 +144,7 @@ func HandleGetClientData(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, err) return } + cdata = cdata.Clean() mdata, err := sstore.GetCmdMigrationInfo(r.Context()) if err != nil { WriteJsonError(w, err) @@ -368,6 +369,9 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, err) return } + if update != nil { + update.Clean() + } WriteJsonSuccess(w, update) return } diff --git a/db/migrations/000019_clientopenai.down.sql b/db/migrations/000019_clientopenai.down.sql new file mode 100644 index 000000000..983f5d519 --- /dev/null +++ b/db/migrations/000019_clientopenai.down.sql @@ -0,0 +1 @@ +ALTER TABLE client DROP COLUMN openaiopts; diff --git a/db/migrations/000019_clientopenai.up.sql b/db/migrations/000019_clientopenai.up.sql new file mode 100644 index 000000000..a591cabc0 --- /dev/null +++ b/db/migrations/000019_clientopenai.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE client ADD COLUMN openaiopts json NOT NULL DEFAULT '{}'; + diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index debea3027..3de56de1b 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -53,6 +53,8 @@ const MaxCommandLen = 4096 const MaxSignalLen = 12 const MaxSignalNum = 64 const MaxEvalDepth = 5 +const MaxOpenAIAPITokenLen = 100 +const MaxOpenAIModelLen = 100 const ( KwArgRenderer = "renderer" @@ -200,7 +202,6 @@ func init() { registerCmdFn("bookmark:delete", BookmarkDeleteCommand) registerCmdFn("openai", OpenAICommand) - registerCmdFn("openai:stream", OpenAICommand) registerCmdFn("_killserver", KillServerCommand) @@ -613,7 +614,7 @@ func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Screens: []*sstore.ScreenType{screen}, } return update, nil @@ -759,7 +760,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if !setNonAnchor { return nil, nil } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Screens: []*sstore.ScreenType{screen}, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)), @@ -798,7 +799,7 @@ func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } mshell := ids.Remote.MShell go mshell.RunInstall() - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, @@ -812,7 +813,7 @@ func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacke } mshell := ids.Remote.MShell go mshell.CancelInstall() - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, @@ -825,7 +826,7 @@ func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return nil, err } go ids.Remote.MShell.Launch(true) - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, @@ -839,7 +840,7 @@ func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy } force := resolveBool(pk.Kwargs["force"], false) go ids.Remote.MShell.Disconnect(force) - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemotePtr.RemoteId, }, @@ -853,7 +854,7 @@ func makeRemoteEditUpdate_new(err error) sstore.UpdatePacket { if err != nil { redit.ErrorStr = err.Error() } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ RemoteEdit: redit, }, @@ -880,7 +881,7 @@ func makeRemoteEditUpdate_edit(ids resolvedIds, err error) sstore.UpdatePacket { if err != nil { redit.ErrorStr = err.Error() } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ RemoteEdit: redit, }, @@ -1081,7 +1082,7 @@ func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return makeRemoteEditErrorReturn_new(visualEdit, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)) } // SUCCESS - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: r.RemoteId, }, @@ -1110,13 +1111,13 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err)) } if visualEdit { - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: ids.Remote.RemoteCopy.RemoteId, }, }, nil } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("remote %q updated", ids.Remote.DisplayName), TimeoutMs: 2000, @@ -1131,7 +1132,7 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return nil, err } state := ids.Remote.RState - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ PtyRemoteId: state.RemoteId, }, @@ -1150,7 +1151,7 @@ func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } buf.WriteString(fmt.Sprintf("%-12s %-5s %8s %s\n", rstate.Status, rstate.RemoteType, rstate.RemoteId[0:8], name)) } - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ RemoteView: &sstore.RemoteViewType{ RemoteShowAll: true, }, @@ -1176,7 +1177,7 @@ func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+archivedStr, screen.ScreenId, screenIdxStr) buf.WriteString(outStr) } - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("all screens for session"), InfoLines: splitLinesForInfo(buf.String()), @@ -1289,7 +1290,7 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re } buf.WriteString(fmt.Sprintf("%-30s %-50s (default)\n", msh.GetDisplayName(), cwdStr)) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoLines: splitLinesForInfo(buf.String()), }, @@ -1452,10 +1453,19 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err) } - opts := &sstore.OpenAIOptsType{ - Model: "gpt-3.5-turbo", - APIToken: OpenAIKey, - MaxTokens: 1000, + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v", err) + } + if clientData.OpenAIOpts == nil || clientData.OpenAIOpts.APIToken == "" { + return nil, fmt.Errorf("no openai API token found, configure in client settings") + } + opts := clientData.OpenAIOpts + if opts.Model == "" { + opts.Model = openai.DefaultModel + } + if opts.MaxTokens == 0 { + opts.MaxTokens = openai.DefaultMaxTokens } promptStr := firstArg(pk) if promptStr == "" { @@ -1476,7 +1486,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("cannot add new line: %v", err) } prompt := []sstore.OpenAIPromptMessageType{{Role: sstore.OpenAIRoleUser, Content: promptStr}} - if pk.MetaSubCmd == "stream" { + if resolveBool(pk.Kwargs["stream"], true) { go doOpenAIStreamCompletion(cmd, opts, prompt) } else { go doOpenAICompletion(cmd, opts, prompt) @@ -1490,7 +1500,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor // ignore error again (nothing to do) log.Printf("/openai error updating screen selected line: %v\n", err) } - update := sstore.ModelUpdate{Line: line, Cmd: cmd, Screens: []*sstore.ScreenType{screen}} + update := &sstore.ModelUpdate{Line: line, Cmd: cmd, Screens: []*sstore.ScreenType{screen}} return update, nil } @@ -1656,7 +1666,7 @@ func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.Upd if len(comps) == 0 { comps = []string{"(no completions)"} } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("%s completions", compType), InfoComps: comps, @@ -1786,7 +1796,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if newSP == nil || cmdSP == *newSP { return nil, nil } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos}, } return update, nil @@ -1814,7 +1824,7 @@ func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto // ignore error again (nothing to do) log.Printf("/comment error updating screen selected line: %v\n", err) } - update := sstore.ModelUpdate{Line: rtnLine, Screens: []*sstore.ScreenType{screen}} + update := &sstore.ModelUpdate{Line: rtnLine, Screens: []*sstore.ScreenType{screen}} return update, nil } @@ -2008,7 +2018,7 @@ func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType if err != nil { return nil, fmt.Errorf("cannot get updated screen: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Screens: []*sstore.ScreenType{screen}, Info: &sstore.InfoMsgType{ InfoMsg: infoMsg, @@ -2129,7 +2139,7 @@ func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( buf.WriteString(fmt.Sprintf(" %-15s %d\n", "cmds", stats.NumCmds)) buf.WriteString(fmt.Sprintf(" %-15s %0.2fM\n", "disksize", float64(stats.DiskStats.TotalSize)/1000000)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "disk-location", stats.DiskStats.Location)) - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: "session info", InfoLines: splitLinesForInfo(buf.String()), @@ -2155,7 +2165,7 @@ func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType outStr := fmt.Sprintf("%-30s %s %s\n", session.Name+archivedStr, session.SessionId, sessionIdxStr) buf.WriteString(outStr) } - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: "all sessions", InfoLines: splitLinesForInfo(buf.String()), @@ -2188,7 +2198,7 @@ func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false)) } bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId) - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Sessions: []*sstore.SessionType{bareSession}, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("session updated %s", formatStrs(varsUpdated, "and", false)), @@ -2215,7 +2225,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, err } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ ActiveSessionId: ritem.Id, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("switched to session %q", ritem.Name), @@ -2303,7 +2313,7 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { return nil, fmt.Errorf("/history:purge error purging items: %v", err) } - update := sstore.ModelUpdate{} + update := &sstore.ModelUpdate{} for _, historyItem := range historyItemsRemoved { if historyItem.LineId == "" { continue @@ -2405,7 +2415,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType } hvdata.Lines = lines hvdata.Cmds = cmds - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ HistoryViewData: hvdata, MainView: sstore.MainViewHistory, } @@ -2456,7 +2466,7 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto log.Printf("error updating current activity (history): %v\n", err) } } - update := sstore.ModelUpdate{} + update := &sstore.ModelUpdate{} update.History = &sstore.HistoryInfoType{ HistoryType: htype, SessionId: ids.SessionId, @@ -2601,7 +2611,7 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto if err != nil { return nil, fmt.Errorf("/line:set cannot retrieve updated line: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Line: updatedLine, Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("line updated %s", formatStrs(varsUpdated, "and", false)), @@ -2672,7 +2682,7 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if err != nil { log.Printf("error updating current activity (bookmarks): %v\n", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ MainView: sstore.MainViewBookmarks, Bookmarks: bms, } @@ -2709,7 +2719,7 @@ func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if err != nil { return nil, fmt.Errorf("error retrieving edited bookmark: %v", err) } - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: "bookmark edited", }, @@ -2734,7 +2744,7 @@ func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType return nil, fmt.Errorf("error deleting bookmark: %v", err) } bm := &sstore.BookmarkType{BookmarkId: bookmarkId, Remove: true} - return sstore.ModelUpdate{ + return &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: "bookmark deleted", }, @@ -2788,7 +2798,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) newBmId = newBm.BookmarkId } bms, err := sstore.GetBookmarks(ctx, "") - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ MainView: sstore.MainViewBookmarks, Bookmarks: bms, SelectedBookmark: newBmId, @@ -2838,7 +2848,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst // no line (which is strange given we checked for it above). just return a nop. return nil, nil } - return sstore.ModelUpdate{Line: lineObj}, nil + return &sstore.ModelUpdate{Line: lineObj}, nil } func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -2873,7 +2883,7 @@ func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // no line (which is strange given we checked for it above). just return a nop. return nil, nil } - return sstore.ModelUpdate{Line: lineObj}, nil + return &sstore.ModelUpdate{Line: lineObj}, nil } func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { @@ -2899,7 +2909,7 @@ func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss if err != nil { return nil, fmt.Errorf("/line:purge error purging lines: %v", err) } - update := sstore.ModelUpdate{} + update := &sstore.ModelUpdate{} for _, lineId := range lineIds { lineObj := &sstore.LineType{ ScreenId: ids.ScreenId, @@ -2975,7 +2985,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr)) } } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("line %d info", line.LineNum), InfoLines: splitLinesForInfo(buf.String()), @@ -3069,7 +3079,7 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor if err != nil { return nil, fmt.Errorf("cannot send signal: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("sent line %s signal %s", lineArg, sigArg), }, @@ -3093,7 +3103,7 @@ func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor func ClientNotifyUpdateWriterCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { pcloud.ResetUpdateWriterNumFailures() sstore.NotifyUpdateWriter() - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("notified update writer"), }, @@ -3123,12 +3133,42 @@ func ClientAcceptTosCommand(ctx context.Context, pk *scpacket.FeCommandPacketTyp if err != nil { return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ ClientData: clientData, } return update, nil } +func validateOpenAIAPIToken(key string) error { + if len(key) == 0 { + return fmt.Errorf("invalid openai token, zero length") + } + if len(key) > MaxOpenAIAPITokenLen { + return fmt.Errorf("invalid openai token, too long") + } + for idx, ch := range key { + if !unicode.IsPrint(ch) { + return fmt.Errorf("invalid openai token, char at idx:%d is invalid %q", idx, string(ch)) + } + } + return nil +} + +func validateOpenAIModel(model string) error { + if len(model) == 0 { + return nil + } + if len(model) > MaxOpenAIModelLen { + return fmt.Errorf("invalid openai model, too long") + } + for idx, ch := range model { + if !unicode.IsPrint(ch) { + return fmt.Errorf("invalid openai model, char at idx:%d is invalid %q", idx, string(ch)) + } + } + return nil +} + func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { clientData, err := sstore.EnsureClientData(ctx) if err != nil { @@ -3151,14 +3191,88 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss } varsUpdated = append(varsUpdated, "termfontsize") } + if apiToken, found := pk.Kwargs["openaiapitoken"]; found { + err = validateOpenAIAPIToken(apiToken) + if err != nil { + return nil, err + } + varsUpdated = append(varsUpdated, "openaiapitoken") + aiOpts := clientData.OpenAIOpts + if aiOpts == nil { + aiOpts = &sstore.OpenAIOptsType{} + clientData.OpenAIOpts = aiOpts + } + aiOpts.APIToken = apiToken + err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts) + if err != nil { + return nil, fmt.Errorf("error updating client openai api token: %v", err) + } + } + if aiModel, found := pk.Kwargs["openaimodel"]; found { + err = validateOpenAIModel(aiModel) + if err != nil { + return nil, err + } + varsUpdated = append(varsUpdated, "openaimodel") + aiOpts := clientData.OpenAIOpts + if aiOpts == nil { + aiOpts = &sstore.OpenAIOptsType{} + clientData.OpenAIOpts = aiOpts + } + aiOpts.Model = aiModel + err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts) + if err != nil { + return nil, fmt.Errorf("error updating client openai model: %v", err) + } + } + if maxTokensStr, found := pk.Kwargs["openaimaxtokens"]; found { + maxTokens, err := strconv.Atoi(maxTokensStr) + if err != nil { + return nil, fmt.Errorf("error updating client openai maxtokens, invalid number: %v", err) + } + if maxTokens < 0 || maxTokens > 1000000 { + return nil, fmt.Errorf("error updating client openai maxtokens, out of range: %d", maxTokens) + } + varsUpdated = append(varsUpdated, "openaimaxtokens") + aiOpts := clientData.OpenAIOpts + if aiOpts == nil { + aiOpts = &sstore.OpenAIOptsType{} + clientData.OpenAIOpts = aiOpts + } + aiOpts.MaxTokens = maxTokens + err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts) + if err != nil { + return nil, fmt.Errorf("error updating client openai maxtokens: %v", err) + } + } + if maxChoicesStr, found := pk.Kwargs["openaimaxchoices"]; found { + maxChoices, err := strconv.Atoi(maxChoicesStr) + if err != nil { + return nil, fmt.Errorf("error updating client openai maxchoices, invalid number: %v", err) + } + if maxChoices < 0 || maxChoices > 10 { + return nil, fmt.Errorf("error updating client openai maxchoices, out of range: %d", maxChoices) + } + varsUpdated = append(varsUpdated, "openaimaxchoices") + aiOpts := clientData.OpenAIOpts + if aiOpts == nil { + aiOpts = &sstore.OpenAIOptsType{} + clientData.OpenAIOpts = aiOpts + } + aiOpts.MaxChoices = maxChoices + err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts) + if err != nil { + return nil, fmt.Errorf("error updating client openai maxchoices: %v", err) + } + } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize"}, "or", false)) + return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize", "openaiapitoken", "openaimodel", "openaimaxtokens", "openaimaxchoices"}, "or", false)) } clientData, err = sstore.EnsureClientData(ctx) if err != nil { return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) } - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)), TimeoutMs: 2000, @@ -3189,7 +3303,7 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s buf.WriteString(fmt.Sprintf(" %-15s %s\n", "client-version", clientVersion)) buf.WriteString(fmt.Sprintf(" %-15s %s %s\n", "server-version", scbase.PromptVersion, scbase.BuildTime)) buf.WriteString(fmt.Sprintf(" %-15s %s (%s)\n", "arch", scbase.ClientArch(), scbase.MacOSRelease())) - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("client info"), InfoLines: splitLinesForInfo(buf.String()), @@ -3273,7 +3387,7 @@ func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) } var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) - update := sstore.ModelUpdate{ + update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("telemetry info"), InfoLines: splitLinesForInfo(buf.String()), diff --git a/pkg/remote/openai/openai.go b/pkg/remote/openai/openai.go index 04eb05d79..ed04582fb 100644 --- a/pkg/remote/openai/openai.go +++ b/pkg/remote/openai/openai.go @@ -13,6 +13,8 @@ import ( // https://github.com/tiktoken-go/tokenizer +const DefaultMaxTokens = 1000 +const DefaultModel = "gpt-3.5-turbo" const DefaultStreamChanSize = 10 func convertUsage(resp openaiapi.ChatCompletionResponse) *packet.OpenAIUsageType { @@ -141,7 +143,7 @@ func marshalResponse(resp openaiapi.ChatCompletionResponse) []*packet.OpenAIPack func CreateErrorPacket(errStr string) *packet.OpenAIPacketType { errPk := packet.MakeOpenAIPacket() - errPk.Text = errStr - errPk.FinishReason = "stop" + errPk.FinishReason = "error" + errPk.Error = errStr return errPk } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index b14465580..76060f334 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -622,6 +622,9 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState { vars["bestname"] = vars["bestuser"] + "@" + vars["besthost"] vars["bestshortname"] = vars["bestuser"] + "@" + vars["bestshorthost"] } + if vars["remoteuser"] == "root" || vars["sudo"] == "1" { + vars["isroot"] = "1" + } state.RemoteVars = vars return state } @@ -1627,7 +1630,7 @@ func (msh *MShellProc) notifyHangups_nolock() { if err != nil { continue } - update := sstore.ModelUpdate{Cmd: cmd} + update := &sstore.ModelUpdate{Cmd: cmd} sstore.MainBus.SendScreenUpdate(ck.GetGroupId(), update) } msh.RunningCmds = make(map[base.CommandKey]RunCmdType) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index a5eb11814..5e28d4376 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -624,6 +624,15 @@ func UpdateClientFeOpts(ctx context.Context, feOpts FeOptsType) error { return txErr } +func UpdateClientOpenAIOpts(ctx context.Context, aiOpts OpenAIOptsType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET openaiopts = ?` + tx.Exec(query, quickJson(aiOpts)) + return nil + }) + return txErr +} + func containsStr(strs []string, testStr string) bool { for _, s := range strs { if s == testStr { @@ -1076,7 +1085,7 @@ func ArchiveScreen(ctx context.Context, sessionId string, screenId string) (Upda if err != nil { return nil, fmt.Errorf("cannot retrive archived screen: %w", err) } - update := ModelUpdate{Screens: []*ScreenType{newScreen}} + update := &ModelUpdate{Screens: []*ScreenType{newScreen}} if isActive { bareSession, err := GetBareSessionById(ctx, sessionId) if err != nil { @@ -1151,7 +1160,7 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP if sessionDel { return nil, nil } - update := ModelUpdate{} + update := &ModelUpdate{} update.Screens = []*ScreenType{&ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}} if isActive { bareSession, err := GetBareSessionById(ctx, sessionId) @@ -1521,7 +1530,7 @@ func PurgeSession(ctx context.Context, sessionId string) (UpdatePacket, error) { if txErr != nil { return nil, txErr } - update := ModelUpdate{} + update := &ModelUpdate{} if newActiveSessionId != "" { update.ActiveSessionId = newActiveSessionId } diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index f1a7b665c..3d9fc3cb3 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 18 +const MaxMigration = 19 const MigratePrimaryScreenVersion = 9 func MakeMigrate() (*migrate.Migrate, error) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 89a8bf6a6..cd07ddb3a 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -38,6 +38,7 @@ const DefaultSessionName = "default" const LocalRemoteAlias = "local" const DefaultCwd = "~" +const APITokenSentinel = "--apitoken--" const ( LineTypeCmd = "cmd" @@ -259,10 +260,30 @@ type ClientData struct { CmdStoreType string `json:"cmdstoretype"` Migration *ClientMigrationData `json:"migration,omitempty" dbmap:"-"` DBVersion int `json:"dbversion" dbmap:"-"` + OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty" dbmap:"openaiopts"` } func (ClientData) UseDBMap() {} +func (cdata *ClientData) Clean() *ClientData { + if cdata == nil { + return nil + } + rtn := *cdata + if rtn.OpenAIOpts != nil { + rtn.OpenAIOpts = &OpenAIOptsType{ + Model: cdata.OpenAIOpts.Model, + MaxTokens: cdata.OpenAIOpts.MaxTokens, + MaxChoices: cdata.OpenAIOpts.MaxChoices, + // omit API Token + } + if cdata.OpenAIOpts.APIToken != "" { + rtn.OpenAIOpts.APIToken = APITokenSentinel + } + } + return &rtn +} + type SessionType struct { SessionId string `json:"sessionid"` Name string `json:"name"` diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index ae505aab6..e53822248 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -14,6 +14,7 @@ const UpdateChSize = 100 type UpdatePacket interface { UpdateType() string + Clean() } type PtyDataUpdate struct { @@ -25,10 +26,12 @@ type PtyDataUpdate struct { PtyDataLen int64 `json:"ptydatalen"` } -func (PtyDataUpdate) UpdateType() string { +func (*PtyDataUpdate) UpdateType() string { return PtyDataUpdateStr } +func (pdu *PtyDataUpdate) Clean() {} + type ModelUpdate struct { Sessions []*SessionType `json:"sessions,omitempty"` ActiveSessionId string `json:"activesessionid,omitempty"` @@ -52,10 +55,17 @@ type ModelUpdate struct { RemoteView *RemoteViewType `json:"remoteview,omitempty"` } -func (ModelUpdate) UpdateType() string { +func (*ModelUpdate) UpdateType() string { return ModelUpdateStr } +func (update *ModelUpdate) Clean() { + if update == nil { + return + } + update.ClientData = update.ClientData.Clean() +} + type RemoteViewType struct { RemoteShowAll bool `json:"remoteshowall,omitempty"` PtyRemoteId string `json:"ptyremoteid,omitempty"` @@ -63,7 +73,7 @@ type RemoteViewType struct { } func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { - modelUpdate, ok := update.(ModelUpdate) + modelUpdate, ok := update.(*ModelUpdate) if !ok { return "", "", nil } @@ -183,7 +193,11 @@ func (bus *UpdateBus) UnregisterChannel(clientId string) { } } -func (bus *UpdateBus) SendUpdate(update interface{}) { +func (bus *UpdateBus) SendUpdate(update UpdatePacket) { + if update == nil { + return + } + update.Clean() bus.Lock.Lock() defer bus.Lock.Unlock() for _, uch := range bus.Channels { @@ -196,7 +210,11 @@ func (bus *UpdateBus) SendUpdate(update interface{}) { } } -func (bus *UpdateBus) SendScreenUpdate(screenId string, update interface{}) { +func (bus *UpdateBus) SendScreenUpdate(screenId string, update UpdatePacket) { + if update == nil { + return + } + update.Clean() bus.Lock.Lock() defer bus.Lock.Unlock() for _, uch := range bus.Channels { From c045b67afec121bb593de1768f8fe522ced1a679 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 May 2023 17:54:38 -0700 Subject: [PATCH 366/397] switch openai to chat for metacmds --- pkg/cmdrunner/cmdrunner.go | 12 ++++++------ pkg/cmdrunner/shparse.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3de56de1b..ed4726d15 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -65,7 +65,7 @@ var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} -var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal"} +var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry", "bookmark", "bookmarks"} @@ -201,7 +201,7 @@ func init() { registerCmdFn("bookmark:set", BookmarkSetCommand) registerCmdFn("bookmark:delete", BookmarkDeleteCommand) - registerCmdFn("openai", OpenAICommand) + registerCmdFn("chat", OpenAICommand) registerCmdFn("_killserver", KillServerCommand) @@ -1469,17 +1469,17 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor } promptStr := firstArg(pk) if promptStr == "" { - return nil, fmt.Errorf("/openai error, prompt string is blank") + return nil, fmt.Errorf("openai error, prompt string is blank") } ptermVal := defaultStr(pk.Kwargs["pterm"], DefaultPTERM) pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal) if err != nil { - return nil, fmt.Errorf("/openai error, invalid 'pterm' value %q: %v", ptermVal, err) + return nil, fmt.Errorf("openai error, invalid 'pterm' value %q: %v", ptermVal, err) } termOpts := convertTermOpts(pkTermOpts) cmd, err := makeDynCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), *termOpts) if err != nil { - return nil, fmt.Errorf("/openai error, cannot make dyn cmd") + return nil, fmt.Errorf("openai error, cannot make dyn cmd") } line, err := sstore.AddOpenAILine(ctx, ids.ScreenId, DefaultUserId, cmd) if err != nil { @@ -1498,7 +1498,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap) if err != nil { // ignore error again (nothing to do) - log.Printf("/openai error updating screen selected line: %v\n", err) + log.Printf("openai error updating screen selected line: %v\n", err) } update := &sstore.ModelUpdate{Line: line, Cmd: cmd, Screens: []*sstore.ScreenType{screen}} return update, nil diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index d1b48c7f3..128039258 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -39,7 +39,7 @@ var CmdParseOverrides map[string]string = map[string]string{ "set": CmdParseTypePositional, "run": CmdParseTypeRaw, "comment": CmdParseTypeRaw, - "openai": CmdParseTypeRaw, + "chat": CmdParseTypeRaw, } func DumpPacket(pk *scpacket.FeCommandPacketType) { From 6a6e51d783895afb050037dd3df9af94872cb7be Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 May 2023 17:57:15 -0700 Subject: [PATCH 367/397] bump version to v0.2.3 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 376775a20..0c09faaec 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.2.2" +const PromptVersion = "v0.2.3" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 7c09d8c09e7253c2e9bf5610563574fed7467199 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 8 May 2023 18:03:09 -0700 Subject: [PATCH 368/397] remove logging message --- pkg/remote/openai/openai.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/remote/openai/openai.go b/pkg/remote/openai/openai.go index ed04582fb..88075bf16 100644 --- a/pkg/remote/openai/openai.go +++ b/pkg/remote/openai/openai.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "log" openaiapi "github.com/sashabaranov/go-openai" "github.com/scripthaus-dev/mshell/pkg/packet" @@ -104,7 +103,6 @@ func RunCompletionStream(ctx context.Context, opts *sstore.OpenAIOptsType, promp rtn <- errPk break } - log.Printf("stream-resp: %#v\n", streamResp) if streamResp.Model != "" && !sentHeader { pk := packet.MakeOpenAIPacket() pk.Model = streamResp.Model From 5d72581683cc33c10b953c7cefa44542ea011014 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 26 Jul 2023 10:10:27 -0700 Subject: [PATCH 369/397] checkpoint --- db/schema.sql | 6 ++---- pkg/cmdrunner/cmdrunner.go | 4 ++-- pkg/sstore/dbops.go | 42 +++++++++++++++++--------------------- pkg/sstore/sstore.go | 13 +++++++++++- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 018e84425..dd4450148 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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'); +, clientopts json NOT NULL DEFAULT '', feopts json NOT NULL DEFAULT '{}', cmdstoretype varchar(20) DEFAULT 'session', openaiopts json NOT NULL DEFAULT '{}'); CREATE TABLE session ( sessionid varchar(36) PRIMARY KEY, name varchar(50) NOT NULL, @@ -43,11 +43,9 @@ CREATE TABLE state_diff ( ); CREATE TABLE remote ( remoteid varchar(36) PRIMARY KEY, - physicalid varchar(36) NOT NULL, remotetype varchar(10) NOT NULL, remotealias varchar(50) NOT NULL, remotecanonicalname varchar(200) NOT NULL, - remotesudo boolean NOT NULL, remoteuser varchar(50) NOT NULL, remotehost varchar(200) NOT NULL, connectmode varchar(20) NOT NULL, @@ -58,7 +56,7 @@ CREATE TABLE remote ( local boolean NOT NULL, archived boolean NOT NULL, remoteidx int NOT NULL -, statevars json NOT NULL DEFAULT '{}'); +, statevars json NOT NULL DEFAULT '{}', openaiopts json NOT NULL DEFAULT '{}'); CREATE TABLE history ( historyid varchar(36) PRIMARY KEY, ts bigint NOT NULL, diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index ed4726d15..47dfe885c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -657,7 +657,7 @@ func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s return nil, err } } - update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, activate) + update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, sstore.ScreenCreateOpts{}, activate) if err != nil { return nil, err } @@ -1937,7 +1937,7 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( return nil, err } } - update, err := sstore.InsertSessionWithName(ctx, newName, sstore.ShareModeLocal, activate) + update, err := sstore.InsertSessionWithName(ctx, newName, activate) if err != nil { return nil, err } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5e28d4376..4e03bb19b 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -525,27 +525,9 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) { return session, nil } -func InsertCloudSession(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { - var updateRtn *ModelUpdate - txErr := WithTx(ctx, func(tx *TxWrap) error { - var err error - updateRtn, err = InsertSessionWithName(tx.Context(), sessionName, shareMode, activate) - if err != nil { - return err - } - sessionId := updateRtn.Sessions[0].SessionId - fmt.Printf("sessionid: %v\n", sessionId) - return nil - }) - if txErr != nil { - return nil, txErr - } - return updateRtn, nil -} - // returns sessionId // if sessionName == "", it will be generated -func InsertSessionWithName(ctx context.Context, sessionName string, shareMode string, activate bool) (*ModelUpdate, error) { +func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*ModelUpdate, error) { var newScreen *ScreenType newSessionId := scbase.GenPromptUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { @@ -553,9 +535,9 @@ func InsertSessionWithName(ctx context.Context, sessionName string, shareMode st sessionName = fmtUniqueName(sessionName, "session-%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, 'local')` - tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, 0) - screenUpdate, err := InsertScreen(tx.Context(), newSessionId, "", true) + VALUES (?, ?, '', ?, 0, 0, 0, ?)` + tx.Exec(query, newSessionId, sessionName, maxSessionIdx+1, ShareModeLocal) + screenUpdate, err := InsertScreen(tx.Context(), newSessionId, "", ScreenCreateOpts{}, true) if err != nil { return err } @@ -666,7 +648,7 @@ func fmtUniqueName(name string, defaultFmtStr string, startIdx int, strs []strin } } -func InsertScreen(ctx context.Context, sessionId string, origScreenName string, activate bool) (*ModelUpdate, error) { +func InsertScreen(ctx context.Context, sessionId string, origScreenName string, opts ScreenCreateOpts, activate bool) (*ModelUpdate, error) { var newScreenId string txErr := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT sessionid FROM session WHERE sessionid = ? AND NOT archived` @@ -685,6 +667,20 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, } else { screenName = origScreenName } + var baseScreen *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 = 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.GenPromptUUID() screen := &ScreenType{ SessionId: sessionId, diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index cd07ddb3a..1dc630c61 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -429,6 +429,17 @@ type ScreenWebShareOpts struct { ViewKey string `json:"viewkey"` } +type ScreenCreateOpts struct { + BaseScreenId string + CopyRemote bool + CopyCwd bool + CopyEnv bool +} + +func (sco ScreenCreateOpts) HasCopy() bool { + return sco.CopyRemote || sco.CopyCwd || sco.CopyEnv +} + type ScreenType struct { SessionId string `json:"sessionid"` ScreenId string `json:"screenid"` @@ -1179,7 +1190,7 @@ func EnsureDefaultSession(ctx context.Context) (*SessionType, error) { if session != nil { return session, nil } - _, err = InsertSessionWithName(ctx, DefaultSessionName, ShareModeLocal, true) + _, err = InsertSessionWithName(ctx, DefaultSessionName, true) if err != nil { return nil, err } From afbc60ce92d2bf6d9714c6ca9f4b4b62aaf559a9 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 26 Jul 2023 14:24:14 -0700 Subject: [PATCH 370/397] update imports to reflect new package names (moved repo) --- cmd/main-server.go | 18 +++++++++--------- go.mod | 4 ++-- pkg/cmdrunner/cmdrunner.go | 22 +++++++++++----------- pkg/cmdrunner/resolver.go | 6 +++--- pkg/cmdrunner/shparse.go | 8 ++++---- pkg/cmdrunner/termopts.go | 10 +++++----- pkg/comp/comp.go | 8 ++++---- pkg/comp/simplecomp.go | 6 +++--- pkg/pcloud/pcloud.go | 8 ++++---- pkg/pcloud/pclouddata.go | 8 ++++---- pkg/remote/openai/openai.go | 4 ++-- pkg/remote/remote.go | 16 ++++++++-------- pkg/remote/updatequeue.go | 2 +- pkg/rtnstate/rtnstate.go | 10 +++++----- pkg/scbase/scbase.go | 2 +- pkg/scpacket/scpacket.go | 6 +++--- pkg/scws/scws.go | 12 ++++++------ pkg/shparse/comp.go | 2 +- pkg/shparse/extend.go | 2 +- pkg/shparse/shparse.go | 2 +- pkg/shparse/shparse_test.go | 2 +- pkg/sstore/dbops.go | 10 +++++----- pkg/sstore/fileops.go | 4 ++-- pkg/sstore/migrate.go | 2 +- pkg/sstore/quick.go | 2 +- pkg/sstore/sstore.go | 8 ++++---- 26 files changed, 92 insertions(+), 92 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 171bb5279..1e1a2e5d1 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -20,15 +20,15 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "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" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" - "github.com/scripthaus-dev/sh2-server/pkg/wsshell" + "github.com/commandlinedev/prompt-server/pkg/cmdrunner" + "github.com/commandlinedev/prompt-server/pkg/pcloud" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/rtnstate" + "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/scws" + "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/wsshell" ) type WebFnType = func(http.ResponseWriter, *http.Request) diff --git a/go.mod b/go.mod index 5c2dade3b..3904a9e84 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/scripthaus-dev/sh2-server +module github.com/commandlinedev/prompt-server go 1.18 @@ -13,7 +13,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.14 github.com/sawka/txwrap v0.1.2 - github.com/scripthaus-dev/mshell v0.0.0 + github.com/commandlinedev/apishell v0.0.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/mod v0.5.1 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 47dfe885c..d0f40778c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -18,17 +18,17 @@ import ( "unicode" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/sh2-server/pkg/comp" - "github.com/scripthaus-dev/sh2-server/pkg/pcloud" - "github.com/scripthaus-dev/sh2-server/pkg/remote" - "github.com/scripthaus-dev/sh2-server/pkg/remote/openai" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/apishell/pkg/base" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/prompt-server/pkg/comp" + "github.com/commandlinedev/prompt-server/pkg/pcloud" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/remote/openai" + "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) const ( diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index ea83da113..64c264fde 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -9,9 +9,9 @@ import ( "strings" "github.com/google/uuid" - "github.com/scripthaus-dev/sh2-server/pkg/remote" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) const ( diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 128039258..8ca9853f4 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -6,10 +6,10 @@ import ( "regexp" "strings" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/mshell/pkg/simpleexpand" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/apishell/pkg/simpleexpand" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/utilfn" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) diff --git a/pkg/cmdrunner/termopts.go b/pkg/cmdrunner/termopts.go index 5b34dfa0a..563690c33 100644 --- a/pkg/cmdrunner/termopts.go +++ b/pkg/cmdrunner/termopts.go @@ -5,11 +5,11 @@ import ( "strconv" "strings" - "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/sh2-server/pkg/remote" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/commandlinedev/apishell/pkg/base" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) // PTERM=MxM,Mx25 diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index 8dc8f457e..610043aed 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -11,10 +11,10 @@ import ( "unicode" "unicode/utf8" - "github.com/scripthaus-dev/mshell/pkg/simpleexpand" - "github.com/scripthaus-dev/sh2-server/pkg/shparse" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/apishell/pkg/simpleexpand" + "github.com/commandlinedev/prompt-server/pkg/shparse" + "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/utilfn" "mvdan.cc/sh/v3/syntax" ) diff --git a/pkg/comp/simplecomp.go b/pkg/comp/simplecomp.go index 2a225113d..b094eb5f0 100644 --- a/pkg/comp/simplecomp.go +++ b/pkg/comp/simplecomp.go @@ -6,9 +6,9 @@ import ( "sync" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/sh2-server/pkg/remote" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) var globalLock = &sync.Mutex{} diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 40a5fe08e..5d6c14bd3 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -15,10 +15,10 @@ import ( "sync" "time" - "github.com/scripthaus-dev/sh2-server/pkg/dbutil" - "github.com/scripthaus-dev/sh2-server/pkg/rtnstate" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/dbutil" + "github.com/commandlinedev/prompt-server/pkg/rtnstate" + "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) const PCloudEndpoint = "https://api.getprompt.dev/central" diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 4fd023c9d..5cca76264 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -5,10 +5,10 @@ import ( "encoding/json" "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" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/rtnstate" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) type NoTelemetryInputType struct { diff --git a/pkg/remote/openai/openai.go b/pkg/remote/openai/openai.go index 88075bf16..a2f646b5c 100644 --- a/pkg/remote/openai/openai.go +++ b/pkg/remote/openai/openai.go @@ -6,8 +6,8 @@ import ( "io" openaiapi "github.com/sashabaranov/go-openai" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) // https://github.com/tiktoken-go/tokenizer diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 76060f334..823e93bd6 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -19,15 +19,15 @@ import ( "time" "github.com/armon/circbuf" + "github.com/commandlinedev/apishell/pkg/base" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/apishell/pkg/statediff" + "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/sstore" "github.com/creack/pty" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/mshell/pkg/statediff" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" "golang.org/x/mod/semver" ) @@ -76,7 +76,7 @@ const ( func init() { if scbase.MShellVersion != base.MShellVersion { - panic(fmt.Sprintf("sh2-server mshell version must match '%s' vs '%s'", scbase.MShellVersion, base.MShellVersion)) + panic(fmt.Sprintf("prompt-server apishell version must match '%s' vs '%s'", scbase.MShellVersion, base.MShellVersion)) } } diff --git a/pkg/remote/updatequeue.go b/pkg/remote/updatequeue.go index 7b978e38d..c93ae6f6a 100644 --- a/pkg/remote/updatequeue.go +++ b/pkg/remote/updatequeue.go @@ -1,7 +1,7 @@ package remote import ( - "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/commandlinedev/apishell/pkg/base" ) func startCmdWait(ck base.CommandKey) { diff --git a/pkg/rtnstate/rtnstate.go b/pkg/rtnstate/rtnstate.go index e882cab5e..a220a7e59 100644 --- a/pkg/rtnstate/rtnstate.go +++ b/pkg/rtnstate/rtnstate.go @@ -7,11 +7,11 @@ import ( "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" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/apishell/pkg/simpleexpand" + "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/utilfn" "mvdan.cc/sh/v3/syntax" ) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 0c09faaec..584abb1bb 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -18,7 +18,7 @@ import ( "time" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/base" + "github.com/commandlinedev/apishell/pkg/base" "golang.org/x/mod/semver" "golang.org/x/sys/unix" ) diff --git a/pkg/scpacket/scpacket.go b/pkg/scpacket/scpacket.go index 3e2bf6330..88255b9e3 100644 --- a/pkg/scpacket/scpacket.go +++ b/pkg/scpacket/scpacket.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/alessio/shellescape" - "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" + "github.com/commandlinedev/apishell/pkg/base" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/prompt-server/pkg/sstore" ) const FeCommandPacketStr = "fecmd" diff --git a/pkg/scws/scws.go b/pkg/scws/scws.go index b61fabc81..57b1c65d5 100644 --- a/pkg/scws/scws.go +++ b/pkg/scws/scws.go @@ -8,12 +8,12 @@ import ( "time" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/sh2-server/pkg/mapqueue" - "github.com/scripthaus-dev/sh2-server/pkg/remote" - "github.com/scripthaus-dev/sh2-server/pkg/scpacket" - "github.com/scripthaus-dev/sh2-server/pkg/sstore" - "github.com/scripthaus-dev/sh2-server/pkg/wsshell" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/prompt-server/pkg/mapqueue" + "github.com/commandlinedev/prompt-server/pkg/remote" + "github.com/commandlinedev/prompt-server/pkg/scpacket" + "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/commandlinedev/prompt-server/pkg/wsshell" ) const WSStatePacketChSize = 20 diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index f33e1ad56..28ddde7b4 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -3,7 +3,7 @@ package shparse import ( "strings" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) const ( diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index 487a3808a..8da19810b 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -5,7 +5,7 @@ import ( "unicode" "unicode/utf8" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) var noEscChars []bool diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index d2e31827d..e96fe3d9b 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) // diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 8ca32cc21..16c8a83af 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/scripthaus-dev/sh2-server/pkg/utilfn" + "github.com/commandlinedev/prompt-server/pkg/utilfn" ) // $(ls f[*]); ./x diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 4e03bb19b..559fddb27 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -13,11 +13,11 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" - "github.com/scripthaus-dev/mshell/pkg/base" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/sh2-server/pkg/dbutil" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" + "github.com/commandlinedev/apishell/pkg/base" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/prompt-server/pkg/dbutil" + "github.com/commandlinedev/prompt-server/pkg/scbase" ) const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.incognito, h.linenum" diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index c6d11828e..61214836b 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -9,8 +9,8 @@ import ( "path" "github.com/google/uuid" - "github.com/scripthaus-dev/mshell/pkg/cirfile" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" + "github.com/commandlinedev/apishell/pkg/cirfile" + "github.com/commandlinedev/prompt-server/pkg/scbase" ) func CreateCmdPtyFile(ctx context.Context, screenId string, cmdId string, maxSize int64) error { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 3d9fc3cb3..93abb3676 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -12,7 +12,7 @@ import ( _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/mattn/go-sqlite3" - sh2db "github.com/scripthaus-dev/sh2-server/db" + sh2db "github.com/commandlinedev/prompt-server/db" "github.com/golang-migrate/migrate/v4" ) diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index 788c404dd..a887d23d6 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -1,7 +1,7 @@ package sstore import ( - "github.com/scripthaus-dev/sh2-server/pkg/dbutil" + "github.com/commandlinedev/prompt-server/pkg/dbutil" ) var quickSetStr = dbutil.QuickSetStr diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 1dc630c61..433f31ad9 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -20,10 +20,10 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" - "github.com/scripthaus-dev/mshell/pkg/packet" - "github.com/scripthaus-dev/mshell/pkg/shexec" - "github.com/scripthaus-dev/sh2-server/pkg/dbutil" - "github.com/scripthaus-dev/sh2-server/pkg/scbase" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/shexec" + "github.com/commandlinedev/prompt-server/pkg/dbutil" + "github.com/commandlinedev/prompt-server/pkg/scbase" _ "github.com/mattn/go-sqlite3" ) From bd515994f89066ef7efa908febf718bb2af38760 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 28 Jul 2023 11:48:11 -0700 Subject: [PATCH 371/397] one by one migrations (so we can run code). remove migration messages from FE (it is fast) --- cmd/main-server.go | 7 -- pkg/sstore/migrate.go | 79 ++++++++++++------ pkg/sstore/sstore.go | 156 ++++------------------------------- pkg/sstore/sstore_migrate.go | 91 ++++++++++++++++++++ 4 files changed, 164 insertions(+), 169 deletions(-) create mode 100644 pkg/sstore/sstore_migrate.go diff --git a/cmd/main-server.go b/cmd/main-server.go index 1e1a2e5d1..6d4488e9f 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -145,12 +145,6 @@ func HandleGetClientData(w http.ResponseWriter, r *http.Request) { return } cdata = cdata.Clean() - mdata, err := sstore.GetCmdMigrationInfo(r.Context()) - if err != nil { - WriteJsonError(w, err) - return - } - cdata.Migration = mdata WriteJsonSuccess(w, cdata) return } @@ -573,7 +567,6 @@ func main() { time.Sleep(10 * time.Second) pcloud.StartUpdateWriter() }() - go sstore.RunCmdScreenMigration() gr := mux.NewRouter() gr.HandleFunc("/api/ptyout", AuthKeyWrap(HandleGetPtyOut)) gr.HandleFunc("/api/remote-pty", AuthKeyWrap(HandleRemotePty)) diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 93abb3676..72b6bc71c 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -8,17 +8,18 @@ import ( "strconv" "time" + sh2db "github.com/commandlinedev/prompt-server/db" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/mattn/go-sqlite3" - sh2db "github.com/commandlinedev/prompt-server/db" "github.com/golang-migrate/migrate/v4" ) const MaxMigration = 19 const MigratePrimaryScreenVersion = 9 +const CmdScreenSpecialMigration = 13 func MakeMigrate() (*migrate.Migrate, error) { fsVar, err := iofs.New(sh2db.MigrationFS, "migrations") @@ -56,46 +57,67 @@ func copyFile(srcFile string, dstFile string) error { return dstFd.Close() } -func MigrateUp() error { +func MigrateUpStep(m *migrate.Migrate, newVersion uint) error { + startTime := time.Now() + err := m.Migrate(newVersion) + if err != nil { + return err + } + if newVersion == CmdScreenSpecialMigration { + mErr := RunCmdScreenMigration13() + if mErr != nil { + return mErr + } + } + log.Printf("[db] migration v%d, elapsed %v\n", newVersion, time.Since(startTime)) + return nil +} + +func MigrateUp(targetVersion uint) error { m, err := MakeMigrate() if err != nil { return err } - curVersion, dirty, err := m.Version() - if err == migrate.ErrNilVersion { - curVersion = 0 - err = nil - } + curVersion, dirty, err := MigrateVersion(m) if dirty { return fmt.Errorf("cannot migrate up, database is dirty") } if err != nil { return fmt.Errorf("cannot get current migration version: %v", err) } - if curVersion >= MaxMigration { + if curVersion >= targetVersion { return nil } - log.Printf("[db] migrating from %d to %d\n", curVersion, MaxMigration) + log.Printf("[db] migrating from %d to %d\n", curVersion, targetVersion) log.Printf("[db] backing up database %s to %s\n", DBFileName, DBFileNameBackup) err = copyFile(GetDBName(), GetDBBackupName()) if err != nil { return fmt.Errorf("error creating database backup: %v", err) } - startTime := time.Now() - err = m.Migrate(MaxMigration) - log.Printf("[db] migration took %v\n", time.Since(startTime)) - if err != nil { - return err + for newVersion := curVersion + 1; newVersion <= targetVersion; newVersion++ { + err = MigrateUpStep(m, newVersion) + if err != nil { + return fmt.Errorf("during migration v%d: %w", err, newVersion) + } } + log.Printf("[db] migration done, new version = %d\n", targetVersion) return nil } -func MigrateVersion() (uint, bool, error) { - m, err := MakeMigrate() - if err != nil { - return 0, false, err +// returns curVersion, dirty, error +func MigrateVersion(m *migrate.Migrate) (uint, bool, error) { + if m == nil { + var err error + m, err = MakeMigrate() + if err != nil { + return 0, false, err + } } - return m.Version() + curVersion, dirty, err := m.Version() + if err == migrate.ErrNilVersion { + return 0, false, nil + } + return curVersion, dirty, err } func MigrateDown() error { @@ -111,6 +133,13 @@ func MigrateDown() error { } func MigrateGoto(n uint) error { + curVersion, _, _ := MigrateVersion(nil) + if curVersion == n { + return nil + } + if curVersion < n { + return MigrateUp(n) + } m, err := MakeMigrate() if err != nil { return err @@ -123,10 +152,12 @@ func MigrateGoto(n uint) error { } func TryMigrateUp() error { - err := MigrateUp() - if err != nil && err.Error() == migrate.ErrNoChange.Error() { - err = nil + curVersion, _, _ := MigrateVersion(nil) + log.Printf("[db] db version = %d\n", curVersion) + if curVersion >= MaxMigration { + return nil } + err := MigrateUp(MaxMigration) if err != nil { return err } @@ -134,7 +165,7 @@ func TryMigrateUp() error { } func MigratePrintVersion() error { - version, dirty, err := MigrateVersion() + version, dirty, err := MigrateVersion(nil) if err != nil { return fmt.Errorf("error getting db version: %v", err) } @@ -150,7 +181,7 @@ func MigrateCommandOpts(opts []string) error { if opts[0] == "--migrate-up" { fmt.Printf("migrate-up %v\n", GetDBName()) time.Sleep(3 * time.Second) - err = MigrateUp() + err = MigrateUp(MaxMigration) } else if opts[0] == "--migrate-down" { fmt.Printf("migrate-down %v\n", GetDBName()) time.Sleep(3 * time.Second) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 433f31ad9..c10a210a8 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -17,13 +17,13 @@ import ( "sync" "time" - "github.com/google/uuid" - "github.com/jmoiron/sqlx" - "github.com/sawka/txwrap" "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" "github.com/commandlinedev/prompt-server/pkg/dbutil" "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/sawka/txwrap" _ "github.com/mattn/go-sqlite3" ) @@ -239,28 +239,20 @@ type FeOptsType struct { TermFontSize int `json:"termfontsize,omitempty"` } -type ClientMigrationData struct { - MigrationType string `json:"migrationtype"` - MigrationPos int `json:"migrationpos"` - MigrationTotal int `json:"migrationtotal"` - MigrationDone bool `json:"migrationdone"` -} - type ClientData struct { - ClientId string `json:"clientid"` - UserId string `json:"userid"` - UserPrivateKeyBytes []byte `json:"-"` - UserPublicKeyBytes []byte `json:"-"` - UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` - UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` - ActiveSessionId string `json:"activesessionid"` - WinSize ClientWinSizeType `json:"winsize"` - ClientOpts ClientOptsType `json:"clientopts"` - FeOpts FeOptsType `json:"feopts"` - CmdStoreType string `json:"cmdstoretype"` - Migration *ClientMigrationData `json:"migration,omitempty" dbmap:"-"` - DBVersion int `json:"dbversion" dbmap:"-"` - OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty" dbmap:"openaiopts"` + ClientId string `json:"clientid"` + UserId string `json:"userid"` + UserPrivateKeyBytes []byte `json:"-"` + UserPublicKeyBytes []byte `json:"-"` + UserPrivateKey *ecdsa.PrivateKey `json:"-" dbmap:"-"` + UserPublicKey *ecdsa.PublicKey `json:"-" dbmap:"-"` + ActiveSessionId string `json:"activesessionid"` + WinSize ClientWinSizeType `json:"winsize"` + ClientOpts ClientOptsType `json:"clientopts"` + FeOpts FeOptsType `json:"feopts"` + CmdStoreType string `json:"cmdstoretype"` + DBVersion int `json:"dbversion" dbmap:"-"` + OpenAIOpts *OpenAIOptsType `json:"openaiopts,omitempty" dbmap:"openaiopts"` } func (ClientData) UseDBMap() {} @@ -1220,8 +1212,8 @@ func createClientData(tx *TxWrap) error { WinSize: ClientWinSizeType{}, CmdStoreType: CmdStoreTypeScreen, } - query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize) - VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize)` + query := `INSERT INTO client ( clientid, userid, activesessionid, userpublickeybytes, userprivatekeybytes, winsize, cmdstoretype) + VALUES (:clientid,:userid,:activesessionid,:userpublickeybytes,:userprivatekeybytes,:winsize,:cmdstoretype)` tx.NamedExec(query, dbutil.ToDBMap(c, false)) log.Printf("create new clientid[%s] userid[%s] with public/private keypair\n", c.ClientId, c.UserId) return nil @@ -1273,28 +1265,6 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { return rtn, nil } -func GetCmdMigrationInfo(ctx context.Context) (*ClientMigrationData, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*ClientMigrationData, error) { - cdata := dbutil.GetMappable[*ClientData](tx, `SELECT * FROM client`) - if cdata == nil { - return nil, fmt.Errorf("no client data found") - } - if cdata.CmdStoreType == "session" { - total := tx.GetInt(`SELECT count(*) FROM cmd`) - posInv := tx.GetInt(`SELECT count(*) FROM cmd_migrate`) - mdata := &ClientMigrationData{ - MigrationType: "cmdscreen", - MigrationPos: total - posInv, - MigrationTotal: total, - MigrationDone: false, - } - return mdata, nil - } - // no migration info - return nil, nil - }) -} - func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { txErr := WithTx(ctx, func(tx *TxWrap) error { query := `UPDATE client SET clientopts = ?` @@ -1303,93 +1273,3 @@ func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { }) return txErr } - -type cmdMigrationType struct { - SessionId string - ScreenId string - CmdId string -} - -func getSliceChunk[T any](slice []T, chunkSize int) ([]T, []T) { - if chunkSize >= len(slice) { - return slice, nil - } - return slice[0:chunkSize], slice[chunkSize:] -} - -func processChunk(ctx context.Context, mchunk []cmdMigrationType) error { - for _, mig := range mchunk { - newFile, err := scbase.PtyOutFile(mig.ScreenId, mig.CmdId) - if err != nil { - log.Printf("ptyoutfile error: %v\n", err) - continue - } - oldFile, err := scbase.PtyOutFile_Sessions(mig.SessionId, mig.CmdId) - if err != nil { - log.Printf("ptyoutfile_sessions error: %v\n", err) - continue - } - err = os.Rename(oldFile, newFile) - if err != nil { - log.Printf("error renaming %s => %s: %v\n", oldFile, newFile, err) - continue - } - } - txErr := WithTx(ctx, func(tx *TxWrap) error { - for _, mig := range mchunk { - query := `DELETE FROM cmd_migrate WHERE cmdid = ?` - tx.Exec(query, mig.CmdId) - } - return nil - }) - if txErr != nil { - return txErr - } - return nil -} - -func RunCmdScreenMigration() { - ctx := context.Background() - startTime := time.Now() - mdata, err := GetCmdMigrationInfo(ctx) - if err != nil { - log.Printf("[prompt] error trying to run cmd migration: %v\n", err) - return - } - if mdata == nil || mdata.MigrationType != "cmdscreen" { - return - } - var migrations []cmdMigrationType - txErr := WithTx(ctx, func(tx *TxWrap) error { - tx.Select(&migrations, `SELECT * FROM cmd_migrate`) - return nil - }) - if txErr != nil { - log.Printf("[prompt] error trying to get cmd migrations: %v\n", txErr) - return - } - log.Printf("[db] got %d cmd migrations\n", len(migrations)) - for len(migrations) > 0 { - var mchunk []cmdMigrationType - mchunk, migrations = getSliceChunk(migrations, 5) - err = processChunk(ctx, mchunk) - if err != nil { - log.Printf("[prompt] cmd migration failed on chunk: %v\n%#v\n", err, mchunk) - return - } - } - err = os.RemoveAll(scbase.GetSessionsDir()) - if err != nil { - log.Printf("[db] cannot remove old sessions dir %s: %v\n", scbase.GetSessionsDir(), err) - } - txErr = WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE client SET cmdstoretype = 'screen'` - tx.Exec(query) - return nil - }) - if txErr != nil { - log.Printf("[db] cannot change client cmdstoretype: %v\n", err) - } - log.Printf("[db] cmd screen migration done: %v\n", time.Since(startTime)) - return -} diff --git a/pkg/sstore/sstore_migrate.go b/pkg/sstore/sstore_migrate.go new file mode 100644 index 000000000..909ff4646 --- /dev/null +++ b/pkg/sstore/sstore_migrate.go @@ -0,0 +1,91 @@ +package sstore + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/commandlinedev/prompt-server/pkg/scbase" +) + +type cmdMigrationType struct { + SessionId string + ScreenId string + CmdId string +} + +func getSliceChunk[T any](slice []T, chunkSize int) ([]T, []T) { + if chunkSize >= len(slice) { + return slice, nil + } + return slice[0:chunkSize], slice[chunkSize:] +} + +func RunCmdScreenMigration13() error { + ctx := context.Background() + startTime := time.Now() + var migrations []cmdMigrationType + txErr := WithTx(ctx, func(tx *TxWrap) error { + tx.Select(&migrations, `SELECT * FROM cmd_migrate`) + return nil + }) + if txErr != nil { + return fmt.Errorf("trying to get cmd migrations: %w", txErr) + } + log.Printf("[db] got %d cmd-screen migrations\n", len(migrations)) + for len(migrations) > 0 { + var mchunk []cmdMigrationType + mchunk, migrations = getSliceChunk(migrations, 5) + err := processMigrationChunk(ctx, mchunk) + if err != nil { + return fmt.Errorf("cmd migration failed on chunk: %w", err) + } + } + err := os.RemoveAll(scbase.GetSessionsDir()) + if err != nil { + return fmt.Errorf("cannot remove old sessions dir %s: %w\n", scbase.GetSessionsDir(), err) + } + txErr = WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET cmdstoretype = 'screen'` + tx.Exec(query) + return nil + }) + if txErr != nil { + return fmt.Errorf("cannot change client cmdstoretype: %w", err) + } + log.Printf("[db] cmd screen migration done: %v\n", time.Since(startTime)) + return nil +} + +func processMigrationChunk(ctx context.Context, mchunk []cmdMigrationType) error { + for _, mig := range mchunk { + newFile, err := scbase.PtyOutFile(mig.ScreenId, mig.CmdId) + if err != nil { + log.Printf("ptyoutfile error: %v\n", err) + continue + } + oldFile, err := scbase.PtyOutFile_Sessions(mig.SessionId, mig.CmdId) + if err != nil { + log.Printf("ptyoutfile_sessions error: %v\n", err) + continue + } + err = os.Rename(oldFile, newFile) + if err != nil { + log.Printf("error renaming %s => %s: %v\n", oldFile, newFile, err) + continue + } + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + for _, mig := range mchunk { + query := `DELETE FROM cmd_migrate WHERE cmdid = ?` + tx.Exec(query, mig.CmdId) + } + return nil + }) + if txErr != nil { + return txErr + } + return nil +} From 2d63b1da9603aa01627e1fbf0e86d0fffb72a78e Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 17:16:43 -0700 Subject: [PATCH 372/397] checkpoint on line migration --- db/migrations/000020_linecmd.down.sql | 2 + db/migrations/000020_linecmd.up.sql | 74 ++++++++++ pkg/cmdrunner/cmdrunner.go | 62 ++++----- pkg/pcloud/pcloud.go | 16 +-- pkg/pcloud/pclouddata.go | 56 ++++---- pkg/remote/remote.go | 41 +++--- pkg/rtnstate/rtnstate.go | 4 +- pkg/scbase/scbase.go | 10 +- pkg/sstore/dbops.go | 187 ++++++++++---------------- pkg/sstore/fileops.go | 40 +++--- pkg/sstore/migrate.go | 13 +- pkg/sstore/quick.go | 1 + pkg/sstore/sstore.go | 99 +++++++------- pkg/sstore/sstore_migrate.go | 77 ++++++++++- pkg/sstore/updatebus.go | 17 +-- 15 files changed, 390 insertions(+), 309 deletions(-) create mode 100644 db/migrations/000020_linecmd.down.sql create mode 100644 db/migrations/000020_linecmd.up.sql diff --git a/db/migrations/000020_linecmd.down.sql b/db/migrations/000020_linecmd.down.sql new file mode 100644 index 000000000..6332dc5ba --- /dev/null +++ b/db/migrations/000020_linecmd.down.sql @@ -0,0 +1,2 @@ +-- invalid, will throw an error, cannot migrate down +SELECT x; diff --git a/db/migrations/000020_linecmd.up.sql b/db/migrations/000020_linecmd.up.sql new file mode 100644 index 000000000..7e2332e95 --- /dev/null +++ b/db/migrations/000020_linecmd.up.sql @@ -0,0 +1,74 @@ +-- remove cmdid from line, history, and cmd (use lineid everywhere) + +CREATE TABLE cmd_new ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + cmdstr text NOT NULL, + rawcmdstr text NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, + termopts json NOT NULL, + origtermopts json NOT NULL, + status varchar(10) NOT NULL, + cmdpid int NOT NULL, + remotepid int NOT NULL, + donets bigint NOT NULL, + exitcode int NOT NULL, + durationms int NOT NULL, + rtnstate boolean NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, + runout json NOT NULL, + PRIMARY KEY (screenid, lineid) +); + +DROP TABLE IF EXISTS cmd_migrate; + +CREATE TABLE cmd_migrate ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (screenid, lineid) +); + +INSERT INTO cmd_migrate +SELECT screenid, lineid, cmdid +FROM line; + +INSERT INTO cmd_new +SELECT + c.screenid, + l.lineid, + c.remoteownerid, + c.remoteid, + c.remotename, + c.cmdstr, + c.rawcmdstr, + c.festate, + c.statebasehash, + c.statediffhasharr, + c.termopts, + c.origtermopts, + c.status, + coalesce(json_extract(startpk, '$.pid'), 0), + coalesce(json_extract(startpk, '$.mshellpid'), 0), + coalesce(json_extract(doneinfo, '$.ts'), 0), + coalesce(json_extract(doneinfo, '$.exitcode'), 0), + coalesce(json_extract(doneinfo, '$.durationms'), 0), + c.rtnstate, + c.rtnbasehash, + c.rtndiffhasharr, + c.runout +FROM cmd c +JOIN line l ON (l.cmdid = c.cmdid); + +DROP TABLE cmd; + +ALTER TABLE cmd_new RENAME TO cmd; + +ALTER TABLE history DROP COLUMN cmdid; +ALTER TABLE line DROP COLUMN cmdid; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index d0f40778c..6d89f4874 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -17,7 +17,6 @@ import ( "time" "unicode" - "github.com/google/uuid" "github.com/commandlinedev/apishell/pkg/base" "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" @@ -29,6 +28,7 @@ import ( "github.com/commandlinedev/prompt-server/pkg/scpacket" "github.com/commandlinedev/prompt-server/pkg/sstore" "github.com/commandlinedev/prompt-server/pkg/utilfn" + "github.com/google/uuid" ) const ( @@ -110,7 +110,6 @@ type SetVarScope struct { type historyContextType struct { LineId string LineNum int64 - CmdId string RemotePtr *sstore.RemotePtrType } @@ -527,7 +526,6 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history LineId: historyContext.LineId, LineNum: historyContext.LineNum, HadError: hadError, - CmdId: historyContext.CmdId, CmdStr: cmdStr, IsMetaCmd: isMetaCmd, Incognito: isIncognito, @@ -1325,7 +1323,7 @@ func writeErrorToPty(cmd *sstore.CmdType, errStr string, outputPos int64) { } errCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - update, err := sstore.AppendToCmdPtyBlob(errCtx, cmd.ScreenId, cmd.CmdId, errBytes, outputPos) + update, err := sstore.AppendToCmdPtyBlob(errCtx, cmd.ScreenId, cmd.LineId, errBytes, outputPos) if err != nil { log.Printf("error writing ptyupdate for openai response: %v\n", err) return @@ -1339,7 +1337,7 @@ func writePacketToPty(ctx context.Context, cmd *sstore.CmdType, pk packet.Packet if err != nil { return err } - update, err := sstore.AppendToCmdPtyBlob(ctx, cmd.ScreenId, cmd.CmdId, outBytes, *outputPos) + update, err := sstore.AppendToCmdPtyBlob(ctx, cmd.ScreenId, cmd.LineId, outBytes, *outputPos) if err != nil { return err } @@ -1364,18 +1362,17 @@ func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt } duration := time.Since(startTime) cmdStatus := sstore.CmdStatusDone - var exitCode int64 + var exitCode int if hadError { cmdStatus = sstore.CmdStatusError exitCode = 1 } - doneInfo := &sstore.CmdDoneInfo{ - Ts: time.Now().UnixMilli(), - ExitCode: exitCode, - DurationMs: duration.Milliseconds(), - } - ck := base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) - update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, doneInfo, cmdStatus) + ck := base.MakeCommandKey(cmd.ScreenId, cmd.LineId) + donePk := packet.MakeCmdDonePacket(ck) + donePk.Ts = time.Now().UnixMilli() + donePk.ExitCode = exitCode + donePk.DurationMs = duration.Milliseconds() + update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus) if err != nil { // nothing to do log.Printf("error updating cmddoneinfo (in openai): %v\n", err) @@ -1414,18 +1411,17 @@ func doOpenAIStreamCompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, } duration := time.Since(startTime) cmdStatus := sstore.CmdStatusDone - var exitCode int64 + var exitCode int if hadError { cmdStatus = sstore.CmdStatusError exitCode = 1 } - doneInfo := &sstore.CmdDoneInfo{ - Ts: time.Now().UnixMilli(), - ExitCode: exitCode, - DurationMs: duration.Milliseconds(), - } - ck := base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) - update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, doneInfo, cmdStatus) + ck := base.MakeCommandKey(cmd.ScreenId, cmd.LineId) + donePk := packet.MakeCmdDonePacket(ck) + donePk.Ts = time.Now().UnixMilli() + donePk.ExitCode = exitCode + donePk.DurationMs = duration.Milliseconds() + update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus) if err != nil { // nothing to do log.Printf("error updating cmddoneinfo (in openai): %v\n", err) @@ -1545,14 +1541,12 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, termOpts sstore.TermOpts) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ ScreenId: ids.ScreenId, - CmdId: scbase.GenPromptUUID(), + LineId: scbase.GenPromptUUID(), CmdStr: cmdStr, RawCmdStr: cmdStr, Remote: ids.Remote.RemotePtr, TermOpts: termOpts, Status: sstore.CmdStatusRunning, - StartPk: nil, - DoneInfo: nil, RunOut: nil, } if ids.Remote.StatePtr != nil { @@ -1561,7 +1555,7 @@ func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr str if ids.Remote.FeState != nil { cmd.FeState = ids.Remote.FeState } - err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) @@ -1572,14 +1566,12 @@ func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr str func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) { cmd := &sstore.CmdType{ ScreenId: ids.ScreenId, - CmdId: scbase.GenPromptUUID(), + LineId: scbase.GenPromptUUID(), CmdStr: cmdStr, RawCmdStr: cmdStr, Remote: ids.Remote.RemotePtr, TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize}, Status: sstore.CmdStatusDone, - StartPk: nil, - DoneInfo: nil, RunOut: nil, } if ids.Remote.StatePtr != nil { @@ -1588,13 +1580,13 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr if ids.Remote.FeState != nil { cmd.FeState = ids.Remote.FeState } - err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err) } // can ignore ptyupdate - _, err = sstore.AppendToCmdPtyBlob(ctx, ids.ScreenId, cmd.CmdId, cmdOutput, 0) + _, err = sstore.AppendToCmdPtyBlob(ctx, ids.ScreenId, cmd.LineId, cmdOutput, 0) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, fmt.Errorf("cannot append to local ptyout file for %s command: %v", metaCmd, err) @@ -1644,7 +1636,6 @@ func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstor hctx.LineNum = line.LineNum } if cmd != nil { - hctx.CmdId = cmd.CmdId hctx.RemotePtr = &cmd.Remote } } @@ -2487,7 +2478,7 @@ func splitLinesForInfo(str string) []string { func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) error { siPk := packet.MakeSpecialInputPacket() - siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) + siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId) siPk.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols} msh := remote.GetRemoteById(cmd.Remote.RemoteId) if msh == nil { @@ -2499,7 +2490,7 @@ func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) } newTermOpts := cmd.TermOpts newTermOpts.Cols = int64(newCols) - err = sstore.UpdateCmdTermOpts(ctx, cmd.ScreenId, cmd.CmdId, newTermOpts) + err = sstore.UpdateCmdTermOpts(ctx, cmd.ScreenId, cmd.LineId, newTermOpts) if err != nil { return err } @@ -2963,7 +2954,6 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "renderer", "terminal")) } if cmd != nil { - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cmdid", cmd.CmdId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef())) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", cmd.Status)) if cmd.FeState["cwd"] != "" { @@ -2976,7 +2966,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if cmd.RtnState { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "rtnstate", "true")) } - stat, _ := sstore.StatCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId) + stat, _ := sstore.StatCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId) if stat == nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", "-")) } else { @@ -3073,7 +3063,7 @@ func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return nil, fmt.Errorf("cannot send signal, remote is not connected") } siPk := packet.MakeSpecialInputPacket() - siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.CmdId) + siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId) siPk.SigName = sigArg err = msh.SendSpecialInput(siPk) if err != nil { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 5d6c14bd3..5e8f1e4b4 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -299,34 +299,34 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* } rtn.TermOpts = &cmd.TermOpts - case sstore.UpdateType_CmdDoneInfo: + case sstore.UpdateType_CmdExitCode, sstore.UpdateType_CmdDurationMs: _, 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 + if update.UpdateType == sstore.UpdateType_CmdExitCode { + rtn.IVal = int64(cmd.ExitCode) + } else if update.UpdateType == sstore.UpdateType_CmdDurationMs { + rtn.IVal = int64(cmd.DurationMs) + } 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) + data, err := rtnstate.GetRtnStateDiff(ctx, update.ScreenId, cmd.LineId) 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+1) + realOffset, data, err := sstore.ReadPtyOutFile(ctx, update.ScreenId, update.LineId, ptyPos, MaxPtyUpdateSize+1) if err != nil { return nil, fmt.Errorf("error getting ptydata: %v", err) } diff --git a/pkg/pcloud/pclouddata.go b/pkg/pcloud/pclouddata.go index 5cca76264..87e43b4b3 100644 --- a/pkg/pcloud/pclouddata.go +++ b/pkg/pcloud/pclouddata.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" - "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/prompt-server/pkg/remote" "github.com/commandlinedev/prompt-server/pkg/rtnstate" "github.com/commandlinedev/prompt-server/pkg/sstore" @@ -37,7 +36,6 @@ type WebShareUpdateType struct { SVal string `json:"sval,omitempty"` IVal int64 `json:"ival,omitempty"` BVal bool `json:"bval,omitempty"` - DoneInfo *sstore.CmdDoneInfo `json:"doneinfo,omitempty"` TermOpts *sstore.TermOpts `json:"termopts,omitempty"` } @@ -125,7 +123,6 @@ type WebShareLineType struct { ContentHeight int64 `json:"contentheight"` Renderer string `json:"renderer,omitempty"` Text string `json:"text,omitempty"` - CmdId string `json:"cmdid,omitempty"` } func webLineFromLine(line *sstore.LineType) (*WebShareLineType, error) { @@ -137,23 +134,25 @@ func webLineFromLine(line *sstore.LineType) (*WebShareLineType, error) { ContentHeight: line.ContentHeight, Renderer: line.Renderer, Text: line.Text, - CmdId: line.CmdId, } return rtn, nil } type WebShareCmdType struct { - LineId string `json:"lineid"` - CmdStr string `json:"cmdstr"` - RawCmdStr string `json:"rawcmdstr"` - Remote *WebShareRemote `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"` + LineId string `json:"lineid"` + CmdStr string `json:"cmdstr"` + RawCmdStr string `json:"rawcmdstr"` + Remote *WebShareRemote `json:"remote"` + FeState sstore.FeStateType `json:"festate"` + TermOpts sstore.TermOpts `json:"termopts"` + Status string `json:"status"` + CmdPid int `json:"cmdpid"` + RemotePid int `json:"remotepid"` + DoneTs int64 `json:"donets,omitempty"` + ExitCode int `json:"exitcode,omitempty"` + DurationMs int `json:"durationms,omitempty"` + RtnState bool `json:"rtnstate,omitempty"` + RtnStateStr string `json:"rtnstatestr,omitempty"` } func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) { @@ -166,21 +165,24 @@ func webCmdFromCmd(lineId string, cmd *sstore.CmdType) (*WebShareCmdType, error) } webRemote := webRemoteFromRemote(cmd.Remote, remote) rtn := &WebShareCmdType{ - LineId: lineId, - CmdStr: cmd.CmdStr, - RawCmdStr: cmd.RawCmdStr, - Remote: webRemote, - FeState: cmd.FeState, - TermOpts: cmd.TermOpts, - Status: cmd.Status, - StartPk: cmd.StartPk, - DoneInfo: cmd.DoneInfo, - RtnState: cmd.RtnState, + LineId: lineId, + CmdStr: cmd.CmdStr, + RawCmdStr: cmd.RawCmdStr, + Remote: webRemote, + FeState: cmd.FeState, + TermOpts: cmd.TermOpts, + Status: cmd.Status, + CmdPid: cmd.CmdPid, + RemotePid: cmd.RemotePid, + DoneTs: cmd.DoneTs, + ExitCode: cmd.ExitCode, + DurationMs: cmd.DurationMs, + RtnState: cmd.RtnState, } if cmd.RtnState { - barr, err := rtnstate.GetRtnStateDiff(context.Background(), cmd.ScreenId, cmd.CmdId) + barr, err := rtnstate.GetRtnStateDiff(context.Background(), cmd.ScreenId, cmd.LineId) if err != nil { - return nil, fmt.Errorf("error creating rtnstate diff for cmd:%s: %v", cmd.CmdId, err) + return nil, fmt.Errorf("error creating rtnstate diff for cmd:%s: %v", cmd.LineId, err) } rtn.RtnStateStr = string(barr) } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 823e93bd6..326b8b919 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1434,7 +1434,7 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt } ok, existingPSC := msh.testAndSetPendingStateCmd(remotePtr.Name, newPSC) if !ok { - line, _, err := sstore.GetLineCmdByCmdId(ctx, screenId, existingPSC.GetCmdId()) + line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId()) if err != nil { return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err) } @@ -1494,21 +1494,23 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt status = sstore.CmdStatusDetached } cmd := &sstore.CmdType{ - ScreenId: runPacket.CK.GetGroupId(), - CmdId: runPacket.CK.GetCmdId(), - CmdStr: runPacket.Command, - RawCmdStr: runPacket.Command, - Remote: remotePtr, - FeState: sstore.FeStateFromShellState(currentState), - StatePtr: *statePtr, - TermOpts: makeTermOpts(runPacket), - Status: status, - StartPk: startPk, - DoneInfo: nil, - RunOut: nil, - RtnState: runPacket.ReturnState, + ScreenId: runPacket.CK.GetGroupId(), + LineId: runPacket.CK.GetCmdId(), + CmdStr: runPacket.Command, + RawCmdStr: runPacket.Command, + Remote: remotePtr, + FeState: sstore.FeStateFromShellState(currentState), + StatePtr: *statePtr, + TermOpts: makeTermOpts(runPacket), + Status: status, + CmdPid: startPk.Pid, + RemotePid: startPk.MShellPid, + ExitCode: 0, + DurationMs: 0, + RunOut: nil, + RtnState: runPacket.ReturnState, } - err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.CmdId, cmd.TermOpts.MaxPtySize) + err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize) if err != nil { // TODO the cmd is running, so this is a tricky error to handle return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err) @@ -1645,12 +1647,7 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) { if donePk.FinalStateDiff != nil { donePk.FinalStateDiff = stripScVarsFromStateDiff(donePk.FinalStateDiff) } - doneInfo := &sstore.CmdDoneInfo{ - Ts: donePk.Ts, - ExitCode: int64(donePk.ExitCode), - DurationMs: donePk.DurationMs, - } - update, err := sstore.UpdateCmdDoneInfo(context.Background(), donePk.CK, doneInfo, sstore.CmdStatusDone) + update, err := sstore.UpdateCmdDoneInfo(context.Background(), donePk.CK, donePk, sstore.CmdStatusDone) if err != nil { msh.WriteToPtyBuffer("*error updating cmddone: %v\n", err) return @@ -1713,7 +1710,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType) log.Printf("error calling GetCmdById in handleCmdFinalPacket: %v\n", err) return } - if rtnCmd == nil || rtnCmd.DoneInfo != nil { + if rtnCmd == nil || rtnCmd.DoneTs > 0 { return } log.Printf("finalpk %s (hangup): %s\n", finalPk.CK, finalPk.Error) diff --git a/pkg/rtnstate/rtnstate.go b/pkg/rtnstate/rtnstate.go index a220a7e59..2a27f0c03 100644 --- a/pkg/rtnstate/rtnstate.go +++ b/pkg/rtnstate/rtnstate.go @@ -168,8 +168,8 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt } } -func GetRtnStateDiff(ctx context.Context, screenId string, cmdId string) ([]byte, error) { - cmd, err := sstore.GetCmdByScreenId(ctx, screenId, cmdId) +func GetRtnStateDiff(ctx context.Context, screenId string, lineId string) ([]byte, error) { + cmd, err := sstore.GetCmdByScreenId(ctx, screenId, lineId) if err != nil { return nil, err } diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 584abb1bb..b234ca480 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -17,8 +17,8 @@ import ( "sync" "time" - "github.com/google/uuid" "github.com/commandlinedev/apishell/pkg/base" + "github.com/google/uuid" "golang.org/x/mod/semver" "golang.org/x/sys/unix" ) @@ -258,7 +258,7 @@ func PtyOutFile_Sessions(sessionId string, cmdId string) (string, error) { return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil } -func PtyOutFile(screenId string, cmdId string) (string, error) { +func PtyOutFile(screenId string, lineId string) (string, error) { sdir, err := EnsureScreenDir(screenId) if err != nil { return "", err @@ -266,10 +266,10 @@ func PtyOutFile(screenId string, cmdId string) (string, error) { if screenId == "" { return "", fmt.Errorf("cannot get ptyout file for blank screenid") } - if cmdId == "" { - return "", fmt.Errorf("cannot get ptyout file for blank cmdid") + if lineId == "" { + return "", fmt.Errorf("cannot get ptyout file for blank lineid") } - return fmt.Sprintf("%s/%s.ptyout.cf", sdir, cmdId), nil + return fmt.Sprintf("%s/%s.ptyout.cf", sdir, lineId), nil } func GenPromptUUID() string { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 559fddb27..c9972ecbb 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -10,17 +10,17 @@ import ( "sync" "time" - "github.com/google/uuid" - "github.com/jmoiron/sqlx" - "github.com/sawka/txwrap" "github.com/commandlinedev/apishell/pkg/base" "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" "github.com/commandlinedev/prompt-server/pkg/dbutil" "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/sawka/txwrap" ) -const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.cmdid, 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.incognito, h.linenum" const DefaultMaxHistoryItems = 1000 var updateWriterCVar = sync.NewCond(&sync.Mutex{}) @@ -217,8 +217,8 @@ 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, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, incognito, linenum) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:incognito,:linenum)` + ( 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)` tx.NamedExec(query, hitem.ToMap()) return nil }) @@ -472,7 +472,7 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, } query = `SELECT * FROM line WHERE screenid = ? ORDER BY linenum` tx.Select(&screen.Lines, query, screen.ScreenId) - query = `SELECT * FROM cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?)` + query = `SELECT * FROM cmd WHERE screenid = ?` screen.Cmds = dbutil.SelectMapsGen[*CmdType](tx, query, screen.ScreenId) return screen, nil }) @@ -759,14 +759,6 @@ 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 @@ -776,24 +768,8 @@ func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*L return nil, nil, nil } var cmdRtn *CmdType - if lineVal.CmdId != "" { - query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmdRtn = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineVal.CmdId) - } - return &lineVal, cmdRtn, nil - }) -} - -func GetLineCmdByCmdId(ctx context.Context, screenId string, cmdId string) (*LineType, *CmdType, error) { - return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { - var lineVal LineType - query := `SELECT * FROM line WHERE screenid = ? AND cmdid = ?` - found := tx.Get(&lineVal, query, screenId, cmdId) - if !found { - return nil, nil, nil - } - query = `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmdRtn := dbutil.GetMapGen[*CmdType](tx, query, screenId, cmdId) + query = `SELECT * FROM cmd WHERE screenid = ? AND lineid = ?` + cmdRtn = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId) return &lineVal, cmdRtn, nil }) } @@ -819,8 +795,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM screen WHERE screenid = ?` nextLineNum := tx.GetInt(query, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, cmdid, renderer, ephemeral, contentheight, star, archived) - VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:cmdid,:renderer,:ephemeral,:contentheight,:star,:archived)` + query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, renderer, ephemeral, contentheight, star, archived) + VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:renderer,:ephemeral,:contentheight,:star,:archived)` tx.NamedExec(query, line) query = `UPDATE screen SET nextlinenum = ? WHERE screenid = ?` tx.Exec(query, nextLineNum+1, line.ScreenId) @@ -828,8 +804,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) - VALUES (:screenid,:cmdid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) +INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExec(query, cmdMap) } @@ -840,11 +816,11 @@ INSERT INTO cmd ( screenid, cmdid, remoteownerid, remoteid, remotename, cmdstr, }) } -func GetCmdByScreenId(ctx context.Context, screenId string, cmdId string) (*CmdType, error) { +func GetCmdByScreenId(ctx context.Context, screenId string, lineId string) (*CmdType, error) { var cmd *CmdType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM cmd WHERE screenid = ? AND cmdid = ?` - cmd = dbutil.GetMapGen[*CmdType](tx, query, screenId, cmdId) + query := `SELECT * FROM cmd WHERE screenid = ? AND lineid = ?` + cmd = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId) return nil }) if err != nil { @@ -853,8 +829,8 @@ func GetCmdByScreenId(ctx context.Context, screenId string, cmdId string) (*CmdT return cmd, nil } -func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDoneInfo, status string) (*ModelUpdate, error) { - if doneInfo == nil { +func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) { + if donePk == nil { return nil, fmt.Errorf("invalid cmddone packet") } if ck.IsEmpty() { @@ -863,16 +839,17 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, doneInfo *CmdDon screenId := ck.GetGroupId() var rtnCmd *CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET status = ?, doneinfo = ? WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, status, quickJson(doneInfo), screenId, ck.GetCmdId()) + 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)) var err error - rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, ck.GetCmdId()) + rtnCmd, err = GetCmdByScreenId(tx.Context(), screenId, lineIdFromCK(ck)) if err != nil { return err } if isWebShare(tx, screenId) { - insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdDoneInfo) - insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdStatus) + insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdExitCode) + insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdDurationMs) + insertScreenLineUpdate(tx, screenId, lineIdFromCK(ck), UpdateType_CmdStatus) } return nil }) @@ -890,11 +867,12 @@ func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellSt return fmt.Errorf("cannot update cmdrtnstate, empty ck") } screenId := ck.GetGroupId() + lineId := lineIdFromCK(ck) txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), screenId, ck.GetCmdId()) + query := `UPDATE cmd SET rtnbasehash = ?, rtndiffhasharr = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, statePtr.BaseHash, quickJsonArr(statePtr.DiffHashArr), screenId, lineId) if isWebShare(tx, screenId) { - insertScreenCmdUpdate(tx, screenId, ck.GetCmdId(), UpdateType_CmdRtnState) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_CmdRtnState) } return nil }) @@ -910,8 +888,8 @@ func AppendCmdErrorPk(ctx context.Context, errPk *packet.CmdErrorPacketType) err } screenId := errPk.CK.GetGroupId() 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()) + query := `UPDATE cmd SET runout = json_insert(runout, '$[#]', ?) WHERE screenid = ? AND lineid = ?` + tx.Exec(query, quickJson(errPk), screenId, lineIdFromCK(errPk.CK)) return nil }) } @@ -927,13 +905,13 @@ func ReInitFocus(ctx context.Context) error { func HangupAllRunningCmds(ctx context.Context) error { return WithTx(ctx, func(tx *TxWrap) error { var cmdPtrs []CmdPtr - query := `SELECT screenid, cmdid FROM cmd WHERE status = ?` + query := `SELECT screenid, lineid 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 { if isWebShare(tx, cmdPtr.ScreenId) { - insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + insertScreenLineUpdate(tx, cmdPtr.ScreenId, cmdPtr.LineId, UpdateType_CmdStatus) } } return nil @@ -944,16 +922,16 @@ func HangupAllRunningCmds(ctx context.Context) error { func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) ([]*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenType, error) { var cmdPtrs []CmdPtr - query := `SELECT screenid, cmdid FROM cmd WHERE status = ? AND remoteid = ?` + query := `SELECT screenid, lineid 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) var rtn []*ScreenType for _, cmdPtr := range cmdPtrs { if isWebShare(tx, cmdPtr.ScreenId) { - insertScreenCmdUpdate(tx, cmdPtr.ScreenId, cmdPtr.CmdId, UpdateType_CmdStatus) + insertScreenLineUpdate(tx, cmdPtr.ScreenId, cmdPtr.LineId, UpdateType_CmdStatus) } - screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), cmdPtr.ScreenId, cmdPtr.CmdId) + screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), cmdPtr.ScreenId, cmdPtr.LineId) if err != nil { return nil, err } @@ -968,12 +946,12 @@ func HangupRunningCmdsByRemoteId(ctx context.Context, remoteId string) ([]*Scree // TODO send update 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 cmdid = ?` - tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), ck.GetCmdId()) + query := `UPDATE cmd SET status = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, CmdStatusHangup, ck.GetGroupId(), lineIdFromCK(ck)) if isWebShare(tx, ck.GetGroupId()) { - insertScreenCmdUpdate(tx, ck.GetGroupId(), ck.GetCmdId(), UpdateType_CmdStatus) + insertScreenLineUpdate(tx, ck.GetGroupId(), lineIdFromCK(ck), UpdateType_CmdStatus) } - screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), ck.GetGroupId(), ck.GetCmdId()) + screen, err := UpdateScreenFocusForDoneCmd(tx.Context(), ck.GetGroupId(), lineIdFromCK(ck)) if err != nil { return nil, err } @@ -1029,17 +1007,17 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (* func cleanScreenCmds(ctx context.Context, screenId string) error { var removedCmds []string txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT cmdid FROM cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE screenid = ?)` + query := `SELECT lineid FROM cmd WHERE screenid = ? AND lineid NOT IN (SELECT lineid FROM line WHERE screenid = ?)` removedCmds = tx.SelectStrings(query, screenId, screenId) - query = `DELETE FROM cmd WHERE screenid = ? AND cmdid NOT IN (SELECT cmdid FROM line WHERE screenid = ?)` + query = `DELETE FROM cmd WHERE screenid = ? AND lineid NOT IN (SELECT lineid FROM line WHERE screenid = ?)` tx.Exec(query, screenId, screenId) return nil }) if txErr != nil { return txErr } - for _, cmdId := range removedCmds { - DeletePtyOutFile(ctx, screenId, cmdId) + for _, lineId := range removedCmds { + DeletePtyOutFile(ctx, screenId, lineId) } return nil } @@ -1461,7 +1439,7 @@ func PurgeScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error func GetRunningScreenCmds(ctx context.Context, screenId string) ([]*CmdType, error) { var rtn []*CmdType txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * from cmd WHERE cmdid IN (SELECT cmdid FROM line WHERE screenid = ?) AND status = ?` + query := `SELECT * FROM cmd WHERE screenid = ? AND status = ?` rtn = dbutil.SelectMapsGen[*CmdType](tx, query, screenId, CmdStatusRunning) return nil }) @@ -1471,11 +1449,11 @@ func GetRunningScreenCmds(ctx context.Context, screenId string) ([]*CmdType, err return rtn, nil } -func UpdateCmdTermOpts(ctx context.Context, screenId string, cmdId string, termOpts TermOpts) error { +func UpdateCmdTermOpts(ctx context.Context, screenId string, lineId string, termOpts TermOpts) error { 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) + query := `UPDATE cmd SET termopts = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, termOpts, screenId, lineId) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_CmdTermOpts) return nil }) return txErr @@ -1792,14 +1770,14 @@ func GetLineResolveItems(ctx context.Context, screenId string) ([]ResolveItem, e return rtn, nil } -func UpdateScreenFocusForDoneCmd(ctx context.Context, screenId string, cmdId string) (*ScreenType, error) { +func UpdateScreenFocusForDoneCmd(ctx context.Context, screenId string, lineId string) (*ScreenType, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) { query := `SELECT screenid FROM screen s WHERE s.screenid = ? AND s.focustype = ? - AND s.selectedline IN (SELECT linenum FROM line l WHERE l.screenid = s.screenid AND l.cmdid = ?) + AND s.selectedline IN (SELECT linenum FROM line l WHERE l.screenid = s.screenid AND l.lineid = ?) ` - if !tx.Exists(query, screenId, ScreenFocusCmd, cmdId) { + if !tx.Exists(query, screenId, ScreenFocusCmd, lineId) { return nil, nil } editMap := make(map[string]interface{}) @@ -1983,11 +1961,15 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar return txErr } -func purgeCmdByScreenId(ctx context.Context, screenId string, cmdId string) error { +func purgeCmdByScreenId(ctx context.Context, screenId string, lineId string) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `DELETE FROM cmd WHERE screenid = ? AND cmdid = ?` - tx.Exec(query, screenId, cmdId) - return DeletePtyOutFile(tx.Context(), screenId, cmdId) + 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 } @@ -1996,17 +1978,13 @@ func PurgeLinesByIds(ctx context.Context, screenId string, lineIds []string) err txErr := WithTx(ctx, func(tx *TxWrap) error { isWS := isWebShare(tx, screenId) for _, lineId := range lineIds { - query := `SELECT cmdid FROM line WHERE screenid = ? AND lineid = ?` - cmdId := tx.GetString(query, screenId, lineId) - query = `DELETE FROM line WHERE screenid = ? AND lineid = ?` + 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) - if cmdId != "" { - err := purgeCmdByScreenId(tx.Context(), screenId, cmdId) - if err != nil { - return err - } + err := purgeCmdByScreenId(tx.Context(), screenId, lineId) + if err != nil { + return err } if isWS { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel) @@ -2412,26 +2390,17 @@ func getLineIdsFromHistoryItems(historyItems []*HistoryItemType) []string { return rtn } -func getCmdIdsFromHistoryItems(historyItems []*HistoryItemType) []string { - var rtn []string - for _, hitem := range historyItems { - if hitem.CmdId != "" { - rtn = append(rtn, hitem.CmdId) - } - } - return rtn -} - func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryItemType) ([]*LineType, []*CmdType, error) { if len(historyItems) == 0 { return nil, nil, nil } return WithTxRtn3(ctx, func(tx *TxWrap) ([]*LineType, []*CmdType, error) { var lineArr []*LineType + lineIdsJsonArr := quickJsonArr(getLineIdsFromHistoryItems(historyItems)) query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` - tx.Select(&lineArr, query, quickJsonArr(getLineIdsFromHistoryItems(historyItems))) - query = `SELECT * FROM cmd WHERE cmdid IN (SELECT value FROM json_each(?))` - cmdArr := dbutil.SelectMapsGen[*CmdType](tx, query, quickJsonArr(getCmdIdsFromHistoryItems(historyItems))) + tx.Select(&lineArr, query, lineIdsJsonArr) + query = `SELECT * FROM cmd WHERE lineid IN (SELECT value FROM json_each(?))` + cmdArr := dbutil.SelectMapsGen[*CmdType](tx, query, lineIdsJsonArr) return lineArr, cmdArr, nil }) } @@ -2560,7 +2529,7 @@ func insertScreenNewUpdate(tx *TxWrap, screenId string) { SELECT screenid, lineid, ?, ? FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum DESC` tx.Exec(query, UpdateType_LineNew, nowTs, screenId) query = `INSERT INTO screenupdate (screenid, lineid, updatetype, updatets) - SELECT c.screenid, l.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.cmdid = c.cmdid AND NOT l.archived ORDER BY l.linenum DESC` + SELECT c.screenid, c.lineid, ?, ? FROM cmd c, line l WHERE c.screenid = ? AND l.lineid = c.lineid AND NOT l.archived ORDER BY l.linenum DESC` tx.Exec(query, UpdateType_PtyPos, nowTs, screenId) NotifyUpdateWriter() } @@ -2600,22 +2569,6 @@ func insertScreenLineUpdate(tx *TxWrap, screenId string, lineId string, updateTy NotifyUpdateWriter() } -func insertScreenCmdUpdate(tx *TxWrap, screenId string, cmdId string, updateType string) { - if screenId == "" { - tx.SetErr(errors.New("invalid screen-update, screenid is empty")) - return - } - if cmdId == "" { - tx.SetErr(errors.New("invalid screen-update, cmdid is empty")) - return - } - query := `SELECT lineid FROM line WHERE screenid = ? AND cmdid = ?` - lineId := tx.GetString(query, screenId, cmdId) - if lineId != "" { - insertScreenLineUpdate(tx, screenId, lineId, updateType) - } -} - func GetScreenUpdates(ctx context.Context, maxNum int) ([]*ScreenUpdateType, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]*ScreenUpdateType, error) { var updates []*ScreenUpdateType @@ -2651,12 +2604,12 @@ func RemoveScreenUpdates(ctx context.Context, updateIds []int64) error { }) } -func MaybeInsertPtyPosUpdate(ctx context.Context, screenId string, cmdId string) error { +func MaybeInsertPtyPosUpdate(ctx context.Context, screenId string, lineId string) error { return WithTx(ctx, func(tx *TxWrap) error { if !isWebShare(tx, screenId) { return nil } - insertScreenCmdUpdate(tx, screenId, cmdId, UpdateType_PtyPos) + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_PtyPos) return nil }) } diff --git a/pkg/sstore/fileops.go b/pkg/sstore/fileops.go index 61214836b..953b56730 100644 --- a/pkg/sstore/fileops.go +++ b/pkg/sstore/fileops.go @@ -3,18 +3,20 @@ package sstore import ( "context" "encoding/base64" + "errors" "fmt" + "io/fs" "log" "os" "path" - "github.com/google/uuid" "github.com/commandlinedev/apishell/pkg/cirfile" "github.com/commandlinedev/prompt-server/pkg/scbase" + "github.com/google/uuid" ) -func CreateCmdPtyFile(ctx context.Context, screenId string, cmdId string, maxSize int64) error { - ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) +func CreateCmdPtyFile(ctx context.Context, screenId string, lineId string, maxSize int64) error { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return err } @@ -25,22 +27,22 @@ func CreateCmdPtyFile(ctx context.Context, screenId string, cmdId string, maxSiz return f.Close() } -func StatCmdPtyFile(ctx context.Context, screenId string, cmdId string) (*cirfile.Stat, error) { - ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) +func StatCmdPtyFile(ctx context.Context, screenId string, lineId string) (*cirfile.Stat, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return nil, err } return cirfile.StatCirFile(ctx, ptyOutFileName) } -func AppendToCmdPtyBlob(ctx context.Context, screenId string, cmdId string, data []byte, pos int64) (*PtyDataUpdate, error) { +func AppendToCmdPtyBlob(ctx context.Context, screenId string, lineId string, data []byte, pos int64) (*PtyDataUpdate, error) { if screenId == "" { return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set") } if pos < 0 { return nil, fmt.Errorf("invalid seek pos '%d' in AppendToCmdPtyBlob", pos) } - ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return nil, err } @@ -56,22 +58,22 @@ func AppendToCmdPtyBlob(ctx context.Context, screenId string, cmdId string, data data64 := base64.StdEncoding.EncodeToString(data) update := &PtyDataUpdate{ ScreenId: screenId, - CmdId: cmdId, + LineId: lineId, PtyPos: pos, PtyData64: data64, PtyDataLen: int64(len(data)), } - err = MaybeInsertPtyPosUpdate(ctx, screenId, cmdId) + err = MaybeInsertPtyPosUpdate(ctx, screenId, lineId) if err != nil { // just log - log.Printf("error inserting ptypos update %s/%s: %v\n", screenId, cmdId, err) + log.Printf("error inserting ptypos update %s/%s: %v\n", screenId, lineId, err) } return update, nil } // returns (real-offset, data, err) -func ReadFullPtyOutFile(ctx context.Context, screenId string, cmdId string) (int64, []byte, error) { - ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) +func ReadFullPtyOutFile(ctx context.Context, screenId string, lineId string) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return 0, nil, err } @@ -84,8 +86,8 @@ func ReadFullPtyOutFile(ctx context.Context, screenId string, cmdId string) (int } // 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) +func ReadPtyOutFile(ctx context.Context, screenId string, lineId string, offset int64, maxSize int64) (int64, []byte, error) { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return 0, nil, err } @@ -160,12 +162,16 @@ func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) { return rtn, nil } -func DeletePtyOutFile(ctx context.Context, screenId string, cmdId string) error { - ptyOutFileName, err := scbase.PtyOutFile(screenId, cmdId) +func DeletePtyOutFile(ctx context.Context, screenId string, lineId string) error { + ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId) if err != nil { return err } - return os.Remove(ptyOutFileName) + err = os.Remove(ptyOutFileName) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err } func DeleteScreenDir(ctx context.Context, screenId string) error { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 72b6bc71c..8578f945d 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,9 +17,10 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 19 +const MaxMigration = 20 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 +const CmdLineSpecialMigration = 20 func MakeMigrate() (*migrate.Migrate, error) { fsVar, err := iofs.New(sh2db.MigrationFS, "migrations") @@ -64,9 +65,15 @@ func MigrateUpStep(m *migrate.Migrate, newVersion uint) error { return err } if newVersion == CmdScreenSpecialMigration { - mErr := RunCmdScreenMigration13() + mErr := RunMigration13() if mErr != nil { - return mErr + return fmt.Errorf("migrating to v%d: %w", newVersion, mErr) + } + } + if newVersion == CmdLineSpecialMigration { + mErr := RunMigration20() + if mErr != nil { + return fmt.Errorf("migrating to v%d: %w", newVersion, mErr) } } log.Printf("[db] migration v%d, elapsed %v\n", newVersion, time.Since(startTime)) diff --git a/pkg/sstore/quick.go b/pkg/sstore/quick.go index a887d23d6..c19ce8a06 100644 --- a/pkg/sstore/quick.go +++ b/pkg/sstore/quick.go @@ -6,6 +6,7 @@ import ( var quickSetStr = dbutil.QuickSetStr var quickSetInt64 = dbutil.QuickSetInt64 +var quickSetInt = dbutil.QuickSetInt var quickSetBool = dbutil.QuickSetBool var quickSetBytes = dbutil.QuickSetBytes var quickSetJson = dbutil.QuickSetJson diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index c10a210a8..47a4a1640 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "github.com/commandlinedev/apishell/pkg/base" "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" "github.com/commandlinedev/prompt-server/pkg/dbutil" @@ -58,7 +59,6 @@ const ( CmdStatusError = "error" CmdStatusDone = "done" CmdStatusHangup = "hangup" - CmdStatusWaiting = "waiting" ) const ( @@ -115,7 +115,8 @@ const ( UpdateType_LineContentHeight = "line:contentheight" UpdateType_CmdStatus = "cmd:status" UpdateType_CmdTermOpts = "cmd:termopts" - UpdateType_CmdDoneInfo = "cmd:doneinfo" + UpdateType_CmdExitCode = "cmd:exitcode" + UpdateType_CmdDurationMs = "cmd:durationms" UpdateType_CmdRtnState = "cmd:rtnstate" UpdateType_PtyPos = "pty:pos" ) @@ -126,6 +127,10 @@ var globalDBLock = &sync.Mutex{} var globalDB *sqlx.DB var globalDBErr error +func lineIdFromCK(ck base.CommandKey) string { + return ck.GetCmdId() +} + func GetDBName() string { scHome := scbase.GetPromptHomeDir() return path.Join(scHome, DBFileName) @@ -174,7 +179,7 @@ func CloseDB() { type CmdPtr struct { ScreenId string - CmdId string + LineId string } type ClientWinSizeType struct { @@ -373,7 +378,6 @@ func (h *HistoryItemType) ToMap() map[string]interface{} { rtn["lineid"] = h.LineId rtn["linenum"] = h.LineNum rtn["haderror"] = h.HadError - rtn["cmdid"] = h.CmdId rtn["cmdstr"] = h.CmdStr rtn["remoteownerid"] = h.Remote.OwnerId rtn["remoteid"] = h.Remote.RemoteId @@ -391,7 +395,6 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool { quickSetStr(&h.ScreenId, m, "screenid") quickSetStr(&h.LineId, m, "lineid") quickSetBool(&h.HadError, m, "haderror") - quickSetStr(&h.CmdId, m, "cmdid") quickSetStr(&h.CmdStr, m, "cmdstr") quickSetStr(&h.Remote.OwnerId, m, "remoteownerid") quickSetStr(&h.Remote.RemoteId, m, "remoteid") @@ -535,7 +538,6 @@ type HistoryItemType struct { ScreenId string `json:"screenid"` LineId string `json:"lineid"` HadError bool `json:"haderror"` - CmdId string `json:"cmdid"` CmdStr string `json:"cmdstr"` Remote RemotePtrType `json:"remote"` IsMetaCmd bool `json:"ismetacmd"` @@ -715,7 +717,6 @@ type LineType struct { LineType string `json:"linetype"` Renderer string `json:"renderer,omitempty"` Text string `json:"text,omitempty"` - CmdId string `json:"cmdid,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` ContentHeight int64 `json:"contentheight,omitempty"` Star bool `json:"star,omitempty"` @@ -932,35 +933,26 @@ func (r *RemoteType) GetName() string { return r.RemoteCanonicalName } -type CmdDoneInfo struct { - Ts int64 `json:"ts"` - ExitCode int64 `json:"exitcode"` - DurationMs int64 `json:"durationms"` -} - -type CmdMapType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - CmdId string `json:"cmdid"` -} - type CmdType struct { - ScreenId string `json:"screenid"` - CmdId string `json:"cmdid"` - Remote RemotePtrType `json:"remote"` - CmdStr string `json:"cmdstr"` - RawCmdStr string `json:"rawcmdstr"` - FeState map[string]string `json:"festate"` - StatePtr ShellStatePtr `json:"state"` - TermOpts TermOpts `json:"termopts"` - OrigTermOpts TermOpts `json:"origtermopts"` - Status string `json:"status"` - StartPk *packet.CmdStartPacketType `json:"startpk,omitempty"` - DoneInfo *CmdDoneInfo `json:"doneinfo,omitempty"` - RunOut []packet.PacketType `json:"runout,omitempty"` - RtnState bool `json:"rtnstate,omitempty"` - RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"` - Remove bool `json:"remove,omitempty"` + ScreenId string `json:"screenid"` + LineId string `json:"lineid"` + Remote RemotePtrType `json:"remote"` + CmdStr string `json:"cmdstr"` + RawCmdStr string `json:"rawcmdstr"` + FeState map[string]string `json:"festate"` + StatePtr ShellStatePtr `json:"state"` + TermOpts TermOpts `json:"termopts"` + OrigTermOpts TermOpts `json:"origtermopts"` + Status string `json:"status"` + CmdPid int `json:"cmdpid"` + RemotePid int `json:"remotepid"` + DoneTs int64 `json:"donets"` + ExitCode int `json:"exitcode"` + DurationMs int `json:"durationms"` + RunOut []packet.PacketType `json:"runout,omitempty"` + RtnState bool `json:"rtnstate,omitempty"` + RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"` + Remove bool `json:"remove,omitempty"` } func (r *RemoteType) ToMap() map[string]interface{} { @@ -1007,7 +999,7 @@ func (r *RemoteType) FromMap(m map[string]interface{}) bool { func (cmd *CmdType) ToMap() map[string]interface{} { rtn := make(map[string]interface{}) rtn["screenid"] = cmd.ScreenId - rtn["cmdid"] = cmd.CmdId + rtn["lineid"] = cmd.LineId rtn["remoteownerid"] = cmd.Remote.OwnerId rtn["remoteid"] = cmd.Remote.RemoteId rtn["remotename"] = cmd.Remote.Name @@ -1019,8 +1011,11 @@ func (cmd *CmdType) ToMap() map[string]interface{} { rtn["termopts"] = quickJson(cmd.TermOpts) rtn["origtermopts"] = quickJson(cmd.OrigTermOpts) rtn["status"] = cmd.Status - rtn["startpk"] = quickJson(cmd.StartPk) - rtn["doneinfo"] = quickJson(cmd.DoneInfo) + rtn["cmdpid"] = cmd.CmdPid + rtn["remotepid"] = cmd.RemotePid + rtn["donets"] = cmd.DoneTs + rtn["exitcode"] = cmd.ExitCode + rtn["durationms"] = cmd.DurationMs rtn["runout"] = quickJson(cmd.RunOut) rtn["rtnstate"] = cmd.RtnState rtn["rtnbasehash"] = cmd.RtnStatePtr.BaseHash @@ -1030,7 +1025,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} { func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetStr(&cmd.ScreenId, m, "screenid") - quickSetStr(&cmd.CmdId, m, "cmdid") + quickSetStr(&cmd.LineId, m, "lineid") quickSetStr(&cmd.Remote.OwnerId, m, "remoteownerid") quickSetStr(&cmd.Remote.RemoteId, m, "remoteid") quickSetStr(&cmd.Remote.Name, m, "remotename") @@ -1042,8 +1037,11 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool { quickSetJson(&cmd.TermOpts, m, "termopts") quickSetJson(&cmd.OrigTermOpts, m, "origtermopts") quickSetStr(&cmd.Status, m, "status") - quickSetJson(&cmd.StartPk, m, "startpk") - quickSetJson(&cmd.DoneInfo, m, "doneinfo") + quickSetInt(&cmd.CmdPid, m, "cmdpid") + quickSetInt(&cmd.RemotePid, m, "remotepid") + quickSetInt64(&cmd.DoneTs, m, "donets") + quickSetInt(&cmd.ExitCode, m, "exitcode") + quickSetInt(&cmd.DurationMs, m, "durationms") quickSetJson(&cmd.RunOut, m, "runout") quickSetBool(&cmd.RtnState, m, "rtnstate") quickSetStr(&cmd.RtnStatePtr.BaseHash, m, "rtnbasehash") @@ -1051,15 +1049,19 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool { return true } -func makeNewLineCmd(screenId string, userId string, cmdId string, renderer string) *LineType { +func (cmd *CmdType) IsRunning() bool { + return cmd.Status == CmdStatusRunning || cmd.Status == CmdStatusDetached +} + +func makeNewLineCmd(screenId string, userId string, lineId string, renderer string) *LineType { rtn := &LineType{} rtn.ScreenId = screenId rtn.UserId = userId - rtn.LineId = scbase.GenPromptUUID() + rtn.LineId = lineId rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeCmd - rtn.CmdId = cmdId + rtn.LineId = lineId rtn.ContentHeight = LineNoHeight rtn.Renderer = renderer return rtn @@ -1078,12 +1080,11 @@ func makeNewLineText(screenId string, userId string, text string) *LineType { return rtn } -func makeNewLineOpenAI(screenId string, userId string, cmdId string) *LineType { +func makeNewLineOpenAI(screenId string, userId string, lineId string) *LineType { rtn := &LineType{} rtn.ScreenId = screenId rtn.UserId = userId - rtn.LineId = scbase.GenPromptUUID() - rtn.CmdId = cmdId + rtn.LineId = lineId rtn.Ts = time.Now().UnixMilli() rtn.LineLocal = true rtn.LineType = LineTypeOpenAI @@ -1102,7 +1103,7 @@ func AddCommentLine(ctx context.Context, screenId string, userId string, comment } func AddOpenAILine(ctx context.Context, screenId string, userId string, cmd *CmdType) (*LineType, error) { - rtnLine := makeNewLineOpenAI(screenId, userId, cmd.CmdId) + rtnLine := makeNewLineOpenAI(screenId, userId, cmd.LineId) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err @@ -1111,7 +1112,7 @@ func AddOpenAILine(ctx context.Context, screenId string, userId string, cmd *Cmd } func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { - rtnLine := makeNewLineCmd(screenId, userId, cmd.CmdId, renderer) + rtnLine := makeNewLineCmd(screenId, userId, cmd.LineId, renderer) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err diff --git a/pkg/sstore/sstore_migrate.go b/pkg/sstore/sstore_migrate.go index 909ff4646..48872c571 100644 --- a/pkg/sstore/sstore_migrate.go +++ b/pkg/sstore/sstore_migrate.go @@ -10,12 +10,20 @@ import ( "github.com/commandlinedev/prompt-server/pkg/scbase" ) -type cmdMigrationType struct { +const MigrationChunkSize = 10 + +type cmdMigration13Type struct { SessionId string ScreenId string CmdId string } +type cmdMigration20Type struct { + ScreenId string + LineId string + CmdId string +} + func getSliceChunk[T any](slice []T, chunkSize int) ([]T, []T) { if chunkSize >= len(slice) { return slice, nil @@ -23,10 +31,65 @@ func getSliceChunk[T any](slice []T, chunkSize int) ([]T, []T) { return slice[0:chunkSize], slice[chunkSize:] } -func RunCmdScreenMigration13() error { +func RunMigration20() error { ctx := context.Background() startTime := time.Now() - var migrations []cmdMigrationType + var migrations []cmdMigration20Type + txErr := WithTx(ctx, func(tx *TxWrap) error { + tx.Select(&migrations, `SELECT * FROM cmd_migrate`) + return nil + }) + if txErr != nil { + return fmt.Errorf("trying to get cmd migrations: %w", txErr) + } + log.Printf("[db] got %d cmd-line migrations\n", len(migrations)) + for len(migrations) > 0 { + var mchunk []cmdMigration20Type + mchunk, migrations = getSliceChunk(migrations, MigrationChunkSize) + err := processMigration20Chunk(ctx, mchunk) + if err != nil { + return fmt.Errorf("cmd migration failed on chunk: %w", err) + } + } + log.Printf("[db] cmd line migration done: %v\n", time.Since(startTime)) + return nil +} + +func processMigration20Chunk(ctx context.Context, mchunk []cmdMigration20Type) error { + for _, mig := range mchunk { + newFile, err := scbase.PtyOutFile(mig.ScreenId, mig.LineId) + if err != nil { + log.Printf("ptyoutfile(lineid) error: %v\n", err) + continue + } + oldFile, err := scbase.PtyOutFile(mig.ScreenId, mig.CmdId) + if err != nil { + log.Printf("ptyoutfile(cmdid) error: %v\n", err) + continue + } + err = os.Rename(oldFile, newFile) + if err != nil { + log.Printf("error renaming %s => %s: %v\n", oldFile, newFile, err) + continue + } + } + txErr := WithTx(ctx, func(tx *TxWrap) error { + for _, mig := range mchunk { + query := `DELETE FROM cmd_migrate WHERE cmdid = ?` + tx.Exec(query, mig.CmdId) + } + return nil + }) + if txErr != nil { + return txErr + } + return nil +} + +func RunMigration13() error { + ctx := context.Background() + startTime := time.Now() + var migrations []cmdMigration13Type txErr := WithTx(ctx, func(tx *TxWrap) error { tx.Select(&migrations, `SELECT * FROM cmd_migrate`) return nil @@ -36,9 +99,9 @@ func RunCmdScreenMigration13() error { } log.Printf("[db] got %d cmd-screen migrations\n", len(migrations)) for len(migrations) > 0 { - var mchunk []cmdMigrationType - mchunk, migrations = getSliceChunk(migrations, 5) - err := processMigrationChunk(ctx, mchunk) + var mchunk []cmdMigration13Type + mchunk, migrations = getSliceChunk(migrations, MigrationChunkSize) + err := processMigration13Chunk(ctx, mchunk) if err != nil { return fmt.Errorf("cmd migration failed on chunk: %w", err) } @@ -59,7 +122,7 @@ func RunCmdScreenMigration13() error { return nil } -func processMigrationChunk(ctx context.Context, mchunk []cmdMigrationType) error { +func processMigration13Chunk(ctx context.Context, mchunk []cmdMigration13Type) error { for _, mig := range mchunk { newFile, err := scbase.PtyOutFile(mig.ScreenId, mig.CmdId) if err != nil { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index e53822248..72316b3a5 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -19,7 +19,7 @@ type UpdatePacket interface { type PtyDataUpdate struct { ScreenId string `json:"screenid,omitempty"` - CmdId string `json:"cmdid,omitempty"` + LineId string `json:"lineid,omitempty"` RemoteId string `json:"remoteid,omitempty"` PtyPos int64 `json:"ptypos"` PtyData64 string `json:"ptydata64"` @@ -72,21 +72,6 @@ type RemoteViewType struct { RemoteEdit *RemoteEditType `json:"remoteedit,omitempty"` } -func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { - modelUpdate, ok := update.(*ModelUpdate) - if !ok { - return "", "", nil - } - if modelUpdate.Line == nil { - return "", "", nil - } - var rptr *RemotePtrType - if modelUpdate.Cmd != nil { - rptr = &modelUpdate.Cmd.Remote - } - return modelUpdate.Line.LineId, modelUpdate.Line.CmdId, rptr -} - func InfoMsgUpdate(infoMsgFmt string, args ...interface{}) *ModelUpdate { msg := fmt.Sprintf(infoMsgFmt, args...) return &ModelUpdate{ From d2cfc5c980295bc2049663a9776a3849f2a515f8 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 17:18:52 -0700 Subject: [PATCH 373/397] fix insert for new doneinfo/startpk fields --- pkg/sstore/dbops.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index c9972ecbb..5c785282c 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -804,8 +804,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { cmd.OrigTermOpts = cmd.TermOpts cmdMap := cmd.ToMap() query = ` -INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, startpk, doneinfo, rtnstate, runout, rtnbasehash, rtndiffhasharr) - VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:startpk,:doneinfo,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) +INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, cmdpid, remotepid, donets, exitcode, durationms, rtnstate, runout, rtnbasehash, rtndiffhasharr) + VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:cmdpid,:remotepid,:donets,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr) ` tx.NamedExec(query, cmdMap) } From 36a20a14c4c5a51c6e10a8252a90849a77bc8adc Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 18:32:56 -0700 Subject: [PATCH 374/397] change functions to use lineid not cmdid --- cmd/main-server.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 6d4488e9f..9baab9240 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -226,10 +226,10 @@ func HandleRtnState(w http.ResponseWriter, r *http.Request) { }() qvals := r.URL.Query() screenId := qvals.Get("screenid") - cmdId := qvals.Get("cmdid") - if screenId == "" || cmdId == "" { + lineId := qvals.Get("lineid") + if screenId == "" || lineId == "" { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("must specify screenid and cmdid"))) + w.Write([]byte(fmt.Sprintf("must specify screenid and lineid"))) return } if _, err := uuid.Parse(screenId); err != nil { @@ -237,12 +237,12 @@ func HandleRtnState(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid screenid: %v", err))) return } - if _, err := uuid.Parse(cmdId); err != nil { + if _, err := uuid.Parse(lineId); err != nil { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) + w.Write([]byte(fmt.Sprintf("invalid lineid: %v", err))) return } - data, err := rtnstate.GetRtnStateDiff(r.Context(), screenId, cmdId) + data, err := rtnstate.GetRtnStateDiff(r.Context(), screenId, lineId) if err != nil { w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("cannot get rtnstate diff: %v", err))) @@ -281,10 +281,10 @@ func HandleRemotePty(w http.ResponseWriter, r *http.Request) { func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { qvals := r.URL.Query() screenId := qvals.Get("screenid") - cmdId := qvals.Get("cmdid") - if screenId == "" || cmdId == "" { + lineId := qvals.Get("lineid") + if screenId == "" || lineId == "" { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("must specify screenid and cmdid"))) + w.Write([]byte(fmt.Sprintf("must specify screenid and lineid"))) return } if _, err := uuid.Parse(screenId); err != nil { @@ -292,12 +292,12 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid screenid: %v", err))) return } - if _, err := uuid.Parse(cmdId); err != nil { + if _, err := uuid.Parse(lineId); err != nil { w.WriteHeader(500) - w.Write([]byte(fmt.Sprintf("invalid cmdid: %v", err))) + w.Write([]byte(fmt.Sprintf("invalid lineid: %v", err))) return } - realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), screenId, cmdId) + realOffset, data, err := sstore.ReadFullPtyOutFile(r.Context(), screenId, lineId) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusOK) From 483bcbe3384417b36ef36d5fb0f06a95c411d51b Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 18:46:57 -0700 Subject: [PATCH 375/397] change cmd_migrate table name to prevent conflict --- db/migrations/000020_linecmd.up.sql | 6 ++---- pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore_migrate.go | 10 ++++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/db/migrations/000020_linecmd.up.sql b/db/migrations/000020_linecmd.up.sql index 7e2332e95..3604e18ce 100644 --- a/db/migrations/000020_linecmd.up.sql +++ b/db/migrations/000020_linecmd.up.sql @@ -26,16 +26,14 @@ CREATE TABLE cmd_new ( PRIMARY KEY (screenid, lineid) ); -DROP TABLE IF EXISTS cmd_migrate; - -CREATE TABLE cmd_migrate ( +CREATE TABLE cmd_migrate20 ( screenid varchar(36) NOT NULL, lineid varchar(36) NOT NULL, cmdid varchar(36) NOT NULL, PRIMARY KEY (screenid, lineid) ); -INSERT INTO cmd_migrate +INSERT INTO cmd_migrate20 SELECT screenid, lineid, cmdid FROM line; diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 8578f945d..91469b92c 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -104,7 +104,7 @@ func MigrateUp(targetVersion uint) error { for newVersion := curVersion + 1; newVersion <= targetVersion; newVersion++ { err = MigrateUpStep(m, newVersion) if err != nil { - return fmt.Errorf("during migration v%d: %w", err, newVersion) + return fmt.Errorf("during migration v%d: %w", newVersion, err) } } log.Printf("[db] migration done, new version = %d\n", targetVersion) diff --git a/pkg/sstore/sstore_migrate.go b/pkg/sstore/sstore_migrate.go index 48872c571..0df95a9e5 100644 --- a/pkg/sstore/sstore_migrate.go +++ b/pkg/sstore/sstore_migrate.go @@ -36,11 +36,13 @@ func RunMigration20() error { startTime := time.Now() var migrations []cmdMigration20Type txErr := WithTx(ctx, func(tx *TxWrap) error { - tx.Select(&migrations, `SELECT * FROM cmd_migrate`) + m := tx.SelectMaps(`SELECT * FROM cmd_migrate20`) + fmt.Printf("got maps: %#v\n", m) + tx.Select(&migrations, `SELECT * FROM cmd_migrate20`) return nil }) if txErr != nil { - return fmt.Errorf("trying to get cmd migrations: %w", txErr) + return fmt.Errorf("trying to get cmd20 migrations: %w", txErr) } log.Printf("[db] got %d cmd-line migrations\n", len(migrations)) for len(migrations) > 0 { @@ -75,7 +77,7 @@ func processMigration20Chunk(ctx context.Context, mchunk []cmdMigration20Type) e } txErr := WithTx(ctx, func(tx *TxWrap) error { for _, mig := range mchunk { - query := `DELETE FROM cmd_migrate WHERE cmdid = ?` + query := `DELETE FROM cmd_migrate20 WHERE cmdid = ?` tx.Exec(query, mig.CmdId) } return nil @@ -95,7 +97,7 @@ func RunMigration13() error { return nil }) if txErr != nil { - return fmt.Errorf("trying to get cmd migrations: %w", txErr) + return fmt.Errorf("trying to get cmd13 migrations: %w", txErr) } log.Printf("[db] got %d cmd-screen migrations\n", len(migrations)) for len(migrations) > 0 { From 2d99d82fbc114d7b8829dc8198b69f0bc0b426a9 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 22:23:20 -0700 Subject: [PATCH 376/397] remove blank cmdids from migration --- db/migrations/000020_linecmd.up.sql | 4 +++- pkg/sstore/sstore_migrate.go | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/db/migrations/000020_linecmd.up.sql b/db/migrations/000020_linecmd.up.sql index 3604e18ce..9e04bf69d 100644 --- a/db/migrations/000020_linecmd.up.sql +++ b/db/migrations/000020_linecmd.up.sql @@ -35,7 +35,9 @@ CREATE TABLE cmd_migrate20 ( INSERT INTO cmd_migrate20 SELECT screenid, lineid, cmdid -FROM line; +FROM line +WHERE cmdid <> '' +; INSERT INTO cmd_new SELECT diff --git a/pkg/sstore/sstore_migrate.go b/pkg/sstore/sstore_migrate.go index 0df95a9e5..7aa1a9ac8 100644 --- a/pkg/sstore/sstore_migrate.go +++ b/pkg/sstore/sstore_migrate.go @@ -37,7 +37,6 @@ func RunMigration20() error { var migrations []cmdMigration20Type txErr := WithTx(ctx, func(tx *TxWrap) error { m := tx.SelectMaps(`SELECT * FROM cmd_migrate20`) - fmt.Printf("got maps: %#v\n", m) tx.Select(&migrations, `SELECT * FROM cmd_migrate20`) return nil }) From 6b330b25f37c6b30ecf25c5c9fcd1f3433c26413 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 30 Jul 2023 22:25:44 -0700 Subject: [PATCH 377/397] remove selectmaps call --- pkg/sstore/sstore_migrate.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/sstore/sstore_migrate.go b/pkg/sstore/sstore_migrate.go index 7aa1a9ac8..b77bacd7c 100644 --- a/pkg/sstore/sstore_migrate.go +++ b/pkg/sstore/sstore_migrate.go @@ -36,7 +36,6 @@ func RunMigration20() error { startTime := time.Now() var migrations []cmdMigration20Type txErr := WithTx(ctx, func(tx *TxWrap) error { - m := tx.SelectMaps(`SELECT * FROM cmd_migrate20`) tx.Select(&migrations, `SELECT * FROM cmd_migrate20`) return nil }) From ed50f1e7d0303d9470bde81a1548b2903e825a50 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 3 Aug 2023 14:58:56 -0700 Subject: [PATCH 378/397] up go.mod versions --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 3904a9e84..71bf3d208 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/alessio/shellescape v1.4.1 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/creack/pty v1.1.18 - github.com/golang-migrate/migrate/v4 v4.15.2 + github.com/golang-migrate/migrate/v4 v4.16.2 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.5 - github.com/mattn/go-sqlite3 v1.14.14 + github.com/mattn/go-sqlite3 v1.14.16 github.com/sawka/txwrap v0.1.2 github.com/commandlinedev/apishell v0.0.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 From 681d80e0a682b715336da6085308410a7bc87c6c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 14 Aug 2023 17:04:16 -0700 Subject: [PATCH 379/397] force ipv4 listening in local server (consistent ipv4 throughout stack) --- cmd/main-server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 9baab9240..eefea4662 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -38,10 +38,10 @@ const HttpWriteTimeout = 21 * time.Second const HttpMaxHeaderBytes = 60000 const HttpTimeoutDuration = 21 * time.Second -const MainServerAddr = "localhost:1619" // PromptServer, P=16, S=19, PS=1619 -const WebSocketServerAddr = "localhost:1623" // PromptWebsock, P=16, W=23, PW=1623 -const MainServerDevAddr = "localhost:8090" -const WebSocketServerDevAddr = "localhost:8091" +const MainServerAddr = "127.0.0.1:1619" // PromptServer, P=16, S=19, PS=1619 +const WebSocketServerAddr = "127.0.0.1:1623" // PromptWebsock, P=16, W=23, PW=1623 +const MainServerDevAddr = "127.0.0.1:8090" +const WebSocketServerDevAddr = "127.0.0.1:8091" const WSStateReconnectTime = 30 * time.Second const WSStatePacketChSize = 20 From 22fb034cc4f21abe39af30b41b9c1747a41b2f2e Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 31 Aug 2023 22:04:31 -0700 Subject: [PATCH 380/397] PE-41 remote file api (#1) * RPC for remote file streaming -- just implemented 'stat' for now (streaming to come) * allow RPC iterators for MShell RPCs. implement two test commands to test viewing files * implement read-file handler * read-file: allow overriding of content-type and use line's cwd not remote instance cwd * checkpoint on write-file impl * implemented metacommand version of write file * checkpoint, untested write-file impl * multipart handling for write-file data * add usetemp param to writefile --- cmd/main-server.go | 279 +++++++++++++++++++++++++++++++++++++ pkg/cmdrunner/cmdrunner.go | 219 ++++++++++++++++++++++++++++- pkg/cmdrunner/resolver.go | 8 +- pkg/remote/remote.go | 30 +++- pkg/utilfn/utilfn.go | 13 ++ 5 files changed, 542 insertions(+), 7 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index eefea4662..7202f9042 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -2,14 +2,19 @@ package main import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" + "io" "io/fs" "log" + "mime/multipart" "net/http" "os" "os/signal" + "path/filepath" + "regexp" "runtime/debug" "strconv" "strings" @@ -20,6 +25,8 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" + "github.com/commandlinedev/apishell/pkg/packet" + "github.com/commandlinedev/apishell/pkg/server" "github.com/commandlinedev/prompt-server/pkg/cmdrunner" "github.com/commandlinedev/prompt-server/pkg/pcloud" "github.com/commandlinedev/prompt-server/pkg/remote" @@ -49,11 +56,14 @@ const InitialTelemetryWait = 30 * time.Second const TelemetryTick = 30 * time.Minute const TelemetryInterval = 8 * time.Hour +const MaxWriteFileMemSize = 20 * (1024 * 1024) // 20M + var GlobalLock = &sync.Mutex{} var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState var GlobalAuthKey string var BuildTime = "0" var shutdownOnce sync.Once +var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`) type ClientActiveState struct { Fg bool `json:"fg"` @@ -312,6 +322,273 @@ func HandleGetPtyOut(w http.ResponseWriter, r *http.Request) { w.Write(data) } +type writeFileParamsType struct { + ScreenId string `json:"screenid"` + LineId string `json:"lineid"` + Path string `json:"path"` + UseTemp bool `json:"usetemp,omitempty"` +} + +func parseWriteFileParams(r *http.Request) (*writeFileParamsType, multipart.File, error) { + err := r.ParseMultipartForm(MaxWriteFileMemSize) + if err != nil { + return nil, nil, fmt.Errorf("cannot parse multipart form data: %v", err) + } + form := r.MultipartForm + if len(form.Value["params"]) == 0 { + return nil, nil, fmt.Errorf("no params found") + } + paramsStr := form.Value["params"][0] + var params writeFileParamsType + err = json.Unmarshal([]byte(paramsStr), ¶ms) + if err != nil { + return nil, nil, fmt.Errorf("bad params json: %v", err) + } + if len(form.File["data"]) == 0 { + return nil, nil, fmt.Errorf("no data found") + } + fileHeader := form.File["data"][0] + file, err := fileHeader.Open() + if err != nil { + return nil, nil, fmt.Errorf("error opening multipart data file: %v", err) + } + return ¶ms, file, nil +} + +func HandleWriteFile(w http.ResponseWriter, r *http.Request) { + defer func() { + r := recover() + if r == nil { + return + } + log.Printf("[error] in write-file: %v\n", r) + debug.PrintStack() + WriteJsonError(w, fmt.Errorf("panic: %v", r)) + return + }() + w.Header().Set("Cache-Control", "no-cache") + params, mpFile, err := parseWriteFileParams(r) + if err != nil { + WriteJsonError(w, fmt.Errorf("error parsing multipart form params: %w", err)) + return + } + if params.ScreenId == "" || params.LineId == "" || params.Path == "" { + WriteJsonError(w, fmt.Errorf("invalid params, must set screenid, lineid, and path")) + return + } + if _, err := uuid.Parse(params.ScreenId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid screenid: %v", err)) + return + } + if _, err := uuid.Parse(params.LineId); err != nil { + WriteJsonError(w, fmt.Errorf("invalid lineid: %v", err)) + return + } + _, cmd, err := sstore.GetLineCmdByLineId(r.Context(), params.ScreenId, params.LineId) + if err != nil { + WriteJsonError(w, fmt.Errorf("cannot retrieve line/cmd: %v", err)) + return + } + if cmd == nil { + WriteJsonError(w, fmt.Errorf("line not found")) + return + } + if cmd.Remote.RemoteId == "" { + WriteJsonError(w, fmt.Errorf("invalid line, no remote")) + return + } + msh := remote.GetRemoteById(cmd.Remote.RemoteId) + if msh == nil { + WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote")) + return + } + cwd := cmd.FeState["cwd"] + writePk := packet.MakeWriteFilePacket() + writePk.ReqId = uuid.New().String() + writePk.UseTemp = params.UseTemp + if filepath.IsAbs(params.Path) { + writePk.Path = params.Path + } else { + writePk.Path = filepath.Join(cwd, params.Path) + } + iter, err := msh.PacketRpcIter(r.Context(), writePk) + if err != nil { + WriteJsonError(w, fmt.Errorf("error: %v", err)) + return + } + // first packet should be WriteFileReady + readyIf, err := iter.Next(r.Context()) + if err != nil { + WriteJsonError(w, fmt.Errorf("error while getting ready response: %w", err)) + return + } + readyPk, ok := readyIf.(*packet.WriteFileReadyPacketType) + if !ok { + WriteJsonError(w, fmt.Errorf("bad ready packet received: %T", readyIf)) + return + } + if readyPk.Error != "" { + WriteJsonError(w, fmt.Errorf("ready error: %s", readyPk.Error)) + return + } + var buffer [server.MaxFileDataPacketSize]byte + bufSlice := buffer[:] + for { + dataPk := packet.MakeFileDataPacket(writePk.ReqId) + nr, err := io.ReadFull(mpFile, bufSlice) + if err == io.ErrUnexpectedEOF || err == io.EOF { + dataPk.Eof = true + } else if err != nil { + dataErr := fmt.Errorf("error reading file data: %v", err) + dataPk.Error = dataErr.Error() + msh.SendFileData(dataPk) + WriteJsonError(w, dataErr) + return + } + if nr > 0 { + dataPk.Data = make([]byte, nr) + copy(dataPk.Data, bufSlice[0:nr]) + } + msh.SendFileData(dataPk) + if dataPk.Eof { + break + } + // slight throttle for sending packets + time.Sleep(10 * time.Millisecond) + } + doneIf, err := iter.Next(r.Context()) + if err != nil { + WriteJsonError(w, fmt.Errorf("error while getting done response: %w", err)) + return + } + donePk, ok := doneIf.(*packet.WriteFileDonePacketType) + if !ok { + WriteJsonError(w, fmt.Errorf("bad done packet received: %T", doneIf)) + return + } + if donePk.Error != "" { + WriteJsonError(w, fmt.Errorf("dne error: %s", donePk.Error)) + return + } + WriteJsonSuccess(w, nil) + return +} + +func HandleReadFile(w http.ResponseWriter, r *http.Request) { + qvals := r.URL.Query() + screenId := qvals.Get("screenid") + lineId := qvals.Get("lineid") + path := qvals.Get("path") // validate path? + contentType := qvals.Get("mimetype") + if contentType == "" { + contentType = "application/octet-stream" + } + if screenId == "" || lineId == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("must specify sessionid, screenid, and lineid"))) + return + } + if path == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("must specify path"))) + return + } + if _, err := uuid.Parse(screenId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid screenid: %v", err))) + return + } + if _, err := uuid.Parse(lineId); err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid lineid: %v", err))) + return + } + if !ContentTypeHeaderValidRe.MatchString(contentType) { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid mimetype specified"))) + return + } + _, cmd, err := sstore.GetLineCmdByLineId(r.Context(), screenId, lineId) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid lineid: %v", err))) + return + } + if cmd == nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid line, no cmd"))) + return + } + if cmd.Remote.RemoteId == "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid line, no remote"))) + return + } + streamPk := packet.MakeStreamFilePacket() + streamPk.ReqId = uuid.New().String() + cwd := cmd.FeState["cwd"] + if filepath.IsAbs(path) { + streamPk.Path = path + } else { + streamPk.Path = filepath.Join(cwd, path) + } + msh := remote.GetRemoteById(cmd.Remote.RemoteId) + if msh == nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("invalid line, cannot resolve remote"))) + return + } + iter, err := msh.StreamFile(r.Context(), streamPk) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error trying to stream file: %v", err))) + return + } + defer iter.Close() + respIf, err := iter.Next(r.Context()) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error getting streamfile response: %v", err))) + return + } + resp, ok := respIf.(*packet.StreamFileResponseType) + if !ok { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("bad response packet type: %T", respIf))) + return + } + if resp.Error != "" { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error response: %s", resp.Error))) + return + } + infoJson, _ := json.Marshal(resp.Info) + w.Header().Set("X-FileInfo", base64.StdEncoding.EncodeToString(infoJson)) + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + for { + dataPkIf, err := iter.Next(r.Context()) + if err != nil { + log.Printf("error in read-file while getting data: %v\n", err) + break + } + if dataPkIf == nil { + break + } + dataPk, ok := dataPkIf.(*packet.FileDataPacketType) + if !ok { + log.Printf("error in read-file, invalid data packet type: %T", dataPkIf) + break + } + if dataPk.Error != "" { + log.Printf("in read-file, data packet error: %s", dataPk.Error) + break + } + w.Write(dataPk.Data) + } + return +} + func WriteJsonError(w http.ResponseWriter, errVal error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) @@ -576,6 +853,8 @@ func main() { gr.HandleFunc("/api/get-client-data", AuthKeyWrap(HandleGetClientData)) gr.HandleFunc("/api/set-winsize", AuthKeyWrap(HandleSetWinSize)) gr.HandleFunc("/api/log-active-state", AuthKeyWrap(HandleLogActiveState)) + gr.HandleFunc("/api/read-file", AuthKeyWrap(HandleReadFile)) + gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST") serverAddr := MainServerAddr if scbase.IsDevMode() { serverAddr = MainServerDevAddr diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 6d89f4874..805546c65 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -6,9 +6,11 @@ import ( "crypto/rand" "encoding/base64" "fmt" + "io/fs" "log" "net/url" "os" + "path/filepath" "regexp" "sort" "strconv" @@ -56,6 +58,8 @@ const MaxEvalDepth = 5 const MaxOpenAIAPITokenLen = 100 const MaxOpenAIModelLen = 100 +const TsFormatStr = "2006-01-02 15:04:05" + const ( KwArgRenderer = "renderer" KwArgView = "view" @@ -205,6 +209,11 @@ func init() { registerCmdFn("_killserver", KillServerCommand) registerCmdFn("set", SetCommand) + + registerCmdFn("view:stat", ViewStatCommand) + registerCmdFn("view:test", ViewTestCommand) + + registerCmdFn("edit:test", EditTestCommand) } func getValidCommands() []string { @@ -2115,7 +2124,7 @@ func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( if session.Archived { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archived", "true")) ts := time.UnixMilli(session.ArchivedTs) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archivedts", ts.Format("2006-01-02 15:04:05"))) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archivedts", ts.Format(TsFormatStr))) } stats, err := sstore.GetSessionStats(ctx, ids.SessionId) if err != nil { @@ -2936,6 +2945,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return nil, fmt.Errorf("line %q not found", lineArg) } var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "screenid", line.ScreenId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "lineid", line.LineId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", line.LineType)) lineNumStr := strconv.FormatInt(line.LineNum, 10) @@ -2944,7 +2954,7 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst } buf.WriteString(fmt.Sprintf(" %-15s %s\n", "linenum", lineNumStr)) ts := time.UnixMilli(line.Ts) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ts", ts.Format("2006-01-02 15:04:05"))) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ts", ts.Format(TsFormatStr))) if line.Ephemeral { buf.WriteString(fmt.Sprintf(" %-15s %v\n", "ephemeral", true)) } @@ -2974,6 +2984,12 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr)) } + if cmd.DoneTs != 0 { + doneTs := time.UnixMilli(cmd.DoneTs) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "donets", doneTs.Format(TsFormatStr))) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "exitcode", cmd.ExitCode)) + buf.WriteString(fmt.Sprintf(" %-15s %dms\n", "duration", cmd.DurationMs)) + } } update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ @@ -3010,6 +3026,205 @@ func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return nil, nil } +func makeStreamFilePk(ids resolvedIds, pk *scpacket.FeCommandPacketType) (*packet.StreamFilePacketType, error) { + cwd := ids.Remote.FeState["cwd"] + fileArg := pk.Args[0] + if fileArg == "" { + return nil, fmt.Errorf("/view:stat file argument must be set (cannot be empty)") + } + streamPk := packet.MakeStreamFilePacket() + streamPk.ReqId = uuid.New().String() + if filepath.IsAbs(fileArg) { + streamPk.Path = fileArg + } else { + streamPk.Path = filepath.Join(cwd, fileArg) + } + return streamPk, nil +} + +func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/view:stat requires an argument (file name)") + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + streamPk, err := makeStreamFilePk(ids, pk) + if err != nil { + return nil, err + } + streamPk.StatOnly = true + msh := ids.Remote.MShell + iter, err := msh.StreamFile(ctx, streamPk) + if err != nil { + return nil, fmt.Errorf("/view:stat error: %v", err) + } + defer iter.Close() + respIf, err := iter.Next(ctx) + if err != nil { + return nil, fmt.Errorf("/view:stat error getting response: %v", err) + } + resp, ok := respIf.(*packet.StreamFileResponseType) + if !ok { + return nil, fmt.Errorf("/view:stat error, bad response packet type: %T", respIf) + } + if resp.Error != "" { + return nil, fmt.Errorf("/view:stat error: %s", resp.Error) + } + if resp.Info == nil { + return nil, fmt.Errorf("/view:stat error, no file info") + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "path", resp.Info.Name)) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "size", resp.Info.Size)) + modTs := time.UnixMilli(resp.Info.ModTs) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "modts", modTs.Format(TsFormatStr))) + buf.WriteString(fmt.Sprintf(" %-15s %v\n", "isdir", resp.Info.IsDir)) + modeStr := fs.FileMode(resp.Info.Perm).String() + if len(modeStr) > 9 { + modeStr = modeStr[len(modeStr)-9:] + } + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "perms", modeStr)) + update := &sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("view stat %q", streamPk.Path), + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil +} + +func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/view:test requires an argument (file name)") + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + streamPk, err := makeStreamFilePk(ids, pk) + if err != nil { + return nil, err + } + msh := ids.Remote.MShell + iter, err := msh.StreamFile(ctx, streamPk) + if err != nil { + return nil, fmt.Errorf("/view:test error: %v", err) + } + defer iter.Close() + respIf, err := iter.Next(ctx) + if err != nil { + return nil, fmt.Errorf("/view:test error getting response: %v", err) + } + resp, ok := respIf.(*packet.StreamFileResponseType) + if !ok { + return nil, fmt.Errorf("/view:test error, bad response packet type: %T", respIf) + } + if resp.Error != "" { + return nil, fmt.Errorf("/view:test error: %s", resp.Error) + } + if resp.Info == nil { + return nil, fmt.Errorf("/view:test error, no file info") + } + var buf bytes.Buffer + var numPackets int + for { + dataPkIf, err := iter.Next(ctx) + if err != nil { + return nil, fmt.Errorf("/view:test error while getting data: %w", err) + } + if dataPkIf == nil { + break + } + dataPk, ok := dataPkIf.(*packet.FileDataPacketType) + if !ok { + return nil, fmt.Errorf("/view:test invalid data packet type: %T", dataPkIf) + } + if dataPk.Error != "" { + return nil, fmt.Errorf("/view:test error returned while getting data: %s", dataPk.Error) + } + numPackets++ + buf.Write(dataPk.Data) + } + buf.WriteString(fmt.Sprintf("\n\ntotal packets: %d\n", numPackets)) + update := &sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("view file %q", streamPk.Path), + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil +} + +func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/edit:test requires an argument (file name)") + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + content, ok := pk.Kwargs["content"] + if !ok { + return nil, fmt.Errorf("/edit:test no content for file specified") + } + fileArg := pk.Args[0] + if fileArg == "" { + return nil, fmt.Errorf("/view:stat file argument must be set (cannot be empty)") + } + writePk := packet.MakeWriteFilePacket() + writePk.ReqId = uuid.New().String() + writePk.UseTemp = true + cwd := ids.Remote.FeState["cwd"] + if filepath.IsAbs(fileArg) { + writePk.Path = fileArg + } else { + writePk.Path = filepath.Join(cwd, fileArg) + } + msh := ids.Remote.MShell + iter, err := msh.PacketRpcIter(ctx, writePk) + if err != nil { + return nil, fmt.Errorf("/edit:test error: %v", err) + } + // first packet should be WriteFileReady + readyIf, err := iter.Next(ctx) + if err != nil { + return nil, fmt.Errorf("/edit:test error while getting ready response: %w", err) + } + readyPk, ok := readyIf.(*packet.WriteFileReadyPacketType) + if !ok { + return nil, fmt.Errorf("/edit:test bad ready packet received: %T", readyIf) + } + if readyPk.Error != "" { + return nil, fmt.Errorf("/edit:test %s", readyPk.Error) + } + dataPk := packet.MakeFileDataPacket(writePk.ReqId) + dataPk.Data = []byte(content) + dataPk.Eof = true + err = msh.SendFileData(dataPk) + if err != nil { + return nil, fmt.Errorf("/edit:test error sending data packet: %v", err) + } + doneIf, err := iter.Next(ctx) + if err != nil { + return nil, fmt.Errorf("/edit:test error while getting done response: %w", err) + } + donePk, ok := doneIf.(*packet.WriteFileDonePacketType) + if !ok { + return nil, fmt.Errorf("/edit:test bad done packet received: %T", doneIf) + } + if donePk.Error != "" { + return nil, fmt.Errorf("/edit:test %s", donePk.Error) + } + update := &sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("edit test, wrote %q", writePk.Path), + }, + } + return update, nil +} + func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { diff --git a/pkg/cmdrunner/resolver.go b/pkg/cmdrunner/resolver.go index 64c264fde..7142a369b 100644 --- a/pkg/cmdrunner/resolver.go +++ b/pkg/cmdrunner/resolver.go @@ -8,10 +8,10 @@ import ( "strconv" "strings" - "github.com/google/uuid" "github.com/commandlinedev/prompt-server/pkg/remote" "github.com/commandlinedev/prompt-server/pkg/scpacket" "github.com/commandlinedev/prompt-server/pkg/sstore" + "github.com/google/uuid" ) const ( @@ -242,7 +242,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if err != nil { return rtn, fmt.Errorf("invalid resolved remote: %v", err) } - rr, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) + rr, err := ResolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) if err != nil { return rtn, err } @@ -263,7 +263,7 @@ func resolveUiIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype i if err != nil { return rtn, fmt.Errorf("error trying to auto-connect remote [%s]: %w", rtn.Remote.DisplayName, err) } - rrNew, err := resolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) + rrNew, err := ResolveRemoteFromPtr(ctx, rptr, rtn.SessionId, rtn.ScreenId) if err != nil { return rtn, err } @@ -450,7 +450,7 @@ func parseFullRemoteRef(fullRemoteRef string) (string, string, string, error) { return fields[0], fields[1], fields[2], nil } -func resolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, screenId string) (*ResolvedRemote, error) { +func ResolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessionId string, screenId string) (*ResolvedRemote, error) { if rptr == nil || rptr.RemoteId == "" { return nil, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 326b8b919..5fcdb4098 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -1119,6 +1119,10 @@ func (msh *MShellProc) ReInit(ctx context.Context) (*packet.InitPacketType, erro return initPk, nil } +func (msh *MShellProc) StreamFile(ctx context.Context, streamPk *packet.StreamFilePacketType) (*packet.RpcResponseIter, error) { + return msh.PacketRpcIter(ctx, streamPk) +} + func addScVarsToState(state *packet.ShellState) *packet.ShellState { if state == nil { return nil @@ -1374,6 +1378,13 @@ func (msh *MShellProc) SendSpecialInput(siPk *packet.SpecialInputPacketType) err return msh.ServerProc.Input.SendPacket(siPk) } +func (msh *MShellProc) SendFileData(dataPk *packet.FileDataPacketType) error { + if !msh.IsConnected() { + return fmt.Errorf("remote is not connected, cannot send input") + } + return msh.ServerProc.Input.SendPacket(dataPk) +} + func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts { return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: true, MaxPtySize: DefaultMaxPtySize} } @@ -1577,9 +1588,25 @@ func (msh *MShellProc) RemoveRunningCmd(ck base.CommandKey) { } } +func (msh *MShellProc) PacketRpcIter(ctx context.Context, pk packet.RpcPacketType) (*packet.RpcResponseIter, error) { + if !msh.IsConnected() { + return nil, fmt.Errorf("remote is not connected") + } + if pk == nil { + return nil, fmt.Errorf("PacketRpc passed nil packet") + } + reqId := pk.GetReqId() + msh.ServerProc.Output.RegisterRpc(reqId) + err := msh.ServerProc.Input.SendPacketCtx(ctx, pk) + if err != nil { + return nil, err + } + return msh.ServerProc.Output.GetResponseIter(reqId), nil +} + func (msh *MShellProc) PacketRpcRaw(ctx context.Context, pk packet.RpcPacketType) (packet.RpcResponsePacketType, error) { if !msh.IsConnected() { - return nil, fmt.Errorf("runner is not connected") + return nil, fmt.Errorf("remote is not connected") } if pk == nil { return nil, fmt.Errorf("PacketRpc passed nil packet") @@ -1812,6 +1839,7 @@ func (msh *MShellProc) ProcessPackets() { go sendScreenUpdates(screens) } }) + // TODO need to clean dataPosMap dataPosMap := make(map[base.CommandKey]int64) for pk := range msh.ServerProc.Output.MainCh { if pk.GetType() == packet.DataPacketStr { diff --git a/pkg/utilfn/utilfn.go b/pkg/utilfn/utilfn.go index 0decc6eb7..5b3e70b1e 100644 --- a/pkg/utilfn/utilfn.go +++ b/pkg/utilfn/utilfn.go @@ -193,3 +193,16 @@ func Sha1Hash(data []byte) string { hval := base64.StdEncoding.EncodeToString(hvalRaw[:]) return hval } + +func ChunkSlice[T any](s []T, chunkSize int) [][]T { + var rtn [][]T + for len(rtn) > 0 { + if len(s) <= chunkSize { + rtn = append(rtn, s) + break + } + rtn = append(rtn, s[:chunkSize]) + s = s[chunkSize:] + } + return rtn +} From 5d89e0cfefc4294e0572e4211b90c1b42895b49f Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Sep 2023 15:21:35 -0700 Subject: [PATCH 381/397] add line.linestate to DB. switch linetype to a dbmappable. update line sql queries. add state to /line:set command. bump migration (#2) --- db/migrations/000021_linestate.down.sql | 1 + db/migrations/000021_linestate.up.sql | 1 + db/schema.sql | 63 ++++++++++++++--------- pkg/cmdrunner/cmdrunner.go | 27 +++++++++- pkg/dbutil/map.go | 2 +- pkg/pcloud/pcloud.go | 3 ++ pkg/sstore/dbops.go | 68 +++++++++++++------------ pkg/sstore/migrate.go | 2 +- pkg/sstore/sstore.go | 35 +++++++------ 9 files changed, 126 insertions(+), 76 deletions(-) create mode 100644 db/migrations/000021_linestate.down.sql create mode 100644 db/migrations/000021_linestate.up.sql diff --git a/db/migrations/000021_linestate.down.sql b/db/migrations/000021_linestate.down.sql new file mode 100644 index 000000000..064dfbf6f --- /dev/null +++ b/db/migrations/000021_linestate.down.sql @@ -0,0 +1 @@ +ALTER TABLE line DROP COLUMN linestate; diff --git a/db/migrations/000021_linestate.up.sql b/db/migrations/000021_linestate.up.sql new file mode 100644 index 000000000..f53e0b510 --- /dev/null +++ b/db/migrations/000021_linestate.up.sql @@ -0,0 +1 @@ +ALTER TABLE line ADD COLUMN linestate json NOT NULL DEFAULT '{}'; diff --git a/db/schema.sql b/db/schema.sql index dd4450148..a2a8d9cd0 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -68,7 +68,6 @@ CREATE TABLE history ( remoteid varchar(36) NOT NULL, remotename varchar(50) NOT NULL, haderror boolean NOT NULL, - cmdid varchar(36) NOT NULL, cmdstr text NOT NULL, ismetacmd boolean, incognito boolean @@ -160,36 +159,13 @@ CREATE TABLE IF NOT EXISTS "line" ( linetype varchar(10) NOT NULL, linelocal boolean NOT NULL, text text NOT NULL, - cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, archived boolean NOT NULL, - renderer varchar(50) NOT NULL, + renderer varchar(50) NOT NULL, linestate json NOT NULL DEFAULT '{}', PRIMARY KEY (screenid, lineid) ); -CREATE TABLE IF NOT EXISTS "cmd" ( - screenid varchar(36) NOT NULL, - cmdid varchar(36) NOT NULL, - remoteownerid varchar(36) NOT NULL, - remoteid varchar(36) NOT NULL, - remotename varchar(50) NOT NULL, - cmdstr text NOT NULL, - rawcmdstr text NOT NULL, - festate json NOT NULL, - statebasehash varchar(36) NOT NULL, - statediffhasharr json NOT NULL, - termopts json NOT NULL, - origtermopts json NOT NULL, - status varchar(10) NOT NULL, - startpk json NOT NULL, - doneinfo json NOT NULL, - runout json NOT NULL, - rtnstate boolean NOT NULL, - rtnbasehash varchar(36) NOT NULL, - rtndiffhasharr json NOT NULL, - PRIMARY KEY (screenid, cmdid) -); CREATE TABLE screenupdate ( updateid integer PRIMARY KEY, screenid varchar(36) NOT NULL, @@ -204,3 +180,40 @@ 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, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + cmdstr text NOT NULL, + rawcmdstr text NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, + termopts json NOT NULL, + origtermopts json NOT NULL, + status varchar(10) NOT NULL, + cmdpid int NOT NULL, + remotepid int NOT NULL, + donets bigint NOT NULL, + exitcode int NOT NULL, + durationms int NOT NULL, + rtnstate boolean NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, + runout json NOT NULL, + PRIMARY KEY (screenid, lineid) +); +CREATE TABLE cmd_migrate20 ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (screenid, lineid) +); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 805546c65..49057e033 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "encoding/json" "fmt" "io/fs" "log" @@ -23,6 +24,7 @@ import ( "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" "github.com/commandlinedev/prompt-server/pkg/comp" + "github.com/commandlinedev/prompt-server/pkg/dbutil" "github.com/commandlinedev/prompt-server/pkg/pcloud" "github.com/commandlinedev/prompt-server/pkg/remote" "github.com/commandlinedev/prompt-server/pkg/remote/openai" @@ -63,6 +65,7 @@ const TsFormatStr = "2006-01-02 15:04:05" const ( KwArgRenderer = "renderer" KwArgView = "view" + KwArgState = "state" ) var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -2604,8 +2607,23 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } varsUpdated = append(varsUpdated, KwArgView) } + if stateJson, found := pk.Kwargs[KwArgState]; found { + if len(stateJson) > sstore.MaxLineStateSize { + return nil, fmt.Errorf("invalid state value (too large), size[%d], max[%d]", len(stateJson), sstore.MaxLineStateSize) + } + var stateMap map[string]any + err = json.Unmarshal([]byte(stateJson), &stateMap) + if err != nil { + return nil, fmt.Errorf("invalid state value, cannot parse json: %v", err) + } + err = sstore.UpdateLineState(ctx, ids.ScreenId, lineId, stateMap) + if err != nil { + return nil, fmt.Errorf("cannot update linestate: %v", err) + } + varsUpdated = append(varsUpdated, KwArgState) + } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView}, "or", false)) + return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView, KwArgState}, "or", false)) } updatedLine, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { @@ -2836,7 +2854,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if starVal > 5 { return nil, fmt.Errorf("/line:star invalid star-value must be in the range of 0-5") } - err = sstore.UpdateLineStar(ctx, lineId, starVal) + err = sstore.UpdateLineStar(ctx, ids.ScreenId, lineId, starVal) if err != nil { return nil, fmt.Errorf("/line:star error updating star value: %v", err) } @@ -2991,6 +3009,11 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %dms\n", "duration", cmd.DurationMs)) } } + stateStr := dbutil.QuickJson(line.LineState) + if len(stateStr) > 80 { + stateStr = stateStr[0:77] + "..." + } + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "state", stateStr)) update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("line %d info", line.LineNum), diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 76d1a09cf..dffe922b4 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -58,11 +58,11 @@ func GetMapGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string, ar } func GetMappable[PT DBMappablePtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) PT { - rtn := PT(new(T)) m := tx.GetMap(query, args...) if len(m) == 0 { return nil } + rtn := PT(new(T)) FromDBMap(rtn, m) return rtn } diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 5e8f1e4b4..89b69802c 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -339,6 +339,9 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data, Eof: true} } + case sstore.UpdateType_LineState: + // TODO implement! + default: return nil, fmt.Errorf("unsupported update type (pcloud/makeWebScreenUpdate): %s\n", update.UpdateType) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5c785282c..1cb11dbdb 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -471,7 +471,7 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, return nil, nil } query = `SELECT * FROM line WHERE screenid = ? ORDER BY linenum` - tx.Select(&screen.Lines, query, screen.ScreenId) + screen.Lines = dbutil.SelectMappable[*LineType](tx, query, screen.ScreenId) query = `SELECT * FROM cmd WHERE screenid = ?` screen.Cmds = dbutil.SelectMapsGen[*CmdType](tx, query, screen.ScreenId) return screen, nil @@ -761,16 +761,15 @@ func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (stri func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { - var lineVal LineType query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` - found := tx.Get(&lineVal, query, screenId, lineId) - if !found { + lineVal := dbutil.GetMappable[*LineType](tx, query, screenId, lineId) + if lineVal == nil { return nil, nil, nil } var cmdRtn *CmdType query = `SELECT * FROM cmd WHERE screenid = ? AND lineid = ?` cmdRtn = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId) - return &lineVal, cmdRtn, nil + return lineVal, cmdRtn, nil }) } @@ -795,9 +794,9 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM screen WHERE screenid = ?` nextLineNum := tx.GetInt(query, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, renderer, ephemeral, contentheight, star, archived) - VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:renderer,:ephemeral,:contentheight,:star,:archived)` - tx.NamedExec(query, line) + query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, linestate, text, renderer, ephemeral, contentheight, star, archived) + VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:linestate,:text,:renderer,:ephemeral,:contentheight,:star,:archived)` + tx.NamedExec(query, dbutil.ToDBMap(line, false)) query = `UPDATE screen SET nextlinenum = ? WHERE screenid = ?` tx.Exec(query, nextLineNum+1, line.ScreenId) if cmd != nil { @@ -1889,10 +1888,10 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, return state, nil } -func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { +func UpdateLineStar(ctx context.Context, screenId string, lineId string, starVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET star = ? WHERE lineid = ?` - tx.Exec(query, starVal, lineId) + query := `UPDATE line SET star = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, starVal, screenId, lineId) return nil }) if txErr != nil { @@ -1903,8 +1902,8 @@ func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { func UpdateLineHeight(ctx context.Context, screenId string, lineId string, heightVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET contentheight = ? WHERE lineid = ?` - tx.Exec(query, heightVal, lineId) + query := `UPDATE line SET contentheight = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, heightVal, screenId, lineId) if isWebShare(tx, screenId) { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineContentHeight) } @@ -1918,8 +1917,8 @@ func UpdateLineHeight(ctx context.Context, screenId string, lineId string, heigh func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, renderer string) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET renderer = ? WHERE lineid = ?` - tx.Exec(query, renderer, lineId) + query := `UPDATE line SET renderer = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, renderer, screenId, lineId) if isWebShare(tx, screenId) { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineRenderer) } @@ -1927,28 +1926,34 @@ func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, ren }) } -// can return nil, nil if line is not found -func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType, error) { - var rtn *LineType - txErr := WithTx(ctx, func(tx *TxWrap) error { - var line LineType - query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` - found := tx.Get(&line, query, screenId, lineId) - if found { - rtn = &line +func UpdateLineState(ctx context.Context, screenId string, lineId string, lineState map[string]any) error { + qjs := dbutil.QuickJson(lineState) + if len(qjs) > MaxLineStateSize { + return fmt.Errorf("linestate for line[%s:%s] exceeds maxsize, size[%d] max[%d]", screenId, lineId, len(qjs), MaxLineStateSize) + } + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET linestate = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, qjs, screenId, lineId) + if isWebShare(tx, screenId) { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineState) } return nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil +} + +// can return nil, nil if line is not found +func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*LineType, error) { + query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` + line := dbutil.GetMappable[*LineType](tx, query, screenId, lineId) + return line, nil + }) } func SetLineArchivedById(ctx context.Context, screenId string, lineId string, archived bool) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET archived = ? WHERE lineid = ?` - tx.Exec(query, archived, lineId) + query := `UPDATE line SET archived = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, archived, screenId, lineId) if isWebShare(tx, screenId) { if archived { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel) @@ -2395,10 +2400,9 @@ func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryIte return nil, nil, nil } return WithTxRtn3(ctx, func(tx *TxWrap) ([]*LineType, []*CmdType, error) { - var lineArr []*LineType lineIdsJsonArr := quickJsonArr(getLineIdsFromHistoryItems(historyItems)) query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` - tx.Select(&lineArr, query, lineIdsJsonArr) + lineArr := dbutil.SelectMappable[*LineType](tx, query, lineIdsJsonArr) query = `SELECT * FROM cmd WHERE lineid IN (SELECT value FROM json_each(?))` cmdArr := dbutil.SelectMapsGen[*CmdType](tx, query, lineIdsJsonArr) return lineArr, cmdArr, nil diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 91469b92c..ca09e5053 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 20 +const MaxMigration = 21 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 const CmdLineSpecialMigration = 20 diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 47a4a1640..216d3a5d4 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -34,6 +34,7 @@ const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const MaxWebShareLineCount = 50 const MaxWebShareScreenCount = 3 +const MaxLineStateSize = 4 * 1024 // 4k for now, can raise if needed const DefaultSessionName = "default" const LocalRemoteAlias = "local" @@ -113,6 +114,7 @@ const ( UpdateType_LineDel = "line:del" UpdateType_LineRenderer = "line:renderer" UpdateType_LineContentHeight = "line:contentheight" + UpdateType_LineState = "line:state" UpdateType_CmdStatus = "cmd:status" UpdateType_CmdTermOpts = "cmd:termopts" UpdateType_CmdExitCode = "cmd:exitcode" @@ -707,23 +709,26 @@ type ScreenUpdateType struct { func (ScreenUpdateType) UseDBMap() {} type LineType struct { - ScreenId string `json:"screenid"` - UserId string `json:"userid"` - LineId string `json:"lineid"` - Ts int64 `json:"ts"` - LineNum int64 `json:"linenum"` - LineNumTemp bool `json:"linenumtemp,omitempty"` - LineLocal bool `json:"linelocal"` - LineType string `json:"linetype"` - Renderer string `json:"renderer,omitempty"` - Text string `json:"text,omitempty"` - Ephemeral bool `json:"ephemeral,omitempty"` - ContentHeight int64 `json:"contentheight,omitempty"` - Star bool `json:"star,omitempty"` - Archived bool `json:"archived,omitempty"` - Remove bool `json:"remove,omitempty"` + ScreenId string `json:"screenid"` + UserId string `json:"userid"` + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineNumTemp bool `json:"linenumtemp,omitempty"` + LineLocal bool `json:"linelocal"` + LineType string `json:"linetype"` + LineState map[string]any `json:"linestate"` + Renderer string `json:"renderer,omitempty"` + Text string `json:"text,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + ContentHeight int64 `json:"contentheight,omitempty"` + Star bool `json:"star,omitempty"` + Archived bool `json:"archived,omitempty"` + Remove bool `json:"remove,omitempty"` } +func (LineType) UseDBMap() {} + type OpenAIUsage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` From fea5f3306e63706863a0dfef946fda46100cb69f Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Sep 2023 16:38:54 -0700 Subject: [PATCH 382/397] add codeedit command (testing) --- pkg/cmdrunner/cmdrunner.go | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 49057e033..e00e52e39 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -217,6 +217,8 @@ func init() { registerCmdFn("view:test", ViewTestCommand) registerCmdFn("edit:test", EditTestCommand) + + registerCmdFn("codeedit", CodeEditCommand) } func getValidCommands() []string { @@ -3180,6 +3182,43 @@ func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return update, nil } +func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("/codeedit requires an argument (file name)") + } + // TODO more error checking on filename format? + if pk.Args[0] == "" { + return nil, fmt.Errorf("/codeedit argument cannot be empty") + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + outputStr := fmt.Sprintf("codeedit %q", pk.Args[0]) + cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "code") + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + // set the line state + // TODO turn these strings into constants + lineState := make(map[string]any) + lineState["prompt:source"] = "file" + lineState["prompt:file"] = pk.Args[0] + err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) + if err != nil { + return nil, fmt.Errorf("/codeedit error updating line state: %v", err) + } + update.Line.LineState = lineState + update.Interactive = pk.Interactive + return update, nil +} + func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("/edit:test requires an argument (file name)") From 9785aaac199ec70d3c88974f13ecdeeccef891f4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Sep 2023 16:45:01 -0700 Subject: [PATCH 383/397] set linestate --- pkg/sstore/sstore.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 216d3a5d4..b9994a9b1 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -1069,6 +1069,7 @@ func makeNewLineCmd(screenId string, userId string, lineId string, renderer stri rtn.LineId = lineId rtn.ContentHeight = LineNoHeight rtn.Renderer = renderer + rtn.LineState = make(map[string]any) return rtn } @@ -1082,6 +1083,7 @@ func makeNewLineText(screenId string, userId string, text string) *LineType { rtn.LineType = LineTypeText rtn.Text = text rtn.ContentHeight = LineNoHeight + rtn.LineState = make(map[string]any) return rtn } @@ -1095,6 +1097,7 @@ func makeNewLineOpenAI(screenId string, userId string, lineId string) *LineType rtn.LineType = LineTypeOpenAI rtn.ContentHeight = LineNoHeight rtn.Renderer = CmdRendererOpenAI + rtn.LineState = make(map[string]any) return rtn } From d84dd75d94ce6094e79ca74dd290d4837532d655 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 1 Sep 2023 20:40:28 -0700 Subject: [PATCH 384/397] resolve ~ in read/write file api --- cmd/main-server.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 7202f9042..69ebfb1c3 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -402,14 +402,20 @@ func HandleWriteFile(w http.ResponseWriter, r *http.Request) { WriteJsonError(w, fmt.Errorf("invalid line, cannot resolve remote")) return } + rrState := msh.GetRemoteRuntimeState() + fullPath, err := rrState.ExpandHomeDir(params.Path) + if err != nil { + WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err)) + return + } cwd := cmd.FeState["cwd"] writePk := packet.MakeWriteFilePacket() writePk.ReqId = uuid.New().String() writePk.UseTemp = params.UseTemp - if filepath.IsAbs(params.Path) { - writePk.Path = params.Path + if filepath.IsAbs(fullPath) { + writePk.Path = fullPath } else { - writePk.Path = filepath.Join(cwd, params.Path) + writePk.Path = filepath.Join(cwd, fullPath) } iter, err := msh.PacketRpcIter(r.Context(), writePk) if err != nil { @@ -524,20 +530,26 @@ func HandleReadFile(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("invalid line, no remote"))) return } - streamPk := packet.MakeStreamFilePacket() - streamPk.ReqId = uuid.New().String() - cwd := cmd.FeState["cwd"] - if filepath.IsAbs(path) { - streamPk.Path = path - } else { - streamPk.Path = filepath.Join(cwd, path) - } msh := remote.GetRemoteById(cmd.Remote.RemoteId) if msh == nil { w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("invalid line, cannot resolve remote"))) return } + rrState := msh.GetRemoteRuntimeState() + fullPath, err := rrState.ExpandHomeDir(path) + if err != nil { + WriteJsonError(w, fmt.Errorf("error expanding homedir: %v", err)) + return + } + streamPk := packet.MakeStreamFilePacket() + streamPk.ReqId = uuid.New().String() + cwd := cmd.FeState["cwd"] + if filepath.IsAbs(fullPath) { + streamPk.Path = fullPath + } else { + streamPk.Path = filepath.Join(cwd, fullPath) + } iter, err := msh.StreamFile(r.Context(), streamPk) if err != nil { w.WriteHeader(500) From 1d77b2d5d642f6d602454928674cfce27269878f Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 5 Sep 2023 22:09:24 -0700 Subject: [PATCH 385/397] add codeview command --- pkg/cmdrunner/cmdrunner.go | 19 +++++++++++++++---- pkg/cmdrunner/shparse.go | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index e00e52e39..3a2504bc1 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -218,7 +218,9 @@ func init() { registerCmdFn("edit:test", EditTestCommand) + // CodeEditCommand is overloaded to do codeedit and codeview registerCmdFn("codeedit", CodeEditCommand) + registerCmdFn("codeview", CodeEditCommand) } func getValidCommands() []string { @@ -3184,17 +3186,17 @@ func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { - return nil, fmt.Errorf("/codeedit requires an argument (file name)") + return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk)) } // TODO more error checking on filename format? if pk.Args[0] == "" { - return nil, fmt.Errorf("/codeedit argument cannot be empty") + return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk)) } ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { return nil, err } - outputStr := fmt.Sprintf("codeedit %q", pk.Args[0]) + outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0]) cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) if err != nil { // TODO tricky error since the command was a success, but we can't show the output @@ -3210,9 +3212,18 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst lineState := make(map[string]any) lineState["prompt:source"] = "file" lineState["prompt:file"] = pk.Args[0] + if GetCmdStr(pk) == "codeview" { + lineState["mode"] = "view" + } else { + lineState["mode"] = "edit" + } + // TODO better error checking for lang + if pk.Kwargs["lang"] != "" && len(pk.Kwargs["lang"]) <= 50 { + lineState["lang"] = pk.Kwargs["lang"] + } err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) if err != nil { - return nil, fmt.Errorf("/codeedit error updating line state: %v", err) + return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) } update.Line.LineState = lineState update.Interactive = pk.Interactive diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 8ca9853f4..d21e4e561 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -26,6 +26,8 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"connect", "cr"}, BareMetaCmdDecl{"clear", "clear"}, BareMetaCmdDecl{"reset", "reset"}, + BareMetaCmdDecl{"codeedit", "codeedit"}, + BareMetaCmdDecl{"codeview", "codeview"}, } const ( From 8cb3bd49b564c20540c82ebacabafdce8383c0c1 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 6 Sep 2023 19:55:52 -0700 Subject: [PATCH 386/397] bump prompt-server version to v0.3.0 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index b234ca480..a8ff7d4b9 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.2.3" +const PromptVersion = "v0.3.0" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.2.0" From 1127d2cfbd07f27140c809eb6aa496a774ba5d75 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 6 Sep 2023 21:50:38 -0700 Subject: [PATCH 387/397] bump mshell version to v0.3.0 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index a8ff7d4b9..85b0ce28e 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -34,7 +34,7 @@ const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" const PromptVersion = "v0.3.0" const PromptAuthKeyFileName = "prompt.authkey" -const MShellVersion = "v0.2.0" +const MShellVersion = "v0.3.0" var SessionDirCache = make(map[string]string) var ScreenDirCache = make(map[string]string) From 0df800a3902011ef7583f15016d54795024ea86b Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 15 Sep 2023 12:57:47 -0700 Subject: [PATCH 388/397] for codeedit, set the focus to the line (not the input) --- pkg/cmdrunner/cmdrunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3a2504bc1..b30890248 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3202,7 +3202,7 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "code") + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "code") if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err From 54c2087ad69892920211a1ce4652aadd01a887ee Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 15 Sep 2023 13:48:58 -0700 Subject: [PATCH 389/397] remove websharing backend, migrate db to stop websharing any screens that are currently shared --- db/migrations/000022_endwebshare.down.sql | 1 + db/migrations/000022_endwebshare.up.sql | 1 + pkg/cmdrunner/cmdrunner.go | 80 +---------------------- pkg/sstore/migrate.go | 2 +- 4 files changed, 4 insertions(+), 80 deletions(-) create mode 100644 db/migrations/000022_endwebshare.down.sql create mode 100644 db/migrations/000022_endwebshare.up.sql diff --git a/db/migrations/000022_endwebshare.down.sql b/db/migrations/000022_endwebshare.down.sql new file mode 100644 index 000000000..b4eaa2125 --- /dev/null +++ b/db/migrations/000022_endwebshare.down.sql @@ -0,0 +1 @@ +-- no down migration diff --git a/db/migrations/000022_endwebshare.up.sql b/db/migrations/000022_endwebshare.up.sql new file mode 100644 index 000000000..6e815154b --- /dev/null +++ b/db/migrations/000022_endwebshare.up.sql @@ -0,0 +1 @@ +UPDATE screen SET sharemode = 'local' AND webshareopts = 'null'; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index b30890248..7fba1f596 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -3,8 +3,6 @@ package cmdrunner import ( "bytes" "context" - "crypto/rand" - "encoding/base64" "encoding/json" "fmt" "io/fs" @@ -1956,83 +1954,7 @@ func makeExternLink(urlStr string) string { } func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveUiIds(ctx, pk, R_Screen) - if err != nil { - return nil, err - } - shouldShare := true - if len(pk.Args) > 0 { - shouldShare = resolveBool(pk.Args[0], true) - } - shareName := pk.Kwargs["sharename"] - if err := validateShareName(shareName); err != nil { - return nil, err - } - var infoMsg string - var infoWebShareLink bool - if shouldShare { - viewKeyBytes := make([]byte, 9) - _, err = rand.Read(viewKeyBytes) - if err != nil { - return nil, fmt.Errorf("cannot create viewkey: %v", err) - } - screen, err := sstore.GetScreenById(ctx, ids.ScreenId) - if err != nil { - return nil, fmt.Errorf("cannot get screen: %v", err) - } - if shareName == "" { - shareName = screen.Name - } - viewKey := base64.RawURLEncoding.EncodeToString(viewKeyBytes) - webShareOpts := sstore.ScreenWebShareOpts{ShareName: shareName, ViewKey: viewKey} - err = sstore.CanScreenWebShare(ctx, screen) - if err != nil { - return nil, err - } - webUpdate := pcloud.MakeScreenNewUpdate(screen, webShareOpts) - err = pcloud.DoSyncWebUpdate(webUpdate) - if err != nil { - return nil, fmt.Errorf("error starting webshare, error contacting prompt cloud server: %v", err) - } - err = sstore.ScreenWebShareStart(ctx, ids.ScreenId, webShareOpts) - if err != nil { - return nil, fmt.Errorf("cannot webshare screen, error updating: %v", err) - } - infoWebShareLink = true - } else { - screen, err := sstore.GetScreenById(ctx, ids.ScreenId) - if err != nil { - return nil, fmt.Errorf("cannot get screen: %v", err) - } - if screen == nil { - return nil, fmt.Errorf("screen not found") - } - if screen.ShareMode != sstore.ShareModeWeb { - return nil, fmt.Errorf("screen is not currently shared") - } - webUpdate := pcloud.MakeScreenDelUpdate(screen, ids.ScreenId) - err = pcloud.DoSyncWebUpdate(webUpdate) - if err != nil { - return nil, fmt.Errorf("error stopping webshare, error contacting prompt cloud server: %v", err) - } - err = sstore.ScreenWebShareStop(ctx, ids.ScreenId) - if err != nil { - return nil, fmt.Errorf("cannot stop web-sharing screen: %v", err) - } - infoMsg = fmt.Sprintf("screen websharing stopped") - } - screen, err := sstore.GetScreenById(ctx, ids.ScreenId) - if err != nil { - return nil, fmt.Errorf("cannot get updated screen: %v", err) - } - update := &sstore.ModelUpdate{ - Screens: []*sstore.ScreenType{screen}, - Info: &sstore.InfoMsgType{ - InfoMsg: infoMsg, - WebShareLink: infoWebShareLink, - }, - } - return update, nil + return nil, fmt.Errorf("websharing is no longer available") } func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index ca09e5053..0a26264b6 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 21 +const MaxMigration = 22 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 const CmdLineSpecialMigration = 20 From de04a700d16f12522a3a386eed5e735b5c19ed9c Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 16 Sep 2023 11:15:09 -0700 Subject: [PATCH 390/397] PE-117, PE-116, PE-149 --- pkg/cmdrunner/cmdrunner.go | 78 ++++++++++++++++++++++++++++++++++++++ pkg/cmdrunner/shparse.go | 26 ++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 7fba1f596..5712b74aa 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -219,6 +219,10 @@ func init() { // CodeEditCommand is overloaded to do codeedit and codeview registerCmdFn("codeedit", CodeEditCommand) registerCmdFn("codeview", CodeEditCommand) + + registerCmdFn("imageview", ImageViewCommand) + registerCmdFn("mdview", MarkdownViewCommand) + registerCmdFn("markdownview", MarkdownViewCommand) } func getValidCommands() []string { @@ -3152,6 +3156,80 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return update, nil } +func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk)) + } + // TODO more error checking on filename format? + if pk.Args[0] == "" { + return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk)) + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0]) + cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image") + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + // set the line state + // TODO turn these strings into constants + lineState := make(map[string]any) + lineState["prompt:source"] = "file" + lineState["prompt:file"] = pk.Args[0] + err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) + if err != nil { + return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) + } + update.Line.LineState = lineState + update.Interactive = pk.Interactive + return update, nil +} + +func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk)) + } + // TODO more error checking on filename format? + if pk.Args[0] == "" { + return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk)) + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0]) + cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "markdown") + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + // set the line state + // TODO turn these strings into constants + lineState := make(map[string]any) + lineState["prompt:source"] = "file" + lineState["prompt:file"] = pk.Args[0] + err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) + if err != nil { + return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) + } + update.Line.LineState = lineState + update.Interactive = pk.Interactive + return update, nil +} + func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("/edit:test requires an argument (file name)") diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index d21e4e561..3fa80c7a5 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -28,6 +28,9 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"reset", "reset"}, BareMetaCmdDecl{"codeedit", "codeedit"}, BareMetaCmdDecl{"codeview", "codeview"}, + BareMetaCmdDecl{"imageview", "imageview"}, + BareMetaCmdDecl{"markdownview", "markdownview"}, + BareMetaCmdDecl{"mdview", "markdownview"}, } const ( @@ -241,6 +244,27 @@ func EvalBracketArgs(origCmdStr string) (map[string]string, string, error) { return rtn, restStr, nil } +func unescapeBackSlashes(s string) string { + if strings.Index(s, "\\") == -1 { + return s + } + var newStr []rune + var lastSlash bool + for _, r := range s { + if lastSlash { + lastSlash = false + newStr = append(newStr, r) + continue + } + if r == '\\' { + lastSlash = true + continue + } + newStr = append(newStr, r) + } + return string(newStr) +} + func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) (*scpacket.FeCommandPacketType, error) { if len(origPk.Args) == 0 { return nil, fmt.Errorf("empty command (no fields)") @@ -299,7 +323,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) rtnPk.Kwargs[varName] = varVal continue } - rtnPk.Args = append(rtnPk.Args, literalVal) + rtnPk.Args = append(rtnPk.Args, unescapeBackSlashes(literalVal)) } if resolveBool(rtnPk.Kwargs["dump"], false) { DumpPacket(rtnPk) From 6563b5855cbe4e69f59fad02777e2ed3e0bf5888 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 17 Sep 2023 14:10:35 -0700 Subject: [PATCH 391/397] better cmdrunner / linestate integration (lang, template, etc.). lineState is now passed to AddCmdLine --- pkg/cmdrunner/cmdrunner.go | 117 +++++++++++++++++++++---------------- pkg/sstore/dbops.go | 4 ++ pkg/sstore/sstore.go | 19 ++++-- 3 files changed, 87 insertions(+), 53 deletions(-) diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 5712b74aa..3cb3ece0d 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -64,6 +64,8 @@ const ( KwArgRenderer = "renderer" KwArgView = "view" KwArgState = "state" + KwArgTemplate = "template" + KwArgLang = "lang" ) var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -445,7 +447,7 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. return nil, err } cmd.RawCmdStr = pk.GetRawStr() - update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal") + update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal", nil) if err != nil { return nil, err } @@ -469,6 +471,23 @@ func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) { return rval, nil } +func getTemplateArg(pk *scpacket.FeCommandPacketType) (string, error) { + rval := pk.Kwargs[KwArgTemplate] + if rval == "" { + return "", nil + } + // TODO validate + return rval, nil +} + +func getLangArg(pk *scpacket.FeCommandPacketType) (string, error) { + // TODO better error checking + if len(pk.Kwargs[KwArgLang]) > 50 { + return "", nil // TODO return error, don't fail silently + } + return pk.Kwargs[KwArgLang], nil +} + func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { @@ -478,6 +497,14 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U if err != nil { return nil, fmt.Errorf("/run error, invalid view/renderer: %w", err) } + templateArg, err := getTemplateArg(pk) + if err != nil { + return nil, fmt.Errorf("/run error, invalid template: %w", err) + } + langArg, err := getLangArg(pk) + if err != nil { + return nil, fmt.Errorf("/run error, invalid lang: %w", err) + } cmdStr := firstArg(pk) expandedCmdStr, err := doCmdHistoryExpansion(ctx, ids, cmdStr) if err != nil { @@ -516,7 +543,14 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return nil, err } cmd.RawCmdStr = pk.GetRawStr() - update, err := addLineForCmd(ctx, "/run", true, ids, cmd, renderer) + lineState := make(map[string]any) + if templateArg != "" { + lineState[sstore.LineState_Template] = templateArg + } + if langArg != "" { + lineState[sstore.LineState_Lang] = langArg + } + update, err := addLineForCmd(ctx, "/run", true, ids, cmd, renderer, lineState) if err != nil { return nil, err } @@ -1227,7 +1261,7 @@ func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd, "") + update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd, "", nil) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1547,7 +1581,7 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "") + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "", nil) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -1612,8 +1646,8 @@ func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr return cmd, nil } -func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType, renderer string) (*sstore.ModelUpdate, error) { - rtnLine, err := sstore.AddCmdLine(ctx, ids.ScreenId, DefaultUserId, cmd, renderer) +func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType, renderer string, lineState map[string]any) (*sstore.ModelUpdate, error) { + rtnLine, err := sstore.AddCmdLine(ctx, ids.ScreenId, DefaultUserId, cmd, renderer, lineState) if err != nil { return nil, err } @@ -2191,7 +2225,7 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) ( // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/reset", false, ids, cmd, "") + update, err := addLineForCmd(ctx, "/reset", false, ids, cmd, "", nil) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err @@ -3118,6 +3152,10 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if pk.Args[0] == "" { return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk)) } + langArg, err := getLangArg(pk) + if err != nil { + return nil, fmt.Errorf("%s invalid 'lang': %v", GetCmdStr(pk), err) + } ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) if err != nil { return nil, err @@ -3128,30 +3166,23 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "code") + // set the line state + lineState := make(map[string]any) + lineState[sstore.LineState_Source] = "file" + lineState[sstore.LineState_File] = pk.Args[0] + if GetCmdStr(pk) == "codeview" { + lineState[sstore.LineState_Mode] = "view" + } else { + lineState[sstore.LineState_Mode] = "edit" + } + if langArg != "" { + lineState[sstore.LineState_Lang] = langArg + } + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "code", lineState) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err } - // set the line state - // TODO turn these strings into constants - lineState := make(map[string]any) - lineState["prompt:source"] = "file" - lineState["prompt:file"] = pk.Args[0] - if GetCmdStr(pk) == "codeview" { - lineState["mode"] = "view" - } else { - lineState["mode"] = "edit" - } - // TODO better error checking for lang - if pk.Kwargs["lang"] != "" && len(pk.Kwargs["lang"]) <= 50 { - lineState["lang"] = pk.Kwargs["lang"] - } - err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) - if err != nil { - return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) - } - update.Line.LineState = lineState update.Interactive = pk.Interactive return update, nil } @@ -3174,21 +3205,15 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image") + // set the line state + lineState := make(map[string]any) + lineState[sstore.LineState_Source] = "file" + lineState[sstore.LineState_File] = pk.Args[0] + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err } - // set the line state - // TODO turn these strings into constants - lineState := make(map[string]any) - lineState["prompt:source"] = "file" - lineState["prompt:file"] = pk.Args[0] - err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) - if err != nil { - return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) - } - update.Line.LineState = lineState update.Interactive = pk.Interactive return update, nil } @@ -3211,21 +3236,15 @@ func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "markdown") + // set the line state + lineState := make(map[string]any) + lineState[sstore.LineState_Source] = "file" + lineState[sstore.LineState_File] = pk.Args[0] + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "markdown", lineState) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err } - // set the line state - // TODO turn these strings into constants - lineState := make(map[string]any) - lineState["prompt:source"] = "file" - lineState["prompt:file"] = pk.Args[0] - err = sstore.UpdateLineState(ctx, ids.ScreenId, update.Line.LineId, lineState) - if err != nil { - return nil, fmt.Errorf("%s error updating line state: %v", GetCmdStr(pk), err) - } - update.Line.LineState = lineState update.Interactive = pk.Interactive return update, nil } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 1cb11dbdb..b8fe56c1f 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -786,6 +786,10 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { if cmd != nil && cmd.ScreenId == "" { return fmt.Errorf("cmd should have screenid set") } + qjs := dbutil.QuickJson(line.LineState) + if len(qjs) > MaxLineStateSize { + return fmt.Errorf("linestate exceeds maxsize, size[%d] max[%d]", len(qjs), MaxLineStateSize) + } return WithTx(ctx, func(tx *TxWrap) error { query := `SELECT screenid FROM screen WHERE screenid = ?` if !tx.Exists(query, line.ScreenId) { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b9994a9b1..cd77c2552 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -48,6 +48,14 @@ const ( LineTypeOpenAI = "openai" ) +const ( + LineState_Source = "prompt:source" + LineState_File = "prompt:file" + LineState_Template = "template" + LineState_Mode = "mode" + LineState_Lang = "lang" +) + const ( MainViewSession = "session" MainViewBookmarks = "bookmarks" @@ -1058,7 +1066,7 @@ func (cmd *CmdType) IsRunning() bool { return cmd.Status == CmdStatusRunning || cmd.Status == CmdStatusDetached } -func makeNewLineCmd(screenId string, userId string, lineId string, renderer string) *LineType { +func makeNewLineCmd(screenId string, userId string, lineId string, renderer string, lineState map[string]any) *LineType { rtn := &LineType{} rtn.ScreenId = screenId rtn.UserId = userId @@ -1069,7 +1077,10 @@ func makeNewLineCmd(screenId string, userId string, lineId string, renderer stri rtn.LineId = lineId rtn.ContentHeight = LineNoHeight rtn.Renderer = renderer - rtn.LineState = make(map[string]any) + if lineState == nil { + lineState = make(map[string]any) + } + rtn.LineState = lineState return rtn } @@ -1119,8 +1130,8 @@ func AddOpenAILine(ctx context.Context, screenId string, userId string, cmd *Cmd return rtnLine, nil } -func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdType, renderer string) (*LineType, error) { - rtnLine := makeNewLineCmd(screenId, userId, cmd.LineId, renderer) +func AddCmdLine(ctx context.Context, screenId string, userId string, cmd *CmdType, renderer string, lineState map[string]any) (*LineType, error) { + rtnLine := makeNewLineCmd(screenId, userId, cmd.LineId, renderer, lineState) err := InsertLine(ctx, rtnLine, cmd) if err != nil { return nil, err From aa5c09368e124b669d64da83e7924307594d1741 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 17 Sep 2023 20:32:35 -0700 Subject: [PATCH 392/397] bump version to v0.3.1 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 85b0ce28e..a16b981bc 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -32,7 +32,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.3.0" +const PromptVersion = "v0.3.1" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.3.0" From 9b380aee02c2da43da2e0ed8fb5a33270081b357 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 5 Oct 2023 09:42:48 -0700 Subject: [PATCH 393/397] add function to detect user's shell --- pkg/scbase/scbase.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index a16b981bc..3e41082be 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -9,6 +9,7 @@ import ( "log" "os" "os/exec" + "os/user" "path" "regexp" "runtime" @@ -35,6 +36,7 @@ const PromptAppPathVarName = "PROMPT_APP_PATH" const PromptVersion = "v0.3.1" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.3.0" +const DefaultMacOSShell = "/bin/bash" var SessionDirCache = make(map[string]string) var ScreenDirCache = make(map[string]string) @@ -376,3 +378,30 @@ func MacOSRelease() string { }) return osRelease } + +var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`) + +// dscl . -read /User/[username] UserShell +// defaults to /bin/bash +func MacUserShell() string { + osUser, err := user.Current() + if err != nil { + log.Printf("error getting current user: %v\n", err) + return DefaultMacOSShell + } + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + userStr := "/Users/" + osUser.Name + out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput() + if err != nil { + log.Printf("error executing macos user shell lookup: %v %q\n", err, string(out)) + return DefaultMacOSShell + } + outStr := strings.TrimSpace(string(out)) + m := userShellRegexp.FindStringSubmatch(outStr) + if m == nil { + log.Printf("error in format of dscl output: %q\n", outStr) + return DefaultMacOSShell + } + return m[1] +} From 50190902caf1af2a95bff4b239433fea9db7e19d Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Thu, 12 Oct 2023 08:59:22 +0800 Subject: [PATCH 394/397] register csvviewer (#3) New csvview command --- go.mod | 16 +- go.sum | 1817 +----------------------------------- pkg/cmdrunner/cmdrunner.go | 33 + pkg/cmdrunner/shparse.go | 1 + 4 files changed, 67 insertions(+), 1800 deletions(-) diff --git a/go.mod b/go.mod index 71bf3d208..a51314ea7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/alessio/shellescape v1.4.1 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 + github.com/commandlinedev/apishell v0.0.0 github.com/creack/pty v1.1.18 github.com/golang-migrate/migrate/v4 v4.16.2 github.com/google/uuid v1.3.0 @@ -12,19 +13,16 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.16 + github.com/sashabaranov/go-openai v1.9.0 github.com/sawka/txwrap v0.1.2 - github.com/commandlinedev/apishell v0.0.0 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - golang.org/x/mod v0.5.1 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad - mvdan.cc/sh/v3 v3.5.1 + golang.org/x/crypto v0.7.0 + golang.org/x/mod v0.10.0 + golang.org/x/sys v0.10.0 + mvdan.cc/sh/v3 v3.7.0 ) require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/sashabaranov/go-openai v1.9.0 // indirect go.uber.org/atomic v1.7.0 // indirect -) - - +) \ No newline at end of file diff --git a/go.sum b/go.sum index e7586676f..7c819d2f6 100644 --- a/go.sum +++ b/go.sum @@ -1,1829 +1,64 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/spanner v1.28.0/go.mod h1:7m6mtQZn/hMbMfx62ct5EWrGND4DNqkXyrmBPRS+OJo= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= -github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= -github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= -github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= -github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= -github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= -github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= -github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= -github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= +github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= -github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= -github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= -github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= -github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sashabaranov/go-openai v1.9.0 h1:NoiO++IISxxJ1pRc0n7uZvMGMake0G+FJ1XPwXtprsA= github.com/sashabaranov/go-openai v1.9.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sawka/txwrap v0.1.0 h1:uWGplmEJUEd9WGYZy9fU+hoC2Z6Yal4NMH5DbKsUTdo= -github.com/sawka/txwrap v0.1.0/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= github.com/sawka/txwrap v0.1.2 h1:v8xS0Z1LE7/6vMZA81PYihI+0TSR6Zm1MalzzBIuXKc= github.com/sawka/txwrap v0.1.2/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= -gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= -k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= -modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= -modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= -modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= -modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= -modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= -modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= -modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= -modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= -mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= -mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 3cb3ece0d..10f5f99ed 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -225,6 +225,8 @@ func init() { registerCmdFn("imageview", ImageViewCommand) registerCmdFn("mdview", MarkdownViewCommand) registerCmdFn("markdownview", MarkdownViewCommand) + + registerCmdFn("csvview", CSVViewCommand) } func getValidCommands() []string { @@ -3187,6 +3189,37 @@ func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst return update, nil } +func CSVViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + if len(pk.Args) == 0 { + return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk)) + } + // TODO more error checking on filename format? + if pk.Args[0] == "" { + return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk)) + } + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0]) + cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr)) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + // set the line state + lineState := make(map[string]any) + lineState[sstore.LineState_Source] = "file" + lineState[sstore.LineState_File] = pk.Args[0] + update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "csv", lineState) + if err != nil { + // TODO tricky error since the command was a success, but we can't show the output + return nil, err + } + update.Interactive = pk.Interactive + return update, nil +} + func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk)) diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 3fa80c7a5..6b7e4ed7b 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -31,6 +31,7 @@ var BareMetaCmds = []BareMetaCmdDecl{ BareMetaCmdDecl{"imageview", "imageview"}, BareMetaCmdDecl{"markdownview", "markdownview"}, BareMetaCmdDecl{"mdview", "markdownview"}, + BareMetaCmdDecl{"csvview", "csvview"}, } const ( From f3f243aca3801e3f421b70c94aada3fd7db34b5f Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 12 Oct 2023 13:36:11 -0700 Subject: [PATCH 395/397] use workspace- instead of session- for names --- pkg/sstore/dbops.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index b8fe56c1f..abfcbb902 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -532,7 +532,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo newSessionId := scbase.GenPromptUUID() txErr := WithTx(ctx, func(tx *TxWrap) error { names := tx.SelectStrings(`SELECT name FROM session`) - sessionName = fmtUniqueName(sessionName, "session-%d", len(names)+1, names) + sessionName = 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, ?)` From 1a9c427d225480ca1c26673c625e82837108a43d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 16 Oct 2023 13:16:57 -0700 Subject: [PATCH 396/397] bump server version to v0.4.0 --- pkg/scbase/scbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scbase/scbase.go b/pkg/scbase/scbase.go index 3e41082be..7907746e2 100644 --- a/pkg/scbase/scbase.go +++ b/pkg/scbase/scbase.go @@ -33,7 +33,7 @@ const PromptLockFile = "prompt.lock" const PromptDirName = "prompt" const PromptDevDirName = "prompt-dev" const PromptAppPathVarName = "PROMPT_APP_PATH" -const PromptVersion = "v0.3.1" +const PromptVersion = "v0.4.0" const PromptAuthKeyFileName = "prompt.authkey" const MShellVersion = "v0.3.0" const DefaultMacOSShell = "/bin/bash" From 77fee4236530530e944b12fa8e5bd457373a5f8c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 16 Oct 2023 13:19:34 -0700 Subject: [PATCH 397/397] move to wavesrv directory to prepare for merge to waveterm --- .gitignore | 6 ------ {cmd => wavesrv/cmd}/main-server.go | 0 {db => wavesrv/db}/db.go | 0 {db => wavesrv/db}/migrations/000001_init.down.sql | 0 {db => wavesrv/db}/migrations/000001_init.up.sql | 0 {db => wavesrv/db}/migrations/000002_activity.down.sql | 0 {db => wavesrv/db}/migrations/000002_activity.up.sql | 0 {db => wavesrv/db}/migrations/000003_renderer.down.sql | 0 {db => wavesrv/db}/migrations/000003_renderer.up.sql | 0 {db => wavesrv/db}/migrations/000004_bookmarks.down.sql | 0 {db => wavesrv/db}/migrations/000004_bookmarks.up.sql | 0 {db => wavesrv/db}/migrations/000005_buildtime.down.sql | 0 {db => wavesrv/db}/migrations/000005_buildtime.up.sql | 0 {db => wavesrv/db}/migrations/000006_feopts.down.sql | 0 {db => wavesrv/db}/migrations/000006_feopts.up.sql | 0 {db => wavesrv/db}/migrations/000007_playbooks.down.sql | 0 {db => wavesrv/db}/migrations/000007_playbooks.up.sql | 0 {db => wavesrv/db}/migrations/000008_cloudsession.down.sql | 0 {db => wavesrv/db}/migrations/000008_cloudsession.up.sql | 0 {db => wavesrv/db}/migrations/000009_screenprimary.down.sql | 0 {db => wavesrv/db}/migrations/000009_screenprimary.up.sql | 0 .../db}/migrations/000010_removewindowid.down.sql | 0 {db => wavesrv/db}/migrations/000010_removewindowid.up.sql | 0 {db => wavesrv/db}/migrations/000011_cmdscreenid.down.sql | 0 {db => wavesrv/db}/migrations/000011_cmdscreenid.up.sql | 0 .../db}/migrations/000012_historylinenum.down.sql | 0 {db => wavesrv/db}/migrations/000012_historylinenum.up.sql | 0 {db => wavesrv/db}/migrations/000013_cmdmigration.down.sql | 0 {db => wavesrv/db}/migrations/000013_cmdmigration.up.sql | 0 .../db}/migrations/000014_simplifybookmarks.down.sql | 0 .../db}/migrations/000014_simplifybookmarks.up.sql | 0 {db => wavesrv/db}/migrations/000015_lineupdates.down.sql | 0 {db => wavesrv/db}/migrations/000015_lineupdates.up.sql | 0 {db => wavesrv/db}/migrations/000016_webptypos.down.sql | 0 {db => wavesrv/db}/migrations/000016_webptypos.up.sql | 0 {db => wavesrv/db}/migrations/000017_remotevars.down.sql | 0 {db => wavesrv/db}/migrations/000017_remotevars.up.sql | 0 {db => wavesrv/db}/migrations/000018_modremote.down.sql | 0 {db => wavesrv/db}/migrations/000018_modremote.up.sql | 0 {db => wavesrv/db}/migrations/000019_clientopenai.down.sql | 0 {db => wavesrv/db}/migrations/000019_clientopenai.up.sql | 0 {db => wavesrv/db}/migrations/000020_linecmd.down.sql | 0 {db => wavesrv/db}/migrations/000020_linecmd.up.sql | 0 {db => wavesrv/db}/migrations/000021_linestate.down.sql | 0 {db => wavesrv/db}/migrations/000021_linestate.up.sql | 0 {db => wavesrv/db}/migrations/000022_endwebshare.down.sql | 0 {db => wavesrv/db}/migrations/000022_endwebshare.up.sql | 0 {db => wavesrv/db}/schema.sql | 0 go.mod => wavesrv/go.mod | 0 go.sum => wavesrv/go.sum | 0 {pkg => wavesrv/pkg}/cmdrunner/cmdrunner.go | 0 {pkg => wavesrv/pkg}/cmdrunner/linux-decls.txt | 0 {pkg => wavesrv/pkg}/cmdrunner/resolver.go | 0 {pkg => wavesrv/pkg}/cmdrunner/shparse.go | 0 {pkg => wavesrv/pkg}/cmdrunner/shparse_test.go | 0 {pkg => wavesrv/pkg}/cmdrunner/termopts.go | 0 {pkg => wavesrv/pkg}/comp/comp.go | 0 {pkg => wavesrv/pkg}/comp/comp_test.go | 0 {pkg => wavesrv/pkg}/comp/simplecomp.go | 0 {pkg => wavesrv/pkg}/dbutil/dbutil.go | 0 {pkg => wavesrv/pkg}/dbutil/map.go | 0 {pkg => wavesrv/pkg}/keygen/keygen.go | 0 {pkg => wavesrv/pkg}/mapqueue/mapqueue.go | 0 {pkg => wavesrv/pkg}/pcloud/pcloud.go | 0 {pkg => wavesrv/pkg}/pcloud/pclouddata.go | 0 {pkg => wavesrv/pkg}/promptenc/promptenc.go | 0 {pkg => wavesrv/pkg}/remote/circlelog.go | 0 {pkg => wavesrv/pkg}/remote/openai/openai.go | 0 {pkg => wavesrv/pkg}/remote/remote.go | 0 {pkg => wavesrv/pkg}/remote/updatequeue.go | 0 {pkg => wavesrv/pkg}/rtnstate/rtnstate.go | 0 {pkg => wavesrv/pkg}/scbase/scbase.go | 0 {pkg => wavesrv/pkg}/scpacket/scpacket.go | 0 {pkg => wavesrv/pkg}/scws/scws.go | 0 {pkg => wavesrv/pkg}/shparse/comp.go | 0 {pkg => wavesrv/pkg}/shparse/expand.go | 0 {pkg => wavesrv/pkg}/shparse/extend.go | 0 {pkg => wavesrv/pkg}/shparse/shparse.go | 0 {pkg => wavesrv/pkg}/shparse/shparse_test.go | 0 {pkg => wavesrv/pkg}/shparse/tokenize.go | 0 {pkg => wavesrv/pkg}/sstore/dbops.go | 0 {pkg => wavesrv/pkg}/sstore/fileops.go | 0 {pkg => wavesrv/pkg}/sstore/map.go | 0 {pkg => wavesrv/pkg}/sstore/migrate.go | 0 {pkg => wavesrv/pkg}/sstore/quick.go | 0 {pkg => wavesrv/pkg}/sstore/sstore.go | 0 {pkg => wavesrv/pkg}/sstore/sstore_migrate.go | 0 {pkg => wavesrv/pkg}/sstore/updatebus.go | 0 {pkg => wavesrv/pkg}/utilfn/linediff.go | 0 {pkg => wavesrv/pkg}/utilfn/utilfn.go | 0 {pkg => wavesrv/pkg}/utilfn/utilfn_test.go | 0 {pkg => wavesrv/pkg}/wsshell/wsshell.go | 0 scripthaus.md => wavesrv/scripthaus.md | 0 93 files changed, 6 deletions(-) delete mode 100644 .gitignore rename {cmd => wavesrv/cmd}/main-server.go (100%) rename {db => wavesrv/db}/db.go (100%) rename {db => wavesrv/db}/migrations/000001_init.down.sql (100%) rename {db => wavesrv/db}/migrations/000001_init.up.sql (100%) rename {db => wavesrv/db}/migrations/000002_activity.down.sql (100%) rename {db => wavesrv/db}/migrations/000002_activity.up.sql (100%) rename {db => wavesrv/db}/migrations/000003_renderer.down.sql (100%) rename {db => wavesrv/db}/migrations/000003_renderer.up.sql (100%) rename {db => wavesrv/db}/migrations/000004_bookmarks.down.sql (100%) rename {db => wavesrv/db}/migrations/000004_bookmarks.up.sql (100%) rename {db => wavesrv/db}/migrations/000005_buildtime.down.sql (100%) rename {db => wavesrv/db}/migrations/000005_buildtime.up.sql (100%) rename {db => wavesrv/db}/migrations/000006_feopts.down.sql (100%) rename {db => wavesrv/db}/migrations/000006_feopts.up.sql (100%) rename {db => wavesrv/db}/migrations/000007_playbooks.down.sql (100%) rename {db => wavesrv/db}/migrations/000007_playbooks.up.sql (100%) rename {db => wavesrv/db}/migrations/000008_cloudsession.down.sql (100%) rename {db => wavesrv/db}/migrations/000008_cloudsession.up.sql (100%) rename {db => wavesrv/db}/migrations/000009_screenprimary.down.sql (100%) rename {db => wavesrv/db}/migrations/000009_screenprimary.up.sql (100%) rename {db => wavesrv/db}/migrations/000010_removewindowid.down.sql (100%) rename {db => wavesrv/db}/migrations/000010_removewindowid.up.sql (100%) rename {db => wavesrv/db}/migrations/000011_cmdscreenid.down.sql (100%) rename {db => wavesrv/db}/migrations/000011_cmdscreenid.up.sql (100%) rename {db => wavesrv/db}/migrations/000012_historylinenum.down.sql (100%) rename {db => wavesrv/db}/migrations/000012_historylinenum.up.sql (100%) rename {db => wavesrv/db}/migrations/000013_cmdmigration.down.sql (100%) rename {db => wavesrv/db}/migrations/000013_cmdmigration.up.sql (100%) rename {db => wavesrv/db}/migrations/000014_simplifybookmarks.down.sql (100%) rename {db => wavesrv/db}/migrations/000014_simplifybookmarks.up.sql (100%) rename {db => wavesrv/db}/migrations/000015_lineupdates.down.sql (100%) rename {db => wavesrv/db}/migrations/000015_lineupdates.up.sql (100%) rename {db => wavesrv/db}/migrations/000016_webptypos.down.sql (100%) rename {db => wavesrv/db}/migrations/000016_webptypos.up.sql (100%) rename {db => wavesrv/db}/migrations/000017_remotevars.down.sql (100%) rename {db => wavesrv/db}/migrations/000017_remotevars.up.sql (100%) rename {db => wavesrv/db}/migrations/000018_modremote.down.sql (100%) rename {db => wavesrv/db}/migrations/000018_modremote.up.sql (100%) rename {db => wavesrv/db}/migrations/000019_clientopenai.down.sql (100%) rename {db => wavesrv/db}/migrations/000019_clientopenai.up.sql (100%) rename {db => wavesrv/db}/migrations/000020_linecmd.down.sql (100%) rename {db => wavesrv/db}/migrations/000020_linecmd.up.sql (100%) rename {db => wavesrv/db}/migrations/000021_linestate.down.sql (100%) rename {db => wavesrv/db}/migrations/000021_linestate.up.sql (100%) rename {db => wavesrv/db}/migrations/000022_endwebshare.down.sql (100%) rename {db => wavesrv/db}/migrations/000022_endwebshare.up.sql (100%) rename {db => wavesrv/db}/schema.sql (100%) rename go.mod => wavesrv/go.mod (100%) rename go.sum => wavesrv/go.sum (100%) rename {pkg => wavesrv/pkg}/cmdrunner/cmdrunner.go (100%) rename {pkg => wavesrv/pkg}/cmdrunner/linux-decls.txt (100%) rename {pkg => wavesrv/pkg}/cmdrunner/resolver.go (100%) rename {pkg => wavesrv/pkg}/cmdrunner/shparse.go (100%) rename {pkg => wavesrv/pkg}/cmdrunner/shparse_test.go (100%) rename {pkg => wavesrv/pkg}/cmdrunner/termopts.go (100%) rename {pkg => wavesrv/pkg}/comp/comp.go (100%) rename {pkg => wavesrv/pkg}/comp/comp_test.go (100%) rename {pkg => wavesrv/pkg}/comp/simplecomp.go (100%) rename {pkg => wavesrv/pkg}/dbutil/dbutil.go (100%) rename {pkg => wavesrv/pkg}/dbutil/map.go (100%) rename {pkg => wavesrv/pkg}/keygen/keygen.go (100%) rename {pkg => wavesrv/pkg}/mapqueue/mapqueue.go (100%) rename {pkg => wavesrv/pkg}/pcloud/pcloud.go (100%) rename {pkg => wavesrv/pkg}/pcloud/pclouddata.go (100%) rename {pkg => wavesrv/pkg}/promptenc/promptenc.go (100%) rename {pkg => wavesrv/pkg}/remote/circlelog.go (100%) rename {pkg => wavesrv/pkg}/remote/openai/openai.go (100%) rename {pkg => wavesrv/pkg}/remote/remote.go (100%) rename {pkg => wavesrv/pkg}/remote/updatequeue.go (100%) rename {pkg => wavesrv/pkg}/rtnstate/rtnstate.go (100%) rename {pkg => wavesrv/pkg}/scbase/scbase.go (100%) rename {pkg => wavesrv/pkg}/scpacket/scpacket.go (100%) rename {pkg => wavesrv/pkg}/scws/scws.go (100%) rename {pkg => wavesrv/pkg}/shparse/comp.go (100%) rename {pkg => wavesrv/pkg}/shparse/expand.go (100%) rename {pkg => wavesrv/pkg}/shparse/extend.go (100%) rename {pkg => wavesrv/pkg}/shparse/shparse.go (100%) rename {pkg => wavesrv/pkg}/shparse/shparse_test.go (100%) rename {pkg => wavesrv/pkg}/shparse/tokenize.go (100%) rename {pkg => wavesrv/pkg}/sstore/dbops.go (100%) rename {pkg => wavesrv/pkg}/sstore/fileops.go (100%) rename {pkg => wavesrv/pkg}/sstore/map.go (100%) rename {pkg => wavesrv/pkg}/sstore/migrate.go (100%) rename {pkg => wavesrv/pkg}/sstore/quick.go (100%) rename {pkg => wavesrv/pkg}/sstore/sstore.go (100%) rename {pkg => wavesrv/pkg}/sstore/sstore_migrate.go (100%) rename {pkg => wavesrv/pkg}/sstore/updatebus.go (100%) rename {pkg => wavesrv/pkg}/utilfn/linediff.go (100%) rename {pkg => wavesrv/pkg}/utilfn/utilfn.go (100%) rename {pkg => wavesrv/pkg}/utilfn/utilfn_test.go (100%) rename {pkg => wavesrv/pkg}/wsshell/wsshell.go (100%) rename scripthaus.md => wavesrv/scripthaus.md (100%) diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7c9c1bf40..000000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*~ -bin/ -*.out -.idea/ -test/ -temp.sql diff --git a/cmd/main-server.go b/wavesrv/cmd/main-server.go similarity index 100% rename from cmd/main-server.go rename to wavesrv/cmd/main-server.go diff --git a/db/db.go b/wavesrv/db/db.go similarity index 100% rename from db/db.go rename to wavesrv/db/db.go diff --git a/db/migrations/000001_init.down.sql b/wavesrv/db/migrations/000001_init.down.sql similarity index 100% rename from db/migrations/000001_init.down.sql rename to wavesrv/db/migrations/000001_init.down.sql diff --git a/db/migrations/000001_init.up.sql b/wavesrv/db/migrations/000001_init.up.sql similarity index 100% rename from db/migrations/000001_init.up.sql rename to wavesrv/db/migrations/000001_init.up.sql diff --git a/db/migrations/000002_activity.down.sql b/wavesrv/db/migrations/000002_activity.down.sql similarity index 100% rename from db/migrations/000002_activity.down.sql rename to wavesrv/db/migrations/000002_activity.down.sql diff --git a/db/migrations/000002_activity.up.sql b/wavesrv/db/migrations/000002_activity.up.sql similarity index 100% rename from db/migrations/000002_activity.up.sql rename to wavesrv/db/migrations/000002_activity.up.sql diff --git a/db/migrations/000003_renderer.down.sql b/wavesrv/db/migrations/000003_renderer.down.sql similarity index 100% rename from db/migrations/000003_renderer.down.sql rename to wavesrv/db/migrations/000003_renderer.down.sql diff --git a/db/migrations/000003_renderer.up.sql b/wavesrv/db/migrations/000003_renderer.up.sql similarity index 100% rename from db/migrations/000003_renderer.up.sql rename to wavesrv/db/migrations/000003_renderer.up.sql diff --git a/db/migrations/000004_bookmarks.down.sql b/wavesrv/db/migrations/000004_bookmarks.down.sql similarity index 100% rename from db/migrations/000004_bookmarks.down.sql rename to wavesrv/db/migrations/000004_bookmarks.down.sql diff --git a/db/migrations/000004_bookmarks.up.sql b/wavesrv/db/migrations/000004_bookmarks.up.sql similarity index 100% rename from db/migrations/000004_bookmarks.up.sql rename to wavesrv/db/migrations/000004_bookmarks.up.sql diff --git a/db/migrations/000005_buildtime.down.sql b/wavesrv/db/migrations/000005_buildtime.down.sql similarity index 100% rename from db/migrations/000005_buildtime.down.sql rename to wavesrv/db/migrations/000005_buildtime.down.sql diff --git a/db/migrations/000005_buildtime.up.sql b/wavesrv/db/migrations/000005_buildtime.up.sql similarity index 100% rename from db/migrations/000005_buildtime.up.sql rename to wavesrv/db/migrations/000005_buildtime.up.sql diff --git a/db/migrations/000006_feopts.down.sql b/wavesrv/db/migrations/000006_feopts.down.sql similarity index 100% rename from db/migrations/000006_feopts.down.sql rename to wavesrv/db/migrations/000006_feopts.down.sql diff --git a/db/migrations/000006_feopts.up.sql b/wavesrv/db/migrations/000006_feopts.up.sql similarity index 100% rename from db/migrations/000006_feopts.up.sql rename to wavesrv/db/migrations/000006_feopts.up.sql diff --git a/db/migrations/000007_playbooks.down.sql b/wavesrv/db/migrations/000007_playbooks.down.sql similarity index 100% rename from db/migrations/000007_playbooks.down.sql rename to wavesrv/db/migrations/000007_playbooks.down.sql diff --git a/db/migrations/000007_playbooks.up.sql b/wavesrv/db/migrations/000007_playbooks.up.sql similarity index 100% rename from db/migrations/000007_playbooks.up.sql rename to wavesrv/db/migrations/000007_playbooks.up.sql diff --git a/db/migrations/000008_cloudsession.down.sql b/wavesrv/db/migrations/000008_cloudsession.down.sql similarity index 100% rename from db/migrations/000008_cloudsession.down.sql rename to wavesrv/db/migrations/000008_cloudsession.down.sql diff --git a/db/migrations/000008_cloudsession.up.sql b/wavesrv/db/migrations/000008_cloudsession.up.sql similarity index 100% rename from db/migrations/000008_cloudsession.up.sql rename to wavesrv/db/migrations/000008_cloudsession.up.sql diff --git a/db/migrations/000009_screenprimary.down.sql b/wavesrv/db/migrations/000009_screenprimary.down.sql similarity index 100% rename from db/migrations/000009_screenprimary.down.sql rename to wavesrv/db/migrations/000009_screenprimary.down.sql diff --git a/db/migrations/000009_screenprimary.up.sql b/wavesrv/db/migrations/000009_screenprimary.up.sql similarity index 100% rename from db/migrations/000009_screenprimary.up.sql rename to wavesrv/db/migrations/000009_screenprimary.up.sql diff --git a/db/migrations/000010_removewindowid.down.sql b/wavesrv/db/migrations/000010_removewindowid.down.sql similarity index 100% rename from db/migrations/000010_removewindowid.down.sql rename to wavesrv/db/migrations/000010_removewindowid.down.sql diff --git a/db/migrations/000010_removewindowid.up.sql b/wavesrv/db/migrations/000010_removewindowid.up.sql similarity index 100% rename from db/migrations/000010_removewindowid.up.sql rename to wavesrv/db/migrations/000010_removewindowid.up.sql diff --git a/db/migrations/000011_cmdscreenid.down.sql b/wavesrv/db/migrations/000011_cmdscreenid.down.sql similarity index 100% rename from db/migrations/000011_cmdscreenid.down.sql rename to wavesrv/db/migrations/000011_cmdscreenid.down.sql diff --git a/db/migrations/000011_cmdscreenid.up.sql b/wavesrv/db/migrations/000011_cmdscreenid.up.sql similarity index 100% rename from db/migrations/000011_cmdscreenid.up.sql rename to wavesrv/db/migrations/000011_cmdscreenid.up.sql diff --git a/db/migrations/000012_historylinenum.down.sql b/wavesrv/db/migrations/000012_historylinenum.down.sql similarity index 100% rename from db/migrations/000012_historylinenum.down.sql rename to wavesrv/db/migrations/000012_historylinenum.down.sql diff --git a/db/migrations/000012_historylinenum.up.sql b/wavesrv/db/migrations/000012_historylinenum.up.sql similarity index 100% rename from db/migrations/000012_historylinenum.up.sql rename to wavesrv/db/migrations/000012_historylinenum.up.sql diff --git a/db/migrations/000013_cmdmigration.down.sql b/wavesrv/db/migrations/000013_cmdmigration.down.sql similarity index 100% rename from db/migrations/000013_cmdmigration.down.sql rename to wavesrv/db/migrations/000013_cmdmigration.down.sql diff --git a/db/migrations/000013_cmdmigration.up.sql b/wavesrv/db/migrations/000013_cmdmigration.up.sql similarity index 100% rename from db/migrations/000013_cmdmigration.up.sql rename to wavesrv/db/migrations/000013_cmdmigration.up.sql diff --git a/db/migrations/000014_simplifybookmarks.down.sql b/wavesrv/db/migrations/000014_simplifybookmarks.down.sql similarity index 100% rename from db/migrations/000014_simplifybookmarks.down.sql rename to wavesrv/db/migrations/000014_simplifybookmarks.down.sql diff --git a/db/migrations/000014_simplifybookmarks.up.sql b/wavesrv/db/migrations/000014_simplifybookmarks.up.sql similarity index 100% rename from db/migrations/000014_simplifybookmarks.up.sql rename to wavesrv/db/migrations/000014_simplifybookmarks.up.sql diff --git a/db/migrations/000015_lineupdates.down.sql b/wavesrv/db/migrations/000015_lineupdates.down.sql similarity index 100% rename from db/migrations/000015_lineupdates.down.sql rename to wavesrv/db/migrations/000015_lineupdates.down.sql diff --git a/db/migrations/000015_lineupdates.up.sql b/wavesrv/db/migrations/000015_lineupdates.up.sql similarity index 100% rename from db/migrations/000015_lineupdates.up.sql rename to wavesrv/db/migrations/000015_lineupdates.up.sql diff --git a/db/migrations/000016_webptypos.down.sql b/wavesrv/db/migrations/000016_webptypos.down.sql similarity index 100% rename from db/migrations/000016_webptypos.down.sql rename to wavesrv/db/migrations/000016_webptypos.down.sql diff --git a/db/migrations/000016_webptypos.up.sql b/wavesrv/db/migrations/000016_webptypos.up.sql similarity index 100% rename from db/migrations/000016_webptypos.up.sql rename to wavesrv/db/migrations/000016_webptypos.up.sql diff --git a/db/migrations/000017_remotevars.down.sql b/wavesrv/db/migrations/000017_remotevars.down.sql similarity index 100% rename from db/migrations/000017_remotevars.down.sql rename to wavesrv/db/migrations/000017_remotevars.down.sql diff --git a/db/migrations/000017_remotevars.up.sql b/wavesrv/db/migrations/000017_remotevars.up.sql similarity index 100% rename from db/migrations/000017_remotevars.up.sql rename to wavesrv/db/migrations/000017_remotevars.up.sql diff --git a/db/migrations/000018_modremote.down.sql b/wavesrv/db/migrations/000018_modremote.down.sql similarity index 100% rename from db/migrations/000018_modremote.down.sql rename to wavesrv/db/migrations/000018_modremote.down.sql diff --git a/db/migrations/000018_modremote.up.sql b/wavesrv/db/migrations/000018_modremote.up.sql similarity index 100% rename from db/migrations/000018_modremote.up.sql rename to wavesrv/db/migrations/000018_modremote.up.sql diff --git a/db/migrations/000019_clientopenai.down.sql b/wavesrv/db/migrations/000019_clientopenai.down.sql similarity index 100% rename from db/migrations/000019_clientopenai.down.sql rename to wavesrv/db/migrations/000019_clientopenai.down.sql diff --git a/db/migrations/000019_clientopenai.up.sql b/wavesrv/db/migrations/000019_clientopenai.up.sql similarity index 100% rename from db/migrations/000019_clientopenai.up.sql rename to wavesrv/db/migrations/000019_clientopenai.up.sql diff --git a/db/migrations/000020_linecmd.down.sql b/wavesrv/db/migrations/000020_linecmd.down.sql similarity index 100% rename from db/migrations/000020_linecmd.down.sql rename to wavesrv/db/migrations/000020_linecmd.down.sql diff --git a/db/migrations/000020_linecmd.up.sql b/wavesrv/db/migrations/000020_linecmd.up.sql similarity index 100% rename from db/migrations/000020_linecmd.up.sql rename to wavesrv/db/migrations/000020_linecmd.up.sql diff --git a/db/migrations/000021_linestate.down.sql b/wavesrv/db/migrations/000021_linestate.down.sql similarity index 100% rename from db/migrations/000021_linestate.down.sql rename to wavesrv/db/migrations/000021_linestate.down.sql diff --git a/db/migrations/000021_linestate.up.sql b/wavesrv/db/migrations/000021_linestate.up.sql similarity index 100% rename from db/migrations/000021_linestate.up.sql rename to wavesrv/db/migrations/000021_linestate.up.sql diff --git a/db/migrations/000022_endwebshare.down.sql b/wavesrv/db/migrations/000022_endwebshare.down.sql similarity index 100% rename from db/migrations/000022_endwebshare.down.sql rename to wavesrv/db/migrations/000022_endwebshare.down.sql diff --git a/db/migrations/000022_endwebshare.up.sql b/wavesrv/db/migrations/000022_endwebshare.up.sql similarity index 100% rename from db/migrations/000022_endwebshare.up.sql rename to wavesrv/db/migrations/000022_endwebshare.up.sql diff --git a/db/schema.sql b/wavesrv/db/schema.sql similarity index 100% rename from db/schema.sql rename to wavesrv/db/schema.sql diff --git a/go.mod b/wavesrv/go.mod similarity index 100% rename from go.mod rename to wavesrv/go.mod diff --git a/go.sum b/wavesrv/go.sum similarity index 100% rename from go.sum rename to wavesrv/go.sum diff --git a/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go similarity index 100% rename from pkg/cmdrunner/cmdrunner.go rename to wavesrv/pkg/cmdrunner/cmdrunner.go diff --git a/pkg/cmdrunner/linux-decls.txt b/wavesrv/pkg/cmdrunner/linux-decls.txt similarity index 100% rename from pkg/cmdrunner/linux-decls.txt rename to wavesrv/pkg/cmdrunner/linux-decls.txt diff --git a/pkg/cmdrunner/resolver.go b/wavesrv/pkg/cmdrunner/resolver.go similarity index 100% rename from pkg/cmdrunner/resolver.go rename to wavesrv/pkg/cmdrunner/resolver.go diff --git a/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go similarity index 100% rename from pkg/cmdrunner/shparse.go rename to wavesrv/pkg/cmdrunner/shparse.go diff --git a/pkg/cmdrunner/shparse_test.go b/wavesrv/pkg/cmdrunner/shparse_test.go similarity index 100% rename from pkg/cmdrunner/shparse_test.go rename to wavesrv/pkg/cmdrunner/shparse_test.go diff --git a/pkg/cmdrunner/termopts.go b/wavesrv/pkg/cmdrunner/termopts.go similarity index 100% rename from pkg/cmdrunner/termopts.go rename to wavesrv/pkg/cmdrunner/termopts.go diff --git a/pkg/comp/comp.go b/wavesrv/pkg/comp/comp.go similarity index 100% rename from pkg/comp/comp.go rename to wavesrv/pkg/comp/comp.go diff --git a/pkg/comp/comp_test.go b/wavesrv/pkg/comp/comp_test.go similarity index 100% rename from pkg/comp/comp_test.go rename to wavesrv/pkg/comp/comp_test.go diff --git a/pkg/comp/simplecomp.go b/wavesrv/pkg/comp/simplecomp.go similarity index 100% rename from pkg/comp/simplecomp.go rename to wavesrv/pkg/comp/simplecomp.go diff --git a/pkg/dbutil/dbutil.go b/wavesrv/pkg/dbutil/dbutil.go similarity index 100% rename from pkg/dbutil/dbutil.go rename to wavesrv/pkg/dbutil/dbutil.go diff --git a/pkg/dbutil/map.go b/wavesrv/pkg/dbutil/map.go similarity index 100% rename from pkg/dbutil/map.go rename to wavesrv/pkg/dbutil/map.go diff --git a/pkg/keygen/keygen.go b/wavesrv/pkg/keygen/keygen.go similarity index 100% rename from pkg/keygen/keygen.go rename to wavesrv/pkg/keygen/keygen.go diff --git a/pkg/mapqueue/mapqueue.go b/wavesrv/pkg/mapqueue/mapqueue.go similarity index 100% rename from pkg/mapqueue/mapqueue.go rename to wavesrv/pkg/mapqueue/mapqueue.go diff --git a/pkg/pcloud/pcloud.go b/wavesrv/pkg/pcloud/pcloud.go similarity index 100% rename from pkg/pcloud/pcloud.go rename to wavesrv/pkg/pcloud/pcloud.go diff --git a/pkg/pcloud/pclouddata.go b/wavesrv/pkg/pcloud/pclouddata.go similarity index 100% rename from pkg/pcloud/pclouddata.go rename to wavesrv/pkg/pcloud/pclouddata.go diff --git a/pkg/promptenc/promptenc.go b/wavesrv/pkg/promptenc/promptenc.go similarity index 100% rename from pkg/promptenc/promptenc.go rename to wavesrv/pkg/promptenc/promptenc.go diff --git a/pkg/remote/circlelog.go b/wavesrv/pkg/remote/circlelog.go similarity index 100% rename from pkg/remote/circlelog.go rename to wavesrv/pkg/remote/circlelog.go diff --git a/pkg/remote/openai/openai.go b/wavesrv/pkg/remote/openai/openai.go similarity index 100% rename from pkg/remote/openai/openai.go rename to wavesrv/pkg/remote/openai/openai.go diff --git a/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go similarity index 100% rename from pkg/remote/remote.go rename to wavesrv/pkg/remote/remote.go diff --git a/pkg/remote/updatequeue.go b/wavesrv/pkg/remote/updatequeue.go similarity index 100% rename from pkg/remote/updatequeue.go rename to wavesrv/pkg/remote/updatequeue.go diff --git a/pkg/rtnstate/rtnstate.go b/wavesrv/pkg/rtnstate/rtnstate.go similarity index 100% rename from pkg/rtnstate/rtnstate.go rename to wavesrv/pkg/rtnstate/rtnstate.go diff --git a/pkg/scbase/scbase.go b/wavesrv/pkg/scbase/scbase.go similarity index 100% rename from pkg/scbase/scbase.go rename to wavesrv/pkg/scbase/scbase.go diff --git a/pkg/scpacket/scpacket.go b/wavesrv/pkg/scpacket/scpacket.go similarity index 100% rename from pkg/scpacket/scpacket.go rename to wavesrv/pkg/scpacket/scpacket.go diff --git a/pkg/scws/scws.go b/wavesrv/pkg/scws/scws.go similarity index 100% rename from pkg/scws/scws.go rename to wavesrv/pkg/scws/scws.go diff --git a/pkg/shparse/comp.go b/wavesrv/pkg/shparse/comp.go similarity index 100% rename from pkg/shparse/comp.go rename to wavesrv/pkg/shparse/comp.go diff --git a/pkg/shparse/expand.go b/wavesrv/pkg/shparse/expand.go similarity index 100% rename from pkg/shparse/expand.go rename to wavesrv/pkg/shparse/expand.go diff --git a/pkg/shparse/extend.go b/wavesrv/pkg/shparse/extend.go similarity index 100% rename from pkg/shparse/extend.go rename to wavesrv/pkg/shparse/extend.go diff --git a/pkg/shparse/shparse.go b/wavesrv/pkg/shparse/shparse.go similarity index 100% rename from pkg/shparse/shparse.go rename to wavesrv/pkg/shparse/shparse.go diff --git a/pkg/shparse/shparse_test.go b/wavesrv/pkg/shparse/shparse_test.go similarity index 100% rename from pkg/shparse/shparse_test.go rename to wavesrv/pkg/shparse/shparse_test.go diff --git a/pkg/shparse/tokenize.go b/wavesrv/pkg/shparse/tokenize.go similarity index 100% rename from pkg/shparse/tokenize.go rename to wavesrv/pkg/shparse/tokenize.go diff --git a/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go similarity index 100% rename from pkg/sstore/dbops.go rename to wavesrv/pkg/sstore/dbops.go diff --git a/pkg/sstore/fileops.go b/wavesrv/pkg/sstore/fileops.go similarity index 100% rename from pkg/sstore/fileops.go rename to wavesrv/pkg/sstore/fileops.go diff --git a/pkg/sstore/map.go b/wavesrv/pkg/sstore/map.go similarity index 100% rename from pkg/sstore/map.go rename to wavesrv/pkg/sstore/map.go diff --git a/pkg/sstore/migrate.go b/wavesrv/pkg/sstore/migrate.go similarity index 100% rename from pkg/sstore/migrate.go rename to wavesrv/pkg/sstore/migrate.go diff --git a/pkg/sstore/quick.go b/wavesrv/pkg/sstore/quick.go similarity index 100% rename from pkg/sstore/quick.go rename to wavesrv/pkg/sstore/quick.go diff --git a/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go similarity index 100% rename from pkg/sstore/sstore.go rename to wavesrv/pkg/sstore/sstore.go diff --git a/pkg/sstore/sstore_migrate.go b/wavesrv/pkg/sstore/sstore_migrate.go similarity index 100% rename from pkg/sstore/sstore_migrate.go rename to wavesrv/pkg/sstore/sstore_migrate.go diff --git a/pkg/sstore/updatebus.go b/wavesrv/pkg/sstore/updatebus.go similarity index 100% rename from pkg/sstore/updatebus.go rename to wavesrv/pkg/sstore/updatebus.go diff --git a/pkg/utilfn/linediff.go b/wavesrv/pkg/utilfn/linediff.go similarity index 100% rename from pkg/utilfn/linediff.go rename to wavesrv/pkg/utilfn/linediff.go diff --git a/pkg/utilfn/utilfn.go b/wavesrv/pkg/utilfn/utilfn.go similarity index 100% rename from pkg/utilfn/utilfn.go rename to wavesrv/pkg/utilfn/utilfn.go diff --git a/pkg/utilfn/utilfn_test.go b/wavesrv/pkg/utilfn/utilfn_test.go similarity index 100% rename from pkg/utilfn/utilfn_test.go rename to wavesrv/pkg/utilfn/utilfn_test.go diff --git a/pkg/wsshell/wsshell.go b/wavesrv/pkg/wsshell/wsshell.go similarity index 100% rename from pkg/wsshell/wsshell.go rename to wavesrv/pkg/wsshell/wsshell.go diff --git a/scripthaus.md b/wavesrv/scripthaus.md similarity index 100% rename from scripthaus.md rename to wavesrv/scripthaus.md