This guide explains how to add a new service to the modular service automation system.
Services are modular diagnostic/maintenance tasks that can be:
- Combined into presets (Diagnostics, General, Complete, Custom)
- Individually enabled/disabled by users
- Configured with custom options
- Run in sequence or in parallel (experimental) with live log output
- Have custom results renderers (for findings view and customer print)
The service system uses a modular architecture:
src-tauri/src/services/ # Backend: One file per service
├── mod.rs # Service trait & registry
├── ping_test.rs # Ping test service
└── [new_service].rs # Your new service
src/components/service-renderers/ # Frontend: Custom renderers
├── index.ts # Renderer registry
├── PingTestRenderer.tsx # Ping test renderer
└── [NewService]Renderer.tsx # Your new renderer
Create a new file in src-tauri/src/services/:
// src-tauri/src/services/my_service.rs
use std::time::Instant;
use chrono::Utc;
use serde_json::json;
use tauri::{AppHandle, Emitter};
use crate::services::Service;
use crate::types::{
FindingSeverity, ServiceDefinition, ServiceFinding, ServiceOptionSchema, ServiceResult,
};
pub struct MyService;
impl Service for MyService {
fn definition(&self) -> ServiceDefinition {
ServiceDefinition {
id: "my-service".to_string(),
name: "My Service".to_string(),
description: "What this service does".to_string(),
category: "diagnostics".to_string(), // or "cleanup", "security", etc.
estimated_duration_secs: 30,
required_programs: vec![], // IDs from REQUIRED_PROGRAMS registry (e.g., "bleachbit")
options: vec![
ServiceOptionSchema {
id: "option_name".to_string(),
label: "Option Label".to_string(),
option_type: "number".to_string(), // or "string", "boolean", "select"
default_value: json!(10),
min: Some(1.0),
max: Some(100.0),
options: None,
description: Some("Help text".to_string()),
},
],
icon: "icon-name".to_string(), // lucide icon name
exclusive_resources: vec![], // Resource tags for parallel mode (see below)
}
}
fn run(&self, options: &serde_json::Value, app: &AppHandle) -> ServiceResult {
let start = Instant::now();
let mut logs: Vec<String> = Vec::new();
let mut findings: Vec<ServiceFinding> = Vec::new();
let service_id = "my-service";
// Emit log helper
let emit_log = |log: &str, logs: &mut Vec<String>, app: &AppHandle| {
logs.push(log.to_string());
let _ = app.emit(
"service-log",
json!({
"serviceId": service_id,
"log": log,
"timestamp": Utc::now().to_rfc3339()
}),
);
};
emit_log("Starting my service...", &mut logs, app);
// Do the work here...
// Add findings with optional data for custom renderer
findings.push(ServiceFinding {
severity: FindingSeverity::Success,
title: "Result Title".to_string(),
description: "Detailed description".to_string(),
recommendation: None,
data: Some(json!({
"type": "my_finding_type", // Used by custom renderer
"value": 42
})),
});
emit_log("Service complete", &mut logs, app);
ServiceResult {
service_id: service_id.to_string(),
success: true,
error: None,
duration_ms: start.elapsed().as_millis() as u64,
findings,
logs,
}
}
}Edit src-tauri/src/services/mod.rs:
mod my_service; // Add module declaration
// In SERVICE_REGISTRY LazyLock:
static SERVICE_REGISTRY: LazyLock<HashMap<String, Box<dyn Service>>> = LazyLock::new(|| {
let services: Vec<Box<dyn Service>> = vec![
Box::new(ping_test::PingTestService),
Box::new(my_service::MyService), // Add your service
];
// ...
});In src-tauri/src/services/mod.rs, update get_all_presets():
ServicePreset {
id: "diagnostics".to_string(),
// ...
services: vec![
// existing services...
PresetServiceConfig {
service_id: "my-service".to_string(),
enabled: true,
options: json!({"option_name": 10}),
},
],
}Edit src/pages/ServicePage.tsx:
import { MyIcon } from 'lucide-react';
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
// existing icons...
'my-icon': MyIcon,
};For enhanced results display, create a custom renderer:
// src/components/service-renderers/MyServiceRenderer.tsx
import type { ServiceRendererProps } from './index';
function FindingsRenderer({ result, definition }: ServiceRendererProps) {
// Extract custom data from findings
const finding = result.findings.find(
(f) => (f.data as any)?.type === 'my_finding_type'
);
const data = finding?.data as { value: number } | undefined;
return (
<div className="p-4 rounded-lg bg-muted/50 border">
<h3>{definition.name}</h3>
{data && <p className="text-2xl font-bold">{data.value}</p>}
</div>
);
}
function CustomerRenderer({ result }: ServiceRendererProps) {
// Simplified view for customer print
return (
<div className="p-4 border border-gray-200 rounded-lg bg-white">
<p className="font-bold">{result.success ? '✓ Passed' : '✗ Failed'}</p>
</div>
);
}
export function MyServiceRenderer(props: ServiceRendererProps) {
if (props.variant === 'customer') {
return <CustomerRenderer {...props} />;
}
return <FindingsRenderer {...props} />;
}Edit src/components/service-renderers/index.ts:
import { MyServiceRenderer } from './MyServiceRenderer';
export const SERVICE_RENDERERS: Partial<Record<string, ServiceRenderer>> = {
'ping-test': PingTestRenderer,
'my-service': MyServiceRenderer, // Add your renderer
};| Category | Description |
|---|---|
diagnostics |
System health checks, tests |
cleanup |
Junk file removal, optimization |
security |
Malware/adware scanning |
maintenance |
Updates, repairs |
| Severity | Use For |
|---|---|
Info |
Neutral information |
Success |
Passed checks, good results |
Warning |
Minor issues, recommendations |
Error |
Problems that need attention |
Critical |
Severe issues requiring immediate action |
If your service requires an external program (like BleachBit or AdwCleaner), you need to:
Edit src-tauri/src/commands/required_programs.rs and add an entry to REQUIRED_PROGRAMS:
RequiredProgramDef {
id: "my-program".to_string(), // Stable ID used in service definitions
name: "My Program".to_string(), // Display name
description: "What this program does".to_string(),
exe_names: vec![ // Executable names to auto-detect
"myprogram.exe".to_string(),
"myprogram64.exe".to_string(), // Include variants
],
url: Some("https://example.com/".to_string()), // Download link (optional)
},In your service's definition() method, add the program ID to required_programs:
required_programs: vec!["my-program".to_string()],Get the executable path via the command:
use crate::commands::required_programs::get_program_exe_path;
// In your run() method:
let exe_path = get_program_exe_path("my-program".to_string())?
.ok_or("my-program executable not found")?;
// Use exe_path to run the program
std::process::Command::new(&exe_path)
.args(["--your", "--args"])
.output()?;- Programs folder scan: The system searches
data/programs/recursively for any exe matching theexe_nameslist - User override: Users can set a custom path in Settings → Programs if auto-detection fails
- Validation: Before running services, the system validates all required programs are available
Users see required programs in Settings → Programs:
- Added: Program auto-detected or custom path set ✓
- Missing: Program not found, with download link if available
If a service run is blocked due to missing programs, a dialog shows what's needed and links to Settings.
- Run
pnpm tauri dev - Navigate to Service tab
- Select a preset that includes your service
- Verify it appears in the queue
- Run the service and check logs/findings
- If you added a custom renderer, verify it displays correctly
Services can optionally run in parallel. When the user enables the Parallel toggle in the queue view, services without conflicting resource tags execute concurrently, while services sharing any resource tag are serialized.
Each ServiceDefinition has an exclusive_resources: Vec<String> field that declares which shared resources this service requires exclusive access to. Services with overlapping tags will never run at the same time.
Resource tags in use:
| Tag | Description | Services |
|---|---|---|
network-bandwidth |
Services that measure network speed | speedtest, iperf |
cpu-stress |
CPU/GPU stress tests and benchmarks | heavyload, winsat, furmark |
disk-exclusive |
Services that lock a disk volume | chkdsk |
disk-heavy |
Heavy disk I/O (repairs, cleanups) | dism, sfc, bleachbit, drivecleanup |
filesystem-scan |
Full filesystem scans (virus/malware) | kvrt-scan, adwcleaner, stinger |
Services with an empty exclusive_resources vec (e.g., ping-test, battery-info, disk-space, network-config) can run in parallel with anything.
- The scheduler maintains a set of currently-held resource tags
- For each unstarted service, it checks if any of its
exclusive_resourcesoverlap with held resources - If no conflict, the service starts on a new thread and its resources are marked as held
- When a service completes, its resources are released, potentially unblocking waiting services
- Services with no resource tags can always start immediately
When adding a new service, consider:
- Does it heavily use the network? → Add
"network-bandwidth" - Does it stress the CPU or GPU? → Add
"cpu-stress" - Does it lock a volume/drive? → Add
"disk-exclusive" - Does it do heavy disk I/O? → Add
"disk-heavy" - Does it scan the entire filesystem? → Add
"filesystem-scan" - Is it a lightweight info-gathering tool? → Use
vec![]
You can also create a new tag if needed. Any string works as a resource tag — services sharing the same tag will be serialized.