Compare commits
5
Commits
a740376ea9
...
32131b0ccb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32131b0ccb | ||
|
|
035aa4b256 | ||
|
|
f85c0c22c7 | ||
|
|
c30418a0f5 | ||
|
|
14245cbf08 |
+33
-1
@@ -65,9 +65,41 @@ pub fn measure_upstream_blocking() -> Result<Measurement> {
|
||||
/// via SAFETY_FACTOR) into a recommended viewer count. Floors to at least 1.
|
||||
pub fn recommended_max_viewers(safe_mbps: f64, bitrate_kbps: u32) -> u32 {
|
||||
let per_viewer_mbps = (bitrate_kbps as f64) / 1000.0;
|
||||
if per_viewer_mbps <= 0.0 {
|
||||
// Guard non-finite / non-positive inputs (only reachable from a corrupted
|
||||
// config): a NaN safe_mbps would cast to 0 and an infinite one to u32::MAX,
|
||||
// both of which break the "at least 1" contract.
|
||||
if !safe_mbps.is_finite() || safe_mbps <= 0.0 || per_viewer_mbps <= 0.0 {
|
||||
return 1;
|
||||
}
|
||||
let n = (safe_mbps / per_viewer_mbps).floor();
|
||||
if n < 1.0 { 1 } else { n as u32 }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::recommended_max_viewers;
|
||||
|
||||
#[test]
|
||||
fn divides_bandwidth_by_per_viewer_bitrate() {
|
||||
// 8 Mbps safe / 2 Mbps each = 4 viewers.
|
||||
assert_eq!(recommended_max_viewers(8.0, 2000), 4);
|
||||
// Floors the fractional part: 7.9 / 2 = 3.95 -> 3.
|
||||
assert_eq!(recommended_max_viewers(7.9, 2000), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floors_to_at_least_one() {
|
||||
// Not even enough for one viewer still allows one (best effort).
|
||||
assert_eq!(recommended_max_viewers(0.5, 2000), 1);
|
||||
// Zero / unknown bitrate can't size a budget; floor to one.
|
||||
assert_eq!(recommended_max_viewers(8.0, 0), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_inputs_floor_to_one() {
|
||||
// A corrupted config must not yield 0 (NaN) or u32::MAX (Inf).
|
||||
assert_eq!(recommended_max_viewers(f64::NAN, 2000), 1);
|
||||
assert_eq!(recommended_max_viewers(f64::INFINITY, 2000), 1);
|
||||
assert_eq!(recommended_max_viewers(-5.0, 2000), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -7,10 +7,17 @@ pub fn install_ctrl_c() -> CancellationToken {
|
||||
let token = CancellationToken::new();
|
||||
let trigger = token.clone();
|
||||
tokio::spawn(async move {
|
||||
if tokio::signal::ctrl_c().await.is_ok() {
|
||||
tracing::info!("ctrl-c received, shutting down");
|
||||
trigger.cancel();
|
||||
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||
// Installing the handler failed — ctrl-c won't trigger a graceful
|
||||
// shutdown. Say so instead of failing silently; the user can still
|
||||
// kill the process, and the second-ctrl-c arm below would only fail
|
||||
// the same way, so bail out of the task.
|
||||
tracing::warn!("could not install ctrl-c handler: {e}; ctrl-c won't shut down cleanly");
|
||||
return;
|
||||
}
|
||||
tracing::info!("ctrl-c received, shutting down");
|
||||
trigger.cancel();
|
||||
|
||||
if tokio::signal::ctrl_c().await.is_ok() {
|
||||
tracing::warn!("second ctrl-c — exiting now");
|
||||
std::process::exit(130);
|
||||
|
||||
@@ -87,6 +87,10 @@ struct PixelPassTray {
|
||||
/// Wakes the winit loop and delivers the action — works even when the
|
||||
/// window has been dropped to the tray (no egui frame is running then).
|
||||
proxy: EventLoopProxy<UserEvent>,
|
||||
/// Shared with [`TrayHandle`]; kept in sync with the watcher's presence via
|
||||
/// the `watcher_online`/`watcher_offline` callbacks so the app never diverts
|
||||
/// a close to a tray that has since disappeared.
|
||||
registered: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PixelPassTray {
|
||||
@@ -131,6 +135,25 @@ impl ksni::Tray for PixelPassTray {
|
||||
self.notify(TrayAction::Show);
|
||||
}
|
||||
|
||||
/// The StatusNotifierWatcher came back (e.g. the panel restarted). Mark the
|
||||
/// tray live again so close-to-tray can resume hiding the window.
|
||||
fn watcher_online(&self) {
|
||||
self.registered.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
/// The watcher went away (panel restart, tray plugin disabled, …). Clear the
|
||||
/// flag so a subsequent close quits normally instead of destroying the window
|
||||
/// into a tray that no longer exists, and force the window back now in case
|
||||
/// it was already hidden (otherwise it'd be stranded with no way to restore).
|
||||
/// Returning `true` keeps the service alive so it re-registers if the watcher
|
||||
/// returns.
|
||||
fn watcher_offline(&self, reason: ksni::OfflineReason) -> bool {
|
||||
tracing::warn!("tray: StatusNotifierWatcher offline ({reason:?}); restoring window");
|
||||
self.registered.store(false, Ordering::Release);
|
||||
self.notify(TrayAction::Show);
|
||||
true
|
||||
}
|
||||
|
||||
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
|
||||
use ksni::menu::{MenuItem, StandardItem};
|
||||
vec.
|
||||
pub fn shutdown(mut self) {
|
||||
self.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Routing {
|
||||
fn drop(&mut self) {
|
||||
if let Some(router) = self.stream_router.take() {
|
||||
router.shutdown();
|
||||
}
|
||||
if let Some(task) = self.event_task.take() {
|
||||
task.abort();
|
||||
}
|
||||
if let Some(id) = self.loopback_module.lock().unwrap().take() {
|
||||
unload_module(id);
|
||||
}
|
||||
if let Some(id) = self.sink_module.take() {
|
||||
unload_module(id);
|
||||
}
|
||||
self.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-25
@@ -50,34 +50,41 @@ pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
|
||||
}
|
||||
},
|
||||
};
|
||||
let (quic_send, quic_recv) = conn.open_bi().await?;
|
||||
// Everything past the established connection runs in one block so any error
|
||||
// (open_bi, bind, local_addr, accept) is captured rather than `?`-propagated
|
||||
// straight out of the function — that would skip the close below and leak the
|
||||
// endpoint. The connect-phase arms above close explicitly for the same reason.
|
||||
let result = async {
|
||||
let (quic_send, quic_recv) = conn.open_bi().await?;
|
||||
|
||||
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let url = format!("http://127.0.0.1:{port}");
|
||||
output::emit(output::Event::Connected { url: &url });
|
||||
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let url = format!("http://127.0.0.1:{port}");
|
||||
output::emit(output::Event::Connected { url: &url });
|
||||
|
||||
if opts.interactive {
|
||||
let player = crate::interactive::prompt_player()?;
|
||||
player
|
||||
.spawn(&url)
|
||||
.with_context(|| "failed to launch player")?;
|
||||
print_viewer_banner_interactive();
|
||||
} else {
|
||||
print_viewer_banner(&url);
|
||||
if opts.interactive {
|
||||
let player = crate::interactive::prompt_player()?;
|
||||
player
|
||||
.spawn(&url)
|
||||
.with_context(|| "failed to launch player")?;
|
||||
print_viewer_banner_interactive();
|
||||
} else {
|
||||
print_viewer_banner(&url);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
accepted = listener.accept() => {
|
||||
let (tcp, peer) = accepted?;
|
||||
tracing::info!(%peer, "local viewer connected");
|
||||
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
|
||||
}
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!("ctrl-c received before local viewer connected");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = tokio::select! {
|
||||
accepted = listener.accept() => {
|
||||
let (tcp, peer) = accepted?;
|
||||
tracing::info!(%peer, "local viewer connected");
|
||||
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
|
||||
}
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!("ctrl-c received before local viewer connected");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
.await;
|
||||
|
||||
endpoint.close().await;
|
||||
result
|
||||
|
||||
Reference in New Issue
Block a user