Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Versions follow [Semantic Versioning](https://semver.org/).

---

## Unreleased
## 1.1.2 — 2026-08-22

### Features
- **Hosts imported from `~/.ssh/config` can be edited in the desktop app.** Imported hosts were shown read-only, so changing a port or a username for one meant deleting it from your SSH config and adding it again by hand — while the terminal app had allowed the edit all along. Editing one now saves your own copy, which OmnySSH uses from then on; `~/.ssh/config` is still never written to. The form says so before you save, since later changes you make to that file stop reaching the host once it has been adopted. The bastion and key path the app parsed are carried over even though the form cannot show them, so an adopted `ProxyJump` host keeps connecting through its jump server. Deleting is still offered for your own hosts only — there is nothing of an import to remove — and deleting a copy you adopted brings the imported version back, which the confirmation now tells you.
Expand All @@ -27,13 +27,19 @@ Versions follow [Semantic Versioning](https://semver.org/).
- Each hop's host key is checked against `known_hosts` under its own name, and each hop authenticates with the usual agent → identity file → default keys → password order. Every hop, including the tunnel opened on a bastion, is bound by the same ten-second budget, so a firewalled target cannot leave a host stuck on "connecting".
- A bastion you renamed after importing it is still found by the alias other entries name it with.
- One-click SSH key setup works for hosts behind a bastion: its verification steps now get the time the longer connection needs, and running out of time after password authentication has been disabled rolls the server back instead of reporting a clean failure.
- Editing an imported host in the TUI no longer drops its `ProxyJump`: the form has no field for it, so the saved copy used to lose the bastion. The GUI already preserved it.
- Editing an imported host in the TUI no longer drops its `ProxyJump`, nor the name it was imported under: the form has no field for either, so the saved copy used to lose the bastion, and a second edit lost the imported name — which brought the `~/.ssh/config` entry back as a duplicate card and left any other host's `ProxyJump` pointing at an alias nothing answered to. The GUI already preserved both.
- **Windows: the console-window fix from 1.1.1 actually reaches you this time.** The fix was in that release's source, but the Windows installer published under the `v1.1.1` tag was an artifact left over from an earlier build — so the `.exe` it installed was the 1.1.0 one, still linked as a console application, and none of 1.1.1's Windows fixes (the console window, the white launch flash, the `ssh-keygen` console) were in it. The release now discards anything a previous build left behind, publishes only the bundle it just produced for the version being released, and refuses to publish at all if the desktop binary is not linked as a GUI application or if any installer is missing.
- **The update banner links to the release instead of offering an install that cannot run.** Self-update has no endpoints configured yet, so the Install button reported "Self-update is not available yet" on every platform. It now opens the release page in your browser; Install returns when the desktop updater is wired up.
- **The Linux AppImage retries a window that never paints.** On some graphics setups the webview the AppImage carries cannot start hardware rendering, so the app opened as an empty dark frame and stayed that way, with the error going to a terminal nobody launched it from. If the interface still has not loaded twelve seconds in, the AppImage now restarts itself once with software rendering. A launch that renders normally is untouched, and the retry never happens for the `.deb`, `.rpm`, macOS or Windows builds.
- **A Linux desktop build that opens to an empty window restarts itself once.** On some graphics setups WebKit cannot start its hardware renderer, so the app opened as an empty dark frame and stayed that way, with the error going to a terminal nobody launched it from. If the interface still has not loaded twelve seconds in, OmnySSH now restarts itself once with software rendering. This covers the `.deb` and `.rpm` as well as the `.AppImage`: the failure is WebKit meeting a particular graphics driver rather than anything the AppImage does, and `install.sh` now prefers the `.rpm` on the distributions where it is reported most, so leaving the native packages out would have moved the affected people onto the one build with no answer for it. A launch that loads normally is untouched, a launch you started with `WEBKIT_DISABLE_DMABUF_RENDERER` set yourself is left alone, and the restart can happen only once. macOS and Windows are unaffected.
- **Editing a host no longer drops every other host's monitoring session.** Putting the monitoring mode on the host form meant the terminal app had to restart its polling pool after an edit, since a running poller does not pick up a changed address, port or mode. It restarted the whole pool for any edit at all, so fixing a typo in one host's notes reconnected and re-authenticated every server on the dashboard. The pool is now restarted only when the edit moved something a connection is actually made from — the name, address, user, port, key, password or monitoring mode. Editing tags or notes leaves every session where it was.
- **macOS: `install.sh` no longer installs an older copy of the app.** The installer read the mount point out of `hdiutil`'s output by stopping at the first space. When a volume called OmnySSH was already mounted — because you had opened the `.dmg` in Finder first, or because an earlier run was interrupted — macOS mounts the new one as `OmnySSH 1`, and the truncated path pointed back at the one already there. The script then copied that older app into `/Applications` and reported success.
- **Linux: a `.deb` that could not be installed is no longer reported as installed.** `apt-get install -f` resolves an unsatisfiable dependency by removing the package `dpkg` has just unpacked, and exits successfully having done so — which the installer read as a working install and announced as one, leaving you with the message and nothing else. It now asks `dpkg` what is actually installed, and falls back to the AppImage when the package did not survive, the way the `.rpm` path already did.
- **Linux: installing the `.deb` removes the AppImage it replaces.** The `.rpm` path already did this and the `.deb` path did not. The package installs to `/usr/bin` under its own name while an AppImage from an earlier run sits in `/usr/local/bin`, which comes first on the default `PATH` — so `omnyssh` on the command line kept starting the old build, and the application menu carried two identical entries.
- **ARM64 Linux and Termux install the terminal app instead of nothing.** No desktop build exists for those targets, and `curl … | sh` — the command the README prints — installs the desktop app by default. It warned and exited without installing anything, although the terminal app ships for exactly those targets. It now installs that instead. A desktop build that does exist and fails to install still fails rather than quietly falling back.

### Packaging
- **A native `.rpm` for Fedora and other RPM distributions.** Releases now carry `OmnySSH-x86_64.rpm` alongside the `.AppImage` and `.deb`, and `install.sh` prefers it on `dnf`-based systems — it lands in your application menu and uninstalls with `dnf remove`, no FUSE involved. The package names the WebKitGTK 4.1, JavaScriptCore and GTK 3 libraries it links, so `dnf` resolves them from your own distribution instead of the app carrying a second copy. Distributions that ship no WebKitGTK 4.1 at all, such as RHEL 9 and its rebuilds, will refuse the package; `install.sh` then tries the AppImage there.
- **The Linux packages are named `omny-ssh`.** The bundler derives the package name from the product name, so the `.deb` and `.rpm` register as `omny-ssh` even though the files are called `OmnySSH-x86_64.deb` and `OmnySSH-x86_64.rpm`. Uninstall with `dnf remove omny-ssh` or `apt remove omny-ssh`.

---

Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = ["crates/omnyssh-core", "crates/omnyssh", "crates/omnyssh-gui"]
default-members = ["crates/omnyssh-core", "crates/omnyssh"]

[workspace.package]
version = "1.1.1"
version = "1.1.2"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/timhartmann7/omnyssh"
Expand Down
17 changes: 14 additions & 3 deletions crates/omnyssh-core/tests/poll_backoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ async fn an_unreachable_host_keeps_retrying_on_its_own_schedule() {
assert!(retried, "the poller stopped retrying an unreachable host");
}

#[tokio::test(start_paused = true)]
// Real clock, unlike its neighbours: a paused clock auto-advances whenever the
// runtime idles, which it does while the loopback connect sits in the IO driver —
// so `TCP_PROBE_TIMEOUT` elapses in virtual time and the open port reads as dead.
#[tokio::test]
async fn a_reachability_host_is_probed_without_an_ssh_session() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let port = listener.local_addr().expect("local addr").port();
Expand All @@ -119,8 +122,10 @@ async fn a_reachability_host_is_probed_without_an_ssh_session() {
let manager = PollManager::start(vec![host], tx, Duration::from_secs(30));

// Bounded: the poller never closes the channel, so an unbounded wait would
// hang instead of failing when the expected status stops arriving.
let reachable = tokio::time::timeout(Duration::from_secs(60), async {
// hang instead of failing when the expected status stops arriving. Real seconds
// now, and comfortably clear of `TCP_PROBE_TIMEOUT`: a budget equal to it would
// expire on the same dial it is meant to watch retry.
let reachable = tokio::time::timeout(Duration::from_secs(20), async {
while let Some(event) = rx.recv().await {
if let CoreEvent::HostStatusChanged(_, ConnectionStatus::Connected) = event {
return true;
Expand All @@ -135,6 +140,12 @@ async fn a_reachability_host_is_probed_without_an_ssh_session() {
"the probe never reported the port as reachable"
);

// The probe has settled, so the clock can be paused for the rest: waiting out
// the cycles below in real seconds would be a two-minute test. A dial that now
// times out against the virtual clock only reports a status, which is what this
// half tolerates anyway.
tokio::time::pause();

// And over the cycles that follow, it reports status only — metrics would be
// invented. Elapsing without a metrics event is the pass.
let stray_metrics = tokio::time::timeout(Duration::from_secs(120), async {
Expand Down
Loading
Loading