feat: Phase 7 — CLI complet (start/serve/stop/status/config/logs)
- Gestion PID (write/read/remove + verification processus) - cmd_start: lance watcher + dashboard avec Ctrl+C graceful shutdown - cmd_serve: dashboard seul (Linux) avec --no-watcher - cmd_stop: kill processus via SIGTERM/OpenProcess + nettoyage PID - cmd_status: affiche PID, dashboard URL, config, polling - cmd_config show/set/reset: gestion dotted keys (imago.url, watcher.*, etc.) - cmd_logs: affiche derniers logs - Fix Default derives: ImagoConfig, DedupConfig, UiConfig, LoggingConfig - Ajout dependances: open, libc (Unix) - Watcher::run(self) au lieu de &self pour Send - tests: start → API /api/health OK → stop → status OK - 0 errors, 0 warnings (cargo check + cargo build --release)
This commit is contained in:
Generated
+39
-1
@@ -1196,7 +1196,9 @@ dependencies = [
|
||||
"arboard",
|
||||
"axum",
|
||||
"clap",
|
||||
"libc",
|
||||
"notify",
|
||||
"open",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"rust-embed",
|
||||
@@ -1259,6 +1261,25 @@ version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -1755,6 +1776,17 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -1809,6 +1841,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1988,7 +2026,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -31,6 +31,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
# CLI utilities
|
||||
open = "5"
|
||||
|
||||
# Web server (axum + tower)
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
@@ -57,6 +59,8 @@ windows-sys = { version = "0.59", features = [
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# Notify pour watcher filesystem (Linux)
|
||||
notify = { version = "7", features = ["macos_kqueue"] }
|
||||
# Signal handling (PID file, process checks)
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z" # Optimize for size
|
||||
|
||||
+30
-3
@@ -72,7 +72,7 @@ fn default_exclude_apps() -> Vec<String> {
|
||||
vec!["mstsc".into()]
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DedupConfig {
|
||||
#[serde(default = "default_dedup_method")]
|
||||
pub method: String,
|
||||
@@ -80,6 +80,15 @@ pub struct DedupConfig {
|
||||
pub cache_size: usize,
|
||||
}
|
||||
|
||||
impl Default for DedupConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
method: default_dedup_method(),
|
||||
cache_size: default_cache_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_dedup_method() -> String {
|
||||
"sha256".into()
|
||||
}
|
||||
@@ -88,7 +97,7 @@ fn default_cache_size() -> usize {
|
||||
10000
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub show_notifications: bool,
|
||||
@@ -98,16 +107,34 @@ pub struct UiConfig {
|
||||
pub notification_timeout_sec: u64,
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_notifications: true,
|
||||
show_tray_icon: true,
|
||||
notification_timeout_sec: default_notification_timeout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_notification_timeout() -> u64 {
|
||||
3
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
#[serde(default = "default_log_level")]
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
"info".into()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//! Client HTTP pour l'API Imago
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use reqwest::{Client, multipart};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
+469
-22
@@ -5,10 +5,11 @@
|
||||
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
|
||||
//! - `stop` : Arrête le service
|
||||
//! - `status`: Affiche l'état
|
||||
//! - `config`: Gère la configuration
|
||||
//! - `config show/set/reset`: Gère la configuration
|
||||
//! - `install` / `uninstall`: Service Windows
|
||||
//! - `logs`: Affiche les derniers logs
|
||||
|
||||
#![allow(unused_imports, dead_code, unreachable_code)]
|
||||
#![allow(dead_code, unreachable_code)]
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
@@ -27,9 +28,13 @@ mod window_info;
|
||||
|
||||
mod server;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
// ─── Logging ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn setup_logging() {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
@@ -40,38 +45,480 @@ fn setup_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
// ─── PID File ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Écrit le PID du processus courant dans le fichier PID
|
||||
fn write_pid() -> Result<()> {
|
||||
let pid_path = pid_path()?;
|
||||
if let Some(parent) = pid_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let pid = std::process::id();
|
||||
std::fs::write(&pid_path, pid.to_string())?;
|
||||
tracing::info!("PID {} écrit dans {}", pid, pid_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Supprime le fichier PID
|
||||
fn remove_pid() -> Result<()> {
|
||||
let pid_path = pid_path()?;
|
||||
if pid_path.exists() {
|
||||
std::fs::remove_file(&pid_path)?;
|
||||
tracing::info!("Fichier PID supprimé : {}", pid_path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lit le PID depuis le fichier PID
|
||||
fn read_pid() -> Result<u32> {
|
||||
let pid_path = pid_path()?;
|
||||
let content = std::fs::read_to_string(&pid_path)
|
||||
.with_context(|| format!("Aucun fichier PID trouvé à {}. imagod-web n'est probablement pas lancé.", pid_path.display()))?;
|
||||
let pid: u32 = content
|
||||
.trim()
|
||||
.parse()
|
||||
.with_context(|| format!("PID invalide dans {}", pid_path.display()))?;
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
/// Vérifie si un processus avec ce PID existe
|
||||
fn process_exists(pid: u32) -> bool {
|
||||
// Sur Unix, envoyer le signal 0 vérifie l'existence
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::kill(pid as i32, 0) == 0
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Windows : on utilise OpenProcess
|
||||
use std::ffi::c_void;
|
||||
extern "system" {
|
||||
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> *mut c_void;
|
||||
fn CloseHandle(handle: *mut c_void) -> i32;
|
||||
}
|
||||
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
|
||||
unsafe {
|
||||
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
||||
if handle.is_null() {
|
||||
false
|
||||
} else {
|
||||
CloseHandle(handle);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chemin du fichier PID
|
||||
fn pid_path() -> Result<PathBuf> {
|
||||
let base = if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").context("Variable APPDATA non définie")?;
|
||||
PathBuf::from(appdata)
|
||||
} else {
|
||||
let home = std::env::var("HOME").context("Variable HOME non définie")?;
|
||||
PathBuf::from(home).join(".config")
|
||||
};
|
||||
Ok(base.join("imagod-web").join("imagod.pid"))
|
||||
}
|
||||
|
||||
/// Vérifie si le serveur est en cours d'exécution
|
||||
fn is_running() -> Option<u32> {
|
||||
read_pid().ok().filter(|&pid| process_exists(pid))
|
||||
}
|
||||
|
||||
fn data_dir() -> Result<PathBuf> {
|
||||
let base = if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").context("APPDATA")?;
|
||||
PathBuf::from(appdata)
|
||||
} else {
|
||||
let home = std::env::var("HOME").context("HOME")?;
|
||||
PathBuf::from(home).join(".config")
|
||||
};
|
||||
Ok(base.join("imagod-web"))
|
||||
}
|
||||
|
||||
// ─── Command Handlers ──────────────────────────────────────────────────────
|
||||
|
||||
/// `start` — Lance le watcher + dashboard web
|
||||
async fn cmd_start(port: u16, open_browser: bool) -> Result<()> {
|
||||
// Vérifier qu'il n'y a pas déjà une instance
|
||||
if let Some(pid) = is_running() {
|
||||
anyhow::bail!(
|
||||
"imagod-web est déjà en cours d'exécution (PID {}).\n\
|
||||
Utilisez `imagod-web stop` pour l'arrêter, ou `imagod-web status` pour voir son état.",
|
||||
pid
|
||||
);
|
||||
}
|
||||
|
||||
// Charger la configuration
|
||||
let cfg = config::Config::load()?;
|
||||
|
||||
// Écrire le PID
|
||||
write_pid()?;
|
||||
|
||||
tracing::info!(
|
||||
"🚀 imagod-web v{} — Démarrage watcher + dashboard",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
tracing::info!(" Dashboard : http://127.0.0.1:{}", port);
|
||||
tracing::info!(" Polling : {}ms", cfg.watcher.poll_interval_ms);
|
||||
tracing::info!(" Imago API : {}", cfg.imago.url);
|
||||
|
||||
// Lancer le watcher dans une tâche séparée
|
||||
let _watcher_task = {
|
||||
let cfg = cfg.clone();
|
||||
tokio::spawn(async move {
|
||||
match watcher::Watcher::new(cfg).await {
|
||||
Ok(w) => {
|
||||
if let Err(e) = w.run().await {
|
||||
tracing::error!("Erreur watcher : {:#}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Échec création watcher : {:#}", e),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// Ouvrir le navigateur si demandé
|
||||
if open_browser {
|
||||
if let Err(e) = open::that(format!("http://127.0.0.1:{}", port)) {
|
||||
tracing::warn!("Impossible d'ouvrir le navigateur : {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Lancer le serveur web (bloquant, graceful shutdown sur Ctrl+C)
|
||||
let server_task = tokio::spawn(server::serve("127.0.0.1", port));
|
||||
|
||||
// Attendre Ctrl+C
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
tracing::info!("⏹️ Ctrl+C reçu — arrêt propre...");
|
||||
}
|
||||
res = server_task => {
|
||||
if let Err(e) = res {
|
||||
tracing::error!("Erreur serveur : {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nettoyage
|
||||
remove_pid()?;
|
||||
tracing::info!("✅ imagod-web arrêté.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `serve` — Lance le dashboard web uniquement
|
||||
async fn cmd_serve(host: &str, port: u16, no_watcher: bool, open_browser: bool) -> Result<()> {
|
||||
// Vérifier qu'il n'y a pas déjà une instance
|
||||
if let Some(pid) = is_running() {
|
||||
anyhow::bail!(
|
||||
"imagod-web est déjà en cours d'exécution (PID {}).\n\
|
||||
Utilisez `imagod-web stop` pour l'arrêter.",
|
||||
pid
|
||||
);
|
||||
}
|
||||
|
||||
let cfg = config::Config::load()?;
|
||||
|
||||
// Écrire le PID
|
||||
write_pid()?;
|
||||
|
||||
tracing::info!(
|
||||
"🚀 imagod-web v{} — Dashboard web uniquement",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
tracing::info!(" Dashboard : http://{}:{}", host, port);
|
||||
|
||||
// Optionnellement lancer le watcher
|
||||
let _watcher_task = if !no_watcher {
|
||||
// Sur Linux, le clipboard n'est pas supporté — avertir mais continuer
|
||||
if !cfg!(target_os = "windows") {
|
||||
tracing::warn!("⚠️ Le watcher clipboard n'est disponible que sur Windows.");
|
||||
tracing::warn!(" Utilisez --no-watcher pour désactiver ce message.");
|
||||
}
|
||||
let cfg = cfg.clone();
|
||||
Some(tokio::spawn(async move {
|
||||
match watcher::Watcher::new(cfg).await {
|
||||
Ok(w) => {
|
||||
if let Err(e) = w.run().await {
|
||||
tracing::error!("Erreur watcher : {:#}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Échec création watcher : {:#}", e),
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Ouvrir le navigateur si demandé
|
||||
if open_browser {
|
||||
if let Err(e) = open::that(format!("http://{}:{}", host, port)) {
|
||||
tracing::warn!("Impossible d'ouvrir le navigateur : {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Lancer le serveur web
|
||||
let host = host.to_string();
|
||||
let server_task = tokio::spawn(async move { server::serve(&host, port).await });
|
||||
|
||||
// Attendre Ctrl+C
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
tracing::info!("⏹️ Ctrl+C reçu — arrêt propre...");
|
||||
}
|
||||
res = server_task => {
|
||||
if let Err(e) = res {
|
||||
tracing::error!("Erreur serveur : {:#}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove_pid()?;
|
||||
tracing::info!("✅ imagod-web arrêté.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `stop` — Arrête le service
|
||||
fn cmd_stop() -> Result<()> {
|
||||
let pid = read_pid().context(
|
||||
"imagod-web n'est pas en cours d'exécution. Aucun fichier PID trouvé."
|
||||
)?;
|
||||
|
||||
if !process_exists(pid) {
|
||||
tracing::warn!("PID {} trouvé mais le processus n'existe pas. Nettoyage du fichier PID...", pid);
|
||||
remove_pid()?;
|
||||
anyhow::bail!(
|
||||
"Le processus imagod-web (PID {}) n'existe plus.\n\
|
||||
Le fichier PID a été nettoyé. Vous pouvez relancer avec `imagod-web start`.",
|
||||
pid
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!("Arrêt du processus imagod-web (PID {})...", pid);
|
||||
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::kill(pid as i32, libc::SIGTERM);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::ffi::c_void;
|
||||
extern "system" {
|
||||
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> *mut c_void;
|
||||
fn TerminateProcess(handle: *mut c_void, exit_code: u32) -> i32;
|
||||
fn CloseHandle(handle: *mut c_void) -> i32;
|
||||
}
|
||||
const PROCESS_TERMINATE: u32 = 0x0001;
|
||||
unsafe {
|
||||
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||
if handle.is_null() {
|
||||
anyhow::bail!("Impossible d'ouvrir le processus PID {} pour l'arrêter.", pid);
|
||||
}
|
||||
TerminateProcess(handle, 0);
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Attendre un peu que le processus se termine et nettoie son PID
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
||||
// Nettoyer le PID si le processus d'origine ne l'a pas fait
|
||||
if process_exists(pid) {
|
||||
tracing::warn!("Le processus PID {} ne répond pas au signal. Utilisez le gestionnaire de tâches.", pid);
|
||||
} else {
|
||||
remove_pid().ok();
|
||||
tracing::info!("✅ imagod-web arrêté (PID {}).", pid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `status` — Affiche l'état du service
|
||||
fn cmd_status() -> Result<()> {
|
||||
let pid = match read_pid() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
println!("📴 imagod-web n'est pas en cours d'exécution.");
|
||||
println!();
|
||||
println!(" Lancez avec : imagod-web start");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let alive = process_exists(pid);
|
||||
|
||||
if alive {
|
||||
println!("🟢 imagod-web est en cours d'exécution.");
|
||||
println!(" PID : {}", pid);
|
||||
println!(" Dashboard : http://127.0.0.1:9090");
|
||||
|
||||
// Essayer de charger la config pour afficher plus d'infos
|
||||
if let Ok(cfg) = config::Config::load() {
|
||||
println!(" Imago API : {}", cfg.imago.url);
|
||||
println!(" Polling : {}ms", cfg.watcher.poll_interval_ms);
|
||||
}
|
||||
} else {
|
||||
println!("🔴 Le fichier PID indique {} mais le processus est mort.", pid);
|
||||
println!(" Nettoyage automatique du fichier PID...");
|
||||
remove_pid()?;
|
||||
println!(" Vous pouvez relancer avec : imagod-web start");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `config` — Gère la configuration
|
||||
fn cmd_config(action: &cli::ConfigAction) -> Result<()> {
|
||||
match action {
|
||||
cli::ConfigAction::Show => {
|
||||
let cfg = config::Config::load()?;
|
||||
let toml_str = toml::to_string_pretty(&cfg)?;
|
||||
println!("{}", toml_str);
|
||||
}
|
||||
cli::ConfigAction::Set { key, value } => {
|
||||
let mut cfg = config::Config::load()?;
|
||||
|
||||
// Parse le chemin dotted (ex: "imago.url")
|
||||
match key.as_str() {
|
||||
"imago.url" => cfg.imago.url = value.clone(),
|
||||
"imago.api_key" => cfg.imago.api_key = value.clone(),
|
||||
"watcher.capture_images" => cfg.watcher.capture_images = value.parse()
|
||||
.context("Valeur booléenne attendue (true/false)")?,
|
||||
"watcher.poll_interval_ms" => cfg.watcher.poll_interval_ms = value.parse()
|
||||
.context("Valeur numérique attendue (ms)")?,
|
||||
"dedup.method" => cfg.dedup.method = value.clone(),
|
||||
"dedup.cache_size" => cfg.dedup.cache_size = value.parse()
|
||||
.context("Valeur numérique attendue")?,
|
||||
"ui.show_notifications" => cfg.ui.show_notifications = value.parse()
|
||||
.context("Valeur booléenne attendue")?,
|
||||
"ui.show_tray_icon" => cfg.ui.show_tray_icon = value.parse()
|
||||
.context("Valeur booléenne attendue")?,
|
||||
"ui.notification_timeout_sec" => cfg.ui.notification_timeout_sec = value.parse()
|
||||
.context("Valeur numérique attendue (secondes)")?,
|
||||
"logging.level" => cfg.logging.level = value.clone(),
|
||||
_ => anyhow::bail!(
|
||||
"Clé inconnue : '{}'.\n\
|
||||
Clés disponibles :\n \
|
||||
imago.url, imago.api_key,\n \
|
||||
watcher.capture_images, watcher.poll_interval_ms,\n \
|
||||
dedup.method, dedup.cache_size,\n \
|
||||
ui.show_notifications, ui.show_tray_icon, ui.notification_timeout_sec,\n \
|
||||
logging.level",
|
||||
key
|
||||
),
|
||||
}
|
||||
|
||||
cfg.save()?;
|
||||
println!("✅ {} = {}", key, value);
|
||||
|
||||
// Avertir que les changements prennent effet au prochain redémarrage
|
||||
if is_running().is_some() {
|
||||
println!("⚠️ imagod-web est en cours d'exécution. Les changements prendront effet au prochain redémarrage.");
|
||||
}
|
||||
}
|
||||
cli::ConfigAction::Reset => {
|
||||
let cfg = config::Config::default();
|
||||
cfg.save()?;
|
||||
println!("✅ Configuration réinitialisée aux valeurs par défaut.");
|
||||
if is_running().is_some() {
|
||||
println!("⚠️ imagod-web est en cours d'exécution. Les changements prendront effet au prochain redémarrage.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `logs` — Affiche les derniers logs
|
||||
fn cmd_logs() -> Result<()> {
|
||||
let log_path = data_dir()?.join("imagod.log");
|
||||
|
||||
if !log_path.exists() {
|
||||
// Essayer de trouver des logs dans le répertoire courant aussi
|
||||
let local_log = PathBuf::from("imagod.log");
|
||||
if local_log.exists() {
|
||||
print_log_file(&local_log)?;
|
||||
} else {
|
||||
println!("📭 Aucun fichier de log trouvé.");
|
||||
println!(" Les logs sont écrits sur stderr par défaut.");
|
||||
println!(" Fichier de log attendu : {}", log_path.display());
|
||||
}
|
||||
} else {
|
||||
print_log_file(&log_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_log_file(path: &PathBuf) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Impossible de lire {}", path.display()))?;
|
||||
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let total = lines.len();
|
||||
|
||||
// Afficher les 50 dernières lignes
|
||||
let start = if total > 50 { total - 50 } else { 0 };
|
||||
let recent = &lines[start..];
|
||||
|
||||
println!("📋 {} derniers logs ({} total) :", recent.len(), total);
|
||||
println!("{}", "─".repeat(60));
|
||||
for line in recent {
|
||||
println!("{}", line);
|
||||
}
|
||||
if total > 50 {
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("... {} lignes supplémentaires. Lancez `imagod-web logs` pour voir les dernières.", total - 50);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── Main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
async fn main() {
|
||||
setup_logging();
|
||||
|
||||
let args = cli::parse();
|
||||
|
||||
match args.command {
|
||||
cli::Command::Start { port: _, open_browser: _ } => {
|
||||
tracing::info!("Démarrage imagod-web — watcher + dashboard");
|
||||
todo!("Phase 6-7: Watcher + serveur web")
|
||||
let result = match args.command {
|
||||
cli::Command::Start { port, open_browser } => {
|
||||
cmd_start(port, open_browser).await
|
||||
}
|
||||
cli::Command::Serve { port: _, host: _, no_watcher: _, open_browser: _ } => {
|
||||
tracing::info!("Démarrage dashboard web uniquement");
|
||||
todo!("Phase 9: Dashboard web")
|
||||
cli::Command::Serve { port, host, no_watcher, open_browser } => {
|
||||
cmd_serve(&host, port, no_watcher, open_browser).await
|
||||
}
|
||||
cli::Command::Stop => {
|
||||
todo!("Phase 7: Stop")
|
||||
cmd_stop()
|
||||
}
|
||||
cli::Command::Status => {
|
||||
todo!("Phase 7: Status")
|
||||
cmd_status()
|
||||
}
|
||||
cli::Command::Config { action: _ } => {
|
||||
todo!("Phase 2: Config")
|
||||
}
|
||||
cli::Command::Install => {
|
||||
todo!("Phase 10: Service install")
|
||||
}
|
||||
cli::Command::Uninstall => {
|
||||
todo!("Phase 10: Service uninstall")
|
||||
cli::Command::Config { action } => {
|
||||
cmd_config(&action)
|
||||
}
|
||||
cli::Command::Logs => {
|
||||
todo!("Phase 7: Logs")
|
||||
cmd_logs()
|
||||
}
|
||||
cli::Command::Install => {
|
||||
tracing::info!("Installation du service Windows...");
|
||||
tracing::warn!("Phase 10: Installation du service — non implémentée");
|
||||
Ok(())
|
||||
}
|
||||
cli::Command::Uninstall => {
|
||||
tracing::info!("Désinstallation du service Windows...");
|
||||
tracing::warn!("Phase 10: Désinstallation du service — non implémentée");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("❌ Erreur : {:#}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ impl Watcher {
|
||||
}
|
||||
|
||||
/// Lance la boucle de polling
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let _interval = Duration::from_millis(self.config.watcher.poll_interval_ms);
|
||||
|
||||
tracing::info!(
|
||||
|
||||
Reference in New Issue
Block a user