fix: wait for Windows sign-in before remote startup

This commit is contained in:
Jonathan Liebig
2026-10-28 01:49:11 +01:00
parent f885218639
commit 5c6ff3c13e
11 changed files with 438 additions and 11 deletions
@@ -49,7 +49,20 @@ For internal testing, `HERDR_MANIFEST_URL` can point the installer at a custom m
Local persistent sessions continue running after the client detaches or its terminal window closes. Servers and pane processes launched through Windows OpenSSH also survive logout; run `herdr` again to reconnect.
When SSH starts a missing server, Herdr prefers an ordinary signed-in Windows session belonging to the same account. This lets tools use that login's desktop and sandbox context. If no such login exists, Herdr starts in the SSH session and reports that sandbox or desktop tools may be unavailable. If several logins belong to the account, start Herdr in the intended login first. Existing servers and their panes stay in their current session.
When SSH starts a missing server, Herdr uses an ordinary signed-in Windows session belonging to the same account. This lets tools use that login's desktop and sandbox context. If no such login exists, startup reports **Waiting for Windows sign-in** and does not start a server. Sign in to the Windows host with the same account: saved connections retry automatically, and an interactive `herdr --remote` stays open and retries before opening the TUI. A disconnected but still signed-in Windows session remains eligible. If several logins belong to the account, start Herdr in the intended login first. Existing servers and their panes stay in their current session, including servers already running in Session 0.
While an interactive command waits, **Esc** or **Ctrl+C** cancels. **F** deliberately starts in the SSH session (normally Session 0) for that attempt; the prompt warns that this may break Codex sandbox, computer use and desktop tools. This choice does not change the host's config. Saved connections never prompt or force startup. Waiting retries use normal SSH authentication; authentication or other startup failures are reported rather than treated as a missing login. Keyboard choices are handled between SSH operations, so an in-flight SSH request retains its usual latency. Non-terminal commands report the sign-in requirement without prompting.
Waiting and the one-attempt force choice require an updated Windows host binary. Older compatible running servers remain attachable; for an older stopped host, update Herdr on the host or start it from the Windows desktop first.
For a host that deliberately runs without a Windows sign-in, opt into SSH-context startup in the **Windows host's** `%APPDATA%\herdr\config.toml` (or its existing `HERDR_CONFIG_PATH` / `XDG_CONFIG_HOME` override):
```toml
[server]
allow_headless_remote_start = true
```
This Windows-only setting is false by default and is read on each cold start with no eligible login. Missing, unreadable or invalid config cannot enable automatic fallback; unknown config fields are tolerated. Config diagnostics remain visible even when you deliberately force a start. The fallback may lack Codex sandbox or desktop support. It does not bypass account, session or privilege checks, resolve multiple-login ambiguity, or retry a failed or uncertain desktop launch in Session 0. Local startup and already-running servers are unaffected.
Elevated Windows servers require an administrator client by default, including servers started through an elevated SSH login. Ordinary servers accept clients from ordinary and administrator terminals belonging to the same account; attaching never changes the server's privileges.
@@ -16,6 +16,12 @@
"id": "server",
"title": "Server",
"keys": [
{
"key": "server.allow_headless_remote_start",
"type": "boolean",
"default": "false",
"description": "Windows only: allow a remote cold start in the SSH session when no signed-in Windows session belongs to the same account. By default saved connections retry and interactive --remote waits with a deliberate one-attempt force choice. This fallback may break Codex sandbox, computer use and desktop tools. Existing servers, multiple-login ambiguity and account/session/privilege checks are unchanged. Read on each no-login cold start from the host config."
},
{
"key": "server.allow_unelevated_clients",
"type": "boolean",
+2
View File
@@ -254,6 +254,7 @@ struct ClientStatusJson {
endpoint_capabilities: Vec<&'static str>,
remote_host_bridge: bool,
remote_bridge_idle_timeout: bool,
remote_windows_start: bool,
binary: String,
session: Option<String>,
}
@@ -302,6 +303,7 @@ fn client_status_json() -> ClientStatusJson {
],
remote_host_bridge: true,
remote_bridge_idle_timeout: crate::platform::REMOTE_BRIDGE_IDLE_TIMEOUT_SUPPORTED,
remote_windows_start: crate::platform::REMOTE_WINDOWS_START_SUPPORTED,
binary: current_exe_label(),
session: crate::session::active_name(),
}
+5
View File
@@ -1039,6 +1039,9 @@ impl ImeCursorShape {
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
/// Windows: allow remote startup without a signed-in same-account session. Default: false.
#[cfg(windows)]
pub allow_headless_remote_start: bool,
/// Windows: allow ordinary same-account clients to control an elevated server. Default: false.
#[cfg(windows)]
pub allow_unelevated_clients: bool,
@@ -1314,6 +1317,8 @@ impl<'de> Deserialize<'de> for ToastConfig {
impl Default for ServerConfig {
fn default() -> Self {
Self {
#[cfg(windows)]
allow_headless_remote_start: false,
#[cfg(windows)]
allow_unelevated_clients: false,
headless_cols: crate::config::DEFAULT_HEADLESS_COLS,
+3
View File
@@ -225,6 +225,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# Size of the virtual terminal used when no client is attached.
# Attached clients always use their own terminal size.
[server]
# Windows only: allow SSH-context startup when this account is not signed in.
# Codex sandbox and desktop tools may be unavailable in that context.
# allow_headless_remote_start = false
# Windows only: allow ordinary same-account clients to control an elevated server.
# Requires a server restart.
# allow_unelevated_clients = false
+7
View File
@@ -3,6 +3,13 @@
//! Centralizes OS-dependent behavior behind a clean boundary so core
//! modules don't scatter `#[cfg]` branches through product logic.
pub(crate) const REMOTE_WINDOWS_START_SUPPORTED: bool = cfg!(windows);
#[cfg(not(windows))]
pub(crate) fn is_windows_sign_in_required(_error: &std::io::Error) -> bool {
false
}
#[cfg(unix)]
pub(crate) mod ssh_agent;
+5 -1
View File
@@ -1499,7 +1499,11 @@ pub(crate) fn configure_background_command_platform(command: &mut std::process::
}
mod remote_start;
pub(crate) use remote_start::{bootstrap_remote_server, launch_remote_server_daemon_command};
#[cfg(test)]
pub(crate) use remote_start::WindowsSignInRequired;
pub(crate) use remote_start::{
bootstrap_remote_server, is_windows_sign_in_required, launch_remote_server_daemon_command,
};
pub fn launch_server_daemon_command(command: &mut std::process::Command) -> std::io::Result<u32> {
if current_job_kills_processes_on_close()? {
+87 -1
View File
@@ -176,11 +176,15 @@ pub(crate) fn bootstrap_remote_server(args: Vec<String>) -> io::Result<Vec<Strin
result
}
pub(crate) fn launch_remote_server_daemon_command(command: &mut Command) -> io::Result<u32> {
pub(crate) fn launch_remote_server_daemon_command(
command: &mut Command,
allow_headless: bool,
) -> io::Result<u32> {
let identity = process_identity(std::process::id())?;
let sessions = eligible_sessions(&identity.sid)?;
let session = match sessions.as_slice() {
[] => {
require_headless_remote_start(allow_headless)?;
eprintln!("No signed-in Windows session is available. Starting Herdr in the SSH session; Codex sandbox and desktop tools may be unavailable.");
tracing::warn!("No signed-in Windows session; starting remote server in the SSH session");
return super::launch_server_daemon_command(command);
@@ -246,6 +250,38 @@ pub(crate) fn launch_remote_server_daemon_command(command: &mut Command) -> io::
outcome
}
#[derive(Debug)]
pub(crate) struct WindowsSignInRequired;
impl std::fmt::Display for WindowsSignInRequired {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("Waiting for Windows sign-in. Sign in to this host with the same Windows account. Saved connections retry automatically; interactive herdr --remote can wait or offer a deliberate Session 0 start. For intentional headless startup, set server.allow_headless_remote_start = true in this host's config.")
}
}
impl std::error::Error for WindowsSignInRequired {}
pub(crate) fn is_windows_sign_in_required(error: &io::Error) -> bool {
error
.get_ref()
.is_some_and(|cause| cause.is::<WindowsSignInRequired>())
}
fn require_headless_remote_start(allow_headless: bool) -> io::Result<()> {
let loaded = crate::config::Config::load();
if let Some(summary) = crate::config::config_diagnostic_summary(&loaded.diagnostics) {
eprintln!("{summary}");
}
if allow_headless || loaded.config.server.allow_headless_remote_start {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::WouldBlock,
WindowsSignInRequired,
))
}
}
fn peer_pid(stream: &crate::ipc::LocalStream) -> io::Result<u32> {
stream
.peer_creds()?
@@ -766,6 +802,56 @@ impl Drop for WtsMemory {
mod tests {
use super::*;
#[test]
fn no_login_fallback_requires_valid_host_config_opt_in() {
const CASE: &str = "HERDR_TEST_HEADLESS_REMOTE_START_CASE";
if let Ok(case) = std::env::var(CASE) {
let result = require_headless_remote_start(false);
if case == "enabled" {
result.unwrap();
} else {
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
assert!(is_windows_sign_in_required(&error));
assert!(error.to_string().contains("Waiting for Windows sign-in"));
require_headless_remote_start(true).unwrap();
}
return;
}
let root = std::env::temp_dir().join(format!(
"herdr-no-login-policy-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir(&root).unwrap();
for (case, content) in [
("missing", None),
("disabled", Some("[server]\nallow_headless_remote_start = false\n")),
("enabled", Some("[server]\nallow_headless_remote_start = true\nfuture_option = true\n[future]\noption = true\n")),
("wrong_type", Some("[server]\nallow_headless_remote_start = 'true'\n")),
("invalid_other_field", Some("[server]\nallow_headless_remote_start = true\nheadless_cols = 'wide'\n")),
("malformed", Some("[server]\nallow_headless_remote_start = true\n[")),
("unreadable", None),
] {
let path = root.join(case);
if let Some(content) = content {
std::fs::write(&path, content).unwrap();
} else if case == "unreadable" {
std::fs::create_dir(&path).unwrap();
}
let status = Command::new(std::env::current_exe().unwrap())
.args(["--exact", "platform::windows::remote_start::tests::no_login_fallback_requires_valid_host_config_opt_in", "--nocapture"])
.env(CASE, case)
.env(crate::config::CONFIG_PATH_ENV_VAR, path)
.status().unwrap();
assert!(status.success(), "no-login policy case {case} failed");
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn missing_task_registration_is_already_clean() {
let (_com, service) = task_service().unwrap();
+206
View File
@@ -4,6 +4,7 @@
use super::process::wait_with_output_limit;
use super::{
args::*,
host::{RemoteStartResponse, RemoteStartStatus},
process::{wait_with_output_timeout, CancellablePipe, OwnedSshChild, PipeControl},
restart_policy::*,
shell_quote,
@@ -86,6 +87,9 @@ pub(crate) fn run_remote(remote: RemoteLaunch) -> io::Result<()> {
remote.live_handoff,
require_surface_interest,
)?;
if prepared_remote.remote_herdr.platform.is_windows() {
ensure_windows_remote_server_started(&remote_ssh, &prepared_remote.remote_herdr)?;
}
let _bridge = SshStdioBridge::start(
remote.target.clone(),
@@ -445,6 +449,25 @@ impl RemoteExecutable {
}
}
fn start_only_command(&self, session_name: &str, allow_headless: bool) -> String {
let args = if allow_headless {
&[
"remote-client-bridge",
"--start-only-v1",
"--allow-headless-start-v1",
][..]
} else {
&["remote-client-bridge", "--start-only-v1"][..]
};
let args = Self::session_args(session_name, args);
match self {
Self::WindowsPath(path) => {
windows_powershell_streaming_application_command(path, &args)
}
Self::PosixShellPath(_) => self.command(&args),
}
}
fn live_handoff_command(&self, session_name: &str, protocol: u32, version: &str) -> String {
let protocol = protocol.to_string();
match self {
@@ -2183,6 +2206,113 @@ impl RemoteServerStatus {
}
}
fn ensure_windows_remote_server_started(ssh: &RemoteSsh, remote: &RemoteHerdr) -> io::Result<()> {
let running = matches!(
remote_server_status(ssh, remote, false)?,
RemoteServerStatus::Running { .. }
);
let mut announced = false;
prepare_windows_remote_start(
running,
|| Ok(remote_client_status(ssh, remote)?.is_some_and(|status| status.remote_windows_start)),
|allow_headless| {
let command = remote
.executable
.start_only_command(&ssh.session_name, allow_headless);
let output = ssh.shell_output(&remote.platform, &command)?;
if !output.status.success() {
return Err(command_failed("remote server startup failed", &output));
}
let response: RemoteStartResponse =
serde_json::from_slice(&output.stdout).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid remote startup response: {error}"),
)
})?;
Ok(response.status)
},
|| {
if !io::stdin().is_terminal() {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "Waiting for Windows sign-in. Sign in to the host with the same Windows account, or run herdr --remote from an interactive terminal to choose a Session 0 start."));
}
if !announced {
eprintln!("The Windows account on {} is not signed in. Waiting for Windows sign-in; Herdr will retry automatically.", ssh.destination());
eprintln!(
"Forcing Session 0 may break Codex sandbox, computer use and desktop tools."
);
eprintln!("F: force Session 0 for this attempt · Esc/Ctrl+C: cancel · otherwise keep waiting");
announced = true;
}
poll_windows_sign_in_choice()
},
)
}
fn prepare_windows_remote_start(
running: bool,
supports_start: impl FnOnce() -> io::Result<bool>,
mut start: impl FnMut(bool) -> io::Result<RemoteStartStatus>,
mut choose_force: impl FnMut() -> io::Result<bool>,
) -> io::Result<()> {
if running {
return Ok(());
}
if !supports_start()? {
return Err(io::Error::new(io::ErrorKind::Unsupported, "This Windows host needs an updated Herdr to wait for sign-in or choose Session 0. Update Herdr on the host, or start it from the Windows desktop, then reconnect."));
}
let mut allow_headless = false;
loop {
match start(allow_headless)? {
RemoteStartStatus::Ready => return Ok(()),
RemoteStartStatus::WindowsSignInRequired if allow_headless => {
return Err(io::Error::other("remote host still requires Windows sign-in after the explicit Session 0 request"));
}
RemoteStartStatus::WindowsSignInRequired => allow_headless = choose_force()?,
}
}
}
fn poll_windows_sign_in_choice() -> io::Result<bool> {
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
struct RawModeGuard;
impl Drop for RawModeGuard {
fn drop(&mut self) {
let _ = crossterm::terminal::disable_raw_mode();
}
}
crossterm::terminal::enable_raw_mode()?;
let _raw_mode = RawModeGuard;
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if !crossterm::event::poll(deadline.saturating_duration_since(Instant::now()))? {
return Ok(false);
}
let Event::Key(key) = crossterm::event::read()? else {
continue;
};
if !matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
continue;
}
match key.code {
KeyCode::Char('f' | 'F')
if !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
return Ok(true);
}
KeyCode::Esc => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
_ => {}
}
}
Err(io::Error::new(
io::ErrorKind::Interrupted,
"Windows sign-in wait cancelled",
))
}
fn ensure_remote_server_ready(
ssh: &RemoteSsh,
remote_herdr: &RemoteHerdr,
@@ -2422,6 +2552,8 @@ struct RemoteClientStatusJson {
remote_host_bridge: bool,
#[serde(default)]
remote_bridge_idle_timeout: bool,
#[serde(default)]
remote_windows_start: bool,
}
impl RemoteClientStatusJson {
@@ -3677,6 +3809,79 @@ fn sanitize_path_component(input: &str) -> String {
mod tests {
use super::*;
#[test]
fn windows_remote_start_reuses_waits_and_only_forces_by_choice() {
if let Ok(choice) = std::env::var("HERDR_TEST_SIGN_IN_CHOICE") {
assert!(io::stdin().is_terminal());
assert!(!crossterm::terminal::is_raw_mode_enabled().unwrap());
let result = poll_windows_sign_in_choice();
assert!(!crossterm::terminal::is_raw_mode_enabled().unwrap());
match choice.as_str() {
"force" => assert!(result.unwrap()),
"cancel" => assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Interrupted),
"timeout" => assert!(!result.unwrap()),
_ => panic!("unknown manual sign-in input case"),
}
return;
}
prepare_windows_remote_start(
true,
|| panic!("running server needs no new capability"),
|_| panic!("must reuse running server"),
|| panic!("must not prompt"),
)
.unwrap();
let error = prepare_windows_remote_start(
false,
|| Ok(false),
|_| panic!("old stopped host must not cold start"),
|| panic!("must not prompt"),
)
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Unsupported);
for force in [false, true] {
let mut requests = Vec::new();
prepare_windows_remote_start(
false,
|| Ok(true),
|allow| {
requests.push(allow);
Ok(if requests.len() == 1 {
RemoteStartStatus::WindowsSignInRequired
} else {
RemoteStartStatus::Ready
})
},
|| Ok(force),
)
.unwrap();
assert_eq!(requests, [false, force]);
}
let error = prepare_windows_remote_start(
false,
|| Ok(true),
|_| Err(io::Error::new(io::ErrorKind::TimedOut, "uncertain launch")),
|| panic!("must not offer force after launch error"),
)
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
let mut requests = 0;
let error = prepare_windows_remote_start(
false,
|| Ok(true),
|_| {
requests += 1;
Ok(RemoteStartStatus::WindowsSignInRequired)
},
|| Err(io::Error::new(io::ErrorKind::Interrupted, "cancel")),
)
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Interrupted);
assert_eq!(requests, 1);
}
#[cfg(unix)]
#[test]
#[ignore = "requires a private loopback OpenSSH fixture"]
@@ -4559,6 +4764,7 @@ mod tests {
],
remote_host_bridge: false,
remote_bridge_idle_timeout: false,
remote_windows_start: false,
};
assert!(status.supports_endpoint_requirement(&linux, true));
for index in 0..status.endpoint_capabilities.len() {
+78 -6
View File
@@ -3,14 +3,30 @@
use std::io;
use std::time::Duration;
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum RemoteStartStatus {
Ready,
WindowsSignInRequired,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub(super) struct RemoteStartResponse {
pub status: RemoteStartStatus,
}
pub(crate) fn run_remote_client_bridge(args: &[String]) -> io::Result<()> {
let idle_timeout = match args {
[] => false,
let (idle_timeout, start_only, allow_headless) = match args {
[] => (false, false, false),
[option]
if option == "--idle-timeout-v1"
&& crate::platform::REMOTE_BRIDGE_IDLE_TIMEOUT_SUPPORTED =>
{
true
(true, false, false)
}
[option] if option == "--start-only-v1" => (false, true, false),
[option, force] if option == "--start-only-v1" && force == "--allow-headless-start-v1" => {
(false, true, true)
}
_ => {
return Err(io::Error::new(
@@ -19,7 +35,18 @@ pub(crate) fn run_remote_client_bridge(args: &[String]) -> io::Result<()> {
))
}
};
ensure_remote_server_running()?;
let result = ensure_remote_server_running(allow_headless);
if start_only {
let response = RemoteStartResponse {
status: remote_start_status(result)?,
};
println!(
"{}",
serde_json::to_string(&response).map_err(io::Error::other)?
);
return Ok(());
}
result?;
#[cfg(unix)]
let _ssh_agent = super::ssh_agent::Registration::start();
@@ -37,10 +64,24 @@ pub(crate) fn run_remote_client_bridge(args: &[String]) -> io::Result<()> {
crate::platform::forward_remote_bridge_stdio(stream, idle_timeout)
}
fn ensure_remote_server_running() -> io::Result<()> {
fn remote_start_status(result: io::Result<()>) -> io::Result<RemoteStartStatus> {
match result {
Ok(()) => Ok(RemoteStartStatus::Ready),
Err(error) if crate::platform::is_windows_sign_in_required(&error) => {
Ok(RemoteStartStatus::WindowsSignInRequired)
}
Err(error) => Err(error),
}
}
fn ensure_remote_server_running(allow_headless: bool) -> io::Result<()> {
let socket_path = crate::server::socket_paths::client_socket_path();
if !crate::server::autodetect::is_server_listening()? {
crate::server::autodetect::spawn_remote_server_daemon()?;
if allow_headless {
crate::server::autodetect::spawn_remote_server_daemon_with_headless(true)?;
} else {
crate::server::autodetect::spawn_remote_server_daemon()?;
}
crate::server::autodetect::wait_for_server_socket(&socket_path, Duration::from_secs(5))?;
}
let status =
@@ -58,3 +99,34 @@ fn ensure_remote_server_running() -> io::Result<()> {
"remote herdr server needs one final update before this bridge can attach; rerun `herdr --remote` from an interactive terminal to approve it",
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_only_never_turns_other_launch_failures_into_waiting() {
for kind in [
io::ErrorKind::WouldBlock,
io::ErrorKind::PermissionDenied,
io::ErrorKind::TimedOut,
] {
let error = remote_start_status(Err(io::Error::new(kind, "unrelated startup failure")))
.unwrap_err();
assert_eq!(error.kind(), kind);
}
assert_eq!(
remote_start_status(Ok(())).unwrap(),
RemoteStartStatus::Ready
);
#[cfg(windows)]
assert_eq!(
remote_start_status(Err(io::Error::new(
io::ErrorKind::WouldBlock,
crate::platform::WindowsSignInRequired
)))
.unwrap(),
RemoteStartStatus::WindowsSignInRequired
);
}
}
+25 -2
View File
@@ -207,8 +207,14 @@ pub fn spawn_server_daemon() -> io::Result<u32> {
/// Remote cold starts may need a different Windows login context from SSH.
pub(crate) fn spawn_remote_server_daemon() -> io::Result<u32> {
spawn_remote_server_daemon_with_headless(false)
}
pub(crate) fn spawn_remote_server_daemon_with_headless(_allow_headless: bool) -> io::Result<u32> {
#[cfg(windows)]
return spawn_server_daemon_with(crate::platform::launch_remote_server_daemon_command);
return spawn_server_daemon_with(|command| {
crate::platform::launch_remote_server_daemon_command(command, _allow_headless)
});
#[cfg(not(windows))]
spawn_server_daemon()
}
@@ -228,7 +234,11 @@ fn spawn_server_daemon_with(
let mut command = build_server_daemon_command(exe);
let pid = launch(&mut command).map_err(|err: io::Error| {
io::Error::new(err.kind(), format!("failed to spawn herdr server: {err}"))
if crate::platform::is_windows_sign_in_required(&err) {
err
} else {
io::Error::new(err.kind(), format!("failed to spawn herdr server: {err}"))
}
})?;
info!(pid, "server daemon spawned");
@@ -363,6 +373,19 @@ mod windows_tests {
use interprocess::os::windows::local_socket::ListenerOptionsExt as _;
use interprocess::os::windows::security_descriptor::SecurityDescriptor;
#[test]
fn remote_sign_in_denial_survives_spawn_context() {
let error = spawn_server_daemon_with(|_| {
Err(io::Error::new(
io::ErrorKind::WouldBlock,
crate::platform::WindowsSignInRequired,
))
})
.unwrap_err();
assert!(crate::platform::is_windows_sign_in_required(&error));
assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn reachability_distinguishes_denied_silent_busy_and_absent_pipes() {
let path =