From 247647cea81e9ba23f08e25d2cc22f07390f41aa Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 1 Sep 2022 12:47:10 -0700 Subject: [PATCH] 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 d265f600..b989bc4f 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 8a1c5c79..ca3e89fc 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 050ded8a..32293816 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 77673437..e747fe54 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 dafdf966..f67db53d 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 0e09578d..56569d32 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"`