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
68 changes: 64 additions & 4 deletions core/src/activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,64 @@ impl ActivitySnapshot {
})
}

#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
pub(crate) fn capture(cancel: &AtomicBool) -> Result<Self> {
let euid = unsafe { libc::geteuid() };
let proc_dir = std::fs::read_dir("/proc").map_err(|e| format!("Cannot read /proc: {e}"))?;
let mut working_directories = Vec::new();
let mut executable_paths = Vec::new();
let self_pid = std::process::id();

for entry in proc_dir {
safety::cancelled(cancel)?;
let Ok(entry) = entry else { continue };
let file_name = entry.file_name();
let Some(name_str) = file_name.to_str() else {
continue;
};
let Ok(pid) = name_str.parse::<u32>() else {
continue;
};

use std::os::unix::fs::MetadataExt;
let Ok(metadata) = entry.metadata() else {
continue;
};
if metadata.uid() != euid {
continue;
}

let exe_link = format!("/proc/{pid}/exe");
if let Some(exe) = std::fs::read_link(&exe_link)
.ok()
.filter(|p| p.is_absolute())
{
executable_paths.push(exe);
}

if pid == self_pid {
continue;
}

let cwd_link = format!("/proc/{pid}/cwd");
if let Some(cwd) = std::fs::read_link(&cwd_link)
.ok()
.filter(|p| p.is_absolute())
{
working_directories.push(cwd);
}
}

Ok(Self {
working_directories,
executable_paths,
running_app_bundle_ids: OnceCell::new(),
})
}

#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub(crate) fn capture(_cancel: &AtomicBool) -> Result<Self> {
Err("Reliable activity checks are supported only by the native macOS engine".into())
Err("Reliable activity checks are supported only on macOS and Linux".into())
}

