Skip to content

Commit

Permalink
BUGFIX-11: Fix breaking cmd.exe on windows platform (#14)
Browse files Browse the repository at this point in the history
BUGFIX-11: Fix breaking cmd.exe on windows platform
  • Loading branch information
grafviktor authored Nov 23, 2023
1 parent 41df624 commit 6f25b1c
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 57 deletions.
6 changes: 6 additions & 0 deletions cmd/goto/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func main() {
fmt.Printf("%+v\n", err)
}

// Check if "ssh" utility is in application path
if err := utils.CheckAppInstalled("ssh"); err != nil {
log.Fatalf("ssh utility is not installed or cannot be found in the executable path: %v", err)
}

commandLineParams := config.User{}
displayApplicationDetailsAndExit := false
// Command line parameters have the highest precedence
Expand All @@ -46,6 +51,7 @@ func main() {
flag.Parse()

var err error
// Get application home folder path
commandLineParams.AppHome, err = utils.GetAppDir(appName, commandLineParams.AppHome)
if err != nil {
log.Fatalf("Can't get application home folder: %v", err)
Expand Down
38 changes: 0 additions & 38 deletions internal/connector/ssh/connect.go

This file was deleted.

2 changes: 1 addition & 1 deletion internal/ui/component/edithost/edit_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func (m editModel) inputsView() string {
}

func (m editModel) headerView() string {
return titleStyle.Render("add a new host")
return titleStyle.Render("edit host")
}

func (m editModel) helpView() string {
Expand Down
23 changes: 5 additions & 18 deletions internal/ui/component/hostlist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
"github.com/charmbracelet/lipgloss"
"golang.org/x/exp/slices"

"github.com/grafviktor/goto/internal/connector/ssh"
"github.com/grafviktor/goto/internal/model"
"github.com/grafviktor/goto/internal/state"
"github.com/grafviktor/goto/internal/storage"
"github.com/grafviktor/goto/internal/ui/message"
"github.com/grafviktor/goto/internal/utils"
"github.com/grafviktor/goto/internal/utils/ssh"
)

var (
Expand Down Expand Up @@ -236,24 +237,10 @@ func (m ListModel) executeCmd(_ tea.Msg) (ListModel, tea.Cmd) {
return m, message.TeaCmd(msgErrorOccured{err})
}

connectSSHCmd := ssh.Connect(host)
return m, tea.ExecProcess(connectSSHCmd, func(err error) tea.Msg {
// return m, tea.ExecProcess(exec.Command("ping", "-t", "localhost"), func(err error) tea.Msg {
command := ssh.ConstructCMD(ssh.BaseCMD(), utils.HostModelToOptionsAdaptor(host))
process := utils.BuildProcess(command)
return m, tea.ExecProcess(process, func(err error) tea.Msg {
if err != nil {
/*
* That's to attempt to restore windows terminal when user pressed ctrl+c when using SSH connection.
* It works, when we close SSH, however it breaks all subsequent ssh connections
*/
/*
if runtime.GOOS == "windows" {
// If try to connect to a remote host and instead of typing a password, type "CTRL+C",
// the application UI will be broken. Flushing terminal window, helps to resolve the problem.
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
}
*/

return msgErrorOccured{err}
}

Expand Down
7 changes: 7 additions & 0 deletions internal/utils/ssh/cmd_nix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package ssh

func BaseCMD() string {
return "ssh"
}
7 changes: 7 additions & 0 deletions internal/utils/ssh/cmd_win.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build windows

package ssh

func BaseCMD() string {
return "cmd /c ssh"
}
57 changes: 57 additions & 0 deletions internal/utils/ssh/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ssh

import (
"fmt"
"strings"
)

type CommandLineOption interface{}

type (
OptionPrivateKey struct{ Value string }
OptionRemotePort struct{ Value string }
OptionLoginName struct{ Value string }
OptionAddress struct{ Value string }
)

func constructKeyValueOption(optionFlag, optionValue string) string {
optionValue = strings.TrimSpace(optionValue)
if optionValue != "" {
return fmt.Sprintf(" %s %s", optionFlag, optionValue)
}
return ""
}

func addOption(sb *strings.Builder, rawParameter CommandLineOption) {
var option string
switch p := rawParameter.(type) {
case OptionPrivateKey:
option = constructKeyValueOption("-i", p.Value)
case OptionRemotePort:
option = constructKeyValueOption("-p", p.Value)
case OptionLoginName:
option = constructKeyValueOption("-l", p.Value)
case OptionAddress:
if p.Value != "" {
option = fmt.Sprintf(" %s", p.Value)
}
default:
return
}

sb.WriteString(option)
}

// ConstructCMD - build connect command from main app and its arguments
// cmd - main executable
// options - set of command line options. See Option... public variables.
func ConstructCMD(cmd string, options ...CommandLineOption) string {
sb := strings.Builder{}
sb.WriteString(cmd)

for _, argument := range options {
addOption(&sb, argument)
}

return sb.String()
}
123 changes: 123 additions & 0 deletions internal/utils/ssh/connect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ssh

import (
"strings"
"testing"
)

func Test_ConstructKeyValueOption(t *testing.T) {
tests := []struct {
name string
optionFlag string
optionValue string
expectedResult string
}{
{
name: "Option with value",
optionFlag: "-i",
optionValue: "private_key",
expectedResult: " -i private_key",
},
{
name: "Option with empty value",
optionFlag: "-p",
optionValue: "",
expectedResult: "",
},
{
name: "Option with space-padded value",
optionFlag: "-l",
optionValue: " login_name ",
expectedResult: " -l login_name",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := constructKeyValueOption(tt.optionFlag, tt.optionValue)

if result != tt.expectedResult {
t.Errorf("Expected result %s, but got %s", tt.expectedResult, result)
}
})
}
}

func Test_AddOption(t *testing.T) {
tests := []struct {
name string
rawParameter CommandLineOption
expectedResult string
}{
{
name: "OptionPrivateKey with value",
rawParameter: OptionPrivateKey{Value: "private_key"},
expectedResult: " -i private_key",
},
{
name: "OptionRemotePort with empty value",
rawParameter: OptionRemotePort{Value: ""},
expectedResult: "",
},
{
name: "OptionLoginName with value",
rawParameter: OptionLoginName{Value: "login_name"},
expectedResult: " -l login_name",
},
{
name: "OptionAddress with empty value",
rawParameter: OptionAddress{Value: ""},
expectedResult: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sb strings.Builder
addOption(&sb, tt.rawParameter)

result := sb.String()
if result != tt.expectedResult {
t.Errorf("Expected result %s, but got %s", tt.expectedResult, result)
}
})
}
}

func Test_ConstructCMD(t *testing.T) {
tests := []struct {
name string
cmd string
options []CommandLineOption
expectedResult string
}{
{
name: "Command with Options",
cmd: "ssh",
options: []CommandLineOption{OptionPrivateKey{Value: "private_key"}, OptionRemotePort{Value: "22"}},
expectedResult: "ssh -i private_key -p 22",
},
{
name: "Command without Options",
cmd: "ls",
options: []CommandLineOption{},
expectedResult: "ls",
},
{
name: "Command with Address Option",
cmd: "ping",
options: []CommandLineOption{OptionAddress{Value: "example.com"}},
expectedResult: "ping example.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ConstructCMD(tt.cmd, tt.options...)

if result != tt.expectedResult {
t.Errorf("Expected result %s, but got %s", tt.expectedResult, result)
}
})
}
}
37 changes: 37 additions & 0 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package utils
import (
"errors"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strings"

"github.com/grafviktor/goto/internal/model"
"github.com/grafviktor/goto/internal/utils/ssh"
)

type Logger interface {
Expand Down Expand Up @@ -65,3 +70,35 @@ func GetCurrentOSUser() string {

return user.Username
}

// CheckAppInstalled - checks if application is installed and can be found in executable path
// appName - name of the application to be looked for in $PATH
func CheckAppInstalled(appName string) error {
_, err := exec.LookPath(appName)

return err
}

// HostModelToOptionsAdaptor - extract values from model.Host into a set of ssh.CommandLineOption
// host - model.Host to be adapted
// returns []ssh.CommandLineOption
func HostModelToOptionsAdaptor(host model.Host) []ssh.CommandLineOption {
return []ssh.CommandLineOption{
ssh.OptionAddress{Value: host.Address},
ssh.OptionLoginName{Value: host.LoginName},
ssh.OptionRemotePort{Value: host.RemotePort},
ssh.OptionPrivateKey{Value: host.PrivateKeyPath},
}
}

func BuildProcess(cmd string) *exec.Cmd {
if strings.TrimSpace(cmd) == "" {
return nil
}

commandWithArguments := strings.Split(cmd, " ")
command := commandWithArguments[0]
arguments := commandWithArguments[1:]

return exec.Command(command, arguments...)
}
Loading

0 comments on commit 6f25b1c

Please sign in to comment.