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
+51 -34
View File
@@ -4,19 +4,19 @@
#include <sys/select.h>
#include <signal.h>
#include <cstring>
#include <cerrno>
SshTerminal::SshTerminal()
: terminal_(nullptr), vte_box_(nullptr), session_(nullptr), channel_(nullptr),
pty_master_(-1), pty_slave_(-1), running_(false), connected_(false), thread_(nullptr) {
: 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_ = 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_);
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);
}
@@ -34,6 +34,10 @@ void SshTerminal::on_size_changed(VteTerminal* term, guint cols, guint rows, gpo
}
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_);
@@ -44,19 +48,23 @@ void SshTerminal::cleanup() {
ssh_free(session_);
session_ = nullptr;
}
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
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);
int ssh_fd = ssh_get_fd(self->session_);
if (ssh_fd < 0) {
self->cleanup();
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 get_fd failed");
if (s->on_status_change) s->on_status_change(false, "SSH fd unavailable");
return G_SOURCE_REMOVE;
}, self);
return nullptr;
@@ -68,40 +76,44 @@ gpointer SshTerminal::io_thread_func(gpointer data) {
while (self->running_) {
FD_ZERO(&rfds);
FD_SET(self->pty_master_, &rfds);
FD_SET(slave_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_usec = 50000;
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 (FD_ISSET(self->pty_master_, &rfds)) {
ssize_t n = read(self->pty_master_, buf, sizeof(buf));
if (FD_ISSET(slave_fd, &rfds)) {
ssize_t n = read(slave_fd, buf, sizeof(buf));
if (n > 0) {
ssh_channel_write(self->channel_, buf, n);
if (ssh_channel_write(chan, buf, n) < 0) break;
} else if (n <= 0) {
break;
}
}
if (FD_ISSET(ssh_fd, &rfds)) {
if (ssh_channel_poll_timeout(self->channel_, 0, 0) > 0) {
ssize_t n = ssh_channel_read_timeout(self->channel_, buf, sizeof(buf), 0, 0);
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) {
write(self->pty_master_, buf, n);
if (write(slave_fd, buf, n) < 0 && errno != EINTR) break;
} else if (n == SSH_EOF || n == SSH_ERROR) {
break;
}
} else if (ssh_channel_poll_timeout(self->channel_, 0, 0) < 0) {
} else if (poll_result < 0) {
break;
}
}
if (ssh_channel_is_eof(self->channel_)) break;
if (ssh_channel_is_eof(chan)) break;
}
self->connected_ = false;
@@ -193,18 +205,19 @@ auth_done:
return false;
}
VtePty *pty = vte_terminal_get_pty(terminal_);
if (pty) {
int vte_fd = vte_pty_get_fd(pty);
if (vte_fd >= 0) close(vte_fd);
}
vte_terminal_set_pty(terminal_, nullptr);
VtePty *new_pty = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
if (new_pty) {
vte_terminal_set_pty(terminal_, new_pty);
/* VTE gets the master side of the pty. It reads terminal output from
master and writes user input to master. Our IO thread uses the slave
side to bridge between the pty and the SSH channel. */
vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
if (vte_pty_) {
vte_terminal_set_pty(terminal_, vte_pty_);
} else {
cleanup();
if (on_status_change) on_status_change(false, "VTE pty setup failed");
return false;
}
cleanup_done_ = false;
connected_ = true;
running_ = true;
thread_ = g_thread_new("ssh_io", io_thread_func, this);
@@ -216,13 +229,17 @@ auth_done:
void SshTerminal::disconnect() {
running_ = false;
if (pty_master_ >= 0) { close(pty_master_); pty_master_ = -1; }
if (pty_slave_ >= 0) { close(pty_slave_); pty_slave_ = -1; }
if (thread_) {
g_thread_join(thread_);
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();
}