Compare commits

..

6 Commits

Author SHA1 Message Date
Ferdinand Schober
4bbf08ed66 use proper css styling for the font colors 2026-06-14 22:34:42 +02:00
Jon Kinney
63864a7fc9 chore: cargo fmt for peer-version code 2026-06-14 18:10:22 +02:00
Jon Kinney
9fb07a119f fix(version-exchange): also store peer commit on the listen side
Previously the Hello handler in `ListenTask` echoed our local commit
back but deliberately threw away the peer's, on the assumption that
the outgoing connect-side path (`connect.rs:278-279` →
`set_peer_commit`) would always populate the visible state for any
bidirectionally-configured peer.

That assumption breaks any time the *outgoing* TCP/DTLS direction is
broken even though the inbound direction is fine — happened just now
when the peer Mac's daemon stopped listening on 4242 (DHCP-renewed
IP, daemon crashed, asymmetric NAT, …). Mac was still happily
connecting in the other direction and sending events, including the
initial Hello, but Linux silently displayed "peer version unknown"
because the listen side dropped Mac's commit on the floor.

Add a `PeerHello { addr, commit }` EmulationEvent variant fired from
the listen-side Hello handler. The service maps `addr → ClientHandle`
via `client_manager.get_client(addr)` and calls `set_peer_commit` +
`broadcast_client` exactly like the connect path does. The connect
path remains the primary source for symmetric setups; this is the
defensive fallback so version visibility doesn't depend on outbound
reachability.

Skips silently when no outgoing client is configured for the peer's
addr (incoming-only setup) — there's no UI row to update in that
case anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 18:10:22 +02:00
Jon Kinney
1c92d9c5c7 ui(client_row): sentence-case "Peer version" and "Ours" labels
These are user-visible labels in the version-status subtitle, so
sentence-case reads better than the lowercase first-pass. "matched"
stays lowercase since it's a status descriptor, not a label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 18:10:22 +02:00
Jon Kinney
3b7570c062 feat: peer version exchange with soft-warn UI indicator
Adds a one-shot Hello message to the lan-mouse wire protocol so each
peer can display the other end's build commit hash and warn on
version mismatch. Soft-warn only — mismatched versions never refuse
traffic.

Wire change (lan-mouse-proto)
* `ProtoEvent::Hello { commit: [u8; 8] }` carries the 8-byte ASCII
  short commit from shadow_rs's `SHORT_COMMIT`. Encoded/decoded
  alongside the existing event variants.
* `EventType::Hello` is appended to the enum so existing IDs are
  untouched. Old peers receive the event, hit `InvalidEventId`, and
  silently skip it via the forward-compat handler in
  `connect.rs::receive_loop` — the connection is unaffected.

Daemon
* Connect side sends one Hello immediately after the DTLS handshake
  authenticates and before the ping_pong loop starts. Best-effort,
  fire-and-forget — `log::debug!` on send error.
* Listen side mirrors the peer's Hello with its own (same shape as
  the existing Ping → Pong reply), so the peer's connect-side
  receive_loop populates `ClientState::peer_commit` for that
  handle.
* The disconnect path clears `peer_commit` so a stale hash isn't
  shown after the connection drops.

IPC
* `ClientState::peer_commit: Option<[u8; 8]>`. `None` means the
  peer hasn't sent Hello yet — either fresh connection or older
  build that predates the event.

GTK
* `ClientObject` exposes `peer-commit` as an `Option<String>`
  property; `peer_commit_to_string` converts the wire `[u8; 8]` to
  the displayable hex.
* `lan_mouse_gtk::run` now takes the local commit and stashes it in
  a `OnceLock` so per-row UI can compare against each peer's hash.
* `ClientRow::refresh_version_status` re-renders the collapsed
  subtitle with Pango markup whenever the property changes:
   - matched → green   "peer version: <hex> · matched"
   - mismatch → orange "peer version: <hex> · ours: <hex>"
   - unknown → orange  "peer version: unknown · ours: <hex>"
* Window invokes `refresh_version_status` from
  `update_client_state` after writing the new property, and
  `bind` calls it once on row construction so the initial
  subtitle isn't blank.

