Merge remote-tracking branch 'origin/main'
# Conflicts: # clients/windows/src/app/mod.rs
This commit is contained in:
@@ -8,6 +8,7 @@ use super::style::*;
|
||||
use super::{Screen, Svc, Target};
|
||||
use crate::discovery::DiscoveredHost;
|
||||
use crate::trust::KnownHosts;
|
||||
use std::collections::HashMap;
|
||||
use windows_reactor::*;
|
||||
|
||||
/// Overflow-menu item labels — `on_item_clicked` reports the clicked item by its text.
|
||||
@@ -42,6 +43,10 @@ const TILE_GAP: f64 = 12.0;
|
||||
pub(crate) struct HostsProps {
|
||||
pub(crate) svc: Svc,
|
||||
pub(crate) hosts: Vec<DiscoveredHost>,
|
||||
/// Saved hosts proven reachable by the periodic QUIC probe (keyed by `fp_hex`), OR'd with
|
||||
/// live-advert presence to drive the Online pip — so a host reached only over a routed
|
||||
/// network (Tailscale/VPN), which never advertises on mDNS, still reads Online.
|
||||
pub(crate) probed: HashMap<String, bool>,
|
||||
pub(crate) status: String,
|
||||
/// Connected-controller count (root state, mirrored from the gamepad service) — a
|
||||
/// pad plus a paired host surfaces the "Open console UI" hint card.
|
||||
@@ -68,6 +73,7 @@ impl PartialEq for HostsProps {
|
||||
// Setters are identity-stable; only the value fields drive re-render.
|
||||
self.svc == other.svc
|
||||
&& self.hosts == other.hosts
|
||||
&& self.probed == other.probed
|
||||
&& self.status == other.status
|
||||
&& self.pads == other.pads
|
||||
&& self.forget == other.forget
|
||||
@@ -327,13 +333,21 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
);
|
||||
}
|
||||
|
||||
// A controller is connected and a paired host exists: offer the couch experience —
|
||||
// the console (gamepad) UI on the most recently used paired host.
|
||||
// A controller is connected and a paired host is REACHABLE (advertising or probed —
|
||||
// an offline host would just open the console onto an error scene): offer the couch
|
||||
// experience — the console (gamepad) UI on the most recently used such host.
|
||||
if CONSOLE_UI_AVAILABLE && props.pads > 0 {
|
||||
let reachable = |k: &&crate::trust::KnownHost| {
|
||||
hosts
|
||||
.iter()
|
||||
.any(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
|
||||
|| props.probed.get(&k.fp_hex).copied().unwrap_or(false)
|
||||
};
|
||||
if let Some(k) = known
|
||||
.hosts
|
||||
.iter()
|
||||
.filter(|h| h.paired)
|
||||
.filter(reachable)
|
||||
.max_by_key(|h| h.last_used.unwrap_or(0))
|
||||
{
|
||||
let target = Target {
|
||||
@@ -404,9 +418,13 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
pair_optional: false,
|
||||
mac: k.mac.clone(),
|
||||
};
|
||||
// Online = advertising on mDNS OR proven reachable by the last probe sweep (the latter
|
||||
// covers a routed/Tailscale host that never advertises — the display companion to
|
||||
// dial-first).
|
||||
let online = hosts
|
||||
.iter()
|
||||
.any(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port));
|
||||
.any(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
|
||||
|| props.probed.get(&k.fp_hex).copied().unwrap_or(false);
|
||||
// Learn this host's wake MAC(s) from its live advert while it's online, so we can wake
|
||||
// it once it sleeps (no-op / no disk write when unchanged).
|
||||
if let Some(a) = hosts.iter().find(|h| {
|
||||
|
||||
@@ -36,12 +36,14 @@ mod style;
|
||||
use crate::discovery::{self, DiscoveredHost};
|
||||
use crate::gamepad::GamepadService;
|
||||
use crate::session::Stats;
|
||||
use crate::trust::Settings;
|
||||
use crate::trust::{KnownHosts, Settings};
|
||||
use hosts::HostsProps;
|
||||
use punktfunk_core::client::NativeClient;
|
||||
use speed::{SpeedProps, SpeedState};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use stream::StreamProps;
|
||||
use windows_reactor::*;
|
||||
|
||||
@@ -265,6 +267,9 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
// Saved-host reachability, keyed by `fp_hex`, refreshed by the probe sweep below. Root state
|
||||
// (thread-driven → must be root to re-render — see the module docs), passed to the hosts page.
|
||||
let (probed, set_probed) = cx.use_async_state(HashMap::<String, bool>::new());
|
||||
|
||||
// Continuous LAN discovery (spawned once).
|
||||
cx.use_effect((), {
|
||||
@@ -309,6 +314,52 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic reachability sweep (spawned once): a saved host reached only over a routed network
|
||||
// (Tailscale/VPN) never advertises on mDNS, so presence can't come from the discovery stream
|
||||
// alone. This probes every saved host (bounded, trust-agnostic QUIC handshake — one thread each
|
||||
// so a slow/unreachable host doesn't delay the rest) and mirrors the results into root state so
|
||||
// the tiles get them as a prop; the pip then reads `advertising OR probed-reachable` — the
|
||||
// display-side companion to the dial-first connect fix. The compare in `call` makes idle free.
|
||||
cx.use_effect((), {
|
||||
let set_probed = set_probed.clone();
|
||||
let shared = ctx.shared.clone();
|
||||
move || {
|
||||
std::thread::Builder::new()
|
||||
.name("pf-probe".into())
|
||||
.spawn(move || loop {
|
||||
// A spawned session/browse child is running: the shell is hidden
|
||||
// (nobody sees the pips) and one of these hosts is mid-stream —
|
||||
// probing it is pure noise. Sleep through and sweep after it ends.
|
||||
if shared.session.lock().unwrap().is_running() {
|
||||
std::thread::sleep(Duration::from_secs(12));
|
||||
continue;
|
||||
}
|
||||
let handles: Vec<_> = KnownHosts::load()
|
||||
.hosts
|
||||
.into_iter()
|
||||
.filter(|h| !h.addr.is_empty())
|
||||
.map(|h| {
|
||||
std::thread::spawn(move || {
|
||||
(
|
||||
h.fp_hex,
|
||||
NativeClient::probe(
|
||||
&h.addr,
|
||||
h.port,
|
||||
Duration::from_millis(2500),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let map: HashMap<String, bool> =
|
||||
handles.into_iter().filter_map(|h| h.join().ok()).collect();
|
||||
set_probed.call(map);
|
||||
std::thread::sleep(Duration::from_secs(12));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
// Screen-entrance animation: each navigation slides the new screen up a few px while fading it
|
||||
// in (the Windows-Settings drill-in). It's a manual tween, not a composition animation, because
|
||||
// reactor's DSL exposes no static transform/translation setter and its one-shot animations run
|
||||
@@ -424,6 +475,7 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
||||
HostsProps {
|
||||
svc,
|
||||
hosts,
|
||||
probed,
|
||||
status,
|
||||
pads,
|
||||
forget,
|
||||
|
||||
@@ -42,6 +42,13 @@ impl SessionChild {
|
||||
let _ = child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a spawned child is currently live (spawned and not yet reaped by its
|
||||
/// reader). The probe sweep pauses while one runs — the shell is hidden, and probing
|
||||
/// the host we're streaming from is just noise.
|
||||
pub(crate) fn is_running(&self) -> bool {
|
||||
self.0.lock().unwrap().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// One parsed stdout line of the session contract; `None` for anything unrecognized.
|
||||
|
||||
Reference in New Issue
Block a user