mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(windows): name the single-instance socket by the session id, so a launch without SESSIONNAME finds its session's window (#3338) (#3340)
* fix(app): a launch without SESSIONNAME hands its files to the console session's window (#3338) Task Scheduler, services and some remote shells start PdfCraft without SESSIONNAME, so the launch looked for instance.sock while the window opened from Explorer listens on instance-Console.sock, and a second window opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q * fix(windows): name the single-instance socket by the session id, not SESSIONNAME (#3338) Review on #3340 (earlvanze): falling back to the console's socket when SESSIONNAME is unset could hand a file from a Remote Desktop session to a console window of the same user in another session. The socket is now named by the session id in the process token (winsafe, no unsafe): a launch from Task Scheduler runs in the user's session and finds its window, and different sessions never share a socket. SESSIONNAME names it only if the id can't be read. Tested with a real scheduled task: on main the task's launch (no SESSIONNAME, session 1) opened a second window; with this change the file went to the running window in session 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6733e073ef
commit
2f21f920a0
@@ -54,8 +54,9 @@ winit = { version = "0.30", default-features = false, features = ["wayland"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# Which GPU drives which display (EnumDisplayDevices), so the window is drawn on the GPU that
|
||||
# shows it (issue #378). Safe wrappers over Win32; only the `user` feature (which brings `kernel` with it).
|
||||
winsafe = { version = "0.0.29", default-features = false, features = ["user"] }
|
||||
# shows it (issue #378). Safe wrappers over Win32; only the `user` feature (which brings `kernel` with it),
|
||||
# and `advapi` for the session id that names the single-instance socket (#3338).
|
||||
winsafe = { version = "0.0.29", default-features = false, features = ["user", "advapi"] }
|
||||
# Settings ▸ Accessibility ▸ Visual effects ▸ Animation effects (src/motion.rs), through WinRT's
|
||||
# `UISettings`, whose bindings are safe to call (winsafe's `SystemParametersInfo` is an unsafe
|
||||
# fn). Already in the tree through wgpu's DX12 backend; this adds one small namespace to it.
|
||||
|
||||
@@ -153,7 +153,7 @@ pub fn claim(files: &[String], may_hand_off: bool) -> Claim {
|
||||
/// `%LOCALAPPDATA%` is unset.
|
||||
#[cfg(windows)]
|
||||
fn socket_path() -> Option<std::path::PathBuf> {
|
||||
let name = socket_name(std::env::var("SESSIONNAME").ok().as_deref());
|
||||
let name = socket_name(session_id(), std::env::var("SESSIONNAME").ok().as_deref());
|
||||
// %LOCALAPPDATA% is private to the user's sign-in, so other accounts can't receive this
|
||||
// user's file paths or send it files to open. A portable copy uses it too (#889), under its
|
||||
// own name so it never hands files to an installed copy (which keeps its settings elsewhere).
|
||||
@@ -184,13 +184,31 @@ fn display_socket_name(display: Option<&str>) -> String {
|
||||
if display.is_empty() { "instance.sock".to_string() } else { format!("instance-{display}.sock") }
|
||||
}
|
||||
|
||||
/// The socket's file name for a Windows session (`SESSIONNAME`: `Console`, `RDP-Tcp#3`…). The
|
||||
/// same user signed in twice (at the console and over Remote Desktop) gets one socket per session,
|
||||
/// so a file never opens in a window on the other screen. Only letters, digits and `-` are kept.
|
||||
/// The Windows session this process runs in, from its access token: the console, each Remote
|
||||
/// Desktop sign-in and session 0 (services) each have their own. `None` if Windows won't say.
|
||||
#[cfg(windows)]
|
||||
fn session_id() -> Option<u32> {
|
||||
use winsafe::{HACCESSTOKEN, TokenInfo, co::TOKEN_INFORMATION_CLASS};
|
||||
match HACCESSTOKEN::GetCurrentProcessToken().GetTokenInformation(TOKEN_INFORMATION_CLASS::SessionId) {
|
||||
Ok(TokenInfo::SessionId(id)) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The socket's file name for a Windows session. The same user signed in twice (at the console
|
||||
/// and over Remote Desktop) gets one socket per session, so a file never opens in a window on the
|
||||
/// other screen. The session is the token's session id (`session`): a launch from Task Scheduler,
|
||||
/// a service or a remote shell has no `SESSIONNAME` but still runs in a session, so it finds the
|
||||
/// window of the session it runs in (#3338), and never one in another session (review on #3340).
|
||||
/// Only if the id can't be read does `SESSIONNAME` (`Console`, `RDP-Tcp#3`…) name it, letters,
|
||||
/// digits and `-` kept.
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn socket_name(session: Option<&str>) -> String {
|
||||
let session: String = session.unwrap_or_default().chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').take(32).collect();
|
||||
if session.is_empty() { "instance.sock".to_string() } else { format!("instance-{session}.sock") }
|
||||
fn socket_name(session: Option<u32>, session_name: Option<&str>) -> String {
|
||||
if let Some(id) = session {
|
||||
return format!("instance-s{id}.sock");
|
||||
}
|
||||
let name: String = session_name.unwrap_or_default().chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').take(32).collect();
|
||||
if name.is_empty() { "instance.sock".to_string() } else { format!("instance-{name}.sock") }
|
||||
}
|
||||
|
||||
/// Held while a launch finds out whether it is the first, so launches that start together (several
|
||||
@@ -359,12 +377,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn each_windows_session_gets_its_own_socket_name() {
|
||||
assert_eq!(socket_name(Some("Console")), "instance-Console.sock");
|
||||
assert_eq!(socket_name(Some("RDP-Tcp#3")), "instance-RDP-Tcp3.sock");
|
||||
assert_ne!(socket_name(Some("RDP-Tcp#3")), socket_name(Some("RDP-Tcp#4")));
|
||||
assert_eq!(socket_name(Some(r"..\x/y:z")), "instance-xyz.sock", "never a path");
|
||||
assert_eq!(socket_name(None), "instance.sock");
|
||||
assert_eq!(socket_name(Some("")), "instance.sock");
|
||||
// The session id decides: #3338, a launch from Task Scheduler (no SESSIONNAME) in the
|
||||
// console's session finds the window opened from Explorer there...
|
||||
assert_eq!(socket_name(Some(1), None), socket_name(Some(1), Some("Console")));
|
||||
// ...and (review on #3340) one in a Remote Desktop session never reaches the console's.
|
||||
assert_ne!(socket_name(Some(2), None), socket_name(Some(1), Some("Console")));
|
||||
assert_ne!(socket_name(Some(2), Some("RDP-Tcp#3")), socket_name(Some(3), Some("RDP-Tcp#3")));
|
||||
assert_eq!(socket_name(Some(4_000_000_000), None), "instance-s4000000000.sock");
|
||||
// Only without an id does SESSIONNAME name it.
|
||||
assert_eq!(socket_name(None, Some("Console")), "instance-Console.sock");
|
||||
assert_eq!(socket_name(None, Some("RDP-Tcp#3")), "instance-RDP-Tcp3.sock");
|
||||
assert_ne!(socket_name(None, Some("RDP-Tcp#3")), socket_name(None, Some("RDP-Tcp#4")));
|
||||
assert_eq!(socket_name(None, Some(r"..\x/y:z")), "instance-xyz.sock", "never a path");
|
||||
assert_eq!(socket_name(None, None), "instance.sock");
|
||||
assert_eq!(socket_name(None, Some("")), "instance.sock");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -394,11 +420,21 @@ mod tests {
|
||||
assert_eq!(display_socket_name(Some("")), "instance.sock");
|
||||
}
|
||||
|
||||
/// The socket is named by the session the process runs in, which Windows always gives.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn this_process_has_a_session_id() {
|
||||
let id = super::session_id();
|
||||
assert!(id.is_some(), "the access token names the session");
|
||||
let name = socket_name(id, None);
|
||||
assert!(socket_path().is_none_or(|p| p.file_name().is_some_and(|f| f.to_string_lossy().ends_with(&name))), "{name}");
|
||||
}
|
||||
|
||||
#[cfg(any(windows, all(unix, not(target_os = "macos"))))]
|
||||
fn socket(test: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-instance-{}-{test}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
dir.join(socket_name(None))
|
||||
dir.join(socket_name(None, None))
|
||||
}
|
||||
|
||||
#[cfg(any(windows, all(unix, not(target_os = "macos"))))]
|
||||
|
||||
Reference in New Issue
Block a user