Known limitation: state-change broadcasts from the network side
(set_alive / set_active_addr / set_peer_commit) don't currently
trigger a `FrontendEvent::State` directly; the UI picks up the
latest values on the next user-driven broadcast. Same pre-existing
behavior as the alive/active_addr fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 18:10:22 +02:00
Jon Kinney
59b6447745 fix(proto): tolerate undecodable peer datagrams instead of disconnecting
The DTLS recv loops in src/listen.rs and src/connect.rs each read
one full datagram per call. A failed `try_into::<ProtoEvent>()`
means the datagram's leading EventType byte didn't match any
known variant — a misalignment is impossible because DTLS is
message-framed, not stream-framed.

Previously, src/listen.rs would `break` out of the loop on parse
failure (tearing down the connection) and src/connect.rs would
silently swallow the error with no log. Both are wrong as
forward-compat behavior: any future protocol addition (e.g. a
new event variant) would force every existing peer to disconnect
rather than gracefully ignoring the unknown event.

Skip-and-continue on both sides, with a debug-level log so the
behavior is observable. Pre-requisite for any future ProtoEvent
variant to land without forcing a coordinated upgrade across
every peer in a deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 18:00:14 +02:00
13 changed files with 308 additions and 156 deletions

View File

@@ -30,19 +30,11 @@ jobs:
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-x86_64
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
- name: Upload build artifact
uses: actions/upload-artifact@v6
with:
name: lan-mouse-linux-x86_64
path: |
lan-mouse-linux-x86_64
target/release/bundle/deb/lan-mouse_*.deb
target/release/bundle/appimage/lan-mouse_*.AppImage
path: lan-mouse-linux-x86_64
linux-arm64-release-build:
runs-on: ubuntu-22.04-arm
@@ -57,19 +49,11 @@ jobs:
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-arm64
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
- name: Upload build artifact
uses: actions/upload-artifact@v6
with:
name: lan-mouse-linux-arm64
path: |
lan-mouse-linux-arm64
target/release/bundle/deb/lan-mouse_*.deb
target/release/bundle/appimage/lan-mouse_*.AppImage
path: lan-mouse-linux-arm64
windows-release-build:
runs-on: windows-latest
@@ -105,7 +89,7 @@ jobs:
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --vs-ver=vs2026 --msys-dir=C:\msys64 gtk4 libadwaita librsvg
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v6
@@ -204,11 +188,7 @@ jobs:
generate_release_notes: true
files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64
lan-mouse-linux-x86_64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-x86_64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-linux-arm64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-arm64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip
@@ -221,11 +201,7 @@ jobs:
generate_release_notes: true
files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64
lan-mouse-linux-x86_64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-x86_64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-linux-arm64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-arm64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip

View File

@@ -40,6 +40,7 @@ jobs:
- test
steps:
- uses: actions/checkout@v6
- uses: Swatinem/rust-cache@v2
- name: Install Linux deps
if: runner.os == 'Linux'
run: |
@@ -48,13 +49,6 @@ jobs:
- name: Install macOS dependencies
if: runner.os == 'macOS'
run: brew install gtk4 libadwaita imagemagick
- name: Record macOS native lib versions for cache key
if: runner.os == 'macOS'
run: |
echo "MACOS_LIB_VER=$(brew list --versions glib gtk4 libadwaita | tr '\n' '_')" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ env.MACOS_LIB_VER }}
- name: Install Windows Dependencies - create gtk dir
if: runner.os == 'Windows'
run: mkdir C:\gtk-build\gtk\x64\release
@@ -83,7 +77,7 @@ jobs:
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
gvsbuild build --vs-ver=vs2026 --msys-dir=C:\msys64 gtk4 libadwaita librsvg
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
- name: cargo build
if: matrix.job == 'build'
run: cargo build

256
Cargo.lock generated
View File

