fix: sshCmd closure bug + error display
CRITICAL BUG: sshConnectCmd wrapped tea.ExecProcess inside an extra closure, so Bubble Tea never executed the SSH process — Enter appeared to do nothing. Fix: - sshConnectCmd now returns tea.ExecProcess directly (no extra closure) - tea.ExecProcess handles all process lifecycle (start, PTY, exit) - sshExitMsg errors are shown in host list view (ErrorStyle) - Error routed via Model.Error → HostListTab.err → View()
This commit is contained in:
@@ -121,6 +121,13 @@ func (t *HostListTab) View() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
if t.err != nil {
|
||||||
|
b.WriteString(ErrorStyle.Render(fmt.Sprintf(" Error: %v ", t.err)))
|
||||||
|
b.WriteString("\n")
|
||||||
|
t.err = nil
|
||||||
|
}
|
||||||
|
|
||||||
// Capped vertical padding
|
// Capped vertical padding
|
||||||
b.WriteString(strings.Repeat("\n", 2))
|
b.WriteString(strings.Repeat("\n", 2))
|
||||||
|
|
||||||
|
|||||||
+86
-80
@@ -16,101 +16,107 @@ import (
|
|||||||
// sshConnectCmd builds and runs a native SSH command via tea.ExecProcess.
|
// sshConnectCmd builds and runs a native SSH command via tea.ExecProcess.
|
||||||
// Password auth: sshpass -e ssh user@host (SSHPASS env)
|
// Password auth: sshpass -e ssh user@host (SSHPASS env)
|
||||||
// Key auth: ssh -i <tmpfile> user@host (SSH_ASKPASS for passphrase)
|
// Key auth: ssh -i <tmpfile> user@host (SSH_ASKPASS for passphrase)
|
||||||
|
//
|
||||||
|
// Must return tea.ExecProcess directly (NOT wrapped in another closure)
|
||||||
|
// so Bubble Tea can execute the process command correctly.
|
||||||
func sshConnectCmd(host *models.Host, dataDir string) tea.Cmd {
|
func sshConnectCmd(host *models.Host, dataDir string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
port := host.Port
|
||||||
port := host.Port
|
if port == 0 {
|
||||||
if port == 0 {
|
port = 22
|
||||||
port = 22
|
}
|
||||||
|
portStr := strconv.Itoa(port)
|
||||||
|
target := fmt.Sprintf("%s@%s", host.Username, host.Hostname)
|
||||||
|
ctrlSock := fmt.Sprintf("/tmp/hk-%s", host.ID)
|
||||||
|
env := os.Environ()
|
||||||
|
|
||||||
|
// Common SSH args
|
||||||
|
sshArgs := []string{
|
||||||
|
"-p", portStr,
|
||||||
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"-o", "ServerAliveInterval=60",
|
||||||
|
"-o", "ServerAliveCountMax=3",
|
||||||
|
"-S", ctrlSock,
|
||||||
|
"-o", "ControlMaster=auto",
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func() {
|
||||||
|
exec.Command("ssh", "-S", ctrlSock, "-O", "exit", target).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch host.Auth.Type {
|
||||||
|
case "password":
|
||||||
|
allArgs := append([]string{"-e", "ssh"}, sshArgs...)
|
||||||
|
allArgs = append(allArgs, target)
|
||||||
|
cmd := exec.Command("sshpass", allArgs...)
|
||||||
|
cmd.Env = append(env, "SSHPASS="+host.Auth.Password)
|
||||||
|
return tea.ExecProcess(cmd, func(err error) tea.Msg {
|
||||||
|
cleanup()
|
||||||
|
return sshExitMsg{err: err}
|
||||||
|
})
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
keyContent, err := loadKeyContent(host, dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return errorCmd(fmt.Errorf("load key: %w", err))
|
||||||
}
|
}
|
||||||
portStr := strconv.Itoa(port)
|
tmpFile, err := os.CreateTemp("", "hk-key-*")
|
||||||
target := fmt.Sprintf("%s@%s", host.Username, host.Hostname)
|
if err != nil {
|
||||||
|
return errorCmd(fmt.Errorf("create temp key: %w", err))
|
||||||
var cmd *exec.Cmd
|
}
|
||||||
env := os.Environ()
|
tmpPath := tmpFile.Name()
|
||||||
|
if _, err := tmpFile.Write([]byte(keyContent)); err != nil {
|
||||||
ctrlSock := fmt.Sprintf("/tmp/hk-%s", host.ID)
|
|
||||||
|
|
||||||
switch host.Auth.Type {
|
|
||||||
case "password":
|
|
||||||
args := []string{
|
|
||||||
"-e",
|
|
||||||
"ssh",
|
|
||||||
"-p", portStr,
|
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
|
||||||
"-o", "ServerAliveInterval=60",
|
|
||||||
"-o", "ServerAliveCountMax=3",
|
|
||||||
"-S", ctrlSock,
|
|
||||||
"-o", "ControlMaster=auto",
|
|
||||||
target,
|
|
||||||
}
|
|
||||||
cmd = exec.Command("sshpass", args...)
|
|
||||||
env = append(env, "SSHPASS="+host.Auth.Password)
|
|
||||||
|
|
||||||
case "key":
|
|
||||||
keyContent, err := loadKeyContent(host, dataDir)
|
|
||||||
if err != nil {
|
|
||||||
return sshExitMsg{err: err}
|
|
||||||
}
|
|
||||||
tmpFile, err := os.CreateTemp("", "hk-key-*")
|
|
||||||
if err != nil {
|
|
||||||
return sshExitMsg{err: fmt.Errorf("create temp key: %w", err)}
|
|
||||||
}
|
|
||||||
tmpPath := tmpFile.Name()
|
|
||||||
if _, err := tmpFile.Write([]byte(keyContent)); err != nil {
|
|
||||||
tmpFile.Close()
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return sshExitMsg{err: fmt.Errorf("write temp key: %w", err)}
|
|
||||||
}
|
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
os.Chmod(tmpPath, 0600)
|
os.Remove(tmpPath)
|
||||||
|
return errorCmd(fmt.Errorf("write temp key: %w", err))
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Chmod(tmpPath, 0600)
|
||||||
|
|
||||||
args := []string{
|
keyArgs := append([]string{"-i", tmpPath}, sshArgs...)
|
||||||
"-i", tmpPath,
|
keyArgs = append(keyArgs, target)
|
||||||
"-p", portStr,
|
cmd := exec.Command("ssh", keyArgs...)
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
|
||||||
"-o", "ServerAliveInterval=60",
|
|
||||||
"-o", "ServerAliveCountMax=3",
|
|
||||||
"-S", ctrlSock,
|
|
||||||
"-o", "ControlMaster=auto",
|
|
||||||
target,
|
|
||||||
}
|
|
||||||
cmd = exec.Command("ssh", args...)
|
|
||||||
|
|
||||||
if host.Auth.Password != "" {
|
if host.Auth.Password != "" {
|
||||||
self, err := os.Executable()
|
self, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
script := fmt.Sprintf("#!/bin/sh\nexec %q askpass\n", self)
|
||||||
|
f, err := os.CreateTemp("", "hk-askpass-*.sh")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
script := fmt.Sprintf("#!/bin/sh\nexec %q askpass\n", self)
|
f.WriteString(script)
|
||||||
f, err := os.CreateTemp("", "hk-askpass-*.sh")
|
f.Close()
|
||||||
if err == nil {
|
os.Chmod(f.Name(), 0700)
|
||||||
f.WriteString(script)
|
env = append(env,
|
||||||
f.Close()
|
"HK_PASSPHRASE="+host.Auth.Password,
|
||||||
os.Chmod(f.Name(), 0700)
|
"SSH_ASKPASS="+f.Name(),
|
||||||
env = append(env,
|
"SSH_ASKPASS_REQUIRE=force",
|
||||||
"HK_PASSPHRASE="+host.Auth.Password,
|
)
|
||||||
"SSH_ASKPASS="+f.Name(),
|
if os.Getenv("DISPLAY") == "" {
|
||||||
"SSH_ASKPASS_REQUIRE=force",
|
env = append(env, "DISPLAY=:0")
|
||||||
)
|
}
|
||||||
if os.Getenv("DISPLAY") == "" {
|
if setsid, err := exec.LookPath("setsid"); err == nil {
|
||||||
env = append(env, "DISPLAY=:0")
|
newArgs := append([]string{"ssh"}, keyArgs...)
|
||||||
}
|
cmd = exec.Command(setsid, newArgs...)
|
||||||
if setsid, err := exec.LookPath("setsid"); err == nil {
|
|
||||||
newArgs := append([]string{"ssh"}, args...)
|
|
||||||
cmd = exec.Command(setsid, newArgs...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
return sshExitMsg{err: fmt.Errorf("unsupported auth type: %s", host.Auth.Type)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
return tea.ExecProcess(cmd, func(err error) tea.Msg {
|
return tea.ExecProcess(cmd, func(err error) tea.Msg {
|
||||||
// Cleanup ControlMaster socket
|
os.Remove(tmpPath)
|
||||||
exec.Command("ssh", "-S", ctrlSock, "-O", "exit", target).Run()
|
cleanup()
|
||||||
return sshExitMsg{err: err}
|
return sshExitMsg{err: err}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errorCmd(fmt.Errorf("unsupported auth type: %s", host.Auth.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorCmd returns a Cmd that sends an sshExitMsg with the given error.
|
||||||
|
func errorCmd(err error) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return sshExitMsg{err: err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, sshConnectCmd(msg.host, m.dataDir)
|
return m, sshConnectCmd(msg.host, m.dataDir)
|
||||||
|
|
||||||
case sshExitMsg:
|
case sshExitMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.Error = msg.err
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case openHostFormMsg:
|
case openHostFormMsg:
|
||||||
@@ -203,6 +206,14 @@ func (m *Model) View() string {
|
|||||||
return "No tabs open. Press 'q' to quit.\n"
|
return "No tabs open. Press 'q' to quit.\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass error to host list tab for display
|
||||||
|
if m.Error != nil {
|
||||||
|
if ht := FindHostListTab(m.tabs.tabs); ht != nil {
|
||||||
|
ht.err = m.Error
|
||||||
|
}
|
||||||
|
m.Error = nil
|
||||||
|
}
|
||||||
|
|
||||||
return m.tabs.View()
|
return m.tabs.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user