diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-27 21:35:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-27 21:36:26 +0300 |
| commit | 8e7317e223bcfe38281a71fb121b0b65aaab1a43 (patch) | |
| tree | 23acae73320ee4c6a186fde9ee51146e9e23a005 | |
| parent | 3325304f89d56990f3134a3d01749e5de498725d (diff) | |
fix: raise macos server fd limit
refs #327
| -rw-r--r-- | CHANGELOG.md | 5 | ||||
| -rw-r--r-- | docs/next/CHANGELOG.md | 5 | ||||
| -rw-r--r-- | src/platform/fallback.rs | 3 | ||||
| -rw-r--r-- | src/platform/linux.rs | 2 | ||||
| -rw-r--r-- | src/platform/macos.rs | 68 | ||||
| -rw-r--r-- | src/server/headless.rs | 1 |
6 files changed, 84 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b03a47..1008bc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Fixed +- Fixed macOS server startup with large restored sessions by raising the server file descriptor soft limit, preventing new panes from failing with `dup of fd N failed` or `Too many open files` around 40 live panes. (#327) + +This is a hotfix for v0.6.3. See the v0.6.3 notes for the full feature release. + ## [0.6.3] - 2026-05-27 ### Added diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index a9b03a47..1008bc65 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Fixed +- Fixed macOS server startup with large restored sessions by raising the server file descriptor soft limit, preventing new panes from failing with `dup of fd N failed` or `Too many open files` around 40 live panes. (#327) + +This is a hotfix for v0.6.3. See the v0.6.3 notes for the full feature release. + ## [0.6.3] - 2026-05-27 ### Added diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index d7360f9b..42abfcb2 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -3,6 +3,9 @@ use std::path::PathBuf; use super::{ClipboardImage, ForegroundJob, Signal}; /// Unsupported platform stub. +pub fn raise_server_nofile_limit() {} + +/// Unsupported platform stub. pub fn foreground_job(_child_pid: u32) -> Option<ForegroundJob> { None } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 2ea26559..046e72c7 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -9,6 +9,8 @@ use super::{ LimitedRead, Signal, }; +pub fn raise_server_nofile_limit() {} + /// Collect the foreground terminal job for a given child PID. pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> { let tpgid = foreground_process_group_id(child_pid)?; diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3ff23251..ac0e160b 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -11,6 +11,53 @@ use super::{ }; const PROC_PGRP_ONLY: u32 = 2; +const SERVER_NOFILE_LIMIT_TARGET: libc::rlim_t = 8192; + +pub fn raise_server_nofile_limit() { + match raise_nofile_limit(SERVER_NOFILE_LIMIT_TARGET) { + Ok(None) => {} + Ok(Some((previous, target))) => { + tracing::info!(previous, target, "raised server file descriptor soft limit") + } + Err(err) => tracing::warn!(err = %err, "failed to raise server file descriptor limit"), + } +} + +fn raise_nofile_limit( + target: libc::rlim_t, +) -> std::io::Result<Option<(libc::rlim_t, libc::rlim_t)>> { + let mut limit = std::mem::MaybeUninit::<libc::rlimit>::uninit(); + if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, limit.as_mut_ptr()) } != 0 { + return Err(std::io::Error::last_os_error()); + } + + let mut limit = unsafe { limit.assume_init() }; + let Some(target) = target_nofile_soft_limit(limit.rlim_cur, limit.rlim_max, target) else { + return Ok(None); + }; + + let previous = limit.rlim_cur; + limit.rlim_cur = target; + if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) } != 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(Some((previous, target))) +} + +fn target_nofile_soft_limit( + current: libc::rlim_t, + hard: libc::rlim_t, + target: libc::rlim_t, +) -> Option<libc::rlim_t> { + let target = if hard == libc::RLIM_INFINITY { + target + } else { + target.min(hard) + }; + + (current < target).then_some(target) +} /// Collect the foreground terminal job for a given child PID. pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> { @@ -670,6 +717,27 @@ pub fn process_exists(pid: u32) -> bool { mod tests { use super::*; + #[test] + fn nofile_target_raises_low_soft_limit_to_cap_when_hard_is_unlimited() { + assert_eq!( + target_nofile_soft_limit(256, libc::RLIM_INFINITY, 8192), + Some(8192) + ); + } + + #[test] + fn nofile_target_respects_finite_hard_limit() { + assert_eq!(target_nofile_soft_limit(256, 4096, 8192), Some(4096)); + } + + #[test] + fn nofile_target_does_not_lower_existing_soft_limit() { + assert_eq!( + target_nofile_soft_limit(16_384, libc::RLIM_INFINITY, 8192), + None + ); + } + fn build_procargs2(exec_path: &str, argv: &[&str], env: &[&str]) -> Vec<u8> { let mut buf = Vec::new(); buf.extend_from_slice(&(argv.len() as i32).to_ne_bytes()); diff --git a/src/server/headless.rs b/src/server/headless.rs index 37c05bf0..f06c66bc 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -2628,6 +2628,7 @@ fn is_keybinding_config_diagnostic(diagnostic: &str) -> bool { /// Run the headless server. This is the entry point called from main.rs. pub fn run_server() -> io::Result<()> { init_logging(); + crate::platform::raise_server_nofile_limit(); let args: Vec<String> = std::env::args().collect(); if args.get(2).map(String::as_str) == Some("--handoff-import") { |
