Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/app-ffi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ on:
- "cmux-tui/crates/cmux-bookmarks-core/**"
- "cmux-tui/crates/cmux-cloud-start-core/**"
- "cmux-tui/crates/cmux-focus-core/**"
- "cmux-tui/crates/cmux-host-budget/**"
- "cmux-tui/crates/cmux-layout-reducer/**"
- "cmux-tui/crates/cmux-layout-reducer-ffi/**"
- "cmux-tui/crates/cmux-palette-ranker/**"
Expand Down
1 change: 0 additions & 1 deletion cmux-tui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmux-tui/crates/acpmux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ cmux-worktree-care.workspace = true
cmux-wg.workspace = true
anyhow = "1"
cmux-agent-pane-policy.workspace = true
# The host CPU/RAM budget (src/host_pressure.rs re-exports it); the macOS app is to link the same crate (cmux-app-ffi).
# The host CPU/RAM budget (src/host_pressure.rs re-exports it); the macOS app links the same crate (cmux-app-ffi).
cmux-host-budget.workspace = true
cmux-unix-socket.workspace = true
cmux-panic-log.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion cmux-tui/crates/acpmux/src/host_pressure.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The host's CPU and RAM budget (cmuxterm-hq#2995): the formula, the verdict,
//! the sampler and the [`Meter`] live in the `cmux-host-budget` crate, which
//! the macOS app is to link too (cmux-app-ffi), so the daemon and the app judge
//! the macOS app links too (cmux-app-ffi), so the daemon and the app judge
//! the host with one implementation. This daemon's `daemon status` `host_pressure`
//! and the warning on a turn's acceptance read [`Meter::shared`]
//! (`hub/views/host_load.rs`). Warns only: nothing is refused, queued or held.
Expand Down
67 changes: 66 additions & 1 deletion cmux-tui/crates/cmux-app-ffi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion cmux-tui/crates/cmux-app-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"
rust-version = "1.91"
license = "GPL-3.0-or-later"
publish = false
description = "The one Rust static library the cmux macOS app links: the C ABIs of the remote desktop core, the sidebar layout reducer, the palette ranker and cmux-motion"
description = "The one Rust static library the cmux macOS app links: the C ABIs of the remote desktop core, the sidebar layout reducer, the palette ranker, cmux-motion and the host budget"

# Own workspace, like its members: built only for the app xcframework
# (scripts/cmux-next/build-app-ffi.sh), so cmux-tui workspace builds and
Expand All @@ -22,6 +22,9 @@ cmux-palette-ranker = { path = "../cmux-palette-ranker" }
# The spring step and motion tokens (src/motion.rs). No `settings`: the app
# passes its own settings per call, so no AppKit or env reading here.
cmux-motion = { path = "../cmux-motion", default-features = false }
# The host CPU/RAM budget's C ABI (cmux-host-budget src/ffi.rs). No `tokio`: the
# app reads the meter synchronously on its own queue.
cmux-host-budget = { path = "../cmux-host-budget", default-features = false }

[profile.release]
opt-level = 3
Expand Down
5 changes: 4 additions & 1 deletion cmux-tui/crates/cmux-app-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
//! Add a new in-tree C ABI here, never as another static library.
//! `motion` is cmux-motion's C ABI (the spring step and motion tokens);
//! `hover` is its hover rules' C ABI (the hover card machine, tooltip hover
//! intent, the safe triangle and the row card's placement).
//! intent, the safe triangle and the row card's placement);
//! `host_budget` is cmux-host-budget's (the host CPU/RAM budget's verdict
//! and level, `include/cmux_host_budget_ffi.h` in that crate).

pub mod hover;
pub mod motion;

pub use cmux_host_budget::ffi as host_budget;
pub use cmux_layout_reducer_ffi as layout_reducer;
pub use cmux_palette_ranker::ffi as palette_ranker;
pub use cmux_rd_ffi as rd;
3 changes: 1 addition & 2 deletions cmux-tui/crates/cmux-host-budget/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
description = "The host's CPU and RAM budget (cmuxterm-hq#2995): the formula, the verdict, the sampler and the level the app shows. acpmux's daemon status use it; its C ABI is for the macOS app (cmux-app-ffi)."
description = "The host's CPU and RAM budget (cmuxterm-hq#2995): the formula, the verdict, the sampler and the level the app shows. acpmux's daemon status uses it, and the macOS app links its C ABI through cmux-app-ffi."

[features]
# The daemon's async reads of the meter (`Meter::current`, `Meter::peek`), which
Expand All @@ -15,7 +15,6 @@ default = ["tokio"]
tokio = ["dep:tokio"]

[dependencies]
dirs = "6"
libc = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
Expand Down
10 changes: 6 additions & 4 deletions cmux-tui/crates/cmux-host-budget/include/cmux_host_budget_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#define CMUX_HOST_BUDGET_FFI_H

