6
Siklus Hidup Koneksi SSH
Satu koneksi SSH di Termimus dipegang oleh session_id yang sama dari awal sampai akhir. Backend menyimpan SshSession — berisi handle russh, cmd_tx untuk perintah tulis/resize, dan _jump_handle opsional — di dalam SessionManager src-tauri/src/ssh/mod.rs:216-2211. SessionManager sendiri memelihara tiga struktur: sessions sebagai map sesi hidup, connecting sebagai penanda koneksi yang sedang berjalan, dan cancelled sebagai penanda sesi yang tabnya sudah ditutup sebelum koneksi selesai src-tauri/src/ssh/mod.rs:223-2302.
Alur masuknya lewat command ssh_connect. Command itu memuat host dari database, menyusun SshAuth lewat resolve_host_auth, dan bila jump_host_id terisi juga memuat konfigurasi bastion menjadi JumpHostConfig src-tauri/src/commands/mod.rs:556-6113. Setelah SessionManager::connect selesai, touch_host_last_connected memperbarui waktu koneksi terakhir host src-tauri/src/commands/mod.rs:556-6113.
Peta langkah handshake
do_connect mengirim progres ke frontend pada setiap tahap melalui event ssh-progress-{session_id}. Payload-nya adalah SshProgressPayload dengan field step (1 sampai 5), step_name, message, timestamp, dan is_error src-tauri/src/ssh/mod.rs:37-454. Closure emit_progress di do_connect membungkus app.emit dengan nama event tersebut src-tauri/src/ssh/mod.rs:334-3465.
stateDiagram-v2
[*] --> connecting
connecting --> host_key: "client::connect_stream"
host_key --> authenticating: "check_server_key ok"
authenticating --> opening_channel: "authenticate_publickey_smart"
opening_channel --> ready: "request_pty + request_shell"
ready --> closed: "Eof / Close / disconnect"
connecting --> error: "Connection failed"
authenticating --> error: "Authentication rejected by server"
opening_channel --> error: "PTY request failed"
error --> [*]
closed --> [*]
classDef ok fill:#2d7d46,color:#fff
classDef warn fill:#f59e0b,color:#000
classDef bad fill:#dc2626,color:#fff
class ready,closed ok
class host_key,authenticating warn
class error bad
%% fig: Mesin keadaan handshake SSH
%% desc: Empat tahap handshake berjalan berurutan sebelum sesi siap, dan setiap kegagalan langsung melompat ke keadaan error.Tabel berikut memetakan nilai yang benar-benar di-emit ke aksi backend-nya.
step | step_name | Aksi backend | Pesan yang di-emit |
|---|---|---|---|
| 1 | connecting | TcpStream::connect langsung, atau tunnel lewat jump host | Resolving and connecting to {address}:{port}... |
| 2 | host_key | check_server_key pada SshClientHandler | Verifying target host key (Trust On First Use)... |
| 3 | authenticating | authenticate_password atau authenticate_publickey_smart | Authenticating as {username}... |
| 4 | opening_channel | channel_open_session, request_pty, request_shell | Opening shell channel... |
| 5 | ready | sesi didaftarkan ke map sessions | Shell session ready. |
Langkah 1 sampai 4 diberi label progres masing-masing di src-tauri/src/ssh/mod.rs:462-4836, src-tauri/src/ssh/mod.rs:521-5477, dan langkah ready di src-tauri/src/ssh/mod.rs:553-567. Bila autentikasi ditolak, backend mengembalikan Authentication rejected by server src-tauri/src/ssh/mod.rs:485-517; bila PTY atau shell gagal dibuka, pesannya PTY request failed atau Shell request failed src-tauri/src/ssh/mod.rs:521-547.
Untuk kunci privat, authenticate_publickey_smart menegosiasikan algoritma RSA terkuat yang diterima server lewat best_supported_rsa_hash, lalu mencoba Sha256, Sha512, dan akhiran tanpa hash sebagai fallback src-tauri/src/ssh/mod.rs:177-21410. Verifikasi kunci host beserta penolakan REMOTE HOST IDENTIFICATION HAS CHANGED dibahas di Verifikasi Host Key TOFU.
Transport dan keepalive
Sebelum handshake, stream yang sudah terbentuk dikonfigurasi oleh configure_tcp_stream: set_nodelay(true) mematikan Nagle, dan socket2::TcpKeepalive dipasang dengan with_time 15 detik serta with_interval 5 detik src-tauri/src/ssh/mod.rs:18-2411. Di lapisan SSH, default_client_config mengisi keepalive_interval 15 detik dan keepalive_max 4 pada client::Config src-tauri/src/ssh/mod.rs:30-3512. Keduanya mencegah router NAT stateful dan firewall VPC memutus koneksi yang menganggur.
graph LR
tcp["TcpStream"] --> nodelay["set_nodelay(true)"]
nodelay --> ka["TcpKeepalive 15s / 5s"]
ka --> cfg["client::Config keepalive_interval 15s"]
cfg --> srv["SSH server"]
cfg -.->|"[email protected]"| srv
style tcp fill:#336791,color:#fff
style nodelay fill:#f59e0b,color:#000
style ka fill:#f59e0b,color:#000
style cfg fill:#2d7d46,color:#fff
style srv fill:#8b5cf6,color:#fff
%% fig: Dua lapis keepalive
%% desc: Keepalive dipasang pada socket TCP dan pada protokol SSH agar koneksi menganggur tidak diputus middlebox.Tanpa jump host, stream berupa SshStream::Direct hasil tokio::net::TcpStream::connect src-tauri/src/ssh/mod.rs:350-36413. Dengan jump host, Termimus membuka kanal channel_open_direct_tcpip di koneksi bastion dan membungkusnya menjadi SshStream::Tunneled src-tauri/src/ssh/mod.rs:426-44214. Detail aturan tunnel ada di Port Forwarding dan Tunnel.
Worker PTY dan koalesensi ~8ms
Setelah ready, do_connect membuat mpsc::unbounded_channel::<SessionCommand>() dan mendaftarkan sesi ke map sessions src-tauri/src/ssh/mod.rs:553-5678. Satu task tokio::spawn kemudian menjadi worker yang membaca channel.wait() dan mengirim byte server ke event ssh-data-{session_id} src-tauri/src/ssh/mod.rs:575-61115.
graph LR
ch["channel.wait()"] --> idle{"flush_deadline"}
idle -->|"None"| direct["emit ssh-data immediately"]
idle -->|"Some"| buf["pending.extend_from_slice"]
buf --> cap{"pending reached 64KB"}
cap -->|"yes"| flush["emit ssh-data, flush_deadline = None"]
cap -->|"no"| timer["sleep_until deadline 8ms"]
timer --> flush
cmd["cmd_rx.recv()"] --> write["channel.data"]
cmd --> resize["window_change"]
style direct fill:#2d7d46,color:#fff
style buf fill:#f59e0b,color:#000
style flush fill:#2d7d46,color:#fff
style timer fill:#8b5cf6,color:#fff
style resize fill:#dc2626,color:#fff
%% fig: Koalesensi output PTY di worker
%% desc: Output saat idle dikirim seketika, sedangkan burst dikumpulkan sampai 8ms atau 64KB sebelum di-emit; perintah resize di bawah ambang diabaikan.Aturan koalesensinya eksplisit di kode. Saat flush_deadline masih None (stream idle, misalnya gema ketikan), byte langsung di-emit dengan jeda 0ms dan timer flush_delay 8ms dimulai. Selama timer itu hidup, byte berikutnya diakumulasi ke pending dan baru dikirim saat pending.len() >= 64 * 1024 atau saat timer jatuh tempo src-tauri/src/ssh/mod.rs:575-61115. Cabang timer mem-flush sisa pending lalu mengembalikan flush_deadline ke None, dan cabang Eof, Close, atau None mem-flush sisa byte sebelum meng-emit ssh-closed-{session_id} src-tauri/src/ssh/mod.rs:629-64416.
Sisi perintah memakai SessionCommand. Varian Data diteruskan ke channel.data, sedangkan varian Resize hanya memanggil channel.window_change bila cols >= 15 && rows >= 4 src-tauri/src/ssh/mod.rs:645-65817.
⚠️ Resize dengan
colsdi bawah 15 ataurowsdi bawah 4 dibuang diam-diam oleh worker. Ukuran itu muncul saat elemen DOM terminal disembunyikan, sehingga mengirimkannya akan memperkecil PTY remote secara keliru src-tauri/src/ssh/mod.rs:645-65817.
SessionManager::write dan SessionManager::resize tidak menyentuh kanal secara langsung; keduanya hanya mengirim SessionCommand lewat cmd_tx, dan mengembalikan Session not found bila session_id tidak ada di map src-tauri/src/ssh/mod.rs:698-72118.
Pembatalan dan penutupan
Frontend memulai semuanya lewat startConnection, yang memanggil api.connectSsh(hostId, sessionId, cols, rows) dan menandai tab connected saat promise selesai src/components/terminal/XtermView.tsx:464-51719. Tiga listener dipasang per sesi: ssh-data-{sessionId} menulis byte ke term, ssh-closed-{sessionId} memanggil closeSession, dan ssh-progress-{sessionId} menambahkan ConnectionLog sekaligus menyimpan connectionStep serta connectionError ke terminalPool agar state bertahan saat pane di-remount src/components/terminal/XtermView.tsx:657-70520. Bentuk payload yang dibaca frontend adalah step, step_name, message, timestamp, is_error src/components/terminal/XtermView.tsx:40-4621.
closeSession melakukan dua hal berurutan: disposeTerminalSession(sessionId) untuk melepas listener dan instance xterm src/components/terminal/XtermView.tsx:312-35222, lalu api.disconnectSsh(sessionId) src/stores/useSessionStore.ts:524-54223. Command ssh_disconnect hanya meneruskan panggilan ke SessionManager::disconnect src-tauri/src/commands/mod.rs:632-63824.
disconnect menangani dua keadaan. Bila sesi sudah terdaftar, ia mengeluarkannya dari map lalu memanggil handle.disconnect(Disconnect::ByApplication, "", "en") beserta koneksi bastion bila ada. Bila sesi belum terdaftar tetapi id-nya ada di connecting, id itu dimasukkan ke cancelled src-tauri/src/ssh/mod.rs:723-74325. Penanda cancelled dibaca check_cancelled di setiap titik henti do_connect, yang mengembalikan Connection cancelled src-tauri/src/ssh/mod.rs:310-31626. connect juga menolak dua percobaan bersamaan untuk satu session_id dengan pesan Connection already in progress for this session, dan membersihkan sesi lama ber-id sama sebelum mencoba lagi src-tauri/src/ssh/mod.rs:255-28027.
Saat aplikasi keluar, RunEvent::ExitRequested memanggil disconnect_all di dalam tokio::time::timeout 2 detik agar server menerima pesan disconnect yang benar, bukan socket TCP yang hilang src-tauri/src/lib.rs:118-13228. disconnect_all menyalin daftar id lalu memanggil disconnect untuk masing-masing src-tauri/src/ssh/mod.rs:748-75629. Instance SessionManager sendiri dibuat sekali di run() dan dibagi lewat AppState src-tauri/src/lib.rs:2530.
Event dan penanganannya
| Event | Payload | Penanganan |
|---|---|---|
ssh-data-{session_id} | byte PTY sebagai Vec<u8> | term.write(bytes) di XtermView |
ssh-progress-{session_id} | SshProgressPayload | log koneksi, connectionStep, connectionError |
ssh-closed-{session_id} | session_id | closeSession(sessionId) |
Ketiga nama event dibentuk dari session_id di src-tauri/src/ssh/mod.rs:569-57031 dan src-tauri/src/ssh/mod.rs:334-3465, lalu didengarkan di src/components/terminal/XtermView.tsx:657-70520. Setelah ready, backend juga menjalankan deteksi OS di latar belakang lewat detect_os_via_exec pada kanal exec terpisah, dimulai setelah jeda 800ms dan dilewati bila koneksi melewati jump host src-tauri/src/ssh/mod.rs:663-69332. Kanvas terminal, split pane, dan perilaku fokusnya dibahas di Terminal, Split Pane, dan Workspace, sedangkan kanal SFTP yang memakai kredensial host yang sama ada di SFTP Dual-Pane dan Editor Remote.