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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ viewer does not reconnect them automatically.
| Item | Meaning |
|---|---|
| `~/.config/uuremote/config.json` | user_id, device_id, client_id, system_id, machine_guid, token, csrf; optional `sign_key`, `ffmpeg_root`. `$UU_CONFIG` overrides the path |
| `~/.config/uuremote/host_settings.json` | persisted 安全 settings: auto_lock, keep_mute, mute_permanent; optional `video_encoder` (`auto`/`cpu`/`vaapi`/`nvenc`) and `video_codec` (`auto`/`h264`/`h265`), written by `set-encoder` / `set-codec` (0600, merge write). Non-`auto` beats `UU_VIDEO_ENCODER` / `UU_VIDEO_CODEC`. Applied on the next desktop stream |
| `~/.config/uuremote/host_settings.json` | persisted 安全 settings: auto_lock, keep_mute, mute_permanent; optional `video_encoder` (`auto`/`cpu`/`vaapi`/`nvenc`) and `video_codec` (`auto`/`h264`/`h265`), written by `set-encoder` / `set-codec` (0600, merge write). Non-`auto` beats `UU_VIDEO_ENCODER` / `UU_VIDEO_CODEC`. `set-encoder` and `set-codec` both send `SIGUSR2` to the pid in `host.pid` (same binary). Live desktop streams restart the encoder (`EncoderTask::restart`, sequence kept, first frame IDR) without dropping the peer. Codec preference is logged and applies on the next desktop stream. `SIGUSR2` does not re-probe. Hand edits: `kill -USR2 $(cat ~/.config/uuremote/host.pid)` |
| `UU_DESKTOP_BACKEND` | `auto` (default) / `x11` / `wayland` / `portal` / `kms` (experimental feature) / `none` (headless: no display; desktop offers ignored; tool peers get a nominal 1920x1080 screen + Allow permissions, which the port-mapping viewer requires) |
| `UU_DESKTOP_RESIZE=0` | X11: the viewer's 分辨率 choice scales the encoded stream instead of changing the display mode. Default: `src/desktop_mode.rs` switches the primary output via xrandr (adds a CVT-RB mode when missing, re-applies once if mutter resets it), restores the original mode for resolution_type 3 and 2 s after the last desktop stream ends |
| `UU_NVENC_RGB=0` / `=1` | NVENC 4:2:0 without scaling takes packed BGRA/BGR0 directly (no CPU swscale). Default: on, except when a connected DRM connector belongs to an nvidia-bound card (desktop composited on the encoding GPU; #16 measured 82.6 vs 98 fps at 4K). `0` forces CPU conversion, `1` forces the direct path |
Expand Down Expand Up @@ -168,7 +168,7 @@ for a new QR login.
| `src/viewer_main.rs`, `src/viewer/` | experimental viewer (`--features native-viewer,viewer-sdl`): cloud join, WebRTC offer side, SDL desktop window (ffmpeg CLI decode), files/terminal/portmap/power/assist tools, device removal (`POST /api/v1/device/{id}/unbind`, verified live; shared devices use `/api/v1/device/share/{id}/unbind`, strings only), GTK launcher |
| `c_engine/viewer_window.c`, `c_engine/viewer_tools.c` | viewer GTK launcher and tool windows (files, port mapping, terminal list) |

Diagnostic subcommands: `doctor` (paste-safe report: session, selected backend, engine plugins, uinput, encoder and codec with source, sign-in/service state; booleans and names only), `set-encoder auto|cpu|vaapi|nvenc`, `set-codec auto|h264|h265` (next desktop stream; illegal values exit 2), `login [--no-gui]`, `unlock-check` (password on stdin, prints only accepted/rejected),
Diagnostic subcommands: `doctor` (paste-safe report: session, selected backend, engine plugins, uinput, encoder and codec with source, sign-in/service state; booleans and names only), `set-encoder auto|cpu|vaapi|nvenc` and `set-codec auto|h264|h265` (both notify a running host with SIGUSR2; live encoder switch, negotiated codec unchanged, codec preference on the next desktop stream; illegal values exit 2), `login [--no-gui]`, `unlock-check` (password on stdin, prints only accepted/rejected),
`desktop-probe [new.png]`, `desktop-video-probe` (`UU_PROBE_FPS`, `UU_PROBE_FRAMES`, `UU_PROBE_QUALITY` 1-5 = viewer bitrate preset, 5 auto; `UU_PROBE_CHROMA=444` 真彩; `UU_PROBE_INPUT` fixed video/image sequence, `UU_PROBE_LOOP=1` repeats), `wayland-input-probe`, `clipboard-acceptance`,
`portal-acceptance`, `help`. Unknown arguments exit with code 2. Builds up to v0.1.0 did **not** reject them and
started a second full host server instead, so check that a subcommand exists in the binary you run.
Expand Down
8 changes: 7 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,13 @@ stays cached until the process exits, except when NVIDIA presence (`/proc/driver
or `/dev/nvidia0`) no longer matches the report: the next desktop connection then starts one
background re-probe (`warm_auto_probe`) and still uses the cached encoder until the new result
is stored. An NVENC failure is kept for 10 minutes, and the next desktop connection after that
starts the same background re-probe while still using the cached encoder. They take effect the next time a desktop connection starts. Explicit `vaapi` with 真彩 stays on VAAPI,
starts the same background re-probe while still using the cached encoder. `set-encoder` and
`set-codec` both send `SIGUSR2` to the host in `host.pid` when that process is this program.
An encoder change switches each live desktop stream without disconnecting and keeps the codec
negotiated for that session. A failed start follows the existing fallback (in-process, then the
FFmpeg process, then CPU). After a hand edit, `kill -USR2 $(cat ~/.config/uuremote/host.pid)`
does the same. `SIGUSR2` does not re-probe. A codec change takes effect the next time a desktop
connection starts; the running host only logs it. Explicit `vaapi` with 真彩 stays on VAAPI,
encodes 4:2:0, and reports chroma 4:2:0 in CaptureSettingSync field 3. If the chosen encoder
cannot start, the stream falls back to CPU and logs a warning. VAAPI is not an auto candidate
until #33.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ systemctl --user restart uuremote-native.service
| `UU_WOL_HELPER=1` | 实验性:让这台主机像官方 Windows 被控端一样充当「远程开机」辅助设备——登录后向网易上报本机局域网信息,收到云端唤醒请求时向同一局域网发送唤醒包(未经端到端验证) |
| `UU_USE_PROXY=1` | 使用环境变量里的 HTTP/SOCKS 代理;默认忽略代理、直连网易服务器 |

「远程结束后自动锁屏」「静音运行」等设置保存在 `~/.config/uuremote/host_settings.json`。同一文件里的 `video_encoder`(`auto`/`cpu`/`vaapi`/`nvenc`)和 `video_codec`(`auto`/`h264`/`h265`)用 `uuremote-server set-encoder`、`uuremote-server set-codec` 写入(非法参数退出码 2)。非 `auto` 的值优先于 `UU_VIDEO_ENCODER` / `UU_VIDEO_CODEC`;`auto` 或留空则落到环境变量,再落到 auto(有 NVIDIA 时 NVENC 优先,失败再 CPU;没有 NVIDIA 时就是 CPU,不探测;codec 仍是硬件优先 H.265、CPU 用 H.264)。探测在启动时后台进行,不挡住第一路出图。成功结果缓存到进程退出,但 NVIDIA 是否存在(`/proc/driver/nvidia/version` 或 `/dev/nvidia0`)与报告不一致时,下一次桌面连接会在后台再探测一次(`warm_auto_probe`),这一路仍用缓存的编码器,直到新结果写入。NVENC 失败只缓存 10 分钟,到期后的桌面连接同样在后台再探测一次,这一路仍用当前结果。下次建立桌面连接时生效。显式 `vaapi` 请求真彩时仍用 VAAPI、编码 4:2:0,并在 CaptureSettingSync 的 field 3 回报 4:2:0。所选编码器在本机不可用时,建流回退 CPU 并记 warning。VAAPI 进入 auto 要等 #33。
「远程结束后自动锁屏」「静音运行」等设置保存在 `~/.config/uuremote/host_settings.json`。同一文件里的 `video_encoder`(`auto`/`cpu`/`vaapi`/`nvenc`)和 `video_codec`(`auto`/`h264`/`h265`)用 `uuremote-server set-encoder`、`uuremote-server set-codec` 写入(非法参数退出码 2)。非 `auto` 的值优先于 `UU_VIDEO_ENCODER` / `UU_VIDEO_CODEC`;`auto` 或留空则落到环境变量,再落到 auto(有 NVIDIA 时 NVENC 优先,失败再 CPU;没有 NVIDIA 时就是 CPU,不探测;codec 仍是硬件优先 H.265、CPU 用 H.264)。探测在启动时后台进行,不挡住第一路出图。成功结果缓存到进程退出,但 NVIDIA 是否存在(`/proc/driver/nvidia/version` 或 `/dev/nvidia0`)与报告不一致时,下一次桌面连接会在后台再探测一次(`warm_auto_probe`),这一路仍用缓存的编码器,直到新结果写入。NVENC 失败只缓存 10 分钟,到期后的桌面连接同样在后台再探测一次,这一路仍用当前结果。`set-encoder` 和 `set-codec` 写完后若 `host.pid` 指向正在运行的本程序,都会发 `SIGUSR2`:编码器变化时进行中的桌面流立刻换编码器(不断开,沿用本会话已协商的 codec;新编码器起不来时按进程内 → FFmpeg 进程 → CPU 回退)。手改文件后可 `kill -USR2 $(cat ~/.config/uuremote/host.pid)`。`SIGUSR2` 不重新探测。codec 偏好仍在下次建立桌面连接时生效(运行中的 host 只记日志,不重开编码)。显式 `vaapi` 请求真彩时仍用 VAAPI、编码 4:2:0,并在 CaptureSettingSync 的 field 3 回报 4:2:0。所选编码器在本机不可用时,建流回退 CPU 并记 warning。VAAPI 进入 auto 要等 #33。

自定义码率按客户端选择的 Mbps 生效,例如 1 Mbps 对应 1000 kbit/s 的编码上限。

Expand Down
114 changes: 108 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Create the PID directory before reporting success

For a host authenticated solely through UU_* environment credentials, or using a fresh UU_CONFIG parent, 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-encoder later creates the directory for host_settings.json but 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

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_pid creates the config directory before writing host.pid.

}

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());
}
}
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install SIGUSR1 before publishing the shared PID

When the host runs with UU_REMOTE_ASSIST=0, or while assistance setup is pending or has failed, this call passes None, so install_host_signals installs only SIGUSR2 but still writes host.pid. show_running_host() treats that PID as SIGUSR1-ready and sends SIGUSR1; because its default action is termination, uuremote-server show kills/restarts the host and returns success instead of opening or failing gracefully. Register a safe SIGUSR1 handler before publishing the shared PID.

AGENTS.md reference: AGENTS.md:L133-L133

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5911a80: install_host_signals registers a no-op SIGUSR1 handler and the SIGUSR2 handler before host.pid is written, including when remote assist is off, pending, or failed.

video::warm_auto_probe();
video::check_ffmpeg_usable();

Expand Down
Loading
Loading