/* cmux-host-budget's C ABI (cmux-tui/crates/cmux-host-budget/src/ffi.rs),
* for the app, through cmux-app-ffi:
* linked into the app through cmux-app-ffi:
* the host CPU/RAM budget (cmuxterm-hq#2995) that acpmux's daemon also uses.
* Verdicts are UTF-8 JSON in acpmux's `host_pressure` shape: pointer and
* length in, a NUL-terminated string out that the caller frees with
Expand All @@ -21,9 +21,11 @@ uint32_t cmux_host_budget_ffi_abi_version(void);
* another read is in flight. */
char *cmux_host_budget_local_verdict_json(void);

/* `verdict` judged again with memory pressure `level` (1 normal, 2 warn,
* 4 critical: DISPATCH_MEMORYPRESSURE_*). NULL when it is not a verdict. */
char *cmux_host_budget_with_pressure_json(const uint8_t *verdict, size_t verdict_len, uint32_t level);
/* `verdict` judged again with the pressure of a dispatch memory pressure
* event: pass its DISPATCH_MEMORYPRESSURE_* flags as they come (1 normal,
* 2 warn, 4 critical); they are read worst first, so 6 is critical. NULL when
* it is not a verdict. */
char *cmux_host_budget_with_pressure_json(const uint8_t *verdict, size_t verdict_len, uint32_t flags);

