mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(daemon): keep mobile input working with many open panes (#1159)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user