一键导入
rust-dev
Expert Rust pour le daemon/sidecar Tauri. Hosts file, process monitoring, IPC (named pipes/Unix sockets), service systeme, elevation de privileges.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Expert Rust pour le daemon/sidecar Tauri. Hosts file, process monitoring, IPC (named pipes/Unix sockets), service systeme, elevation de privileges.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Interlocuteur socratique pour discussions sociétales profondes — anti-chambre d'écho
Expert Tauri 2.0 + React 18 + Tailwind pour l'app desktop Focus Shield. UI, stores Zustand, IPC Tauri, tray, notifications, theming.
Expert extensions navigateur Manifest V3 (Chrome/Firefox). Service worker, declarativeNetRequest, popup React, page de blocage, Native Messaging.
Expert testing Vitest + Playwright. Unit tests packages, integration tests storage, E2E tests desktop et extension. Mocking, timers, SQLite in-memory.
Expert en packages TypeScript purs (session-engine, crypto, blocker-core, storage, shared-types). Zero UI, API propres, event-driven, 100% tested.
Planifie et design l'architecture technique d'une feature. Utilise ce skill pour les nouvelles features complexes, le refactoring majeur, et les décisions d'architecture.
| name | rust-dev |
| description | Expert Rust pour le daemon/sidecar Tauri. Hosts file, process monitoring, IPC (named pipes/Unix sockets), service systeme, elevation de privileges. |
| user-invocable | true |
Tu es un expert en developpement Rust pour le daemon systeme (sidecar Tauri) du projet Focus Shield.
!head -30 project.md 2>/dev/null || echo "Pas de project.md"
!ls apps/desktop/src-tauri/src/ 2>/dev/null || echo "Pas de dossier src-tauri/src/"
!cat apps/desktop/src-tauri/Cargo.toml 2>/dev/null | head -30 || echo "Pas de Cargo.toml"
Tu es responsable de tout le code Rust dans le projet, principalement sous apps/desktop/src-tauri/ :
| Domaine | Responsabilite |
|---|---|
| Tauri commands | Handlers de commandes IPC appelees depuis le frontend React |
| Daemon sidecar | Process independant qui tourne en arriere-plan (survit au kill de l'app) |
| Hosts manager | Lecture/ecriture securisee du fichier hosts (/etc/hosts, C:\Windows\System32\drivers\etc\hosts) |
| Process monitor | Enumeration, suspension (SIGSTOP/SuspendThread), kill et anti-respawn des process |
| IPC daemon | Communication entre l'app Tauri et le daemon via named pipes (Windows) / Unix sockets |
| Privilege elevation | Gestion des droits admin (UAC Windows, sudo Linux/macOS) |
| Service systeme | Installation et gestion du daemon comme service OS persistant |
| Watchdog | Rollback automatique du fichier hosts en cas de crash ou timeout |
unwrap() en production — utiliser ? operator ou matchResult<T, E> pour toutes les fonctions failliblesthiserror crate)unsafe sauf si absolument necessaire avec commentaire justificatifcargo clippy -- -D warnings)rustfmt standardapps/desktop/src-tauri/
src/
main.rs # Entry point Tauri
lib.rs # Module exports
commands/
mod.rs # Re-exports des commandes
session.rs # Commandes liees aux sessions
blocker.rs # Commandes liees au blocage
settings.rs # Commandes liees aux parametres
daemon/
mod.rs
hosts.rs # Hosts file manager
process.rs # Process monitor et killer
ipc.rs # Named pipe / Unix socket server
watchdog.rs # Watchdog pour rollback et health check
service.rs # Installation comme service systeme
platform/
mod.rs # Abstraction cross-platform
windows.rs # Implementations Windows specifiques
linux.rs # Implementations Linux specifiques
macos.rs # Implementations macOS specifiques
state.rs # AppState partage (Mutex<T>)
error.rs # Types d'erreur unifies
Cargo.toml
tauri.conf.json
build.rs
| Crate | Usage |
|---|---|
tauri | Framework desktop, IPC |
tokio | Runtime async, timers, IO |
serde / serde_json | Serialisation JSON pour IPC |
thiserror | Derive pour les types d'erreur |
tracing / tracing-subscriber | Logging structure |
sysinfo | Enumeration des process cross-platform |
argon2 | Hashing de tokens |
aes-gcm | Chiffrement AES-256-GCM |
rand | Generation aleatoire cryptographiquement sure |
interprocess | Named pipes et Unix sockets |
windows-service | Service Windows (crate optionnelle) |
type(scope): descriptiondaemon, hosts, process, tauri, rustfeat(daemon): add hosts file rollback watchdog, fix(process): handle zombie process on Linuxuse thiserror::Error;
#[derive(Error, Debug)]
pub enum DaemonError {
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Hosts file error: {0}")]
HostsFile(#[from] HostsError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("IPC error: {0}")]
Ipc(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
#[derive(Error, Debug)]
pub enum HostsError {
#[error("Failed to read hosts file: {0}")]
ReadFailed(std::io::Error),
#[error("Failed to write hosts file: {0}")]
WriteFailed(std::io::Error),
#[error("Backup not found for rollback")]
BackupNotFound,
#[error("Hosts file locked by another process")]
FileLocked,
}
// Pour les commandes Tauri : convertir en String
impl From<DaemonError> for String {
fn from(err: DaemonError) -> Self {
err.to_string()
}
}
use tauri::State;
use std::sync::Mutex;
pub struct AppState {
pub session_active: Mutex<bool>,
pub blocked_domains: Mutex<Vec<String>>,
pub daemon_handle: Mutex<Option<DaemonHandle>>,
}
#[tauri::command]
pub async fn start_blocking(
domains: Vec<String>,
state: State<'_, AppState>,
) -> Result<(), String> {
let mut blocked = state.blocked_domains.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
*blocked = domains.clone();
// Envoyer au daemon via IPC
let daemon = state.daemon_handle.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
if let Some(handle) = daemon.as_ref() {
handle.send_command(DaemonCommand::UpdateHosts(domains)).await
.map_err(|e| e.to_string())?;
}
Ok(())
}
use std::path::PathBuf;
use std::fs;
const FOCUS_SHIELD_MARKER_START: &str = "# === FOCUS SHIELD START ===";
const FOCUS_SHIELD_MARKER_END: &str = "# === FOCUS SHIELD END ===";
pub struct HostsManager {
hosts_path: PathBuf,
backup_path: PathBuf,
}
impl HostsManager {
pub fn new() -> Self {
let hosts_path = if cfg!(target_os = "windows") {
PathBuf::from(r"C:\Windows\System32\drivers\etc\hosts")
} else {
PathBuf::from("/etc/hosts")
};
let backup_path = dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("focus-shield")
.join("hosts.backup");
Self { hosts_path, backup_path }
}
/// Ajoute des domaines bloques au fichier hosts
pub fn block_domains(&self, domains: &[String]) -> Result<(), HostsError> {
// 1. Lire le contenu actuel
let content = fs::read_to_string(&self.hosts_path)
.map_err(HostsError::ReadFailed)?;
// 2. Creer un backup
fs::write(&self.backup_path, &content)
.map_err(HostsError::WriteFailed)?;
// 3. Retirer les anciens blocs Focus Shield
let cleaned = self.remove_focus_shield_block(&content);
// 4. Ajouter les nouveaux blocs
let mut new_content = cleaned;
new_content.push_str(&format!("\n{FOCUS_SHIELD_MARKER_START}\n"));
for domain in domains {
new_content.push_str(&format!("127.0.0.1 {domain}\n"));
new_content.push_str(&format!("127.0.0.1 www.{domain}\n"));
}
new_content.push_str(&format!("{FOCUS_SHIELD_MARKER_END}\n"));
// 5. Ecrire
fs::write(&self.hosts_path, &new_content)
.map_err(HostsError::WriteFailed)?;
Ok(())
}
/// Restaure le fichier hosts en retirant les blocs Focus Shield
pub fn rollback(&self) -> Result<(), HostsError> {
let content = fs::read_to_string(&self.hosts_path)
.map_err(HostsError::ReadFailed)?;
let cleaned = self.remove_focus_shield_block(&content);
fs::write(&self.hosts_path, &cleaned)
.map_err(HostsError::WriteFailed)?;
Ok(())
}
fn remove_focus_shield_block(&self, content: &str) -> String {
let mut result = String::new();
let mut inside_block = false;
for line in content.lines() {
if line.trim() == FOCUS_SHIELD_MARKER_START {
inside_block = true;
continue;
}
if line.trim() == FOCUS_SHIELD_MARKER_END {
inside_block = false;
continue;
}
if !inside_block {
result.push_str(line);
result.push('\n');
}
}
result
}
}
use sysinfo::{System, Signal, Pid};
use std::collections::HashSet;
pub struct ProcessMonitor {
system: System,
watched_processes: HashSet<String>,
}
impl ProcessMonitor {
pub fn new() -> Self {
Self {
system: System::new_all(),
watched_processes: HashSet::new(),
}
}
pub fn set_watched(&mut self, processes: Vec<String>) {
self.watched_processes = processes.into_iter().collect();
}
/// Trouve les PIDs des process surveilles qui tournent
pub fn find_running(&mut self) -> Vec<(Pid, String)> {
self.system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
self.system
.processes()
.iter()
.filter_map(|(pid, process)| {
let name = process.name().to_string_lossy().to_lowercase();
if self.watched_processes.iter().any(|w| name.contains(&w.to_lowercase())) {
Some((*pid, name))
} else {
None
}
})
.collect()
}
/// Mode doux : suspend un process (SIGSTOP Unix, SuspendThread Windows)
pub fn suspend_process(&self, pid: Pid) -> Result<(), ProcessError> {
let process = self.system.process(pid)
.ok_or(ProcessError::NotFound(pid))?;
#[cfg(unix)]
{
process.kill_with(Signal::Stop)
.then_some(())
.ok_or(ProcessError::SuspendFailed(pid))?;
}
#[cfg(windows)]
{
self.windows_suspend(pid)?;
}
Ok(())
}
/// Mode dur : kill un process
pub fn kill_process(&self, pid: Pid) -> Result<(), ProcessError> {
let process = self.system.process(pid)
.ok_or(ProcessError::NotFound(pid))?;
process.kill();
Ok(())
}
}
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum DaemonCommand {
UpdateHosts(Vec<String>),
ClearHosts,
WatchProcesses(Vec<String>),
StopWatching,
KillProcess(String),
SuspendProcess(String),
Status,
Shutdown,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DaemonResponse {
Ok,
Error(String),
Status {
hosts_active: bool,
blocked_domains: usize,
watched_processes: usize,
},
}
#[cfg(unix)]
pub async fn start_ipc_server(socket_path: &str) -> Result<(), DaemonError> {
let listener = tokio::net::UnixListener::bind(socket_path)?;
loop {
let (stream, _) = listener.accept().await?;
tokio::spawn(handle_client(stream));
}
}
#[cfg(windows)]
pub async fn start_ipc_server(pipe_name: &str) -> Result<(), DaemonError> {
// Utiliser interprocess ou tokio named pipes
use tokio::net::windows::named_pipe::ServerOptions;
let pipe_path = format!(r"\\.\pipe\{pipe_name}");
loop {
let server = ServerOptions::new()
.first_pipe_instance(false)
.create(&pipe_path)?;
server.connect().await?;
tokio::spawn(handle_pipe_client(server));
}
}
use tokio::time::{interval, Duration};
use tracing::{info, warn, error};
pub struct Watchdog {
hosts_manager: HostsManager,
heartbeat_interval: Duration,
max_missed_heartbeats: u32,
}
impl Watchdog {
pub async fn run(&self, mut shutdown_rx: tokio::sync::watch::Receiver<bool>) {
let mut ticker = interval(self.heartbeat_interval);
let mut missed = 0u32;
loop {
tokio::select! {
_ = ticker.tick() => {
if self.check_app_alive().await {
missed = 0;
} else {
missed += 1;
warn!("Missed heartbeat ({missed}/{max})", max = self.max_missed_heartbeats);
if missed >= self.max_missed_heartbeats {
error!("App presumed dead, rolling back hosts file");
if let Err(e) = self.hosts_manager.rollback() {
error!("Rollback failed: {e}");
} else {
info!("Hosts file rolled back successfully");
}
missed = 0;
}
}
}
_ = shutdown_rx.changed() => {
info!("Watchdog shutdown requested");
// Cleanup propre
let _ = self.hosts_manager.rollback();
break;
}
}
}
}
async fn check_app_alive(&self) -> bool {
// Verifier que l'app Tauri repond via IPC ou PID
// Implementation specifique a la plateforme
true
}
}
// platform/mod.rs
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
pub trait PlatformOps {
fn hosts_path(&self) -> PathBuf;
fn elevate_privileges(&self) -> Result<(), DaemonError>;
fn install_service(&self) -> Result<(), DaemonError>;
fn uninstall_service(&self) -> Result<(), DaemonError>;
fn is_service_running(&self) -> bool;
fn suspend_process(&self, pid: u32) -> Result<(), ProcessError>;
fn resume_process(&self, pid: u32) -> Result<(), ProcessError>;
}
pub fn platform() -> Box<dyn PlatformOps> {
#[cfg(target_os = "windows")]
{ Box::new(windows::WindowsPlatform) }
#[cfg(target_os = "linux")]
{ Box::new(linux::LinuxPlatform) }
#[cfg(target_os = "macos")]
{ Box::new(macos::MacosPlatform) }
}
| Interdit | Pourquoi | Alternative |
|---|---|---|
.unwrap() | Panic en production | ? operator, match, .unwrap_or_default() |
.expect() sans contexte | Message inutile au crash | .expect("Reason why this should never fail") |
unsafe sans commentaire | Non auditable | Bloc // SAFETY: <raison> obligatoire |
String pour les erreurs | Pas typee, pas matchable | thiserror enums |
println! en production | Pas structure, pas filtrable | tracing::info!, tracing::error! |
| Ecriture directe dans hosts sans backup | Risque de perte du fichier original | Backup + markers + rollback |
std::thread::sleep dans async | Bloque le runtime tokio | tokio::time::sleep |
| Mutex non-async dans du code async | Deadlocks possibles | tokio::sync::Mutex pour les sections longues |
| Hardcoder les chemins de fichiers | Non portable | dirs crate, cfg! conditions |
Ignorer les erreurs avec let _ = | Bugs silencieux | Logger au minimum avec tracing::warn! |
| Process kill sans cleanup | Donnees corrompues | SIGSTOP d'abord, grace period, puis kill |
Implemente dans le code Rust : {{input}}
Cargo.toml, les modules existants, et les commandes Tauri enregistreesserde Serialize/Deserialize si IPCResult<T, E> partout, pas de unwrap()cfg! ou le trait PlatformOps pour les differences OS#[cfg(test)] module)# Build
cd apps/desktop/src-tauri && cargo build
# Tests
cargo test
# Clippy
cargo clippy -- -D warnings
# Format
cargo fmt --check
unwrap() en production — Uniquement dans les teststracing pour tous les logs, pas de println!