fix: critical bugs + .desktop file + system install

Bug fixes:
- Fixed PTY bridge: IO thread now uses pty_slave_ instead of pty_master_
  (remote output was never reaching the terminal)
- Fixed double-delete crash on tab close by removing page-removed callback
- Fixed port being ignored in password dialog (always used port 22)
- Added EINTR handling in select() for IO thread
- Added mutex for thread-safe cleanup
- Added try/catch for std::stoi in server config parsing
- VtePty lifecycle now managed properly (no double-close)
- Redundant ssh_channel_poll_timeout call removed

New:
- Added .desktop file for system-wide application menu entry
- Makefile.am installs desktop file to $(datadir)/applications
- Makefile.am installs icon to $(datadir)/icons/hicolor/256x256/apps
This commit is contained in:
x3
2026-09-07 15:00:08 +00:00
parent 77f9568cb1
commit 4a2b2eac33
7 changed files with 109 additions and 72 deletions
+9 -1
View File
@@ -1,2 +1,10 @@
SUBDIRS = src SUBDIRS = src
EXTRA_DIST = README.md EXTRA_DIST = README.md desktop/x3-ssh.desktop icons/x3-ssh.png
# Install desktop file to system applications directory
applicationsdir = $(datadir)/applications
dist_applications_DATA = desktop/x3-ssh.desktop
# Install icon to system hicolor theme
iconsdir = $(datadir)/icons/hicolor/256x256/apps
dist_icons_DATA = icons/x3-ssh.png
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=x3-ssh
GenericName=SSH Client
Comment=A GTK3 SSH client with tabbed terminal, server management and key storage
Exec=x3-ssh
Icon=x3-ssh
Terminal=false
Categories=Network;RemoteAccess;SSH;
Keywords=ssh;remote;terminal;shell;secure;
StartupNotify=true
+1 -1
View File
@@ -52,7 +52,7 @@ void ServerStore::load() {
std::string key = trim(line.substr(0, eq)); std::string key = trim(line.substr(0, eq));
std::string val = trim(line.substr(eq + 1)); std::string val = trim(line.substr(eq + 1));
if (key == "host") current.host = val; if (key == "host") current.host = val;
else if (key == "port") current.port = std::stoi(val); else if (key == "port") { try { current.port = std::stoi(val); } catch (...) { current.port = 22; } }
else if (key == "user") current.user = val; else if (key == "user") current.user = val;
else if (key == "keyfile") current.keyfile = val; else if (key == "keyfile") current.keyfile = val;
else if (key == "use_key") current.use_key = (val == "true"); else if (key == "use_key") current.use_key = (val == "true");
+32 -34
View File
@@ -52,7 +52,10 @@ SshClientWindow::SshClientWindow()
} }
SshClientWindow::~SshClientWindow() { SshClientWindow::~SshClientWindow() {
for (auto* t : terminals_) delete t; for (auto* t : terminals_) {
t->disconnect();
delete t;
}
terminals_.clear(); terminals_.clear();
if (window_) gtk_widget_destroy(window_); if (window_) gtk_widget_destroy(window_);
} }
@@ -67,7 +70,7 @@ void SshClientWindow::build_ui() {
const char* icon_paths[] = { const char* icon_paths[] = {
"icons/x3-ssh.png", "icons/x3-ssh.png",
"/usr/share/icons/x3-ssh.png", "/usr/share/icons/hicolor/256x256/apps/x3-ssh.png",
"/usr/local/share/icons/x3-ssh.png", "/usr/local/share/icons/x3-ssh.png",
nullptr nullptr
}; };
@@ -133,7 +136,6 @@ void SshClientWindow::build_toolbar() {
void SshClientWindow::build_notebook() { void SshClientWindow::build_notebook() {
notebook_ = gtk_notebook_new(); notebook_ = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE); gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
g_signal_connect(notebook_, "page-removed", G_CALLBACK(on_tab_close), this);
} }
void SshClientWindow::build_statusbar() { void SshClientWindow::build_statusbar() {
@@ -183,31 +185,40 @@ void SshClientWindow::close_current_tab() {
void SshClientWindow::close_tab_at(int idx) { void SshClientWindow::close_tab_at(int idx) {
GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx); GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
if (!page) return; if (!page) return;
/* Find and disconnect the terminal first, before removing from notebook */
SshTerminal* found = nullptr;
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) { for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if ((*it)->widget() == page) { if ((*it)->widget() == page) {
(*it)->disconnect(); found = *it;
delete *it;
terminals_.erase(it); terminals_.erase(it);
break; break;
} }
} }
if (found) {
found->disconnect();
delete found;
}
/* Now safe to remove page — no page-removed callback will cause issues */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx); gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
} }
void SshClientWindow::on_tab_close(GtkNotebook* nb, GtkWidget* page, guint idx, gpointer d) { void SshClientWindow::disconnect() {
(void)nb; (void)idx; int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_));
auto* self = static_cast<SshClientWindow*>(d); if (idx < 0) return;
for (auto it = self->terminals_.begin(); it != self->terminals_.end(); ++it) { GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
if ((*it)->widget() == page) { for (auto* t : terminals_) {
(*it)->disconnect(); if (t->widget() == page) {
delete *it; t->disconnect();
self->terminals_.erase(it); update_status("Disconnected");
return; return;
} }
} }
} }
/* ─── connect / disconnect ─────────────────────────────────── */ /* ─── connect ─────────────────────────────────────────────── */
void SshClientWindow::connect_to_server(const std::string& host, int port, void SshClientWindow::connect_to_server(const std::string& host, int port,
const std::string& user, const std::string& keyfile, const std::string& user, const std::string& keyfile,
@@ -239,19 +250,6 @@ void SshClientWindow::connect_to_server(const std::string& host, int port,
} }
} }
void SshClientWindow::disconnect() {
int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_));
if (idx < 0) return;
GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
for (auto* t : terminals_) {
if (t->widget() == page) {
t->disconnect();
update_status("Disconnected");
return;
}
}
}
/* ─── quick connect ───────────────────────────────────────── */ /* ─── quick connect ───────────────────────────────────────── */
void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) { void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
@@ -273,17 +271,17 @@ void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name); self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
return; return;
} }
self->show_password_dialog(s.host, s.user, s.keyfile, s.name); self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
return; return;
} }
self->show_password_dialog(host, u, "", host); self->show_password_dialog(host, port, u, "", host);
} }
/* ─── password dialog ─────────────────────────────────────── */ /* ─── password dialog ─────────────────────────────────────── */
void SshClientWindow::show_password_dialog(const std::string& host, const std::string& user, void SshClientWindow::show_password_dialog(const std::string& host, int port,
const std::string& keyfile, const std::string& name) { const std::string& user, const std::string& keyfile, const std::string& name) {
GtkWidget* dialog = gtk_dialog_new_with_buttons( GtkWidget* dialog = gtk_dialog_new_with_buttons(
"SSH Authentication", GTK_WINDOW(window_), "SSH Authentication", GTK_WINDOW(window_),
@@ -303,7 +301,7 @@ void SshClientWindow::show_password_dialog(const std::string& host, const std::s
GtkWidget* info_label = gtk_label_new(nullptr); GtkWidget* info_label = gtk_label_new(nullptr);
char markup[512]; char markup[512];
snprintf(markup, sizeof(markup), "<b>Connect to %s@%s</b>", user.c_str(), host.c_str()); snprintf(markup, sizeof(markup), "<b>Connect to %s@%s:%d</b>", user.c_str(), host.c_str(), port);
gtk_label_set_markup(GTK_LABEL(info_label), markup); gtk_label_set_markup(GTK_LABEL(info_label), markup);
gtk_grid_attach(GTK_GRID(grid), info_label, 0, 0, 2, 1); gtk_grid_attach(GTK_GRID(grid), info_label, 0, 0, 2, 1);
@@ -322,7 +320,7 @@ void SshClientWindow::show_password_dialog(const std::string& host, const std::s
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
const char* password = gtk_entry_get_text(GTK_ENTRY(pass_entry)); const char* password = gtk_entry_get_text(GTK_ENTRY(pass_entry));
connect_to_server(host, 22, user, keyfile, password ? password : "", name); connect_to_server(host, port, user, keyfile, password ? password : "", name);
} }
gtk_widget_destroy(dialog); gtk_widget_destroy(dialog);
} }
@@ -662,7 +660,7 @@ void SshClientWindow::on_close_tab(GtkWidget*, gpointer d) {
void SshClientWindow::on_connect(GtkWidget*, gpointer d) { void SshClientWindow::on_connect(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d); auto* self = static_cast<SshClientWindow*>(d);
self->show_password_dialog("localhost", "root", "", "Local"); self->show_password_dialog("localhost", 22, "root", "", "Local");
} }
void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) { void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) {
+1 -2
View File
@@ -30,7 +30,7 @@ private:
void show_server_dialog(int edit_index = -1); void show_server_dialog(int edit_index = -1);
void show_key_dialog(); void show_key_dialog();
void show_password_dialog(const std::string& host, const std::string& user, void show_password_dialog(const std::string& host, int port, const std::string& user,
const std::string& keyfile, const std::string& name); const std::string& keyfile, const std::string& name);
void update_server_combo(); void update_server_combo();
void update_status(const std::string& msg); void update_status(const std::string& msg);
@@ -42,7 +42,6 @@ private:
static void on_manage_servers(GtkWidget* w, gpointer d); static void on_manage_servers(GtkWidget* w, gpointer d);
static void on_manage_keys(GtkWidget* w, gpointer d); static void on_manage_keys(GtkWidget* w, gpointer d);
static void on_quick_connect(GtkWidget* w, gpointer d); static void on_quick_connect(GtkWidget* w, gpointer d);
static void on_tab_close(GtkNotebook* nb, GtkWidget* page, guint idx, gpointer d);
GtkWidget* window_; GtkWidget* window_;
GtkWidget* notebook_; GtkWidget* notebook_;
+51 -34
View File
@@ -4,19 +4,19 @@
#include <sys/select.h> #include <sys/select.h>
#include <signal.h> #include <signal.h>
#include <cstring> #include <cstring>
#include <cerrno>
SshTerminal::SshTerminal() SshTerminal::SshTerminal()
: terminal_(nullptr), vte_box_(nullptr), session_(nullptr), channel_(nullptr), : terminal_(nullptr), vte_box_(nullptr), vte_pty_(nullptr),
pty_master_(-1), pty_slave_(-1), running_(false), connected_(false), thread_(nullptr) { session_(nullptr), channel_(nullptr),
pty_master_(-1), pty_slave_(-1),
running_(false), connected_(false), cleanup_done_(true), thread_(nullptr) {
vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); vte_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
terminal_ = VTE_TERMINAL(vte_terminal_new()); terminal_ = VTE_TERMINAL(vte_terminal_new());
gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vte_box_), GTK_WIDGET(terminal_), TRUE, TRUE, 0);
gtk_widget_show_all(vte_box_); gtk_widget_show_all(vte_box_);
VtePty *pty = vte_terminal_pty_new_sync(terminal_, VTE_PTY_DEFAULT, nullptr, nullptr);
(void)pty;
g_signal_connect(terminal_, "size-changed", G_CALLBACK(on_size_changed), this); g_signal_connect(terminal_, "size-changed", G_CALLBACK(on_size_changed), this);
} }
@@ -34,6 +34,10 @@ void SshTerminal::on_size_changed(VteTerminal* term, guint cols, guint rows, gpo
} }
void SshTerminal::cleanup() { void SshTerminal::cleanup() {
std::lock_guard<std::mutex> lock(mutex_);
if (cleanup_done_) return;
cleanup_done_ = true;
if (channel_) { if (channel_) {
ssh_channel_close(channel_); ssh_channel_close(channel_);
ssh_channel_free(channel_); ssh_channel_free(channel_);
@@ -44,19 +48,23 @@ void SshTerminal::cleanup() {
ssh_free(session_); ssh_free(session_);
session_ = nullptr; session_ = nullptr;
} }
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; } if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
connected_ = false; connected_ = false;
} }
gpointer SshTerminal::io_thread_func(gpointer data) { gpointer SshTerminal::io_thread_func(gpointer data) {
auto* self = static_cast<SshTerminal*>(data); auto* self = static_cast<SshTerminal*>(data);
int ssh_fd = ssh_get_fd(self->session_);
if (ssh_fd < 0) { ssh_session sess = self->session_;
self->cleanup(); 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 { g_idle_add([](gpointer d) -> gboolean {
auto* s = static_cast<SshTerminal*>(d); auto* s = static_cast<SshTerminal*>(d);
if (s->on_status_change) s->on_status_change(false, "SSH get_fd failed"); if (s->on_status_change) s->on_status_change(false, "SSH fd unavailable");
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
}, self); }, self);
return nullptr; return nullptr;
@@ -68,40 +76,44 @@ gpointer SshTerminal::io_thread_func(gpointer data) {
while (self->running_) { while (self->running_) {
FD_ZERO(&rfds); FD_ZERO(&rfds);
FD_SET(self->pty_master_, &rfds); FD_SET(slave_fd, &rfds);
FD_SET(ssh_fd, &rfds); FD_SET(ssh_fd, &rfds);
int maxfd = (self->pty_master_ > ssh_fd ? self->pty_master_ : ssh_fd) + 1; int maxfd = (slave_fd > ssh_fd ? slave_fd : ssh_fd) + 1;
tv.tv_sec = 0; tv.tv_sec = 0;
tv.tv_usec = 50000; tv.tv_usec = 50000;
int ret = select(maxfd, &rfds, nullptr, nullptr, &tv); int ret = select(maxfd, &rfds, nullptr, nullptr, &tv);
if (ret < 0) break; if (ret < 0) {
if (errno == EINTR) continue;
break;
}
if (ret == 0) continue; if (ret == 0) continue;
if (FD_ISSET(self->pty_master_, &rfds)) { if (FD_ISSET(slave_fd, &rfds)) {
ssize_t n = read(self->pty_master_, buf, sizeof(buf)); ssize_t n = read(slave_fd, buf, sizeof(buf));
if (n > 0) { if (n > 0) {
ssh_channel_write(self->channel_, buf, n); if (ssh_channel_write(chan, buf, n) < 0) break;
} else if (n <= 0) { } else if (n <= 0) {
break; break;
} }
} }
if (FD_ISSET(ssh_fd, &rfds)) { if (FD_ISSET(ssh_fd, &rfds)) {
if (ssh_channel_poll_timeout(self->channel_, 0, 0) > 0) { int poll_result = ssh_channel_poll_timeout(chan, 50, 0);
ssize_t n = ssh_channel_read_timeout(self->channel_, buf, sizeof(buf), 0, 0); if (poll_result > 0) {
ssize_t n = ssh_channel_read_timeout(chan, buf, sizeof(buf), 0, 0);
if (n > 0) { if (n > 0) {
write(self->pty_master_, buf, n); if (write(slave_fd, buf, n) < 0 && errno != EINTR) break;
} else if (n == SSH_EOF || n == SSH_ERROR) { } else if (n == SSH_EOF || n == SSH_ERROR) {
break; break;
} }
} else if (ssh_channel_poll_timeout(self->channel_, 0, 0) < 0) { } else if (poll_result < 0) {
break; break;
} }
} }
if (ssh_channel_is_eof(self->channel_)) break; if (ssh_channel_is_eof(chan)) break;
} }
self->connected_ = false; self->connected_ = false;
@@ -193,18 +205,19 @@ auth_done:
return false; return false;
} }
VtePty *pty = vte_terminal_get_pty(terminal_); /* VTE gets the master side of the pty. It reads terminal output from
if (pty) { master and writes user input to master. Our IO thread uses the slave
int vte_fd = vte_pty_get_fd(pty); side to bridge between the pty and the SSH channel. */
if (vte_fd >= 0) close(vte_fd); vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
} if (vte_pty_) {
vte_terminal_set_pty(terminal_, nullptr); vte_terminal_set_pty(terminal_, vte_pty_);
} else {
VtePty *new_pty = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr); cleanup();
if (new_pty) { if (on_status_change) on_status_change(false, "VTE pty setup failed");
vte_terminal_set_pty(terminal_, new_pty); return false;
} }
cleanup_done_ = false;
connected_ = true; connected_ = true;
running_ = true; running_ = true;
thread_ = g_thread_new("ssh_io", io_thread_func, this); thread_ = g_thread_new("ssh_io", io_thread_func, this);
@@ -216,13 +229,17 @@ auth_done:
void SshTerminal::disconnect() { void SshTerminal::disconnect() {
running_ = false; running_ = false;
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
if (thread_) { if (thread_) {
g_thread_join(thread_); g_thread_join(thread_);
thread_ = nullptr; thread_ = nullptr;
} }
/* Clean up VTE pty first (releases master fd ownership) */
if (vte_pty_) {
g_object_unref(vte_pty_);
vte_pty_ = nullptr;
vte_terminal_set_pty(terminal_, nullptr);
}
cleanup(); cleanup();
} }
+4
View File
@@ -4,6 +4,7 @@
#include <libssh/libssh.h> #include <libssh/libssh.h>
#include <atomic> #include <atomic>
#include <string> #include <string>
#include <mutex>
#include <functional> #include <functional>
class SshTerminal { class SshTerminal {
@@ -28,12 +29,15 @@ private:
GtkWidget* vte_box_; GtkWidget* vte_box_;
VteTerminal* terminal_; VteTerminal* terminal_;
VtePty* vte_pty_;
ssh_session session_; ssh_session session_;
ssh_channel channel_; ssh_channel channel_;
int pty_master_; int pty_master_;
int pty_slave_; int pty_slave_;
std::atomic<bool> running_; std::atomic<bool> running_;
std::atomic<bool> connected_; std::atomic<bool> connected_;
std::atomic<bool> cleanup_done_;
GThread* thread_; GThread* thread_;
std::mutex mutex_;
std::string server_name_; std::string server_name_;
}; };