/* The verdict for given numbers with the built-in formula:
* {"host", "sample": {...}, "swap_mb_per_min"}. NULL for a bad input. */
Expand Down
11 changes: 6 additions & 5 deletions cmux-tui/crates/cmux-host-budget/src/ffi.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! The C ABI of the host budget (`include/cmux_host_budget_ffi.h`, Swift module
//! `CCmuxHostBudgetFFI`), for the macOS app, which links it through cmux-app-ffi: the host CPU/RAM budget's verdict from
//! `CCmuxHostBudgetFFI`), linked into the macOS app through cmux-app-ffi: the host CPU/RAM budget's verdict from
//! this process's own meter, a verdict judged again with the memory pressure
//! the dispatch source reports, and the tracker that turns verdicts into the
//! level the app shows. The formula lives once, in this crate, which acpmux's
Expand Down Expand Up @@ -61,8 +61,9 @@ pub extern "C" fn cmux_host_budget_local_verdict_json() -> *mut c_char {
answer(json::local_verdict)
}

/// `verdict` judged again with memory pressure `level` (1 normal, 2 warn,
/// 4 critical: `DISPATCH_MEMORYPRESSURE_*`); null when it is not a verdict.
/// `verdict` judged again with the memory pressure of a dispatch source event:
/// its `DISPATCH_MEMORYPRESSURE_*` flags as they come (1 normal, 2 warn,
/// 4 critical), read worst first, so 6 is critical. Null when it is not a verdict.
///
/// # Safety
///
Expand All @@ -71,13 +72,13 @@ pub extern "C" fn cmux_host_budget_local_verdict_json() -> *mut c_char {
pub unsafe extern "C" fn cmux_host_budget_with_pressure_json(
verdict: *const u8,
verdict_len: usize,
level: u32,
flags: u32,
) -> *mut c_char {
// SAFETY: forwarded from the caller's contract.
let Some(verdict) = (unsafe { text(verdict, verdict_len) }) else {
return std::ptr::null_mut();
};
answer(|| json::verdict_with_pressure(verdict, level))
answer(|| json::verdict_with_pressure(verdict, flags))
}

/// The verdict for given numbers with the built-in formula
Expand Down
8 changes: 8 additions & 0 deletions cmux-tui/crates/cmux-host-budget/src/ffi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ fn a_verdict_is_judged_again_through_the_abi() {
let warned = take(unsafe { cmux_host_budget_with_pressure_json(fine.as_ptr(), fine.len(), 2) });
let warned: serde_json::Value = serde_json::from_str(&warned.unwrap()).unwrap();
assert_eq!(warned["over"], serde_json::json!(["memory pressure warn"]));
// The dispatch source's flags: 6 (warn and critical) is critical, 3 (normal and warn) is warn.
for (flags, word) in [(6, "critical"), (3, "warn")] {
// SAFETY: `fine` is readable for its length.
let v =
take(unsafe { cmux_host_budget_with_pressure_json(fine.as_ptr(), fine.len(), flags) });
let v: serde_json::Value = serde_json::from_str(&v.unwrap()).unwrap();
assert_eq!(v["pressure"], word, "{flags}");
}
// SAFETY: a null pointer is allowed.
assert_eq!(take(unsafe { cmux_host_budget_with_pressure_json(std::ptr::null(), 0, 2) }), None);
let bad = b"\xff\xfe";
Expand Down
80 changes: 80 additions & 0 deletions cmux-tui/crates/cmux-host-budget/src/home.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! The current user's home directory, without the `dirs` crate. `dirs` pulls
//! `option-ext` (MPL-2.0) into whatever links it, and the macOS app links this
//! crate (cmux-app-ffi), whose crates must be GPL-compatible permissive or
//! first-party GPL (scripts/cmux-next/check_app_crate_licenses.py).
//!
//! Unix keeps `dirs::home_dir`'s rule (dirs-sys 0.5) exactly: `$HOME` when it
//! is set and not empty, else the home directory in the passwd entry of the
//! real user (`getuid`, `getpwuid_r`), else none; Android, iOS and Emscripten
//! read no passwd entry. Other platforms use `std::env::home_dir`.

#[cfg(unix)]
use std::ffi::OsString;
use std::path::PathBuf;

/// The current user's home directory (module docs).
pub fn home_dir() -> Option<PathBuf> {
#[cfg(unix)]
{
home_from(std::env::var_os("HOME"), passwd_home)
}
#[cfg(not(unix))]
{
std::env::home_dir()
}
}

/// `$HOME` when set and not empty, else what `passwd` finds.
#[cfg(unix)]
pub(crate) fn home_from(
home: Option<OsString>,
passwd: impl FnOnce() -> Option<PathBuf>,
) -> Option<PathBuf> {
home.filter(|h| !h.is_empty()).map(PathBuf::from).or_else(passwd)
}

/// The home directory of the real user's passwd entry, as `dirs-sys` reads it:
/// `getuid`, a buffer of `_SC_GETPW_R_SIZE_MAX` bytes (512 when the system
/// gives no size), no retry; none on an error, a missing entry or an empty
/// directory.
#[cfg(all(unix, not(any(target_os = "android", target_os = "ios", target_os = "emscripten"))))]
pub(crate) fn passwd_home() -> Option<PathBuf> {
use std::ffi::CStr;
use std::os::unix::ffi::OsStringExt;

// SAFETY: sysconf has no preconditions.
let size = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
let size = usize::try_from(size).unwrap_or(512);
let mut buf: Vec<libc::c_char> = vec![0; size];
// SAFETY: an all-zero passwd is a valid value of this plain C struct.
let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
// SAFETY: `passwd`, `buf` (of `buf.len()` bytes) and `result` are valid
// storage for the call; getuid has no preconditions.
let rc = unsafe {
libc::getpwuid_r(
libc::getuid(),
&raw mut passwd,
buf.as_mut_ptr(),
buf.len(),
&raw mut result,
)
};
if rc != 0 || result.is_null() || passwd.pw_dir.is_null() {
return None;
}
// SAFETY: on success `pw_dir` points to a NUL-terminated string inside `buf`, which lives
// to the end of this function.
let dir = unsafe { CStr::from_ptr(passwd.pw_dir) }.to_bytes();
(!dir.is_empty()).then(|| PathBuf::from(OsString::from_vec(dir.to_vec())))
}

/// Android, iOS and Emscripten: no passwd entry, as dirs-sys.
#[cfg(all(unix, any(target_os = "android", target_os = "ios", target_os = "emscripten")))]
pub(crate) fn passwd_home() -> Option<PathBuf> {
None
}

#[cfg(all(test, unix))]
#[path = "home_tests.rs"]
mod tests;
46 changes: 46 additions & 0 deletions cmux-tui/crates/cmux-host-budget/src/home_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use super::*;
use std::cell::Cell;

/// A set, non-empty `$HOME` wins, and the passwd entry is not read.
#[test]
fn a_set_home_wins_without_reading_passwd() {
let asked = Cell::new(false);
let home = home_from(Some("/Users/someone".into()), || {
asked.set(true);
Some("/from/passwd".into())
});
assert_eq!(home, Some(PathBuf::from("/Users/someone")));
assert!(!asked.get());
}

/// An unset or empty `$HOME` falls back to the passwd entry, as `dirs::home_dir` does.
#[test]
fn an_unset_or_empty_home_reads_the_passwd_entry() {
for home in [None, Some(OsString::new())] {
let asked = Cell::new(false);
let found = home_from(home.clone(), || {
asked.set(true);
Some("/from/passwd".into())
});
assert_eq!(found, Some(PathBuf::from("/from/passwd")), "{home:?}");
assert!(asked.get(), "{home:?}");
assert_eq!(home_from(home, || None), None);
}
}

/// The real passwd read: the test user's entry, when it has one, names an absolute directory.
#[test]
fn the_passwd_entry_is_an_absolute_directory() {
if let Some(dir) = passwd_home() {
assert!(dir.is_absolute(), "{dir:?}");
}
}

/// With `$HOME` set, `home_dir` is `$HOME`, as `dirs::home_dir` returns it.
#[test]
fn home_dir_is_home_when_it_is_set() {
match std::env::var_os("HOME").filter(|h| !h.is_empty()) {
Some(home) => assert_eq!(home_dir(), Some(PathBuf::from(home))),
None => assert_eq!(home_dir(), passwd_home()),
}
}
Loading
Loading