fix(daemon): keep mobile input working with many open panes (#1159)

This commit is contained in:
l0ng-ai
2026-10-12 23:17:20 +08:00
committed by GitHub
parent 5c45d17513
commit f995a06902
2 changed files with 111 additions and 1 deletions
+48
View File
@@ -386,6 +386,8 @@ macro_rules! startup_note {
}
pub fn run_daemon() -> anyhow::Result<()> {
#[cfg(unix)]
raise_open_file_limit();
let result = run_daemon_inner();
if let Err(e) = &result {
log::error!("daemon exited with error: {e:#}");
@@ -393,6 +395,52 @@ pub fn run_daemon() -> anyhow::Result<()> {
result
}
/// A GUI-launched macOS process commonly inherits a 256-descriptor soft
/// limit. Each pane and desktop/phone connection owns several descriptors;
/// around fifty tabs can leave accept() enough room for a socket but not
/// try_clone(), so existing screens work while new input connections get EOF.
/// Do this before opening services, including when adopting a live handoff.
#[cfg(unix)]
fn raise_open_file_limit() {
const TARGET: libc::rlim_t = 4096;
let mut limits = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) } != 0 {
log::warn!(
"could not read the open-file limit: {}",
std::io::Error::last_os_error()
);
return;
}
let before = limits.rlim_cur;
let target = TARGET.min(limits.rlim_max);
if before < target {
// Preserve the hard limit and never lower an already larger soft
// limit. A finite target also avoids macOS rejecting RLIM_INFINITY.
limits.rlim_cur = target;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limits) } != 0 {
log::warn!(
"could not raise the open-file limit from {before} to {target}: {}",
std::io::Error::last_os_error()
);
return;
}
log::info!(
"open-file soft limit raised from {before} to {target}; hard limit={}",
limits.rlim_max
);
}
if limits.rlim_cur < TARGET {
log::warn!(
"open-file limit is only {}; many panes may exhaust it (hard limit={})",
limits.rlim_cur,
limits.rlim_max
);
}
}
fn run_daemon_inner() -> anyhow::Result<()> {
// First, before a handoff is adopted: the re-exec keeps every inherited
// descriptor open, and adopting one marks it close-on-exec again.
+63 -1
View File
@@ -17,8 +17,14 @@ struct Daemon {
impl Daemon {
fn start() -> Daemon {
Self::start_with(|_| {})
}
fn start_with(configure: impl FnOnce(&mut Command)) -> Daemon {
let dir = tempfile::TempDir::new().unwrap();
let child = Command::new(env!("CARGO_BIN_EXE_tty7-server"))
let mut command = Command::new(env!("CARGO_BIN_EXE_tty7-server"));
configure(&mut command);
let child = command
.arg("--daemon")
.arg("--config-dir")
.arg(dir.path())
@@ -138,6 +144,62 @@ fn collect_until(session: &mut tty7_core::client::PaneSession, marker: &[u8]) ->
}
}
#[test]
#[cfg(unix)]
fn a_gui_file_limit_does_not_break_mobile_input_with_many_desktop_panes() {
use std::os::unix::process::CommandExt;
let daemon = Daemon::start_with(|command| unsafe {
// Launch Services on macOS normally gives an app a soft limit of
// 256. Set only this child to that limit; keep the runner unchanged.
command.pre_exec(|| {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) != 0 {
return Err(io::Error::last_os_error());
}
limit.rlim_cur = 256.min(limit.rlim_max);
if libc::setrlimit(libc::RLIMIT_NOFILE, &limit) != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
});
let panes = daemon.panes();
let mut desktops = Vec::new();
for n in 0..60 {
let pane = panes
.spawn(None, size(), Some(interactive_shell()), None, None)
.unwrap_or_else(|e| panic!("desktop pane {}: {e}", n + 1));
desktops.push(pane);
}
let id = desktops[0].pane_id();
for round in 0..3 {
let mut phone = panes.observe(id, size()).expect("phone can open the pane");
phone.set_recv_timeout(Some(STREAM_WITHIN)).unwrap();
let marker = format!("mobile-still-works-{round}");
panes
.send_input(
id,
format!("printf 'mobile-%s\\n' still-works-{round}\r").as_bytes(),
)
.expect("phone input is acknowledged beside the desktop connections");
collect_until(&mut phone, marker.as_bytes());
phone.detach().unwrap();
}
// The desktop's existing connection still types and receives output too.
desktops[0].set_recv_timeout(Some(STREAM_WITHIN)).unwrap();
desktops[0]
.input(b"printf 'desktop-%s\\n' still-works\r")
.unwrap();
collect_until(&mut desktops[0], b"desktop-still-works");
for pane in desktops {
pane.kill().unwrap();
}
}
#[test]
fn send_input_reaches_the_shell_without_displacing_the_controller() {
let daemon = Daemon::start();