@@ -549,6 +549,30 @@ dependencies = [
"libc",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -562,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
@@ -574,7 +598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"typenum",
]
@@ -711,7 +735,7 @@ dependencies = [
"hkdf",
"pem-rfc7468",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
@@ -723,6 +747,18 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -816,7 +852,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -1245,7 +1281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -1359,6 +1395,52 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"thiserror 2.0.18",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"moka",
"once_cell",
"parking_lot",
"rand 0.9.2",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "hkdf"
version = "0.12.4"
@@ -1631,6 +1713,19 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "ipconfig"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
dependencies = [
"socket2",
"widestring",
"windows-registry",
"windows-result 0.4.1",
"windows-sys 0.61.2",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1750,6 +1845,7 @@ dependencies = [
"clap",
"env_logger",
"futures",
"hickory-resolver",
"input-capture",
"input-emulation",
"input-event",
@@ -1898,6 +1994,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -1994,6 +2096,23 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moka"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"equivalent",
"parking_lot",
"portable-atomic",
"smallvec",
"tagptr",
"uuid",
]
[[package]]
name = "nix"
version = "0.26.4"
@@ -2123,6 +2242,10 @@ name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "once_cell_polyfill"
@@ -2407,8 +2530,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
@@ -2418,7 +2551,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
@@ -2430,6 +2573,15 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rcgen"
version = "0.13.2"
@@ -2483,17 +2635,21 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reis"
version = "0.7.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3fedd2777cde52c1be5e572efbec485eac7b801c47820eda388d4f13b9c4b"
checksum = "00939c5c526a1b4054ef8d9d96b3f92227f08ca355965e986741b556eda6d289"
dependencies = [
"enumflags2",
"futures-util",
"log",
"rustix",
"futures",
"rustix 0.38.44",
"tokio",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "rfc6979"
version = "0.4.0"
@@ -2536,6 +2692,19 @@ dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -2545,7 +2714,7 @@ dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@@ -2760,7 +2929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -2848,6 +3017,12 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "target-lexicon"
version = "0.13.3"
@@ -2863,7 +3038,7 @@ dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
@@ -2950,6 +3125,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.51.1"
@@ -3227,6 +3417,7 @@ version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -3371,7 +3562,7 @@ checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"rustix 1.1.4",
"smallvec",
"wayland-sys",
]
@@ -3383,7 +3574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
"bitflags 2.11.0",
"rustix",
"rustix 1.1.4",
"wayland-backend",
"wayland-scanner",
]
@@ -3467,8 +3658,8 @@ dependencies = [
"p384",
"pem",
"portable-atomic",
"rand",
"rand_core",
"rand 0.8.5",
"rand_core 0.6.4",
"rcgen",
"ring",
"rustls",
@@ -3499,12 +3690,18 @@ dependencies = [
"log",
"nix",
"portable-atomic",
"rand",
"rand 0.8.5",
"thiserror 1.0.69",
"tokio",
"winapi",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -3639,6 +3836,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -3978,7 +4186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core",
"rand_core 0.6.4",
"serde",
"zeroize",
]
@@ -4048,7 +4256,7 @@ dependencies = [
"hex",
"libc",
"ordered-stream",
"rustix",
"rustix 1.1.4",
"serde",
"serde_repr",
"tokio",

View File

@@ -36,6 +36,7 @@ lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.3.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.3.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.25.2"
toml = "0.8"
toml_edit = { version = "0.22", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
@@ -93,8 +94,7 @@ rdp_emulation = ["input-emulation/remote_desktop_portal"]
[package.metadata.bundle]
name = "Lan Mouse"
icon = ["target/icon.icns", "lan-mouse-gtk/resources/de.feschber.LanMouse.svg"]
icon = ["target/icon.icns"]
identifier = "de.feschber.LanMouse"
deb_depends = [ "libadwaita-1-0" ]
osx_info_plist_exts = ["build-aux/macos-lsui-element.plist"]
resources = ["target/menubar-template.png"]

View File

@@ -45,7 +45,7 @@ ashpd = { version = "0.13.9", default-features = false, features = [
"input_capture",
"tokio",
], optional = true }
reis = { version = "0.7.0", features = ["tokio"], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.25.0", features = ["highsierra"] }

View File

@@ -123,6 +123,7 @@ struct State {
focused: Option<Arc<Window>>,
global_list: GlobalList,
globals: Globals,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
@@ -137,7 +138,7 @@ struct Inner {
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.queue.as_fd().as_raw_fd()
self.state.wayland_fd
}
}
@@ -331,6 +332,7 @@ impl LayerShellInputCapture {
active_windows: Vec::new(),
focused: None,
qh,
wayland_fd: queue.as_fd().as_raw_fd(),
read_guard: None,
pending_events: VecDeque::new(),
outputs: vec![],
@@ -546,12 +548,13 @@ impl State {
}
impl Inner {
fn read(&mut self) {
fn read(&mut self) -> bool {
match self.state.read_guard.take().unwrap().read() {
Ok(_) => {}
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => {}
Ok(_) => true,
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => false,
Err(WaylandError::Io(e)) => {
log::error!("error reading from wayland socket: {e}");
false
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
@@ -561,11 +564,16 @@ impl Inner {
fn prepare_read(&mut self) -> io::Result<()> {
loop {
if let Some(guard) = self.queue.prepare_read() {
self.state.read_guard = Some(guard);
break Ok(());
} else {
self.dispatch_events();
match self.queue.prepare_read() {
None => match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
Err(e) => panic!("failed to dispatch wayland events: {e}"),
},
Some(r) => {
self.state.read_guard = Some(r);
break Ok(());
}
}
}
}
@@ -647,7 +655,13 @@ impl Stream for LayerShellInputCapture {
let inner = guard.get_inner_mut();
// read events
inner.read();
while inner.read() {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
}
}
// dispatch the events
inner.dispatch_events();

View File

@@ -18,12 +18,12 @@ use reis::{
use std::{
cell::Cell,
collections::HashMap,
env, io,
io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::{Arc, LazyLock},
sync::Arc,
task::{Context, Poll},
};
use tokio::{
@@ -50,25 +50,6 @@ use super::{
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to be recreated when the barriers are updated */
/* mutter also kills the session whenever ei devices come and go, so there the
* whole session has to be torn down and recreated on every device change.
* Elsewhere that is pure overhead: each restart costs a CreateSession +
* ConnectToEIS round trip, and compositors that keep per-session state around
* (hyprland leaks a keymap fd per eis session, see hyprwm/Hyprland) can be
* driven out of file descriptors by the churn.
* Set LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 to override the default. */
static RESTART_SESSION_ON_DEVICE_CHANGE: LazyLock<bool> =
LazyLock::new(restart_session_on_device_change);
fn restart_session_on_device_change() -> bool {
match env::var("LM_RESTART_SESSION_ON_DEVICE_CHANGE").as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => env::var("XDG_CURRENT_DESKTOP")
.is_ok_and(|desktops| desktops.to_uppercase().split(':').any(|d| d == "GNOME")),
}
}
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
@@ -432,15 +413,7 @@ async fn do_capture_session(
};
// find client corresponding to barrier
let pos = match pos_for_barrier_id.get(&barrier_id) {
Some(id) => *id,
None => {
log::warn!("INVALID BARRIER ID: Id {barrier_id} does not exist!");
let id = find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor"));
let pos = *pos_for_barrier_id.get(&id).expect("invalid barrier id");
pos
},
};
let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
current_pos.replace(Some(pos));
// client entered => send event
@@ -556,6 +529,15 @@ fn distance_to_line(line: ((f32, f32), (f32, f32)), p: (f32, f32)) -> f32 {
distance
}
static ALL_CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<Position>,
@@ -563,43 +545,25 @@ async fn handle_ei_event(
event_tx: &Sender<(Position, CaptureEvent)>,
release_session: &Notify,
) -> Result<(), CaptureError> {
let all_capabilities = DeviceCapability::Pointer
| DeviceCapability::PointerAbsolute
| DeviceCapability::Keyboard
| DeviceCapability::Touch
| DeviceCapability::Scroll
| DeviceCapability::Button;
match ei_event {
EiEvent::SeatAdded(s) => {
s.seat.bind_capabilities(all_capabilities);
s.seat.bind_capabilities(ALL_CAPABILITIES);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) => {
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
/* EiEvent::DeviceAdded(_) | */
EiEvent::DeviceRemoved(_) => {
if *RESTART_SESSION_ON_DEVICE_CHANGE {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
} else {
log::debug!("ignoring device change: {ei_event:?}");
}
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)));
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx
.send((pos, CaptureEvent::Input(event)))
.await
.expect("no channel");
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
}
}
}

View File

@@ -45,7 +45,7 @@ ashpd = { version = "0.13.9", default-features = false, features = [
"screencast",
"tokio",
], optional = true }
reis = { version = "0.7.0", features = ["tokio"], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0"

View File

@@ -291,12 +291,14 @@ async fn ei_event_handler(
) -> Result<(), EmulationError> {
loop {
let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
let capabilities = DeviceCapability::Pointer
| DeviceCapability::PointerAbsolute
| DeviceCapability::Keyboard
| DeviceCapability::Touch
| DeviceCapability::Scroll
| DeviceCapability::Button;
const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
log::debug!("{event:?}");
match event {
EiEvent::Disconnected(e) => {
@@ -304,7 +306,7 @@ async fn ei_event_handler(
return Err(EmulationError::EndOfStream);
}
EiEvent::SeatAdded(e) => {
e.seat().bind_capabilities(capabilities);
e.seat().bind_capabilities(CAPABILITIES);
}
EiEvent::SeatRemoved(e) => {
log::debug!("seat removed: {:?}", e.seat());
@@ -312,6 +314,7 @@ async fn ei_event_handler(
EiEvent::DeviceAdded(e) => {
let device_type = e.device().device_type();
log::debug!("device added: {device_type:?}");
e.device().device();
let device = e.device();
if let Some(pointer) = e.device().interface::<Pointer>() {
devices

View File

@@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.7.0", optional = true }
reis = { version = "0.5.0", optional = true }
[features]
default = ["libei"]

View File

@@ -518,7 +518,7 @@ impl Config {
}
pub fn read_from_disk(&mut self) -> Result<bool, io::Error> {
log::info!("reading config from {:?}", self.config_path);
log::info!("reading config from {:?}", &self.config_path);
let current_config = fs::read_to_string(&self.config_path)?;
let current_config = match current_config.parse::<DocumentMut>() {
@@ -548,7 +548,7 @@ impl Config {
}
pub fn write_back(&mut self) -> Result<(), io::Error> {
log::info!("writing config to {:?}", self.config_path);
log::info!("writing config to {:?}", &self.config_path);
/* the new config */
let new_config = self.config_toml.clone().unwrap_or_default();
let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config");

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, io, net::IpAddr};
use std::{collections::HashMap, net::IpAddr};
use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::net::lookup_host;
use tokio::task::{JoinHandle, spawn_local};
use hickory_resolver::{ResolveError, TokioResolver};
use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle;
@@ -22,10 +22,11 @@ struct DnsRequest {
pub(crate) enum DnsEvent {
Resolving(ClientHandle),
Resolved(ClientHandle, String, io::Result<Vec<IpAddr>>),
Resolved(ClientHandle, String, Result<Vec<IpAddr>, ResolveError>),
}
struct DnsTask {
resolver: TokioResolver,
request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken,
@@ -33,12 +34,14 @@ struct DnsTask {
}
impl DnsResolver {
pub(crate) fn new() -> io::Result<Self> {
pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioResolver::builder_tokio()?.build();
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();
let dns_task = DnsTask {
active_tasks: Default::default(),
resolver,
request_rx,
event_tx,
cancellation_token: cancellation_token.clone(),
@@ -94,13 +97,15 @@ impl DnsTask {
/* spawn task for dns request */
let event_tx = self.event_tx.clone();
let resolver = self.resolver.clone();
let cancellation_token = self.cancellation_token.clone();
let task = tokio::task::spawn_local(async move {
tokio::select! {
result = resolve_hostname(&hostname) => {
ips = resolver.lookup_ip(&hostname) => {
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
event_tx
.send(DnsEvent::Resolved(handle, hostname, result))
.send(DnsEvent::Resolved(handle, hostname, ips))
.expect("channel closed");
}
_ = cancellation_token.cancelled() => {},
@@ -110,18 +115,3 @@ impl DnsTask {
}
}
}
/// Resolve `hostname` via the operating system's full name-resolution
/// stack (`getaddrinfo` on Unix, GetAddrInfoEx on Windows). This walks
/// `/etc/nsswitch.conf` on Linux — picking up mDNS via Avahi, /etc/hosts,
/// and DNS — and uses Bonjour for `.local` names on macOS. Pure-DNS
/// resolvers like hickory miss all of those, which is why a Bonjour
/// hostname (e.g. `JKMBP-M4-Max.local`) wouldn't resolve before.
///
/// Port `0` is a placeholder — `lookup_host` requires `host:port` but we
/// only care about the IPs at this stage; the actual port is appended at
/// connection time.
async fn resolve_hostname(hostname: &str) -> io::Result<Vec<IpAddr>> {
let addrs = lookup_host((hostname, 0)).await?;
Ok(addrs.map(|sa| sa.ip()).collect())
}

View File

@@ -9,6 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, IpcError,
IpcListenerCreationError, Position, Status,
@@ -25,6 +26,8 @@ use tokio::{process::Command, signal, sync::Notify};
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
IpcListen(#[from] IpcListenerCreationError),
#[error(transparent)]