# Process Name Filtering Feature
## Overview
The process name filtering feature allows zinit to detect and optionally kill processes that match a specified name pattern before starting a service. This is useful for:
- Detecting conflicting processes that may interfere with service startup
- Automatically cleaning up old instances before starting a new one
- Preventing port binding failures due to lingering processes
The feature mirrors the existing TCP port checking implementation and provides both conflict detection and optional process termination.
## Configuration
### ServiceDef Field
Add the `process_filters` field to your service configuration. Multiple filters can be specified:
```toml
[service]
name = "my_server"
exec = "/usr/bin/myServer"
process_filters = ["Server", "OldServer"] # Match processes containing "Server" or "OldServer"
```
### With kill_others
Combine with `kill_others` to automatically terminate matching processes:
```toml
[service]
name = "my_server"
exec = "/usr/bin/myServer"
process_filters = ["Server", "OldServer"]
kill_others = true # Kill all matching processes before startup
```
### Single Filter (Backward Compatible)
You can still use a single filter:
```toml
[service]
name = "my_server"
exec = "/usr/bin/myServer"
process_filters = ["Server"]
```
## Behavior
### Process Detection
- **Pattern Matching**: Case-insensitive substring matching
- **Examples**:
- `process_filter = "nginx"` matches: nginx, nginx-worker, nginx-reloader
- `process_filter = "Server"` matches: myServer, TestServer, ServerApp
- `process_filter = "python"` matches: python, python3, python3.9
### Conflict Detection (Without kill_others)
When a service with `process_filter` cannot start:
1. Zinit checks for running processes matching the filter
2. If found, the service is blocked with reason `ProcessNameConflict`
3. The blocked reason includes:
- Filter pattern used
- List of matching processes (PID and name)
- Full command line arguments (if available)
**Error Message Example:**
```
process filter 'Server' matched 2 running process(es):
myServer (PID 1234), TestServer (PID 5678)
```
### Process Termination (With kill_others)
When `kill_others = true` and the service is about to start:
1. Zinit finds all processes matching the filter
2. For each process found:
- Sends SIGKILL to the process
- Kills all child processes (process tree)
- Waits 100ms for termination
- Logs the result
3. Proceeds with service startup regardless of kill outcome
**Logging Example:**
```
INFO killing processes matching filter
INFO killed processes: myServer (PID 1234), TestServer (PID 5678)
```
## API Usage
### Rust API
```rust
use zinit::{ZinitHandle, client::client::ServiceConfigBuilder};
let z = ZinitHandle::new()?;
// Create service with multiple process filters
let config = ServiceConfigBuilder::new("my_server")
.exec("/usr/bin/myServer")
.process_filter("Server") // Can be called multiple times
.process_filter("OldServer")
.build();
z.service_set(config)?;
z.start("my_server")?; // Blocked if "Server" or "OldServer" process exists
```
**With kill_others:**
```rust
let config = ServiceConfigBuilder::new("my_server")
.exec("/usr/bin/myServer")
.process_filter("Server")
.process_filter("OldServer")
.kill_others() // Kill all matching processes first
.build();
z.service_set(config)?;
z.start("my_server")?; // Kills matching processes, then starts
```
### Rhai API
**Using .start() - Simplest approach (register + start in one call):**
```rhai
let z = zinit.connect();
// Create and start service with multiple process filters
service("my_server")
.exec("/usr/bin/myServer")
.process_filter("Server") // Can be called multiple times
.process_filter("OldServer")
.start(); // Registers and starts service!
```
**With kill_others:**
```rhai
service("my_server")
.exec("/usr/bin/myServer")
.process_filter("Server")
.process_filter("OldServer")
.kill_others()
.start(); // Kills matching processes, then starts
```
**Using .register() - For manual control:**
```rhai
service("my_server")
.exec("/usr/bin/myServer")
.process_filter("Server")
.process_filter("OldServer")
.register(); // Just register, start manually later
z.start("my_server"); // Start when ready
```
## Checking for Conflicts
### Using why() API
Get detailed information about why a service is blocked:
```rhai
let why = z.why("my_server");
if why.blocked {
if why.process_conflict {
print("Process conflict detected:");
print(" Filter: " + why.process_conflict.filter);
print(" Matching processes:");
for proc in why.process_conflict.processes {
print(" - " + proc.name + " (PID " + proc.pid + ")");
}
}
}
```
### Rust equivalent:
```rust
let why = z.why("my_server")?;
if let Some(conflict) = why.process_conflict {
println!("Filter: {}", conflict.filter);
for proc in conflict.processes {
println!(" - {} (PID {})", proc.name, proc.pid);
}
}
```
## Platform Support
### Linux
- Reads `/proc/[pid]/comm` for process names
- Reads `/proc/[pid]/cmdline` for full command arguments
- Case-insensitive substring matching
### macOS
- Uses `ps -A -o pid=,comm=` to list processes
- Uses `ps -p PID -o args=` for full command arguments
- Case-insensitive substring matching
### Other Platforms
- Returns empty list (no conflict detection)
- Service starts without checking
## Examples
### Example 1: Simple Conflict Detection
Service configuration that blocks if an "nginx" process exists:
```toml
[service]
name = "web_server"
exec = "/usr/bin/nginx"
process_filters = ["nginx"]
```
**Behavior:**
- If any process named "nginx", "nginx-worker", etc. is running → service blocked
- Error message: `process filter 'nginx' matched N running process(es)`
### Example 2: Auto-cleanup on Startup
Service configuration that kills existing processes before starting:
```toml
[service]
name = "web_server"
exec = "/usr/bin/nginx"
process_filters = ["nginx"]
kill_others = true
```
**Behavior:**
- If "nginx" processes exist → kill them all (including children)
- Proceed with service startup
- Log messages show which processes were killed
### Example 3: Multiple Process Filters
Detect and kill multiple different processes:
```toml
[service]
name = "web_server"
exec = "/usr/bin/nginx"
process_filters = ["nginx", "apache2", "httpd"]
kill_others = true
```
**Behavior:**
- Before starting, check for "nginx", "apache2", and "httpd" processes
- Kill all matching processes (and their children)
- Proceed with service startup
### Example 4: Multiple Conflicts (Ports + Process Filters)
Combine port checking with process filtering:
```toml
[service]
name = "web_server"
exec = "/usr/bin/nginx"
ports = [80, 443]
process_filters = ["nginx", "apache2"]
kill_others = true
```
**Behavior:**
- Kill processes on ports 80, 443 (if any)
- Kill processes matching filters "nginx" or "apache2" (if any)
- Start the service
## Edge Cases
### Case Sensitivity
Process name matching is **case-insensitive**:
```
process_filter = "Server" matches: server, SERVER, SeRvEr, TestServer
```
### Empty Filters List
An empty `process_filters` list means no process filtering is done:
```rust
.process_filter("") // Ignored, no matching done
```
Or simply don't add any filters:
```rust
ServiceConfigBuilder::new("my_server")
.exec("/usr/bin/server")
// No process_filter() calls = no filtering
.build()
```
### Substring Matching
Matching is based on substring, not exact match:
```
process_filter = "test" matches: test, testing, latest, mytest
process_filter = "nginx" matches: nginx, nginx-worker, nginx-ssl
```
### Kill Failure
If `kill_others` fails to kill some processes:
- Warning is logged: `failed to kill some processes in tree`
- Service startup **proceeds anyway**
- This ensures startup resilience
### Process Reappearance
If processes matching the filter reappear after being killed:
- The service will already be running at that point
- Restart logic applies normally
## Error Messages
### Conflict Detected (Without kill_others)
```
Error: Cannot start service 'my_server':
process filter 'Server' matched 2 running process(es):
myServer (PID 1234), TestServer (PID 5678)
```
**In Rhai:**
```
Error: zinit_start 'my_server' failed:
process filter 'Server' matched 2 running process(es):
myServer (PID 1234), TestServer (PID 5678)
```
### Kill Outcome (With kill_others)
**Success:**
```
INFO killed processes: myServer (PID 1234), TestServer (PID 5678)
```
**Partial Failure:**
```
WARN failed to kill some processes in tree: [5679, 5680]
INFO proceeding with service startup anyway
```
## Logging
Process filtering operations are logged at different levels:
- **WARN**: Conflict detected (blocking startup)
- **INFO**: Starting process kill for kill_others
- **INFO**: Successfully killed process tree
- **WARN**: Failed to kill some processes (but proceeding)
Enable `RUST_LOG=debug` for verbose logging.
## Performance Considerations
### Process Enumeration
- **Linux**: Reads `/proc` filesystem (typically fast, <100ms)
- **macOS**: Spawns `ps` command (typically <50ms)
- Runs before each start attempt (not continuous)
### Process Killing
- Uses SIGKILL (immediate termination)
- Waits 100ms for all processes to die
- Scans process tree to kill children
- Typically completes in <500ms
## Comparison with Port Checking
| Detection method | TCP bind attempt | Process listing |
| Pattern type | Exact port number | Substring match |
| Multiple checks | Per service | Per service |
| Scope | Network ports only | Any process |
| Use case | Network services | Generic processes |
Both features can be used together for comprehensive conflict detection.
## Best Practices
1. **Use descriptive filters**: Choose patterns that are specific enough
- Good: `process_filter = "nginx"` or `"postgres"`
- Bad: `process_filter = "python"` (too generic)
2. **Combine with kill_others**: For services that need clean startup
```toml
process_filter = "myapp"
kill_others = true
```
3. **Test before deployment**: Verify the filter matches intended processes
```bash
ps aux | grep "pattern"
```
4. **Monitor logs**: Check for unexpected kills
```bash
journalctl -u zinit -f | grep "process"
```
5. **Use with critical services**: Helps ensure critical services stay running
```toml
[service]
critical = true
process_filter = "my_critical_service"
kill_others = true
```
## Testing
Example Rhai script to test the feature with multiple filters:
```rhai
// Test 1: Create service with multiple filters (without kill_others)
print("Test 1: Conflict detection...");
try {
service("test_detector")
.exec("/bin/sleep 300")
.process_filter("sleep")
.process_filter("rest") // Multiple filters
.start(); // Should be blocked if sleep processes exist
print("Started (no conflicts)");
} catch(err) {
print("Blocked: " + err);
// Check details
let why = z.why("test_detector");
if why.process_conflict {
print(" Filter: " + why.process_conflict.filter);
print(" Processes: " + why.process_conflict.processes.len());
}
}
// Test 2: Enable kill_others and try again
print("\nTest 2: Auto-cleanup with kill_others...");
try {
service("test_app")
.exec("/bin/sleep 300")
.process_filter("sleep")
.process_filter("rest") // Multiple filters
.kill_others() // Kill matching processes first
.start(); // Should start now - processes killed
print("Started successfully");
let status = z.status("test_app");
print(" State: " + status.state);
print(" PID: " + status.pid);
} catch(err) {
print("Error: " + err);
}
// Clean up
z.stop("test_app");
z.service_delete("test_app");
z.service_delete("test_detector");
```
## Troubleshooting
### Service Still Blocked After kill_others
1. Verify the filter pattern:
```bash
ps aux | grep "pattern"
```
2. Check logs for kill errors:
```bash
RUST_LOG=debug zinit serve
```
3. Ensure kill_others is actually enabled in config
### Filter Not Matching Expected Processes
1. Remember: matching is case-insensitive but must be exact substring
2. Check full command: `ps aux` vs `ps -A -o comm=`
3. May need to use exact process name, not full path
### Performance Issues
1. Very broad filters (like "python") may match many processes
2. Consider more specific patterns
3. Check logs for enumeration time