mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 23:51:04 +03:00
feat(rdp): title the mstsc window after the peer instead of "localhost" (#15781)
* feat(rdp): title the mstsc window after the peer instead of "localhost" The RDP tunnel launched `mstsc /v:localhost:<port>`, so with several sessions open every window is titled "localhost" and servers cannot be told apart. mstsc titles the session window after the launched .rdp file's base name, so write a temp .rdp file (containing only the tunnel address) named after the peer alias, cached hostname, or id, and launch that instead. Falls back to the old /v: form when no usable name remains after filename sanitization or the file cannot be written. Credential handling is unchanged: cmdkey targets "localhost", which is still the host mstsc resolves credentials against. Fixes rustdesk/rustdesk#15775 (discussion) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rdp): set mstsc title without temporary files Keep launching mstsc with /v so Default.rdp settings are preserved and unsigned RDP file warnings and policy restrictions are avoided. Track the launched mstsc process and reapply the peer name when the window title is reset during connection or reconnection. Signed-off-by: 21pages <sunboeasy@gmail.com> * docs(rdp): clarify mstsc title limitation Signed-off-by: 21pages <sunboeasy@gmail.com> * feat(rdp): show peer identity with hostname in mstsc title Signed-off-by: 21pages <sunboeasy@gmail.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
@@ -2642,6 +2642,91 @@ pub fn wide_string(s: &str) -> Vec<u16> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// This only changes mstsc's top-level window title. The full-screen connection
|
||||
// bar is rendered separately and cannot be customized when mstsc.exe is
|
||||
// launched as an independent process.
|
||||
pub fn set_rdp_window_title(mut child: std::process::Child, name: String) {
|
||||
let name: String = name.chars().filter(|c| !c.is_control()).take(120).collect();
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
let process_id = child.id();
|
||||
// mstsc owns the title and can restore "localhost" while connecting or
|
||||
// reconnecting. Follow only the process we launched and reapply the peer
|
||||
// name until it exits, so concurrent RDP sessions cannot rename each other.
|
||||
if let Err(err) = std::thread::Builder::new()
|
||||
.name("rdp-window-title".to_owned())
|
||||
.spawn(move || {
|
||||
let mut warned = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to query mstsc process: {}", err);
|
||||
break;
|
||||
}
|
||||
Ok(None) => match set_process_rdp_window_title(process_id, &name) {
|
||||
Ok(()) => warned = false,
|
||||
Err(err) if !warned => {
|
||||
log::warn!("Failed to set RDP window title: {}", err);
|
||||
warned = true;
|
||||
}
|
||||
Err(_) => {}
|
||||
},
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start RDP window title thread: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_process_rdp_window_title(process_id: DWORD, name: &str) -> io::Result<()> {
|
||||
struct Context {
|
||||
process_id: DWORD,
|
||||
title: Vec<u16>,
|
||||
error: Option<io::Error>,
|
||||
}
|
||||
|
||||
unsafe extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let context = &mut *(lparam as *mut Context);
|
||||
let mut window_process_id = 0;
|
||||
GetWindowThreadProcessId(hwnd, &mut window_process_id);
|
||||
if window_process_id != context.process_id || IsWindowVisible(hwnd) == FALSE {
|
||||
return TRUE;
|
||||
}
|
||||
let len = GetWindowTextLengthW(hwnd);
|
||||
if len <= 0 {
|
||||
return TRUE;
|
||||
}
|
||||
let mut title = vec![0u16; len as usize + 1];
|
||||
let len = GetWindowTextW(hwnd, title.as_mut_ptr(), title.len() as _);
|
||||
if len > 0 && String::from_utf16_lossy(&title[..len as usize]).contains("localhost") {
|
||||
if SetWindowTextW(hwnd, context.title.as_ptr()) == FALSE {
|
||||
context.error = Some(io::Error::last_os_error());
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
TRUE
|
||||
}
|
||||
|
||||
let mut context = Context {
|
||||
process_id,
|
||||
title: wide_string(name),
|
||||
error: None,
|
||||
};
|
||||
let enumerated =
|
||||
unsafe { EnumWindows(Some(enum_window), &mut context as *mut Context as LPARAM) };
|
||||
if let Some(err) = context.error {
|
||||
return Err(err);
|
||||
}
|
||||
if enumerated == FALSE {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// send message to currently shown window
|
||||
pub fn send_message_to_hnwd(
|
||||
class_name: &str,
|
||||
|
||||
@@ -15,7 +15,7 @@ use hbb_common::{
|
||||
ResultType, Stream,
|
||||
};
|
||||
|
||||
fn run_rdp(port: u16) {
|
||||
fn run_rdp(port: u16, name: &str) {
|
||||
std::process::Command::new("cmdkey")
|
||||
.arg("/delete:localhost")
|
||||
.output()
|
||||
@@ -35,10 +35,37 @@ fn run_rdp(port: u16) {
|
||||
.output()
|
||||
.ok();
|
||||
}
|
||||
std::process::Command::new("mstsc")
|
||||
// Keep using /v instead of a generated .rdp file: mstsc then preserves the
|
||||
// user's Default.rdp settings and avoids unsigned-file warnings or policies.
|
||||
match std::process::Command::new("mstsc")
|
||||
.arg(format!("/v:localhost:{}", port))
|
||||
.spawn()
|
||||
.ok();
|
||||
{
|
||||
Ok(child) => {
|
||||
#[cfg(windows)]
|
||||
crate::platform::set_rdp_window_title(child, name.to_owned());
|
||||
#[cfg(not(windows))]
|
||||
let _ = (child, name);
|
||||
}
|
||||
Err(err) => log::warn!("Failed to launch mstsc: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
// Show the peer identity with its hostname, using the ID when no alias exists.
|
||||
fn rdp_display_name(lc: &Arc<RwLock<LoginConfigHandler>>, id: &str) -> String {
|
||||
let lc = lc.read().unwrap();
|
||||
let alias = lc
|
||||
.options
|
||||
.get("alias")
|
||||
.map(|s| s.trim())
|
||||
.unwrap_or_default();
|
||||
let hostname = lc.info.hostname.trim();
|
||||
let identity = if !alias.is_empty() { alias } else { id };
|
||||
if hostname.is_empty() || hostname == identity {
|
||||
identity.to_owned()
|
||||
} else {
|
||||
format!("{} ({})", identity, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn listen(
|
||||
@@ -58,7 +85,7 @@ pub async fn listen(
|
||||
log::info!("listening on port {:?}", addr);
|
||||
let is_rdp = port == 0;
|
||||
if is_rdp {
|
||||
run_rdp(addr.port());
|
||||
run_rdp(addr.port(), &rdp_display_name(&lc, &id));
|
||||
}
|
||||
let mut ui_receiver = ui_receiver;
|
||||
loop {
|
||||
@@ -96,7 +123,7 @@ pub async fn listen(
|
||||
}
|
||||
Some(Data::NewRDP) => {
|
||||
println!("receive run_rdp from ui_receiver");
|
||||
run_rdp(addr.port());
|
||||
run_rdp(addr.port(), &rdp_display_name(&lc, &id));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user