fs_usage_sys 0.1.4

A Rust wrapper for macOS fs_usage to monitor file system events with advanced filtering
Documentation
# Detecting Write Operations from Applications

This guide explains how to identify write operations from various applications using fs_usage_sys, based on real-world patterns observed in fs_usage output.

## Important Note on fs_usage Behavior

Modern macOS fs_usage may not always show explicit `WrData` operations. Instead, write operations often manifest as patterns of `open`, `stat64`, and other filesystem calls. This guide helps you identify these patterns.

## Key Concepts

### 1. Don't Use `watch_writes_only()`

The `watch_writes_only()` method filters too aggressively and will miss many write operations. Instead, monitor all filesystem events and use pattern matching to identify writes:

```rust
// DON'T DO THIS:
let monitor = FsUsageMonitorBuilder::new()
    .watch_writes_only()  // This filters out important events!
    .build()?;

// DO THIS INSTEAD:
let monitor = FsUsageMonitorBuilder::new()
    .watch_path("/your/path/**/*")
    .exclude_process("mds")
    .exclude_process("mdworker")
    .exclude_process("Spotlight")
    .build()?;
```

### 2. Write Operation Patterns

Different applications have distinct patterns when writing files:

## Application-Specific Patterns

### 1. Vim

Vim uses a swap file pattern for safe editing:

```
# Vim creates a swap file when editing begins
22:22:36.481747 open [Helper:26258] /path/to/.file.txt.swp -> OK

# During editing, writes go to the swap file
22:22:39.390287 WrData[A] [vim:738463] /path/to/.file.txt.swp -> OK

# On save (:w), vim writes and removes the swap
22:22:39.390373 unlink [vim:738463] /path/to/.file.txt.swp -> OK
22:22:39.391679 open [Helper:26258] /path/to/file.txt -> OK
```

**Detection pattern for vim:**
```rust
fn is_vim_write(event: &FsEvent) -> bool {
    // Vim swap file creation
    if event.path.contains(".swp") && event.operation == "open" {
        return true;
    }
    
    // Vim writing to swap
    if event.process_name == "vim" && event.operation.contains("WrData") {
        return true;
    }
    
    // Vim saving (unlinking swap file)
    if event.process_name == "vim" && event.operation == "unlink" && event.path.contains(".swp") {
        return true;
    }
    
    false
}
```

### 2. Shell Redirects (echo, cat, etc.)

Shell commands like `echo "text" > file.txt` appear as simple open operations:

```
# Echo creating/writing to a file shows as open by Helper process
22:06:09.989848 open [Helper:26258] /path/to/write_test.txt -> OK

# The actual write might not show as WrData in fs_usage
# But the open by Helper process indicates a write operation
```

**Detection pattern for shell writes:**
```rust
fn is_shell_write(event: &FsEvent) -> bool {
    // Helper process opening files often indicates shell redirects
    if event.process_name == "Helper" && event.operation == "open" {
        // Could check if preceded by shell process activity
        return true;
    }
    
    // Direct shell operations
    if matches!(event.process_name.as_str(), "bash" | "zsh" | "sh") {
        if event.operation == "open" || event.operation.contains("write") {
            return true;
        }
    }
    
    false
}
```

### 3. Cursor/VS Code

Cursor and VS Code show a pattern of plugin and helper process interactions:

```
# Cursor exploring files (reads)
21:56:21.290627 open [(Plugin):555012] /path/to/file.txt 0.000012 Cursor -> OK

# When saving, Helper process opens the file
21:56:24.113083 open [Helper:26258] /path/to/file.txt 0.000029 -> OK

# Multiple processes may access during save
22:06:21.309605 open [(Plugin):26539] /path/to/file.txt 0.000008 Cursor -> OK
```

**Detection pattern for Cursor/VS Code:**
```rust
fn is_cursor_write(event: &FsEvent) -> bool {
    // Helper process after Plugin activity suggests a write
    if event.process_name == "Helper" && 
       (event.path.contains("Cursor") || previous_was_cursor_plugin) {
        return true;
    }
    
    // Look for Cursor in the extended info
    if event.result.contains("Cursor") && event.operation == "open" {
        // May indicate Cursor-initiated write
        return true;
    }
    
    false
}
```

### 4. General Application Writes

Many applications follow common patterns:

```
# stat64 calls to check file status
22:22:36.471201 stat64 [app:12345] /path/to/file.txt -> OK

# open call (may include flags in wide mode)
22:14:19.037972 open F=30 (R________S____) /path/to/file.txt -> OK

# Actual write (when visible)
22:22:39.390287 WrData[A] [app:12345] /path/to/file.txt -> OK
```

## Comprehensive Write Detection Function

Here's a complete function that combines all patterns:

```rust
use fs_usage_sys::FsEvent;

pub fn is_write_operation(event: &FsEvent, recent_events: &[FsEvent]) -> bool {
    // Direct write operations (when visible)
    if event.operation.contains("WrData") || 
       event.operation.contains("WrMeta") ||
       event.operation.contains("write") {
        return true;
    }
    
    // File creation/modification operations
    if matches!(event.operation.as_str(), 
        "creat" | "truncate" | "ftruncate" | "rename") {
        return true;
    }
    
    // Vim patterns
    if event.process_name == "vim" {
        if event.operation == "unlink" && event.path.contains(".swp") {
            return true; // Vim saving
        }
    }
    if event.path.contains(".swp") && event.operation == "open" {
        return true; // Vim swap file creation
    }
    
    // Shell redirect patterns
    if event.process_name == "Helper" && event.operation == "open" {
        // Check if this might be a shell redirect
        // You might want to look at recent events to confirm
        return true;
    }
    
    // Shell commands
    if matches!(event.process_name.as_str(), "bash" | "zsh" | "sh") {
        if event.operation == "open" {
            return true;
        }
    }
    
    // Editor backup files
    if event.path.ends_with("~") && 
       (event.operation == "open" || event.operation == "unlink") {
        return true;
    }
    
    // File deletion is a form of write
    if event.operation == "unlink" {
        return true;
    }
    
    false
}

// Enhanced detection using context
pub fn is_likely_write_with_context(
    event: &FsEvent, 
    index: usize, 
    all_events: &[FsEvent]
) -> bool {
    // Check the event itself
    if is_write_operation(event, &all_events[0..index]) {
        return true;
    }
    
    // Look for patterns in surrounding events
    if event.operation == "open" {
        // Check if followed by file size change or modification
        for i in 1..=5 {
            if index + i < all_events.len() {
                let next = &all_events[index + i];
                if next.path == event.path {
                    if next.operation.contains("write") || 
                       next.operation.contains("WrData") {
                        return true;
                    }
                }
            }
        }
        
        // Check if preceded by stat64 from same process
        if index > 0 {
            let prev = &all_events[index - 1];
            if prev.path == event.path && 
               prev.process_name == event.process_name &&
               prev.operation == "stat64" {
                // stat64 followed by open often indicates write intent
                return true;
            }
        }
    }
    
    false
}
```

## Usage Example

```rust
use fs_usage_sys::{FsEvent, FsUsageMonitorBuilder};
use std::collections::VecDeque;

fn monitor_writes() -> Result<(), Box<dyn std::error::Error>> {
    let mut monitor = FsUsageMonitorBuilder::new()
        .watch_path("/path/to/monitor/**/*")
        .exclude_process("mds")
        .exclude_process("mdworker")
        .exclude_process("Spotlight")
        .build()?;
    
    monitor.start()?;
    
    // Keep recent events for context
    let mut recent_events: VecDeque<FsEvent> = VecDeque::with_capacity(100);
    
    loop {
        match monitor.events().recv_timeout(Duration::from_millis(100)) {
            Ok(event) => {
                recent_events.push_back(event.clone());
                if recent_events.len() > 100 {
                    recent_events.pop_front();
                }
                
                let events_vec: Vec<_> = recent_events.iter().cloned().collect();
                let current_index = events_vec.len() - 1;
                
                if is_likely_write_with_context(&event, current_index, &events_vec) {
                    println!("WRITE DETECTED: {} wrote to {} via {}", 
                        event.process_name, 
                        event.path, 
                        event.operation
                    );
                    
                    // Handle the write event
                    handle_write_event(&event);
                }
            }
            Err(_) => continue,
        }
    }
}
```

## Tips for Accurate Write Detection

1. **Monitor patterns, not just single events** - Write operations often involve multiple system calls
2. **Track Helper process activity** - Many applications use the Helper process for file operations
3. **Watch for swap/backup files** - Editors create temporary files with patterns like `.swp`, `.tmp`, `~`
4. **Consider the process name** - Different applications have characteristic behaviors
5. **Use context** - Look at events before and after to understand the full operation
6. **Don't rely solely on WrData** - Modern macOS may not always emit these events

## Common Pitfalls

1. **Over-filtering with `watch_writes_only()`** - This will miss many legitimate write operations
2. **Ignoring open operations** - Many writes appear as simple open calls
3. **Not considering Helper process** - This process handles many file operations for GUI applications
4. **Missing editor patterns** - Editors use complex sequences of operations for safe saving

## Platform Notes

- This guide is specific to macOS fs_usage output
- Different macOS versions may show slightly different event patterns
- The `-w` flag in fs_usage means "wide format", not "writes only"
- Adding `-f diskio` to fs_usage flags can help capture more write events