/// Reuse one bounded-age snapshot across nearby gates. A failed capture is
Expand Down Expand Up @@ -232,9 +287,14 @@ impl ActivitySnapshot {
native::bundle_identifier,
)
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
{
let _ = cancel;
Ok(BTreeSet::new())
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Err("Reliable activity checks are supported only by the native macOS engine".into())
Err("Reliable activity checks are supported only on macOS and Linux".into())
}
});
match identifiers {
Expand Down
45 changes: 23 additions & 22 deletions core/src/cleanup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2006,7 +2006,7 @@ mod tests {
assert_eq!(std::fs::read(outside).unwrap(), [0x61; 8192]);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn review_fixture(large: bool) -> (tempfile::TempDir, Store, Root, Candidate) {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
Expand Down Expand Up @@ -2035,6 +2035,7 @@ mod tests {
} else {
file.write_all(b"disposable reviewed payload").unwrap();
}
file.sync_all().unwrap();
}
let modified = std::time::SystemTime::now() - Duration::from_secs(8 * 86_400);
for path in [
Expand Down Expand Up @@ -2073,28 +2074,28 @@ mod tests {
(temp, store, root, candidate)
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
struct FakeTrashState {
destination: PathBuf,
calls: usize,
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
thread_local! {
static FAKE_TRASH_STATE: RefCell<Option<FakeTrashState>> = const { RefCell::new(None) };
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
struct FakeTrashGuard;

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
impl Drop for FakeTrashGuard {
fn drop(&mut self) {
FAKE_TRASH_STATE.with(|state| *state.borrow_mut() = None);
}
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn install_fake_trash(destination: PathBuf) -> FakeTrashGuard {
FAKE_TRASH_STATE.with(|state| {
let mut state = state.borrow_mut();
Expand All @@ -2110,14 +2111,14 @@ mod tests {
FakeTrashGuard
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn fake_trash_calls() -> usize {
FAKE_TRASH_STATE.with(|state| state.borrow().as_ref().map_or(0, |state| state.calls))
}

/// Test Trash never invokes AppKit or touches the user's Trash. It only
/// moves the staged file to a preconfigured sibling inside the temp fixture.
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
unsafe extern "C" fn fake_trash(
input: *const libc::c_char,
output: *mut libc::c_char,
Expand Down Expand Up @@ -2150,7 +2151,7 @@ mod tests {
})
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
struct DuplicateCleanupFixture {
store: Store,
root: Root,
Expand All @@ -2160,7 +2161,7 @@ mod tests {
_temp: tempfile::TempDir,
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn write_old_installer(path: &Path) {
let mut file = File::create(path).unwrap();
let block = vec![0x5a; 1024 * 1024];
Expand All @@ -2177,7 +2178,7 @@ mod tests {
.unwrap();
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn duplicate_cleanup_fixture() -> DuplicateCleanupFixture {
let temp = tempfile::Builder::new()
.prefix("chippytea-duplicate-cleanup-")
Expand Down Expand Up @@ -2239,14 +2240,14 @@ mod tests {
}
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn read_prefix(path: &Path) -> [u8; 8] {
let mut prefix = [0; 8];
File::open(path).unwrap().read_exact(&mut prefix).unwrap();
prefix
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn assert_no_cleanup_credit(store: &Store, receipt: &Receipt) {
assert_eq!((receipt.credited_bytes, receipt.coins), (0, 0));
let wallet = store.wallet().unwrap();
Expand All @@ -2261,7 +2262,7 @@ mod tests {
);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn duplicate_guard_restores_copy_when_keeper_changes_after_staging() {
let mut fixture = duplicate_cleanup_fixture();
Expand Down Expand Up @@ -2308,7 +2309,7 @@ mod tests {
assert_no_cleanup_credit(&fixture.store, &receipt);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn duplicate_guard_restores_copy_when_cancelled_after_staging() {
let mut fixture = duplicate_cleanup_fixture();
Expand Down Expand Up @@ -2351,7 +2352,7 @@ mod tests {
assert_no_cleanup_credit(&fixture.store, &receipt);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn duplicate_guard_fake_trash_preserves_the_verified_keeper() {
let mut fixture = duplicate_cleanup_fixture();
Expand Down Expand Up @@ -2462,7 +2463,7 @@ mod tests {
(temp, store, root, candidate, interpreter)
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn venv_with_internal_symlink_is_measured_and_permanently_removed() {
let (_temp, mut store, root, candidate, interpreter) = venv_review_fixture();
Expand All @@ -2484,7 +2485,7 @@ mod tests {
);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn manifest_storage_failure_preserves_contents_and_rolls_back_parent_evidence() {
let (_temp, mut store, root, candidate) = review_fixture(false);
Expand Down Expand Up @@ -2519,7 +2520,7 @@ mod tests {
assert_eq!(store.wallet().unwrap().credited_bytes, 0);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn cancellation_during_manifest_capture_never_stages_the_artifact() {
let (_temp, mut store, root, candidate) = review_fixture(false);
Expand Down Expand Up @@ -2559,7 +2560,7 @@ mod tests {
assert_eq!(store.wallet().unwrap().credited_bytes, 0);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn changed_contents_after_manifest_commit_are_preserved_by_staged_verification() {
let (_temp, mut store, root, candidate) = review_fixture(true);
Expand Down Expand Up @@ -2594,7 +2595,7 @@ mod tests {
assert_eq!(store.wallet().unwrap().credited_bytes, 0);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn completed_cleanup_reports_real_entry_counts_and_preserves_project_sources() {
let (_temp, mut store, root, candidate) = review_fixture(true);
Expand Down Expand Up @@ -2633,7 +2634,7 @@ mod tests {
);
}

#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn cancellation_after_removal_releases_manifest_snapshot_and_persists_recovery() {
let (temp, mut store, root, candidate) = review_fixture(true);
Expand Down
16 changes: 15 additions & 1 deletion core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::unnecessary_cast, clippy::nonminimal_bool)]

pub mod accounting;
mod activity;
pub mod cleanup;
Expand Down Expand Up @@ -4381,7 +4383,19 @@ mod controller_tests {
std::thread::sleep(Duration::from_millis(1));
}
drop(engine);
Engine::open(&temp.path().join("library.sqlite"), None).unwrap()
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match Engine::open(&temp.path().join("library.sqlite"), None) {
Ok(engine) => return engine,
Err(err)
if err == "This chippytea library is already open in another process."
&& Instant::now() < deadline =>
{
std::thread::sleep(Duration::from_millis(5));
}
Err(err) => panic!("{err}"),
}
}
}

// Seed derived rows without allocating cleanup-sized payloads or making
Expand Down
10 changes: 10 additions & 0 deletions core/src/scan_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,7 @@ mod tests {
use std::os::unix::fs::PermissionsExt;

let directory = tempfile::tempdir().unwrap();
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
let executable = directory.path().join("chippytea-cli");
let helper = directory.path().join("chippytea-scan-helper");
std::fs::write(&executable, b"cli").unwrap();
Expand Down Expand Up @@ -1535,6 +1536,15 @@ mod tests {
let helpers = bundle.join("Contents/Helpers");
std::fs::create_dir_all(&macos).unwrap();
std::fs::create_dir_all(&helpers).unwrap();
for context in [
directory.path(),
&bundle,
&bundle.join("Contents"),
&macos,
&helpers,
] {
std::fs::set_permissions(context, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let executable = macos.join("Chippytea");
let helper = helpers.join("chippytea-scan-helper");
std::fs::write(&executable, b"app").unwrap();
Expand Down
Loading