mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
fabro(01KKS6XZ71N7148WNFMPME6ZSS): implement (success)
Fabro-Run: 01KKS6XZ71N7148WNFMPME6ZSS
Fabro-Completed: 5
Fabro-Checkpoint: d280e16919
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
beed9c934a
commit
a5245f92f5
13 changed files with 379 additions and 3 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -1218,6 +1218,15 @@ dependencies = [
|
|||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-beastie"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-cli"
|
||||
version = "0.4.0"
|
||||
|
|
@ -1235,6 +1244,7 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"fabro-agent",
|
||||
"fabro-api",
|
||||
"fabro-beastie",
|
||||
"fabro-config",
|
||||
"fabro-github",
|
||||
"fabro-llm",
|
||||
|
|
@ -1550,6 +1560,7 @@ dependencies = [
|
|||
"dirs",
|
||||
"dotenvy",
|
||||
"fabro-agent",
|
||||
"fabro-beastie",
|
||||
"fabro-devcontainer",
|
||||
"fabro-exe",
|
||||
"fabro-git-storage",
|
||||
|
|
|
|||
|
|
@ -69,4 +69,4 @@ opt-level = 2
|
|||
[profile.dev.package.regex-automata]
|
||||
opt-level = 2
|
||||
[profile.dev.package.regex-syntax]
|
||||
opt-level = 2
|
||||
opt-level = 2
|
||||
18
lib/crates/fabro-beastie/Cargo.toml
Normal file
18
lib/crates/fabro-beastie/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "fabro-beastie"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
description = "Cross-platform idle sleep prevention (No Sleep Till Brooklyn)"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
tracing.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
16
lib/crates/fabro-beastie/src/dummy.rs
Normal file
16
lib/crates/fabro-beastie/src/dummy.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use tracing::debug;
|
||||
|
||||
pub(crate) struct DummySleepInhibitor;
|
||||
|
||||
impl DummySleepInhibitor {
|
||||
pub(crate) fn acquire() -> Option<Self> {
|
||||
debug!("Sleep inhibitor: using dummy backend (no-op)");
|
||||
Some(DummySleepInhibitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DummySleepInhibitor {
|
||||
fn drop(&mut self) {
|
||||
debug!("Sleep inhibitor: dummy backend released");
|
||||
}
|
||||
}
|
||||
36
lib/crates/fabro-beastie/src/iokit_bindings.rs
Normal file
36
lib/crates/fabro-beastie/src/iokit_bindings.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#![allow(non_upper_case_globals, dead_code)]
|
||||
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::CFString;
|
||||
|
||||
// IOKit power management assertion types
|
||||
pub type IOPMAssertionID = u32;
|
||||
pub const kIOPMAssertionIDInvalid: IOPMAssertionID = 0;
|
||||
|
||||
// IOReturn type
|
||||
pub type IOReturn = i32;
|
||||
pub const kIOReturnSuccess: IOReturn = 0;
|
||||
|
||||
extern "C" {
|
||||
pub fn IOPMAssertionCreateWithName(
|
||||
assertion_type: core_foundation::string::CFStringRef,
|
||||
assertion_level: u32,
|
||||
reason_for_activity: core_foundation::string::CFStringRef,
|
||||
assertion_id: *mut IOPMAssertionID,
|
||||
) -> IOReturn;
|
||||
|
||||
pub fn IOPMAssertionRelease(assertion_id: IOPMAssertionID) -> IOReturn;
|
||||
}
|
||||
|
||||
// Assertion level
|
||||
pub const kIOPMAssertionLevelOn: u32 = 255;
|
||||
|
||||
/// Create the CFString for "PreventUserIdleSystemSleep".
|
||||
pub fn prevent_idle_sleep_type() -> CFString {
|
||||
CFString::new("PreventUserIdleSystemSleep")
|
||||
}
|
||||
|
||||
/// Create a CFString reason.
|
||||
pub fn assertion_reason() -> CFString {
|
||||
CFString::new("Fabro workflow running")
|
||||
}
|
||||
90
lib/crates/fabro-beastie/src/lib.rs
Normal file
90
lib/crates/fabro-beastie/src/lib.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
//! Cross-platform idle sleep prevention (No Sleep Till Brooklyn).
|
||||
//!
|
||||
//! Call [`guard(true)`] to acquire a sleep inhibitor that prevents the system
|
||||
//! from idle-sleeping while Fabro is working. The guard is released on drop.
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod iokit_bindings;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
mod dummy;
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
/// RAII guard that prevents idle system sleep while held.
|
||||
pub struct SleepInhibitorGuard {
|
||||
_inner: InnerGuard,
|
||||
}
|
||||
|
||||
// Fields are held for their Drop implementations, not read directly.
|
||||
#[allow(dead_code)]
|
||||
enum InnerGuard {
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOS(macos::MacOSSleepInhibitor),
|
||||
#[cfg(target_os = "linux")]
|
||||
Linux(linux::LinuxSleepInhibitor),
|
||||
Dummy(dummy::DummySleepInhibitor),
|
||||
}
|
||||
|
||||
/// Acquire a sleep inhibitor guard.
|
||||
///
|
||||
/// If `enabled` is `false`, returns `None` immediately.
|
||||
/// If `enabled` is `true`, attempts to acquire a platform-specific sleep
|
||||
/// inhibitor. Falls back to a dummy (no-op) backend if the platform backend
|
||||
/// is unavailable.
|
||||
pub fn guard(enabled: bool) -> Option<SleepInhibitorGuard> {
|
||||
if !enabled {
|
||||
debug!("Sleep inhibitor: disabled by configuration");
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(inner) = macos::MacOSSleepInhibitor::acquire() {
|
||||
return Some(SleepInhibitorGuard {
|
||||
_inner: InnerGuard::MacOS(inner),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(inner) = linux::LinuxSleepInhibitor::acquire() {
|
||||
return Some(SleepInhibitorGuard {
|
||||
_inner: InnerGuard::Linux(inner),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to dummy
|
||||
dummy::DummySleepInhibitor::acquire().map(|inner| SleepInhibitorGuard {
|
||||
_inner: InnerGuard::Dummy(inner),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guard_enabled_returns_some() {
|
||||
let g = guard(true);
|
||||
assert!(g.is_some(), "guard(true) should return Some");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_disabled_returns_none() {
|
||||
let g = guard(false);
|
||||
assert!(g.is_none(), "guard(false) should return None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_drop_does_not_panic() {
|
||||
let g = guard(true);
|
||||
drop(g);
|
||||
}
|
||||
}
|
||||
124
lib/crates/fabro-beastie/src/linux.rs
Normal file
124
lib/crates/fabro-beastie/src/linux.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
use std::process::{Child, Command};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub(crate) struct LinuxSleepInhibitor {
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl LinuxSleepInhibitor {
|
||||
pub(crate) fn acquire() -> Option<Self> {
|
||||
// Try systemd-inhibit first, then gnome-session-inhibit as fallback
|
||||
if let Some(inhibitor) = Self::try_systemd_inhibit() {
|
||||
return Some(inhibitor);
|
||||
}
|
||||
if let Some(inhibitor) = Self::try_gnome_inhibit() {
|
||||
return Some(inhibitor);
|
||||
}
|
||||
warn!("Sleep inhibitor: no supported inhibitor found on this system");
|
||||
None
|
||||
}
|
||||
|
||||
fn try_systemd_inhibit() -> Option<Self> {
|
||||
let result = Command::new("systemd-inhibit")
|
||||
.args([
|
||||
"--what=idle",
|
||||
"--mode=block",
|
||||
"--who=fabro",
|
||||
"--reason=Fabro workflow running",
|
||||
"sleep",
|
||||
"infinity",
|
||||
])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
match result {
|
||||
Ok(mut child) => {
|
||||
// Set PR_SET_PDEATHSIG so the child is killed if the parent dies
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
// The child is already spawned, but we can set pdeathsig via /proc
|
||||
// Actually, PR_SET_PDEATHSIG must be set from within the child process.
|
||||
// For a pre-spawned child, we rely on explicit Drop cleanup.
|
||||
// The safer approach is to use pre_exec, so let's re-spawn.
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
|
||||
let result = unsafe {
|
||||
Command::new("systemd-inhibit")
|
||||
.args([
|
||||
"--what=idle",
|
||||
"--mode=block",
|
||||
"--who=fabro",
|
||||
"--reason=Fabro workflow running",
|
||||
"sleep",
|
||||
"infinity",
|
||||
])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.pre_exec(|| {
|
||||
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
};
|
||||
match result {
|
||||
Ok(child) => {
|
||||
debug!("Sleep inhibitor: acquired via systemd-inhibit");
|
||||
Some(Self { child })
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Sleep inhibitor: failed to respawn systemd-inhibit: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
debug!("Sleep inhibitor: acquired via systemd-inhibit");
|
||||
Some(Self { child })
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Sleep inhibitor: systemd-inhibit not available: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_gnome_inhibit() -> Option<Self> {
|
||||
let result = Command::new("gnome-session-inhibit")
|
||||
.args([
|
||||
"--inhibit=idle",
|
||||
"--reason",
|
||||
"Fabro workflow running",
|
||||
"sleep",
|
||||
"infinity",
|
||||
])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
match result {
|
||||
Ok(child) => {
|
||||
debug!("Sleep inhibitor: acquired via gnome-session-inhibit");
|
||||
Some(Self { child })
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Sleep inhibitor: gnome-session-inhibit not available: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LinuxSleepInhibitor {
|
||||
fn drop(&mut self) {
|
||||
debug!("Sleep inhibitor: releasing (killing inhibitor child process)");
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
55
lib/crates/fabro-beastie/src/macos.rs
Normal file
55
lib/crates/fabro-beastie/src/macos.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use core_foundation::base::TCFType;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::iokit_bindings::*;
|
||||
|
||||
pub(crate) struct MacOSSleepInhibitor {
|
||||
assertion_id: IOPMAssertionID,
|
||||
}
|
||||
|
||||
impl MacOSSleepInhibitor {
|
||||
pub(crate) fn acquire() -> Option<Self> {
|
||||
let assertion_type = prevent_idle_sleep_type();
|
||||
let reason = assertion_reason();
|
||||
let mut assertion_id: IOPMAssertionID = kIOPMAssertionIDInvalid;
|
||||
|
||||
let result = unsafe {
|
||||
IOPMAssertionCreateWithName(
|
||||
assertion_type.as_concrete_TypeRef(),
|
||||
kIOPMAssertionLevelOn,
|
||||
reason.as_concrete_TypeRef(),
|
||||
&mut assertion_id,
|
||||
)
|
||||
};
|
||||
|
||||
if result == kIOReturnSuccess {
|
||||
debug!(
|
||||
assertion_id,
|
||||
"Sleep inhibitor: acquired IOKit power assertion"
|
||||
);
|
||||
Some(Self { assertion_id })
|
||||
} else {
|
||||
warn!(
|
||||
result,
|
||||
"Sleep inhibitor: failed to create IOKit power assertion"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MacOSSleepInhibitor {
|
||||
fn drop(&mut self) {
|
||||
debug!(
|
||||
assertion_id = self.assertion_id,
|
||||
"Sleep inhibitor: releasing IOKit power assertion"
|
||||
);
|
||||
let result = unsafe { IOPMAssertionRelease(self.assertion_id) };
|
||||
if result != kIOReturnSuccess {
|
||||
warn!(
|
||||
result,
|
||||
"Sleep inhibitor: failed to release IOKit power assertion"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ path = "src/main.rs"
|
|||
default = []
|
||||
server = ["dep:fabro-api"]
|
||||
exedev = ["fabro-config/exedev", "fabro-workflows/exedev"]
|
||||
sleep_inhibitor = ["dep:fabro-beastie", "fabro-workflows/sleep_inhibitor"]
|
||||
|
||||
[dependencies]
|
||||
fabro-config = { path = "../fabro-config" }
|
||||
|
|
@ -23,6 +24,7 @@ fabro-agent = { path = "../fabro-agent" }
|
|||
fabro-mcp = { path = "../fabro-mcp" }
|
||||
fabro-workflows = { path = "../fabro-workflows" }
|
||||
fabro-api = { path = "../fabro-api", optional = true }
|
||||
fabro-beastie = { path = "../fabro-beastie", optional = true }
|
||||
fabro-util = { path = "../fabro-util" }
|
||||
clap.workspace = true
|
||||
console.workspace = true
|
||||
|
|
@ -67,4 +69,4 @@ predicates = "3"
|
|||
tempfile = "3"
|
||||
serde_json.workspace = true
|
||||
httpmock = "0.8"
|
||||
trycmd = "0.15"
|
||||
trycmd = "0.15"
|
||||
|
|
@ -522,6 +522,8 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
Command::Exec(mut args) => {
|
||||
let cli_config = cli_config::load_cli_config(None)?;
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = fabro_beastie::guard(cli_config.prevent_idle_sleep);
|
||||
let exec_defaults = cli_config.exec.as_ref();
|
||||
args.apply_cli_defaults(
|
||||
exec_defaults.and_then(|a| a.provider.as_deref()),
|
||||
|
|
@ -604,6 +606,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
styles,
|
||||
github_app,
|
||||
git_author,
|
||||
cli_config.prevent_idle_sleep,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ pub struct CliConfig {
|
|||
pub exec: Option<ExecDefaults>,
|
||||
pub git: Option<CliGitConfig>,
|
||||
#[serde(default)]
|
||||
pub prevent_idle_sleep: bool,
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
#[serde(default)]
|
||||
pub log: crate::server::LogConfig,
|
||||
|
|
@ -246,6 +248,18 @@ email = "me@local"
|
|||
assert_eq!(config.git, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prevent_idle_sleep_true() {
|
||||
let config: CliConfig = toml::from_str("prevent_idle_sleep = true").unwrap();
|
||||
assert!(config.prevent_idle_sleep);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prevent_idle_sleep_defaults_to_false() {
|
||||
let config: CliConfig = toml::from_str("").unwrap();
|
||||
assert!(!config.prevent_idle_sleep);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_verbose_true() {
|
||||
let config: CliConfig = toml::from_str("verbose = true").unwrap();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ doctest = false
|
|||
[features]
|
||||
default = []
|
||||
exedev = ["dep:fabro-exe"]
|
||||
sleep_inhibitor = ["dep:fabro-beastie"]
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
|
|
@ -22,6 +23,7 @@ anyhow.workspace = true
|
|||
dotenvy.workspace = true
|
||||
fabro-agent = { path = "../fabro-agent" }
|
||||
fabro-devcontainer = { path = "../fabro-devcontainer" }
|
||||
fabro-beastie = { path = "../fabro-beastie", optional = true }
|
||||
fabro-exe = { path = "../fabro-exe", optional = true }
|
||||
fabro-ssh = { path = "../fabro-ssh" }
|
||||
fabro-mcp = { path = "../fabro-mcp" }
|
||||
|
|
@ -66,4 +68,4 @@ tokio = { workspace = true, features = ["test-util", "macros"] }
|
|||
tempfile = "3"
|
||||
dotenvy.workspace = true
|
||||
assert_cmd = "2"
|
||||
predicates = "3"
|
||||
predicates = "3"
|
||||
|
|
@ -294,7 +294,12 @@ pub async fn run_command(
|
|||
styles: &'static Styles,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
git_author: crate::git::GitAuthor,
|
||||
prevent_idle_sleep: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = fabro_beastie::guard(prevent_idle_sleep);
|
||||
#[cfg(not(feature = "sleep_inhibitor"))]
|
||||
let _ = prevent_idle_sleep;
|
||||
// Handle --run-branch resume: read everything from git metadata
|
||||
if let Some(branch) = args.run_branch.clone() {
|
||||
return run_from_branch(args, &branch, styles, git_author, run_defaults, github_app).await;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue