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",
|
"arboard",
|
||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
|
"libc",
|
||||||
"notify",
|
"notify",
|
||||||
|
"open",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -1259,6 +1261,25 @@ version = "2.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
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]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -1755,6 +1776,17 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
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]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1809,6 +1841,12 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -1988,7 +2026,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
|||||||
# Error handling
|
# Error handling
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
# CLI utilities
|
||||||
|
open = "5"
|
||||||
|
|
||||||
# Web server (axum + tower)
|
# Web server (axum + tower)
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
@@ -57,6 +59,8 @@ windows-sys = { version = "0.59", features = [
|
|||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
# Notify pour watcher filesystem (Linux)
|
# Notify pour watcher filesystem (Linux)
|
||||||
notify = { version = "7", features = ["macos_kqueue"] }
|
notify = { version = "7", features = ["macos_kqueue"] }
|
||||||
|
# Signal handling (PID file, process checks)
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z" # Optimize for size
|
opt-level = "z" # Optimize for size
|
||||||
|
|||||||
+30
-3
@@ -72,7 +72,7 @@ fn default_exclude_apps() -> Vec<String> {
|
|||||||
vec!["mstsc".into()]
|
vec!["mstsc".into()]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DedupConfig {
|
pub struct DedupConfig {
|
||||||
#[serde(default = "default_dedup_method")]
|
#[serde(default = "default_dedup_method")]
|
||||||
pub method: String,
|
pub method: String,
|
||||||
@@ -80,6 +80,15 @@ pub struct DedupConfig {
|
|||||||
pub cache_size: usize,
|
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 {
|
fn default_dedup_method() -> String {
|
||||||
"sha256".into()
|
"sha256".into()
|
||||||
}
|
}
|
||||||
@@ -88,7 +97,7 @@ fn default_cache_size() -> usize {
|
|||||||
10000
|
10000
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UiConfig {
|
pub struct UiConfig {
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub show_notifications: bool,
|
pub show_notifications: bool,
|
||||||
@@ -98,16 +107,34 @@ pub struct UiConfig {
|
|||||||
pub notification_timeout_sec: u64,
|
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 {
|
fn default_notification_timeout() -> u64 {
|
||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoggingConfig {
|
pub struct LoggingConfig {
|
||||||
#[serde(default = "default_log_level")]
|
#[serde(default = "default_log_level")]
|
||||||
pub level: String,
|
pub level: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for LoggingConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
level: default_log_level(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn default_log_level() -> String {
|
fn default_log_level() -> String {
|
||||||
"info".into()
|
"info".into()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
//! Client HTTP pour l'API Imago
|
//! Client HTTP pour l'API Imago
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use reqwest::{Client, multipart};
|
use reqwest::{Client, multipart};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|||||||
+469
-22
@@ -5,10 +5,11 @@
|
|||||||
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
|
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
|
||||||
//! - `stop` : Arrête le service
|
//! - `stop` : Arrête le service
|
||||||
//! - `status`: Affiche l'état
|
//! - `status`: Affiche l'état
|
||||||
//! - `config`: Gère la configuration
|
//! - `config show/set/reset`: Gère la configuration
|
||||||
//! - `install` / `uninstall`: Service Windows
|
//! - `install` / `uninstall`: Service Windows
|
||||||
|
//! - `logs`: Affiche les derniers logs
|
||||||
|
|
||||||
#![allow(unused_imports, dead_code, unreachable_code)]
|
#![allow(dead_code, unreachable_code)]
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -27,9 +28,13 @@ mod window_info;
|
|||||||
|
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
use tracing_subscriber::{fmt, EnvFilter};
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
|
// ─── Logging ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn setup_logging() {
|
fn setup_logging() {
|
||||||
let filter = EnvFilter::try_from_default_env()
|
let filter = EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||||
@@ -40,38 +45,480 @@ fn setup_logging() {
|
|||||||
.init();
|
.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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() {
|
||||||
setup_logging();
|
setup_logging();
|
||||||
|
|
||||||
let args = cli::parse();
|
let args = cli::parse();
|
||||||
|
|
||||||
match args.command {
|
let result = match args.command {
|
||||||
cli::Command::Start { port: _, open_browser: _ } => {
|
cli::Command::Start { port, open_browser } => {
|
||||||
tracing::info!("Démarrage imagod-web — watcher + dashboard");
|
cmd_start(port, open_browser).await
|
||||||
todo!("Phase 6-7: Watcher + serveur web")
|
|
||||||
}
|
}
|
||||||
cli::Command::Serve { port: _, host: _, no_watcher: _, open_browser: _ } => {
|
cli::Command::Serve { port, host, no_watcher, open_browser } => {
|
||||||
tracing::info!("Démarrage dashboard web uniquement");
|
cmd_serve(&host, port, no_watcher, open_browser).await
|
||||||
todo!("Phase 9: Dashboard web")
|
|
||||||
}
|
}
|
||||||
cli::Command::Stop => {
|
cli::Command::Stop => {
|
||||||
todo!("Phase 7: Stop")
|
cmd_stop()
|
||||||
}
|
}
|
||||||
cli::Command::Status => {
|
cli::Command::Status => {
|
||||||
todo!("Phase 7: Status")
|
cmd_status()
|
||||||
}
|
}
|
||||||
cli::Command::Config { action: _ } => {
|
cli::Command::Config { action } => {
|
||||||
todo!("Phase 2: Config")
|
cmd_config(&action)
|
||||||
}
|
|
||||||
cli::Command::Install => {
|
|
||||||
todo!("Phase 10: Service install")
|
|
||||||
}
|
|
||||||
cli::Command::Uninstall => {
|
|
||||||
todo!("Phase 10: Service uninstall")
|
|
||||||
}
|
}
|
||||||
cli::Command::Logs => {
|
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
|
/// 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);
|
let _interval = Duration::from_millis(self.config.watcher.poll_interval_ms);
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
Reference in New Issue
Block a user