Repository navigation
Switch the desktop encoder on SIGUSR2 without dropping the session #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7074e43
dd2e24b
5911a80
54844ef
b9de098
e3588c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,10 +144,46 @@ fn set_video_pref() -> Result<(), Box<dyn std::error::Error>> { | |
| let selection = video_select::selection(); | ||
| let codec = match selection.codec_pref { video_select::CodecPref::Auto => "auto", video_select::CodecPref::H264 => "h264", video_select::CodecPref::H265 => "h265" }; | ||
| println!("encoder={:?} source={} codec_pref={codec} source={}", selection.encoder, video_select::encoder_source_label(selection.encoder_source), video_select::source_label(selection.codec_source)); | ||
| println!("takes effect the next time a desktop connection starts"); | ||
| // Both subcommands wake the host. Encoder changes restart live desktop streams; codec changes are logged only. | ||
| match notify_host(libc::SIGUSR2) { | ||
| Ok(()) => println!("notified the running host"), | ||
| Err(_) => println!( | ||
| "host not running, or the running host is not this binary: takes effect the next time a desktop connection starts, or after restarting the service" | ||
| ), | ||
| } | ||
| if which == "set-codec" { | ||
| println!("takes effect the next time a desktop connection starts"); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Signal the host recorded in host.pid only when `/proc/<pid>/exe` is exactly | ||
| /// `canonicalize(current_exe())`. A filename match is not enough: an older host | ||
| /// has no SIGUSR2 handler, and the default action terminates the process. | ||
| /// A replaced binary reads back as `… (deleted)` and does not match. | ||
| fn notify_host(signal: i32) -> Result<(), ()> { | ||
| let pid = std::fs::read_to_string(host_pid_path()) | ||
| .ok() | ||
| .and_then(|s| s.trim().parse::<i32>().ok()) | ||
| .filter(|p| *p > 1) | ||
| .ok_or(())?; | ||
| let exe = std::fs::read_link(format!("/proc/{pid}/exe")).map_err(|_| ())?; | ||
| let own = std::fs::canonicalize(std::env::current_exe().map_err(|_| ())?).map_err(|_| ())?; | ||
| if !same_host_binary(&exe, &own) { | ||
| return Err(()); | ||
| } | ||
| if unsafe { libc::kill(pid, signal) } == 0 { | ||
| Ok(()) | ||
| } else { | ||
| Err(()) | ||
| } | ||
| } | ||
|
|
||
| /// `/proc/<pid>/exe` must equal the canonical path of this binary. A trailing ` (deleted)` does not match. | ||
| fn same_host_binary(proc_exe: &std::path::Path, own: &std::path::Path) -> bool { | ||
| proc_exe == own | ||
| } | ||
|
|
||
| /// Explicit command words for set-encoder / set-codec. Empty and whitespace are illegal. | ||
| pub(crate) fn cli_pref_ok(which: &str, value: &str) -> bool { | ||
| let value = value.trim(); | ||
|
|
@@ -196,21 +232,85 @@ mod cli_exit_tests { | |
| assert_eq!(super::reject_cli(Some("set-encoder"), 3, super::cli_pref_ok("set-encoder", "")), Some(2)); | ||
| assert_eq!(super::reject_cli(Some("set-codec"), 3, super::cli_pref_ok("set-codec", " ")), Some(2)); | ||
| } | ||
|
|
||
| /// SIGUSR1 and SIGUSR2 are installed with no assist UI. `host.pid` is written only after both exist. | ||
| #[test] | ||
| fn signal_handlers_install_without_assist() { | ||
| let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); | ||
| rt.block_on(async { | ||
| let _guard = crate::config::test_env_lock().lock().unwrap(); | ||
| let dir = | ||
| std::env::temp_dir().join(format!("uuremote-signal-test-{}", std::process::id())); | ||
| let _ = std::fs::remove_dir_all(&dir); | ||
| std::env::set_var("UU_CONFIG", dir.join("config.json")); | ||
| assert!(super::install_host_signals(None)); | ||
| let pid = std::fs::read_to_string(super::host_pid_path()).unwrap(); | ||
| assert_eq!(pid.trim(), std::process::id().to_string()); | ||
| std::env::remove_var("UU_CONFIG"); | ||
| let _ = std::fs::remove_dir_all(&dir); | ||
| }); | ||
| } | ||
|
|
||
| #[test] | ||
| fn sigusr2_requires_the_same_canonical_binary() { | ||
| let own = std::path::Path::new("/home/user/.local/bin/uuremote-server"); | ||
| assert!(super::same_host_binary(own, own)); | ||
| assert!(!super::same_host_binary( | ||
| std::path::Path::new("/tmp/unpack/uuremote-server"), | ||
| own | ||
| )); | ||
| assert!(!super::same_host_binary( | ||
| std::path::Path::new("/home/user/.local/bin/uuremote-server (deleted)"), | ||
| own | ||
| )); | ||
| } | ||
| } | ||
| /// SIGUSR1 (`uuremote-server show`) opens the host window. | ||
| fn present_on_usr1(ui: std::sync::Arc<assist::LocalUi>) { | ||
| // Install the handler before publishing the pid: SIGUSR1's default action terminates the process. | ||
| let Ok(mut usr1) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1()) else { return }; | ||
| /// SIGUSR2 (set-encoder, set-codec, or `kill -USR2`) re-reads host_settings. Encoder changes switch live desktop streams; codec changes are logged for the next connection. | ||
| /// The default action of either signal terminates the process, so both handlers are installed before `host.pid` is written. | ||
| /// SIGUSR2 is independent of remote assistance: `UU_REMOTE_ASSIST=0` and a failed assist setup still switch live encoders. | ||
| /// SIGUSR1 is installed first as a no-op so `show` cannot terminate a host that has no assist window yet. | ||
| fn install_host_signals(ui: Option<std::sync::Arc<assist::LocalUi>>) -> bool { | ||
| static USR1: std::sync::Once = std::sync::Once::new(); | ||
| static USR2: std::sync::Once = std::sync::Once::new(); | ||
| let mut usr1_ok = true; | ||
| USR1.call_once(|| { | ||
| match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1()) { | ||
| Ok(mut usr1) => { tokio::spawn(async move { while usr1.recv().await.is_some() {} }); } | ||
| Err(_) => usr1_ok = false, | ||
| } | ||
| }); | ||
| if !usr1_ok { return false; } | ||
| let mut usr2_ok = true; | ||
| USR2.call_once(|| { | ||
| match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined2()) { | ||
| Ok(mut usr2) => { tokio::spawn(async move { while usr2.recv().await.is_some() { crate::video_select::publish_reload(); } }); } | ||
| Err(_) => usr2_ok = false, | ||
| } | ||
| }); | ||
| if !usr2_ok { return false; } | ||
| // Assist attaches later and replaces the no-op with the window. A second registration replaces the first. | ||
| if let Some(ui) = ui { | ||
| if let Ok(mut usr1) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1()) { | ||
| tokio::spawn(async move { while usr1.recv().await.is_some() { ui.present(); } }); | ||
| } | ||
| } | ||
| write_host_pid(); | ||
| tokio::spawn(async move { while usr1.recv().await.is_some() { ui.present(); } }); | ||
| true | ||
| } | ||
|
|
||
| fn present_on_usr1(ui: std::sync::Arc<assist::LocalUi>) { | ||
| let _ = install_host_signals(Some(ui)); | ||
| } | ||
|
|
||
| fn host_pid_path() -> std::path::PathBuf { config::path().with_file_name("host.pid") } | ||
| /// Lets `uuremote-server show` find this host service to open its window (SIGUSR1). | ||
| fn write_host_pid() { | ||
| use std::os::unix::fs::OpenOptionsExt; | ||
| use std::io::Write; | ||
| if let Ok(mut f) = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(host_pid_path()) { | ||
| let path = host_pid_path(); | ||
| if let Some(dir) = path.parent() { let _ = std::fs::create_dir_all(dir); } | ||
| if let Ok(mut f) = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(path) { | ||
| let _ = write!(f, "{}", std::process::id()); | ||
| } | ||
| } | ||
|
|
@@ -521,6 +621,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| virtual_audio::init(); | ||
| let _audio_cleanup=virtual_audio::Cleanup; | ||
| log::info!("Starting uu-remote-linux host v{}...", env!("CARGO_PKG_VERSION")); | ||
| // Before host.pid, and before assist: a missing or failed assist session must still handle SIGUSR2. | ||
| let _ = install_host_signals(None); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the host runs with AGENTS.md reference: AGENTS.md:L133-L133 Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 5911a80: |
||
| video::warm_auto_probe(); | ||
| video::check_ffmpeg_usable(); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a host authenticated solely through
UU_*environment credentials, or using a freshUU_CONFIGparent, the config directory need not exist when startup first calls this function.write_host_pid()silently fails because it does not create the parent, yet this returns success and is never retried when assistance is disabled;set-encoderlater creates the directory forhost_settings.jsonbut finds no PID, so it cannot notify the running host. Create the parent before writing or propagate the failure for retry.AGENTS.md reference: AGENTS.md:L28-L29
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5911a80:
write_host_pidcreates the config directory before writinghost.pid.