Fix: crash on failed connect, add sidebar edit/delete, password saving

- Fix crash when SSH connect fails: remove page BEFORE deleting terminal
- Sidebar right-click context menu: Connect / Edit / Delete
- Password field + Save password checkbox in server dialog
- ServerConfig stores password and save_password fields
- Auto-connect uses saved password when available
- Restore original flaticon icon
This commit is contained in:
x3
2026-09-07 15:34:23 +00:00
parent 16ae7ffde2
commit 6e719c3236
7 changed files with 202 additions and 124 deletions
+32 -8
View File
@@ -2,15 +2,16 @@
A GTK3-based SSH client with a tabbed terminal interface, server sidebar, notifications, and SSH key storage.
![Icon](icons/x3-ssh.svg)
![Icon](icons/x3-ssh.png)
## Features
- **Tabbed terminal** - Multiple SSH sessions in tabs
- **Server sidebar** - Saved servers displayed in a sidebar, double-click to connect
- **Server sidebar** - Saved servers in a sidebar, double-click to connect, right-click for Edit/Delete
- **Quick connect** - Connect directly via host/user/port fields in the toolbar
- **Password storage** - Optionally save passwords with servers (stored in config file)
- **SSH key management** - Generate, import, and manage SSH keys
- **Notifications** - Animated notification bar for status messages (saved, connected, disconnected, errors)
- **Notifications** - Animated notification bar for status messages
- **VTE terminal** - Full terminal emulation with 256 color support
- **Resizable pty** - Terminal auto-resizes the remote pty on window resize
- **System integration** - `.desktop` file and icon for application menu entry
@@ -63,11 +64,25 @@ SSH keys are stored in:
## Usage
1. Start the application: `x3-ssh`
2. **Server Sidebar**: Saved servers appear in the left sidebar. Double-click to connect.
2. **Server Sidebar**: Saved servers appear in the left sidebar
- **Double-click** to connect
- **Right-click** for context menu (Connect / Edit / Delete)
- Click `+` to add a new server
3. **Quick Connect**: Enter host, user, and port in the toolbar, click "Connect"
4. **Add Server**: Click the `+` button in the sidebar header, or use the "Add Server" dialog
5. **SSH Keys**: Click "Keys" in the toolbar to generate or import SSH keys
6. **Tabs**: Click "+ Tab" for a new empty tab, "Close Tab" to close the current one
4. **SSH Keys**: Click "Keys" in the toolbar to generate or import SSH keys
5. **Tabs**: Click "+ Tab" for a new empty tab, "Close Tab" to close the current one
## Server Dialog Fields
| Field | Description |
|-------|-------------|
| Name | Display name for the server |
| Host | Hostname or IP address |
| User | SSH username |
| Port | SSH port (default: 22) |
| Password | Optional, can be saved |
| Save password | Checkbox to store password in config |
| SSH Key | Optional key for public key authentication |
## Notifications
@@ -90,7 +105,7 @@ Notifications auto-hide after 4 seconds.
│ [Server▼] [host___] [user___] [port] [Connect] ...│
├───────────┬────────────────────────────────────────┤
│ Servers │ [Tab: MyServer] [Tab: Prod] [+] │
│ ─────── │ │
│ +─────── │ │
│ MyServer │ user@host:~$ │
│ root@h:22 │ │
│ │ │
@@ -101,6 +116,15 @@ Notifications auto-hide after 4 seconds.
└────────────────────────────────────────────────────┘
```
Right-click on a server in the sidebar:
```
┌──────────┐
│ Connect │
│ Edit │
│ Delete │
└──────────┘
```
## License
MIT
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+6 -2
View File
@@ -55,7 +55,9 @@ void ServerStore::load() {
else if (key == "port") { try { current.port = std::stoi(val); } catch (...) { current.port = 22; } }
else if (key == "user") current.user = val;
else if (key == "keyfile") current.keyfile = val;
else if (key == "password") current.password = val;
else if (key == "use_key") current.use_key = (val == "true");
else if (key == "save_password") current.save_password = (val == "true");
}
if (in_server && !current.name.empty()) {
servers_.push_back(current);
@@ -65,7 +67,7 @@ void ServerStore::load() {
void ServerStore::save() const {
std::string dir = config_path_.substr(0, config_path_.rfind('/'));
std::string cmd = "mkdir -p \"" + dir + "\"";
system(cmd.c_str());
(void)system(cmd.c_str());
std::ofstream f(config_path_);
if (!f.is_open()) return;
@@ -79,7 +81,9 @@ void ServerStore::save() const {
f << "port=" << s.port << "\n";
f << "user=" << s.user << "\n";
f << "keyfile=" << s.keyfile << "\n";
f << "use_key=" << (s.use_key ? "true" : "false") << "\n\n";
f << "password=" << s.password << "\n";
f << "use_key=" << (s.use_key ? "true" : "false") << "\n";
f << "save_password=" << (s.save_password ? "true" : "false") << "\n\n";
}
}
+2
View File
@@ -9,7 +9,9 @@ struct ServerConfig {
int port = 22;
std::string user;
std::string keyfile;
std::string password;
bool use_key = false;
bool save_password = false;
};
class ServerStore {
+110 -97
View File
@@ -35,6 +35,7 @@ static void refresh_server_list_widget(GtkWidget* listbox, const std::vector<Ser
std::string detail = s.user + "@" + s.host + ":" + std::to_string(s.port);
if (s.use_key) detail += " [key]";
else if (s.save_password) detail += " [saved]";
GtkWidget* detail_label = gtk_label_new(detail.c_str());
gtk_widget_set_halign(detail_label, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(detail_label), PANGO_ELLIPSIZE_END);
@@ -101,7 +102,7 @@ void SshClientWindow::build_ui() {
GtkWidget* main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(window_), main_vbox);
/* ── notification bar (top, hidden by default) ── */
/* ── notification bar ── */
notif_bar_ = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(notif_bar_), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), FALSE);
@@ -121,7 +122,6 @@ void SshClientWindow::build_ui() {
}), notif_bar_);
gtk_box_pack_end(GTK_BOX(notif_box), notif_close, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(notif_bar_), notif_box);
/* colored background */
GtkCssProvider* css = gtk_css_provider_new();
gtk_css_provider_load_from_data(css, ".notif-ok { background: #2ecc71; color: #000; }"
".notif-err { background: #e74c3c; color: #fff; }", -1, nullptr);
@@ -173,7 +173,7 @@ void SshClientWindow::build_ui() {
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_box_pack_start(GTK_BOX(main_vbox), paned, TRUE, TRUE, 0);
/* ── sidebar: server list ── */
/* ── sidebar ── */
GtkWidget* sidebar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_paned_pack1(GTK_PANED(paned), sidebar, FALSE, FALSE);
gtk_widget_set_size_request(sidebar, 220, -1);
@@ -200,12 +200,13 @@ void SshClientWindow::build_ui() {
server_listbox_ = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(server_listbox_), GTK_SELECTION_SINGLE);
g_signal_connect(server_listbox_, "row-activated", G_CALLBACK(on_server_row_activated), this);
g_signal_connect(server_listbox_, "button-press-event", G_CALLBACK(on_server_row_button), this);
gtk_container_add(GTK_CONTAINER(scrolled), server_listbox_);
gtk_box_pack_start(GTK_BOX(sidebar), scrolled, TRUE, TRUE, 0);
update_server_list();
/* ── notebook for terminals ── */
/* ── notebook ── */
notebook_ = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
gtk_paned_pack2(GTK_PANED(paned), notebook_, TRUE, TRUE);
@@ -217,13 +218,11 @@ void SshClientWindow::build_ui() {
void SshClientWindow::show_notification(const std::string& msg, bool is_error) {
gtk_label_set_text(GTK_LABEL(notif_label_), msg.c_str());
GtkStyleContext* ctx = gtk_widget_get_style_context(notif_bar_);
gtk_style_context_remove_class(ctx, "notif-ok");
gtk_style_context_remove_class(ctx, "notif-err");
gtk_style_context_add_class(ctx, is_error ? "notif-err" : "notif-ok");
GtkStyleContext* nctx = gtk_widget_get_style_context(notif_bar_);
gtk_style_context_remove_class(nctx, "notif-ok");
gtk_style_context_remove_class(nctx, "notif-err");
gtk_style_context_add_class(nctx, is_error ? "notif-err" : "notif-ok");
gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), TRUE);
/* auto-hide after 4 seconds */
g_timeout_add(4000, [](gpointer data) -> gboolean {
gtk_revealer_set_reveal_child(GTK_REVEALER(data), FALSE);
return G_SOURCE_REMOVE;
@@ -266,7 +265,6 @@ void SshClientWindow::add_new_tab() {
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook_), page, TRUE);
gtk_widget_show_all(page);
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), idx);
update_status("New tab opened");
}
void SshClientWindow::close_current_tab() {
@@ -288,12 +286,13 @@ void SshClientWindow::close_tab_at(int idx) {
}
}
/* Remove page BEFORE deleting terminal to avoid crash */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
if (found) {
found->disconnect();
delete found;
}
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
}
void SshClientWindow::disconnect() {
@@ -336,8 +335,9 @@ void SshClientWindow::connect_to_server(const std::string& host, int port,
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if (*it == term) { terminals_.erase(it); break; }
}
delete term;
/* Remove page FIRST, then delete terminal */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
delete term;
} else {
update_status("Connected to " + user + "@" + host);
show_notification("Server connected: " + user + "@" + host + ":" + std::to_string(port));
@@ -365,7 +365,12 @@ void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
return;
}
std::string pw = s.save_password ? s.password : "";
if (pw.empty()) {
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
} else {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name);
}
return;
}
@@ -383,8 +388,79 @@ void SshClientWindow::on_server_row_activated(GtkListBox*, GtkListBoxRow* row, g
if (s.use_key && !s.keyfile.empty()) {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
} else {
std::string pw = s.save_password ? s.password : "";
if (pw.empty()) {
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
} else {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name);
}
}
}
/* ─── server right-click context menu ─────────────────────── */
gboolean SshClientWindow::on_server_row_button(GtkWidget*, GdkEventButton* event, gpointer d) {
if (event->type != GDK_BUTTON_PRESS || event->button != 3) return FALSE;
auto* self = static_cast<SshClientWindow*>(d);
GtkListBoxRow* row = gtk_list_box_get_row_at_y(GTK_LIST_BOX(self->server_listbox_), (int)event->y);
if (!row) return FALSE;
int idx = gtk_list_box_row_get_index(row);
if (idx < 0 || idx >= (int)self->server_store_.servers().size()) return FALSE;
const auto& s = self->server_store_.servers()[idx];
bool use_key = s.use_key && !s.keyfile.empty();
bool has_saved_pw = s.save_password && !s.password.empty();
GtkWidget* menu = gtk_menu_new();
/* Connect item */
GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect");
struct ConnectCtx { SshClientWindow* w; int i; bool uk; bool sp; };
auto* connect_ctx = new ConnectCtx{self, idx, use_key, has_saved_pw};
g_signal_connect_data(connect_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<ConnectCtx*>(d);
const auto& sv = c->w->server_store_.servers()[c->i];
if (c->uk) {
c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, "", sv.name);
} else if (c->sp) {
c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, sv.password, sv.name);
} else {
c->w->show_password_dialog(sv.host, sv.port, sv.user, sv.keyfile, sv.name);
}
delete c;
}), connect_ctx, nullptr, (GConnectFlags)0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item);
/* Edit item */
GtkWidget* edit_item = gtk_menu_item_new_with_label("Edit");
struct EditCtx { SshClientWindow* w; int i; };
auto* edit_ctx = new EditCtx{self, idx};
g_signal_connect_data(edit_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<EditCtx*>(d);
c->w->show_server_dialog(c->i);
c->w->update_server_list();
delete c;
}), edit_ctx, nullptr, (GConnectFlags)0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), edit_item);
/* Delete item */
GtkWidget* del_item = gtk_menu_item_new_with_label("Delete");
struct DelCtx { SshClientWindow* w; int i; };
auto* del_ctx = new DelCtx{self, idx};
g_signal_connect_data(del_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<DelCtx*>(d);
std::string name = c->w->server_store_.servers()[c->i].name;
c->w->server_store_.remove(c->i);
c->w->update_server_list();
c->w->show_notification("Server removed: " + name);
delete c;
}), del_ctx, nullptr, (GConnectFlags)0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), del_item);
gtk_widget_show_all(menu);
gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
return TRUE;
}
/* ─── add server button ───────────────────────────────────── */
@@ -489,9 +565,25 @@ void SshClientWindow::show_server_dialog(int edit_index) {
else gtk_spin_button_set_value(GTK_SPIN_BUTTON(port_s), 22);
gtk_grid_attach(GTK_GRID(grid), port_s, 1, 3, 1, 1);
/* Password field */
GtkWidget* pass_l = gtk_label_new("Password:");
gtk_widget_set_halign(pass_l, GTK_ALIGN_START);
gtk_grid_attach(GTK_GRID(grid), pass_l, 0, 4, 1, 1);
GtkWidget* pass_e = gtk_entry_new();
gtk_entry_set_visibility(GTK_ENTRY(pass_e), FALSE);
gtk_entry_set_placeholder_text(GTK_ENTRY(pass_e), "optional");
if (editing) gtk_entry_set_text(GTK_ENTRY(pass_e), existing.password.c_str());
gtk_widget_set_hexpand(pass_e, TRUE);
gtk_grid_attach(GTK_GRID(grid), pass_e, 1, 4, 1, 1);
GtkWidget* save_pass_cb = gtk_check_button_new_with_label("Save password");
if (editing) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(save_pass_cb), existing.save_password);
gtk_grid_attach(GTK_GRID(grid), save_pass_cb, 1, 5, 1, 1);
/* SSH Key field */
GtkWidget* key_l = gtk_label_new("SSH Key:");
gtk_widget_set_halign(key_l, GTK_ALIGN_START);
gtk_grid_attach(GTK_GRID(grid), key_l, 0, 4, 1, 1);
gtk_grid_attach(GTK_GRID(grid), key_l, 0, 6, 1, 1);
GtkWidget* key_combo = gtk_combo_box_text_new();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(key_combo), "(None / Password)");
@@ -511,7 +603,7 @@ void SshClientWindow::show_server_dialog(int edit_index) {
gtk_combo_box_set_active(GTK_COMBO_BOX(key_combo), 0);
}
gtk_widget_set_hexpand(key_combo, TRUE);
gtk_grid_attach(GTK_GRID(grid), key_combo, 1, 4, 1, 1);
gtk_grid_attach(GTK_GRID(grid), key_combo, 1, 6, 1, 1);
gtk_container_add(GTK_CONTAINER(content), grid);
gtk_widget_show_all(dialog);
@@ -522,6 +614,8 @@ void SshClientWindow::show_server_dialog(int edit_index) {
s.host = gtk_entry_get_text(GTK_ENTRY(host_e));
s.user = gtk_entry_get_text(GTK_ENTRY(user_e));
s.port = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(port_s));
s.password = gtk_entry_get_text(GTK_ENTRY(pass_e));
s.save_password = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(save_pass_cb));
int key_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(key_combo));
if (key_idx > 0 && key_idx <= (int)key_store_.keys().size()) {
s.use_key = true;
@@ -690,87 +784,6 @@ void SshClientWindow::show_key_dialog() {
gtk_widget_destroy(dialog);
}
/* ─── server management dialog ────────────────────────────── */
struct SrvCtx { SshClientWindow* self; GtkWidget* listbox; };
void SshClientWindow::on_manage_servers(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d);
GtkWidget* dialog = gtk_dialog_new_with_buttons(
"Manage Servers", GTK_WINDOW(self->window_),
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
"_Close", GTK_RESPONSE_CLOSE,
nullptr);
GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_margin_start(vbox, 12);
gtk_widget_set_margin_end(vbox, 12);
gtk_widget_set_margin_top(vbox, 12);
GtkWidget* scrolled = gtk_scrolled_window_new(nullptr, nullptr);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_size_request(scrolled, 500, 250);
GtkWidget* listbox = gtk_list_box_new();
gtk_container_add(GTK_CONTAINER(scrolled), listbox);
gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
refresh_server_list_widget(listbox, self->server_store_.servers());
GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* add_btn = gtk_button_new_with_label("Add");
GtkWidget* edit_btn = gtk_button_new_with_label("Edit");
GtkWidget* del_btn = gtk_button_new_with_label("Remove");
gtk_box_pack_start(GTK_BOX(hbox), add_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), edit_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), del_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
auto* ctx = new SrvCtx{self, listbox};
g_signal_connect(add_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(d);
c->self->show_server_dialog(-1);
c->self->update_server_list();
refresh_server_list_widget(c->listbox, c->self->server_store_.servers());
}), ctx);
g_signal_connect(edit_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(d);
GtkListBoxRow* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(c->listbox));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
if (idx >= 0 && idx < (int)c->self->server_store_.servers().size()) {
c->self->show_server_dialog(idx);
c->self->update_server_list();
refresh_server_list_widget(c->listbox, c->self->server_store_.servers());
}
}), ctx);
g_signal_connect(del_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(d);
GtkListBoxRow* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(c->listbox));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
if (idx >= 0 && idx < (int)c->self->server_store_.servers().size()) {
std::string name = c->self->server_store_.servers()[idx].name;
c->self->server_store_.remove(idx);
c->self->update_server_list();
refresh_server_list_widget(c->listbox, c->self->server_store_.servers());
c->self->show_notification("Server removed: " + name);
}
}), ctx);
gtk_container_add(GTK_CONTAINER(content), vbox);
gtk_widget_show_all(dialog);
gtk_dialog_run(GTK_DIALOG(dialog));
delete ctx;
gtk_widget_destroy(dialog);
self->update_server_list();
}
/* ─── callbacks ───────────────────────────────────────────── */
void SshClientWindow::on_new_tab(GtkWidget*, gpointer d) {
+1 -1
View File
@@ -37,11 +37,11 @@ private:
static void on_close_tab(GtkWidget* w, gpointer d);
static void on_connect(GtkWidget* w, gpointer d);
static void on_disconnect(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_quick_connect(GtkWidget* w, gpointer d);
static void on_add_server(GtkWidget* w, gpointer d);
static void on_server_row_activated(GtkListBox* list, GtkListBoxRow* row, gpointer d);
static gboolean on_server_row_button(GtkWidget* widget, GdkEventButton* event, gpointer d);
GtkWidget* window_;
GtkWidget* notebook_;
+49 -14
View File
@@ -22,7 +22,10 @@ SshTerminal::SshTerminal()
SshTerminal::~SshTerminal() {
disconnect();
if (vte_box_) gtk_widget_destroy(vte_box_);
if (vte_box_) {
gtk_widget_destroy(vte_box_);
vte_box_ = nullptr;
}
}
void SshTerminal::on_size_changed(VteTerminal* term, guint cols, guint rows, gpointer data) {
@@ -130,6 +133,14 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
const std::string& keyfile, const std::string& password) {
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;
session_ = ssh_new();
if (!session_) return false;
@@ -142,7 +153,9 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
if (ssh_connect(session_) != SSH_OK) {
std::string err = ssh_get_error(session_);
cleanup();
/* Manually free since cleanup_done_ is still true */
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "Connection failed: " + err);
return false;
}
@@ -164,7 +177,9 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
int rc = ssh_userauth_password(session_, nullptr, password.c_str());
if (rc != SSH_AUTH_SUCCESS) {
std::string err = ssh_get_error(session_);
cleanup();
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "Auth failed: " + err);
return false;
}
@@ -173,13 +188,19 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
auth_done:
channel_ = ssh_channel_new(session_);
if (!channel_) {
cleanup();
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "Channel creation failed");
return false;
}
if (ssh_channel_open_session(channel_) != SSH_OK) {
cleanup();
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;
}
@@ -187,32 +208,47 @@ auth_done:
guint cols = vte_terminal_get_column_count(terminal_);
guint rows = vte_terminal_get_row_count(terminal_);
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
cleanup();
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");
return false;
}
if (ssh_channel_request_shell(channel_) != SSH_OK) {
cleanup();
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;
}
pty_master_ = openpty(&pty_slave_, nullptr, nullptr, nullptr, nullptr);
if (pty_master_ < 0) {
cleanup();
ssh_channel_free(channel_);
channel_ = nullptr;
ssh_disconnect(session_);
ssh_free(session_);
session_ = nullptr;
if (on_status_change) on_status_change(false, "PTY creation failed");
return false;
}
/* 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();
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;
}
@@ -234,11 +270,10 @@ void SshTerminal::disconnect() {
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);
if (terminal_) vte_terminal_set_pty(terminal_, nullptr);
}
cleanup();