feat: implémentation réelle du service Windows via NSSM (install/uninstall)

This commit is contained in:
2026-06-26 08:47:46 -04:00
parent ce93740a5e
commit 05d13a7725
6 changed files with 175 additions and 13 deletions
Generated
+31
View File
@@ -536,6 +536,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -545,6 +551,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -1302,6 +1314,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tray-icon", "tray-icon",
"which",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -3246,6 +3259,18 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "which"
version = "7.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
dependencies = [
"either",
"env_home",
"rustix",
"winsafe",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -3510,6 +3535,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"
+2
View File
@@ -57,6 +57,8 @@ image = { version = "0.25", default-features = false, features = ["png"] }
arboard = "3" arboard = "3"
# System tray # System tray
tray-icon = "0.19" tray-icon = "0.19"
# Find executables in PATH (NSSM discovery)
which = "7"
# Windows API (pour titre fenêtre active) # Windows API (pour titre fenêtre active)
windows-sys = { version = "0.59", features = [ windows-sys = { version = "0.59", features = [
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
+3 -3
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog — imagod-web v0.2.0+build.29-9cabfb5-dirty</title> <title>Changelog — imagod-web v0.2.0+build.30-ce93740-dirty</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>">
<style> <style>
* { margin:0; padding:0; box-sizing:border-box; } * { margin:0; padding:0; box-sizing:border-box; }
@@ -37,7 +37,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
</head> </head>
<body> <body>
<header> <header>
<h1>📋 imagod-web <span class="version-badge">v0.2.0+build.29-9cabfb5-dirty</span></h1> <h1>📋 imagod-web <span class="version-badge">v0.2.0+build.30-ce93740-dirty</span></h1>
<nav> <nav>
<a href="/">🖼️ Galerie</a> <a href="/">🖼️ Galerie</a>
<a href="/changelog" class="active">📋 Changelog</a> <a href="/changelog" class="active">📋 Changelog</a>
@@ -107,6 +107,6 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
<h2>Légende des tags</h2> <h2>Légende des tags</h2>
</main> </main>
<footer>imagod-web v0.2.0+build.29-9cabfb5-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer> <footer>imagod-web v0.2.0+build.30-ce93740-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
</body> </body>
</html> </html>
+3 -3
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aide & Roadmap — imagod-web v0.2.0+build.29-9cabfb5-dirty</title> <title>Aide & Roadmap — imagod-web v0.2.0+build.30-ce93740-dirty</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>">
<style> <style>
* { margin:0; padding:0; box-sizing:border-box; } * { margin:0; padding:0; box-sizing:border-box; }
@@ -37,7 +37,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
</head> </head>
<body> <body>
<header> <header>
<h1>❓ imagod-web <span class="version-badge">v0.2.0+build.29-9cabfb5-dirty</span></h1> <h1>❓ imagod-web <span class="version-badge">v0.2.0+build.30-ce93740-dirty</span></h1>
<nav> <nav>
<a href="/">🖼️ Galerie</a> <a href="/">🖼️ Galerie</a>
<a href="/changelog">📋 Changelog</a> <a href="/changelog">📋 Changelog</a>
@@ -169,6 +169,6 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
<p>5. <strong>Corriger le backend Imago</strong> (thumbnails, upload, stockage)</p> <p>5. <strong>Corriger le backend Imago</strong> (thumbnails, upload, stockage)</p>
</main> </main>
<footer>imagod-web v0.2.0+build.29-9cabfb5-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer> <footer>imagod-web v0.2.0+build.30-ce93740-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
</body> </body>
</html> </html>
+1 -1
View File
@@ -465,7 +465,7 @@
<button class="btn" id="clear-btn" style="display:none">✕</button> <button class="btn" id="clear-btn" style="display:none">✕</button>
</div> </div>
<button class="btn accent" id="header-upload-btn" onclick="document.getElementById('drop-zone').scrollIntoView({behavior:'smooth'}); document.getElementById('file-input').click()">📤 Upload</button> <button class="btn accent" id="header-upload-btn" onclick="document.getElementById('drop-zone').scrollIntoView({behavior:'smooth'}); document.getElementById('file-input').click()">📤 Upload</button>
<span class="version-badge" id="version-badge">v0.2.0+build.29-9cabfb5-dirty</span> <span class="version-badge" id="version-badge">v0.2.0+build.30-ce93740-dirty</span>
</div> </div>
<!-- ─── Navigation ─── --> <!-- ─── Navigation ─── -->
+135 -6
View File
@@ -1,12 +1,114 @@
//! Service Windows — Installation via NSSM (stub) //! Service Windows — Installation via NSSM
//! Phase 10: Code NSSM complet activé sur build Windows natif. //!
//! `install` : crée un service Windows qui lance `imagod-web serve`
//! `uninstall` : arrête et supprime le service
//!
//! NSSM doit être présent dans le même dossier que imagod-web.exe
//! ou dans le PATH. Téléchargeable sur https://nssm.cc/download
use std::path::PathBuf;
use std::process::{Command, Stdio};
/// Trouve l'exécutable NSSM
fn find_nssm() -> Option<PathBuf> {
// 1. À côté de l'exécutable imagod-web.exe
if let Ok(exe) = std::env::current_exe() {
let nssm_next = exe.with_file_name("nssm.exe");
if nssm_next.exists() {
return Some(nssm_next);
}
}
// 2. Dans le PATH
which::which("nssm").ok()
}
/// Retourne le chemin de l'exécutable imagod-web
fn own_exe_path() -> anyhow::Result<String> {
Ok(std::env::current_exe()?
.to_string_lossy()
.to_string())
}
pub fn install() -> anyhow::Result<()> { pub fn install() -> anyhow::Result<()> {
if !cfg!(windows) { if !cfg!(windows) {
anyhow::bail!("L'installation en service est disponible uniquement sous Windows.\nCompilation Windows: cargo build --release --target x86_64-pc-windows-gnu"); anyhow::bail!("L'installation en service est disponible uniquement sous Windows");
} }
println!("Installation du service Windows imagod-web...");
println!("(Build Windows natif requis pour l'installation NSSM automatique)"); let nssm = find_nssm().ok_or_else(|| {
anyhow::anyhow!(
"NSSM introuvable.\n\n\
Pour installer le service Windows, téléchargez NSSM :\n \
https://nssm.cc/download\n\n\
Puis placez nssm.exe dans le même dossier que imagod-web.exe,\n\
ou ajoutez-le à votre PATH.\n\n\
Alternative manuelle :\n \
nssm install imagod-web \"C:\\chemin\\vers\\imagod-web.exe\" serve"
)
})?;
let exe_path = own_exe_path()?;
let service_name = "imagod-web";
println!("🔧 Installation du service Windows '{}'...", service_name);
println!(" Binaire : {}", exe_path);
println!(" NSSM : {}", nssm.display());
// Arrêter le service s'il tourne déjà
let _ = Command::new(&nssm)
.args(["stop", service_name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
// Installer le service
let install_status = Command::new(&nssm)
.args(["install", service_name, &exe_path])
.status()?;
if !install_status.success() {
anyhow::bail!("Échec de l'installation du service avec NSSM");
}
// Configurer le service
let settings = [
// Commande : "serve" (watcher + dashboard)
(["set", service_name, "AppParameters", "serve"], "Paramètres de l'application"),
// Démarrage automatique
(["set", service_name, "Start", "SERVICE_AUTO_START"], "Type de démarrage"),
// Redémarrage automatique en cas d'erreur
(["set", service_name, "AppExit", "Default", "Restart"], "Redémarrage auto"),
// Nom affiché
(["set", service_name, "DisplayName", "Imago Dashboard + Watcher"], "Nom affiché"),
// Description
(["set", service_name, "Description", "Dashboard web Imago + surveillance du presse-papier"], "Description"),
];
for (args, label) in &settings {
let status = Command::new(&nssm).args(args).status()?;
if !status.success() {
println!("⚠️ Attention : échec configuration '{}'", label);
}
}
// Démarrer le service
println!("🚀 Démarrage du service...");
let start_status = Command::new(&nssm)
.args(["start", service_name])
.status()?;
if start_status.success() {
println!("✅ Service '{}' installé et démarré !", service_name);
println!(" Dashboard : http://127.0.0.1:9199");
println!("\n Commandes utiles :");
println!(" nssm status imagod-web — Voir l'état");
println!(" nssm restart imagod-web — Redémarrer");
println!(" nssm stop imagod-web — Arrêter");
println!(" imagod-web uninstall — Désinstaller");
} else {
println!("⚠️ Service installé mais le démarrage a échoué.");
println!(" Vérifiez avec : nssm status imagod-web");
}
Ok(()) Ok(())
} }
@@ -14,6 +116,33 @@ pub fn uninstall() -> anyhow::Result<()> {
if !cfg!(windows) { if !cfg!(windows) {
anyhow::bail!("Désinstallation disponible uniquement sous Windows"); anyhow::bail!("Désinstallation disponible uniquement sous Windows");
} }
println!("Désinstallation du service...");
let nssm = find_nssm().ok_or_else(|| {
anyhow::anyhow!(
"NSSM introuvable.\n\
Désinstallation manuelle : nssm remove imagod-web confirm"
)
})?;
let service_name = "imagod-web";
println!("🛑 Arrêt du service '{}'...", service_name);
let _ = Command::new(&nssm)
.args(["stop", service_name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
println!("🗑️ Suppression du service...");
let status = Command::new(&nssm)
.args(["remove", service_name, "confirm"])
.status()?;
if status.success() {
println!("✅ Service '{}' désinstallé.", service_name);
} else {
anyhow::bail!("Échec de la désinstallation. Essayez manuellement : nssm remove imagod-web confirm");
}
Ok(()) Ok(())
} }