feat: Phase 10 — Service Windows NSSM + cross-compilation Windows
Phase 10 (service.rs): - install: télécharge NSSM, installe le service 'ImagodWeb' (auto-start, restart on crash) - uninstall: arrête et supprime le service - Service configuré avec: démarrage auto, redémarrage sur crash, délai 5s - Extraction ZIP NSSM intégrée (crate zip, deflate only) Phase 12 (cross-compilation Windows): - Cible x86_64-pc-windows-gnu configurée - Linker mingw-w64 - Corrections compatibilité: Cow→Vec dans clipboard.rs, API zip v2 - Binaire Windows .exe généré (8.1 MB, statique) Fix: compilation croisée Windows fonctionnelle
This commit is contained in:
Generated
+50
@@ -94,6 +94,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.6.1"
|
version = "3.6.1"
|
||||||
@@ -478,6 +487,17 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -1296,6 +1316,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3645,12 +3666,41 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"displaydoc",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ toml = "0.8"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
# SQLite
|
# SQLite
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
# ZIP (extraction NSSM) — deflate seulement pour éviter bzip2
|
||||||
|
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ pub fn get_image_bytes() -> Result<Vec<u8>> {
|
|||||||
let height = img_data.height as u32;
|
let height = img_data.height as u32;
|
||||||
|
|
||||||
// arboard retourne BGRA sur Windows, RGBA sur Linux/macOS
|
// arboard retourne BGRA sur Windows, RGBA sur Linux/macOS
|
||||||
// On convertit BGRA → RGBA en swapant les canaux B et R
|
// Linux/macOS: déjà RGBA — convertir Cow en Vec
|
||||||
let rgba_bytes: Vec<u8> = if cfg!(target_os = "windows") {
|
let rgba_bytes: Vec<u8> = if cfg!(target_os = "windows") {
|
||||||
img_data
|
img_data
|
||||||
.bytes
|
.bytes
|
||||||
@@ -38,7 +38,7 @@ pub fn get_image_bytes() -> Result<Vec<u8>> {
|
|||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
// Linux/macOS: déjà RGBA
|
// Linux/macOS: déjà RGBA
|
||||||
img_data.bytes
|
img_data.bytes.to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
let img = image::RgbaImage::from_raw(width, height, rgba_bytes)
|
let img = image::RgbaImage::from_raw(width, height, rgba_bytes)
|
||||||
|
|||||||
+2
-4
@@ -520,13 +520,11 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
cli::Command::Install => {
|
cli::Command::Install => {
|
||||||
tracing::info!("Installation du service Windows...");
|
tracing::info!("Installation du service Windows...");
|
||||||
tracing::warn!("Phase 10: Installation du service — non implémentée");
|
service::install()
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
cli::Command::Uninstall => {
|
cli::Command::Uninstall => {
|
||||||
tracing::info!("Désinstallation du service Windows...");
|
tracing::info!("Désinstallation du service Windows...");
|
||||||
tracing::warn!("Phase 10: Désinstallation du service — non implémentée");
|
service::uninstall()
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+261
-5
@@ -1,13 +1,269 @@
|
|||||||
//! Service Windows (NSSM / windows-service)
|
//! Service Windows — Installation via NSSM
|
||||||
//!
|
//!
|
||||||
//! Phase 10: Installation comme service Windows
|
//! Phase 10: Gestion du service Windows pour imagod-web.
|
||||||
|
//! Utilise NSSM (Non-Sucking Service Manager) pour enregistrer
|
||||||
|
//! imagod-web comme service Windows natif (démarrage auto, redémarrage sur crash).
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Chemin vers l'exécutable NSSM (téléchargé automatiquement si absent)
|
||||||
|
fn nssm_path() -> Result<PathBuf> {
|
||||||
|
let appdata = env::var("APPDATA").context("Variable APPDATA non définie — on est bien sous Windows?")?;
|
||||||
|
let dir = PathBuf::from(&appdata).join("imagod-web");
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
let nssm = dir.join("nssm.exe");
|
||||||
|
Ok(nssm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chemin vers l'exécutable imagod-web actuel
|
||||||
|
fn imagod_exe() -> Result<PathBuf> {
|
||||||
|
env::current_exe().context("Impossible de déterminer le chemin de l'exécutable")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Télécharge NSSM depuis le site officiel
|
||||||
|
fn download_nssm() -> Result<()> {
|
||||||
|
let nssm = nssm_path()?;
|
||||||
|
if nssm.exists() {
|
||||||
|
tracing::info!("NSSM déjà présent: {}", nssm.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = "https://nssm.cc/release/nssm-2.24.zip";
|
||||||
|
let zip_path = nssm.with_extension("zip");
|
||||||
|
|
||||||
|
tracing::info!("Téléchargement de NSSM depuis {}...", url);
|
||||||
|
|
||||||
|
// Télécharger avec reqwest (bloquant)
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
let response = rt.block_on(async {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(60))
|
||||||
|
.build()?
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
anyhow::bail!("Échec du téléchargement: HTTP {}", response.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = rt.block_on(response.bytes())?;
|
||||||
|
std::fs::write(&zip_path, &bytes)?;
|
||||||
|
tracing::info!("NSSM téléchargé: {} ({} octets)", zip_path.display(), bytes.len());
|
||||||
|
|
||||||
|
// Extraire nssm.exe du zip (le zip contient nssm-2.24/win64/nssm.exe)
|
||||||
|
let file = std::fs::File::open(&zip_path)?;
|
||||||
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
// Chercher nssm.exe dans l'archive (windows/win64)
|
||||||
|
let mut found = false;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut entry = archive.by_index(i)?;
|
||||||
|
let name = entry.name().to_lowercase();
|
||||||
|
if name.ends_with("nssm.exe") && !name.contains("nssm.exe/") {
|
||||||
|
// Extraire vers le répertoire cible
|
||||||
|
let mut out = std::fs::File::create(&nssm)?;
|
||||||
|
std::io::copy(&mut entry, &mut out)?;
|
||||||
|
found = true;
|
||||||
|
tracing::info!("NSSM extrait: {}", nssm.display());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
anyhow::bail!("nssm.exe introuvable dans l'archive ZIP");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le zip
|
||||||
|
let _ = std::fs::remove_file(&zip_path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installe imagod-web comme service Windows
|
||||||
|
///
|
||||||
|
/// Équivalent à:
|
||||||
|
/// nssm install ImagodWeb "C:\...\imagod-web.exe" serve --host 127.0.0.1 --port 9199
|
||||||
|
/// nssm set ImagodWeb Start SERVICE_AUTO_START
|
||||||
|
/// nssm set ImagodWeb AppExit Default Restart
|
||||||
|
/// nssm start ImagodWeb
|
||||||
pub fn install() -> Result<()> {
|
pub fn install() -> Result<()> {
|
||||||
todo!("Phase 10: Service install via NSSM")
|
if !cfg!(windows) {
|
||||||
|
anyhow::bail!("L'installation en service Windows n'est disponible que sous Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("=== Installation du service Windows imagod-web ===");
|
||||||
|
|
||||||
|
// 1. Télécharger NSSM si nécessaire
|
||||||
|
download_nssm().context("Échec du téléchargement de NSSM")?;
|
||||||
|
let nssm = nssm_path()?;
|
||||||
|
|
||||||
|
// 2. Vérifier que l'exécutable existe
|
||||||
|
let exe = imagod_exe()?;
|
||||||
|
tracing::info!("Exécutable: {}", exe.display());
|
||||||
|
|
||||||
|
// 3. Désinstaller d'abord si déjà présent (évite les conflits)
|
||||||
|
if nssm_installed()? {
|
||||||
|
tracing::info!("Service déjà installé, suppression préalable...");
|
||||||
|
let _ = uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Installer le service
|
||||||
|
let service_name = "ImagodWeb";
|
||||||
|
tracing::info!("Installation du service '{}'...", service_name);
|
||||||
|
|
||||||
|
let status = Command::new(&nssm)
|
||||||
|
.args([
|
||||||
|
"install",
|
||||||
|
service_name,
|
||||||
|
&exe.to_string_lossy(),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.context("Échec de la commande nssm install")?;
|
||||||
|
|
||||||
|
if !status.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&status.stderr);
|
||||||
|
anyhow::bail!("nssm install a échoué: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Configurer les arguments de lancement: serve --host 127.0.0.1 --port 9199
|
||||||
|
Command::new(&nssm)
|
||||||
|
.args(["set", service_name, "AppParameters", "serve --host 127.0.0.1 --port 9199"])
|
||||||
|
.output()
|
||||||
|
.context("Échec configuration AppParameters")?;
|
||||||
|
|
||||||
|
// 6. Démarrage automatique
|
||||||
|
Command::new(&nssm)
|
||||||
|
.args(["set", service_name, "Start", "SERVICE_AUTO_START"])
|
||||||
|
.output()
|
||||||
|
.context("Échec configuration Start")?;
|
||||||
|
|
||||||
|
// 7. Redémarrage automatique sur crash
|
||||||
|
Command::new(&nssm)
|
||||||
|
.args(["set", service_name, "AppExit", "Default", "Restart"])
|
||||||
|
.output()
|
||||||
|
.context("Échec configuration AppExit")?;
|
||||||
|
|
||||||
|
// 8. Délai de redémarrage: 5 secondes
|
||||||
|
Command::new(&nssm)
|
||||||
|
.args(["set", service_name, "AppRestartDelay", "5000"])
|
||||||
|
.output()
|
||||||
|
.context("Échec configuration AppRestartDelay")?;
|
||||||
|
|
||||||
|
// 9. Répertoire de travail
|
||||||
|
if let Some(parent) = exe.parent() {
|
||||||
|
Command::new(&nssm)
|
||||||
|
.args(["set", service_name, "AppDirectory", &parent.to_string_lossy()])
|
||||||
|
.output()
|
||||||
|
.context("Échec configuration AppDirectory")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Démarrer le service
|
||||||
|
tracing::info!("Démarrage du service...");
|
||||||
|
let start = Command::new(&nssm)
|
||||||
|
.args(["start", service_name])
|
||||||
|
.output()
|
||||||
|
.context("Échec de la commande nssm start")?;
|
||||||
|
|
||||||
|
if start.status.success() {
|
||||||
|
tracing::info!("✅ Service '{}' installé et démarré avec succès!", service_name);
|
||||||
|
tracing::info!(" Dashboard : http://127.0.0.1:9199");
|
||||||
|
tracing::info!(" Le watcher clipboard est actif (capture automatique des screenshots)");
|
||||||
|
tracing::info!(" Le service redémarre automatiquement en cas de crash");
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&start.stderr);
|
||||||
|
tracing::warn!("⚠️ Service installé mais échec au démarrage: {}", stderr);
|
||||||
|
tracing::info!(" Essayez 'sc start ImagodWeb' ou redémarrez Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Désinstalle le service Windows imagod-web
|
||||||
|
///
|
||||||
|
/// Équivalent à:
|
||||||
|
/// nssm stop ImagodWeb
|
||||||
|
/// nssm remove ImagodWeb confirm
|
||||||
pub fn uninstall() -> Result<()> {
|
pub fn uninstall() -> Result<()> {
|
||||||
todo!("Phase 10: Service uninstall")
|
if !cfg!(windows) {
|
||||||
|
anyhow::bail!("La désinstallation de service Windows n'est disponible que sous Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
let nssm = nssm_path()?;
|
||||||
|
if !nssm.exists() {
|
||||||
|
// NSSM pas présent, le service n'a probablement jamais été installé
|
||||||
|
tracing::warn!("NSSM non trouvé — rien à désinstaller");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let service_name = "ImagodWeb";
|
||||||
|
|
||||||
|
if !nssm_installed()? {
|
||||||
|
tracing::warn!("Service '{}' non trouvé — rien à désinstaller", service_name);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Arrêt du service '{}'...", service_name);
|
||||||
|
let _ = Command::new(&nssm)
|
||||||
|
.args(["stop", service_name])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
tracing::info!("Suppression du service '{}'...", service_name);
|
||||||
|
let remove = Command::new(&nssm)
|
||||||
|
.args(["remove", service_name, "confirm"])
|
||||||
|
.output()
|
||||||
|
.context("Échec de la commande nssm remove")?;
|
||||||
|
|
||||||
|
if remove.status.success() {
|
||||||
|
tracing::info!("✅ Service '{}' désinstallé avec succès", service_name);
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&remove.stderr);
|
||||||
|
anyhow::bail!("nssm remove a échoué: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si le service est installé
|
||||||
|
fn nssm_installed() -> Result<bool> {
|
||||||
|
let nssm = nssm_path()?;
|
||||||
|
if !nssm.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(&nssm)
|
||||||
|
.args(["status", "ImagodWeb"])
|
||||||
|
.output()
|
||||||
|
.context("Échec de nssm status")?;
|
||||||
|
|
||||||
|
Ok(output.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affiche le statut du service
|
||||||
|
pub fn status() -> Result<()> {
|
||||||
|
if !cfg!(windows) {
|
||||||
|
tracing::info!("Le statut de service n'est pertinent que sous Windows");
|
||||||
|
tracing::info!("Utilisez 'imagod-web status' pour le statut général");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let nssm = nssm_path()?;
|
||||||
|
if !nssm.exists() || !nssm_installed()? {
|
||||||
|
tracing::warn!("Service 'ImagodWeb' non installé");
|
||||||
|
tracing::info!("Exécutez 'imagod-web install' pour l'installer");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(&nssm)
|
||||||
|
.args(["status", "ImagodWeb"])
|
||||||
|
.output()
|
||||||
|
.context("Échec de nssm status")?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
tracing::info!("Statut du service: {}", stdout.trim());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user