diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index 281b7bb0..7bd73b0d 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 503a7246..d3bf932b 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 ab56f0ce..9c58bbaa 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 1a7f293d..ed5b5c11 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 1d728517..c7e8f189 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 2c6ba812..326934a3 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 }