remote connect/disconnect working. fix issue with remoteconnected in resolver. working on remote:new

This commit is contained in:
sawka
2022-09-01 12:47:10 -07:00
parent d55bb8812b
commit 247647cea8
6 changed files with 236 additions and 34 deletions
+134 -1
View File
@@ -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"]
+3 -3
View File
@@ -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 {
+55 -20
View File
@@ -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 {
+29 -9
View File
@@ -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 {
+4
View File
@@ -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")
+11 -1
View File
@@ -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"`