feat: rewrite terminal to use vte_terminal_spawn_sync
Replaced the SSH library + IO thread + intermediate PTY bridge with vte_terminal_spawn_sync(). This eliminates the broken terminal behavior (backspace, Ctrl+C, copy/paste, bash completion) caused by the intermediate PTY with no process on the slave side. - SshTerminal now spawns 'ssh' as a VTE subprocess - Removed libssh dependency from terminal (still used for key validation) - Removed IO thread, manual PTY creation, and thread synchronization - VTE now handles all terminal I/O, line discipline, resize, etc. - Password saved: uses sshpass -e with SSHPASS env var if available - Password not saved: ssh prompts interactively in the terminal - Removed show_password_dialog (no longer needed) - Simplified connect flow in ssh_client.cpp
This commit is contained in:
+111
-312
@@ -1,24 +1,18 @@
|
||||
#include "ssh_terminal.h"
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/select.h>
|
||||
#include <signal.h>
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <glib.h>
|
||||
|
||||
SshTerminal::SshTerminal()
|
||||
: terminal_(nullptr), vte_box_(nullptr), vte_pty_(nullptr),
|
||||
session_(nullptr), channel_(nullptr),
|
||||
pty_master_(-1), pty_slave_(-1),
|
||||
running_(false), connected_(false), cleanup_done_(true), thread_(nullptr) {
|
||||
: vte_box_(nullptr), terminal_(nullptr),
|
||||
child_pid_(0), connected_(false) {
|
||||
|
||||
vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
terminal_ = VTE_TERMINAL(vte_terminal_new());
|
||||
gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0);
|
||||
gtk_widget_show_all(vte_box_);
|
||||
|
||||
g_signal_connect(terminal_, "size-allocate", G_CALLBACK(on_size_changed), this);
|
||||
g_signal_connect(terminal_, "child-exited", G_CALLBACK(on_child_exited), this);
|
||||
}
|
||||
|
||||
SshTerminal::~SshTerminal() {
|
||||
@@ -29,108 +23,17 @@ SshTerminal::~SshTerminal() {
|
||||
}
|
||||
}
|
||||
|
||||
void SshTerminal::on_size_changed(GtkWidget* widget, GtkAllocation* alloc, gpointer data) {
|
||||
(void)alloc;
|
||||
void SshTerminal::on_child_exited(VteTerminal* terminal, int status, gpointer data) {
|
||||
(void)terminal;
|
||||
auto* self = static_cast<SshTerminal*>(data);
|
||||
if (self->channel_ && self->connected_) {
|
||||
VteTerminal* term = VTE_TERMINAL(widget);
|
||||
guint cols = vte_terminal_get_column_count(term);
|
||||
guint rows = vte_terminal_get_row_count(term);
|
||||
ssh_channel_change_pty_size(self->channel_, cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
void SshTerminal::cleanup() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (cleanup_done_) return;
|
||||
cleanup_done_ = true;
|
||||
|
||||
if (channel_) {
|
||||
ssh_channel_close(channel_);
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
}
|
||||
if (session_) {
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
|
||||
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
gpointer SshTerminal::io_thread_func(gpointer data) {
|
||||
auto* self = static_cast<SshTerminal*>(data);
|
||||
|
||||
ssh_session sess = self->session_;
|
||||
ssh_channel chan = self->channel_;
|
||||
int ssh_fd = ssh_get_fd(sess);
|
||||
int slave_fd = self->pty_slave_;
|
||||
|
||||
if (ssh_fd < 0 || slave_fd < 0) {
|
||||
g_idle_add([](gpointer d) -> gboolean {
|
||||
auto* s = static_cast<SshTerminal*>(d);
|
||||
if (s->on_status_change) s->on_status_change(false, "SSH fd unavailable");
|
||||
return G_SOURCE_REMOVE;
|
||||
}, self);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
fd_set rfds;
|
||||
struct timeval tv;
|
||||
|
||||
while (self->running_) {
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(slave_fd, &rfds);
|
||||
FD_SET(ssh_fd, &rfds);
|
||||
int maxfd = (slave_fd > ssh_fd ? slave_fd : ssh_fd) + 1;
|
||||
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 50000;
|
||||
int ret = select(maxfd, &rfds, nullptr, nullptr, &tv);
|
||||
|
||||
if (ret < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
if (ret == 0) continue;
|
||||
|
||||
if (FD_ISSET(slave_fd, &rfds)) {
|
||||
ssize_t n = read(slave_fd, buf, sizeof(buf));
|
||||
if (n > 0) {
|
||||
if (ssh_channel_write(chan, buf, n) < 0) break;
|
||||
} else if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (FD_ISSET(ssh_fd, &rfds)) {
|
||||
int poll_result = ssh_channel_poll_timeout(chan, 50, 0);
|
||||
if (poll_result > 0) {
|
||||
ssize_t n = ssh_channel_read_timeout(chan, buf, sizeof(buf), 0, 0);
|
||||
if (n > 0) {
|
||||
if (write(slave_fd, buf, n) < 0 && errno != EINTR) break;
|
||||
} else if (n == SSH_EOF || n == SSH_ERROR) {
|
||||
break;
|
||||
}
|
||||
} else if (poll_result < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ssh_channel_is_eof(chan)) break;
|
||||
}
|
||||
|
||||
self->connected_ = false;
|
||||
g_idle_add([](gpointer d) -> gboolean {
|
||||
auto* s = static_cast<SshTerminal*>(d);
|
||||
if (s->on_status_change) s->on_status_change(false, "Disconnected");
|
||||
return G_SOURCE_REMOVE;
|
||||
}, self);
|
||||
self->child_pid_ = 0;
|
||||
|
||||
return nullptr;
|
||||
if (self->on_status_change) {
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "Process exited with status %d", status);
|
||||
self->on_status_change(false, msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool SshTerminal::connect(const std::string& host, int port, const std::string& user,
|
||||
@@ -138,232 +41,128 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
||||
std::string& out_error) {
|
||||
if (connected_) disconnect();
|
||||
|
||||
/* Reset state for a fresh connect attempt */
|
||||
cleanup_done_ = true;
|
||||
session_ = nullptr;
|
||||
channel_ = nullptr;
|
||||
pty_master_ = -1;
|
||||
pty_slave_ = -1;
|
||||
vte_pty_ = nullptr;
|
||||
g_message("Connecting to %s@%s:%d ...", user.c_str(), host.c_str(), port);
|
||||
|
||||
g_message("Connecting to %s:%d as %s ...", host.c_str(), port, user.c_str());
|
||||
/* Build ssh command arguments */
|
||||
std::vector<std::string> args;
|
||||
|
||||
g_message("ssh_new...");
|
||||
session_ = ssh_new();
|
||||
if (!session_) { out_error = "Failed to create SSH session"; g_warning("ssh_new() failed"); return false; }
|
||||
|
||||
g_message("ssh_options_set host...");
|
||||
ssh_options_set(session_, SSH_OPTIONS_HOST, host.c_str());
|
||||
unsigned int p = port;
|
||||
g_message("ssh_options_set port...");
|
||||
ssh_options_set(session_, SSH_OPTIONS_PORT, &p);
|
||||
g_message("ssh_options_set user...");
|
||||
ssh_options_set(session_, SSH_OPTIONS_USER, user.c_str());
|
||||
int strict = 0;
|
||||
g_message("ssh_options_set stricthostkeycheck...");
|
||||
ssh_options_set(session_, SSH_OPTIONS_STRICTHOSTKEYCHECK, &strict);
|
||||
|
||||
g_message("ssh_connect...");
|
||||
if (ssh_connect(session_) != SSH_OK) {
|
||||
std::string err = ssh_get_error(session_);
|
||||
g_warning("ssh_connect failed: %s", err.c_str());
|
||||
out_error = "Connection failed: " + err;
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, out_error);
|
||||
return false;
|
||||
/* Use sshpass if password is provided and sshpass is available */
|
||||
bool use_sshpass = false;
|
||||
if (!password.empty()) {
|
||||
if (g_find_program_in_path("sshpass")) {
|
||||
use_sshpass = true;
|
||||
args.push_back("sshpass");
|
||||
args.push_back("-e");
|
||||
g_message("Using sshpass for auto-login");
|
||||
} else {
|
||||
g_message("sshpass not found, ssh will prompt for password");
|
||||
}
|
||||
}
|
||||
g_message("ssh_connect OK");
|
||||
|
||||
g_message("ssh_session_update_known_hosts...");
|
||||
ssh_session_update_known_hosts(session_);
|
||||
g_message("known_hosts OK");
|
||||
args.push_back("ssh");
|
||||
args.push_back("-p");
|
||||
args.push_back(std::to_string(port));
|
||||
args.push_back("-o");
|
||||
args.push_back("StrictHostKeyChecking=no");
|
||||
args.push_back("-o");
|
||||
args.push_back("UserKnownHostsFile=/dev/null");
|
||||
args.push_back("-o");
|
||||
args.push_back("LogLevel=QUIET");
|
||||
|
||||
if (!keyfile.empty()) {
|
||||
g_message("Trying keyfile auth...");
|
||||
ssh_key key = nullptr;
|
||||
if (ssh_pki_import_privkey_file(keyfile.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) {
|
||||
if (ssh_userauth_publickey(session_, nullptr, key) == SSH_AUTH_SUCCESS) {
|
||||
ssh_key_free(key);
|
||||
goto auth_done;
|
||||
args.push_back("-i");
|
||||
args.push_back(keyfile);
|
||||
g_message("Using keyfile: %s", keyfile.c_str());
|
||||
}
|
||||
|
||||
args.push_back(user + "@" + host);
|
||||
|
||||
/* Convert to char** for spawn */
|
||||
std::vector<char*> c_args;
|
||||
for (auto& arg : args) {
|
||||
c_args.push_back(const_cast<char*>(arg.c_str()));
|
||||
}
|
||||
c_args.push_back(nullptr);
|
||||
|
||||
/* Set up environment with password if using sshpass */
|
||||
gchar** envp = nullptr;
|
||||
if (use_sshpass) {
|
||||
GHashTable* env_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
|
||||
/* Copy current environment */
|
||||
for (char** ep = environ; *ep; ep++) {
|
||||
gchar** parts = g_strsplit(*ep, "=", 2);
|
||||
if (parts[0] && parts[1]) {
|
||||
g_hash_table_insert(env_table, g_strdup(parts[0]), g_strdup(parts[1]));
|
||||
}
|
||||
ssh_key_free(key);
|
||||
g_strfreev(parts);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
g_message("ssh_userauth_password...");
|
||||
int rc = ssh_userauth_password(session_, nullptr, password.c_str());
|
||||
g_message("auth result: %d", rc);
|
||||
if (rc != SSH_AUTH_SUCCESS) {
|
||||
std::string err = ssh_get_error(session_);
|
||||
g_warning("Auth failed: %s", err.c_str());
|
||||
out_error = "Authentication failed: " + err;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, out_error);
|
||||
return false;
|
||||
g_hash_table_insert(env_table, g_strdup("SSHPASS"), g_strdup(password.c_str()));
|
||||
|
||||
/* Convert hash table to envp array */
|
||||
GHashTableIter iter;
|
||||
gpointer key, value;
|
||||
GArray* env_arr = g_array_new(TRUE, TRUE, sizeof(gchar*));
|
||||
|
||||
g_hash_table_iter_init(&iter, env_table);
|
||||
while (g_hash_table_iter_next(&iter, &key, &value)) {
|
||||
gchar* entry = g_strdup_printf("%s=%s", (const char*)key, (const char*)value);
|
||||
g_array_append_val(env_arr, entry);
|
||||
}
|
||||
envp = (gchar**)g_array_free(env_arr, FALSE);
|
||||
g_hash_table_destroy(env_table);
|
||||
}
|
||||
|
||||
auth_done:
|
||||
g_message("ssh_channel_new...");
|
||||
channel_ = ssh_channel_new(session_);
|
||||
if (!channel_) { out_error = "Channel creation failed"; g_warning("ssh_channel_new() failed"); return false; }
|
||||
/* Spawn ssh process */
|
||||
GError* error = nullptr;
|
||||
GPid child_pid = 0;
|
||||
|
||||
g_message("ssh_channel_open_session...");
|
||||
if (ssh_channel_open_session(channel_) != SSH_OK) {
|
||||
out_error = "Channel open failed";
|
||||
g_warning("ssh_channel_open_session failed");
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Session open failed");
|
||||
return false;
|
||||
g_message("Spawning: %s", args[0].c_str());
|
||||
|
||||
gboolean ok = vte_terminal_spawn_sync(
|
||||
terminal_,
|
||||
VTE_PTY_DEFAULT,
|
||||
nullptr, /* working directory */
|
||||
c_args.data(),
|
||||
envp, /* environment */
|
||||
(GSpawnFlags)(G_SPAWN_CHILD_INHERITS_STDIN),
|
||||
nullptr, /* child_setup */
|
||||
nullptr, /* child_setup_data */
|
||||
&child_pid,
|
||||
nullptr, /* cancellable */
|
||||
&error
|
||||
);
|
||||
|
||||
if (envp) {
|
||||
for (gchar** p = envp; *p; p++) g_free(*p);
|
||||
g_free(envp);
|
||||
}
|
||||
g_message("channel open OK");
|
||||
|
||||
guint cols = vte_terminal_get_column_count(terminal_);
|
||||
guint rows = vte_terminal_get_row_count(terminal_);
|
||||
g_message("requesting PTY %dx%d...", cols, rows);
|
||||
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
|
||||
out_error = "PTY request failed";
|
||||
g_warning("PTY request failed");
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY request failed");
|
||||
if (!ok) {
|
||||
out_error = error ? error->message : "Failed to spawn SSH process";
|
||||
g_warning("spawn failed: %s", out_error.c_str());
|
||||
if (error) g_error_free(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_message("requesting shell...");
|
||||
if (ssh_channel_request_shell(channel_) != SSH_OK) {
|
||||
out_error = "Shell request failed";
|
||||
g_warning("Shell request failed");
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Shell request failed");
|
||||
return false;
|
||||
}
|
||||
g_message("shell OK");
|
||||
|
||||
g_message("openpty (posix_openpt)...");
|
||||
pty_master_ = posix_openpt(O_RDWR | O_NOCTTY);
|
||||
if (pty_master_ < 0) {
|
||||
g_warning("posix_openpt failed: %s", strerror(errno));
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY master open failed");
|
||||
return false;
|
||||
}
|
||||
g_message("grantpt...");
|
||||
if (grantpt(pty_master_) < 0) {
|
||||
g_warning("grantpt failed: %s", strerror(errno));
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY grantpt failed");
|
||||
return false;
|
||||
}
|
||||
g_message("unlockpt...");
|
||||
if (unlockpt(pty_master_) < 0) {
|
||||
g_warning("unlockpt failed: %s", strerror(errno));
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY unlockpt failed");
|
||||
return false;
|
||||
}
|
||||
const char* slave_name = ptsname(pty_master_);
|
||||
g_message("slave name: %s", slave_name ? slave_name : "(null)");
|
||||
if (!slave_name) {
|
||||
g_warning("ptsname failed");
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY ptsname failed");
|
||||
return false;
|
||||
}
|
||||
g_message("opening slave %s...", slave_name);
|
||||
pty_slave_ = open(slave_name, O_RDWR | O_NOCTTY);
|
||||
if (pty_slave_ < 0) {
|
||||
g_warning("slave open failed: %s", strerror(errno));
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY slave open failed");
|
||||
return false;
|
||||
}
|
||||
g_message("PTY ready (master=%d, slave=%d)", pty_master_, pty_slave_);
|
||||
|
||||
g_message("vte_pty_new_foreign_sync...");
|
||||
vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
|
||||
if (vte_pty_) {
|
||||
g_message("setting VTE pty...");
|
||||
vte_terminal_set_pty(terminal_, vte_pty_);
|
||||
} else {
|
||||
close(pty_slave_); pty_slave_ = -1;
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "VTE pty setup failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
cleanup_done_ = false;
|
||||
child_pid_ = child_pid;
|
||||
connected_ = true;
|
||||
running_ = true;
|
||||
g_message("SSH connection established, starting IO thread");
|
||||
thread_ = g_thread_new("ssh_io", io_thread_func, this);
|
||||
|
||||
if (on_status_change) on_status_change(true, "Connected to " + user + "@" + host);
|
||||
g_message("SSH process spawned (PID %d)", (int)child_pid);
|
||||
|
||||
if (on_status_change)
|
||||
on_status_change(true, "Connected to " + user + "@" + host + ":" + std::to_string(port));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SshTerminal::disconnect() {
|
||||
running_ = false;
|
||||
|
||||
if (thread_) {
|
||||
g_thread_join(thread_);
|
||||
thread_ = nullptr;
|
||||
if (child_pid_ > 0) {
|
||||
g_message("Killing SSH process (PID %d)", (int)child_pid_);
|
||||
kill(child_pid_, SIGHUP);
|
||||
g_spawn_close_pid(child_pid_);
|
||||
child_pid_ = 0;
|
||||
}
|
||||
|
||||
/* Flush any pending idle callbacks from the IO thread
|
||||
that might still reference this object */
|
||||
while (g_main_context_pending(NULL))
|
||||
g_main_context_iteration(NULL, FALSE);
|
||||
|
||||
if (vte_pty_) {
|
||||
g_object_unref(vte_pty_);
|
||||
vte_pty_ = nullptr;
|
||||
if (terminal_) vte_terminal_set_pty(terminal_, nullptr);
|
||||
}
|
||||
|
||||
cleanup();
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user