mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
En conteneur, l'image se met à jour par Docker : /api/update rend la version du Go sans mise à jour disponible et la note Docker, /api/update/apply la refuse (409). La façade sert ces deux routes quand LOKI_UPDATE=rust et LOKI_CONTAINER=1 (sysupdate.rs) ; hors conteneur, loki-rs ne passe pas la variable et le Go garde le remplacement du binaire. Parité : parity_update_test.go tire du Go les quatre réponses en conteneur, rejouées par tests/sysupdate.rs ; TestFacadeMiseAJour côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
462 lines
18 KiB
Rust
462 lines
18 KiB
Rust
//! loki-rs — façade Rust de Loki.
|
|
//!
|
|
//! ```text
|
|
//! loki-rs serve façade sur LOKI_WEB_PORT (8090), Go sur 127.0.0.1:LOKI_GO_PORT (8091)
|
|
//! loki-rs self-update met à jour les binaires dans $LOKI_HOME/bin (jamais bloquant)
|
|
//! loki-rs which-bin dossier d'où prendre loki et loki-rs
|
|
//! loki-rs engine <start|stop|restart|status> supervision du moteur (conteneur)
|
|
//! loki-rs version
|
|
//! ```
|
|
|
|
use loki_rs::{auth, child, engine, proxy, update};
|
|
use std::net::SocketAddr;
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn main() -> ExitCode {
|
|
let cmd = std::env::args().nth(1).unwrap_or_default();
|
|
match cmd.as_str() {
|
|
"serve" => serve(),
|
|
"engine" => {
|
|
let action = std::env::args().nth(2).unwrap_or_default();
|
|
let go_bin = child::go_bin(std::env::var("LOKI_GO_BIN").ok(), &exe());
|
|
let paths = engine::Paths::from_env();
|
|
// La ligne de commande du moteur composée ici (lot 14) : lancée
|
|
// par le superviseur quand LOKI_SERVE=rust.
|
|
if action == "serve" {
|
|
return loki_rs::serve::run::run(&go_bin, &paths.home);
|
|
}
|
|
let mut opts = engine::Opts::new(go_bin);
|
|
if held_by_facade("LOKI_SERVE") {
|
|
opts.serve = (exe(), vec!["engine".into(), "serve".into()]);
|
|
}
|
|
match engine::run(&action, &paths, &opts) {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
// Dernière ligne de stderr : le Go en fait son message d'erreur.
|
|
eprintln!("[loki-rs engine] {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
"self-update" => {
|
|
let outcome = update::run(&update::UpdateConfig::from_env());
|
|
eprintln!("[loki-rs update] {}", outcome.message());
|
|
ExitCode::SUCCESS
|
|
}
|
|
"which-bin" => {
|
|
let cfg = update::UpdateConfig::from_env();
|
|
let dir = update::state::select_dir(
|
|
&cfg.data_bin,
|
|
&image_dir(),
|
|
cfg.image_build,
|
|
cfg.image_abi,
|
|
);
|
|
println!("{}", dir.display());
|
|
ExitCode::SUCCESS
|
|
}
|
|
"version" | "--version" | "-V" => {
|
|
let cfg = update::UpdateConfig::from_env();
|
|
println!(
|
|
"loki-rs {} (build image {}, ABI {})",
|
|
env!("CARGO_PKG_VERSION"),
|
|
cfg.image_build,
|
|
cfg.image_abi
|
|
);
|
|
ExitCode::SUCCESS
|
|
}
|
|
_ => {
|
|
eprintln!(
|
|
"usage : loki-rs serve | engine <action> | self-update | which-bin | version"
|
|
);
|
|
ExitCode::from(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn exe() -> PathBuf {
|
|
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("loki-rs"))
|
|
}
|
|
|
|
fn image_dir() -> PathBuf {
|
|
exe()
|
|
.parent()
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
}
|
|
|
|
fn env_port(key: &str, default: u16) -> u16 {
|
|
std::env::var(key)
|
|
.ok()
|
|
.and_then(|v| v.trim().parse().ok())
|
|
.unwrap_or(default)
|
|
}
|
|
|
|
fn serve() -> ExitCode {
|
|
let rt = match tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
{
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] runtime : {e}");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
rt.block_on(async {
|
|
let web_port = env_port("LOKI_WEB_PORT", 8090);
|
|
let go_port = env_port("LOKI_GO_PORT", 8091);
|
|
let go_bin = child::go_bin(std::env::var("LOKI_GO_BIN").ok(), &exe());
|
|
let backend = SocketAddr::from(([127, 0, 0, 1], go_port));
|
|
|
|
eprintln!(
|
|
"[loki-rs] serveur Go : {} web {go_port} (local)",
|
|
go_bin.display()
|
|
);
|
|
// Jeton de l'API interne du Go : tiré au hasard à chaque démarrage,
|
|
// connu seulement de la façade et du Go qu'elle lance.
|
|
let token = match internal_token() {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] aléa système indisponible : {e}");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
// LOKI_CHAT=rust : la conversation est tenue par la façade. Le Go le
|
|
// sait (il remet alors à la façade le trafic de chat du tunnel) et
|
|
// connaît son adresse locale.
|
|
let chat_rust = chat_held_by_facade();
|
|
// LOKI_MODELS=rust : les routes des modèles aussi (le Go remet alors
|
|
// celles du tunnel et ne vide plus les .part).
|
|
let models_rust = models_held_by_facade();
|
|
// LOKI_DICTATE=rust : la dictée vocale aussi (le Go lui demande alors
|
|
// d'arrêter son serveur quand il libère la VRAM).
|
|
let dictate_rust = held_by_facade("LOKI_DICTATE");
|
|
// LOKI_SETTINGS=rust : les réglages partagés (préférences, prompt
|
|
// système, intensité du raisonnement, adresse publique).
|
|
let settings_rust = held_by_facade("LOKI_SETTINGS");
|
|
// LOKI_FILES=rust : les fichiers de la discussion (dépôt, panneau
|
|
// Fichiers, archive, téléchargement, images).
|
|
let files_rust = held_by_facade("LOKI_FILES");
|
|
// LOKI_PUSH=rust : les notifications Web Push (le Go lui délègue ses
|
|
// envois de fin de tour et de fin de tâche).
|
|
let push_rust = held_by_facade("LOKI_PUSH");
|
|
// LOKI_MEM=rust : la mémoire (pages, index, trackers, mode mémoire,
|
|
// interrupteurs du mode agent).
|
|
let mem_rust = held_by_facade("LOKI_MEM");
|
|
// LOKI_PRESETS=rust : les presets (liste, éditeur, aperçus, externes,
|
|
// bascule ; l'application au moteur reste au Go).
|
|
let presets_rust = held_by_facade("LOKI_PRESETS");
|
|
// LOKI_ENGINE=rust : le pilotage du moteur (démarrage, arrêt,
|
|
// déchargement de la VRAM), en conteneur avec le superviseur Rust
|
|
// seulement — sous systemd, le Go pilote son unité.
|
|
// LOKI_SYSTEM=rust : les interrupteurs système (accès internet,
|
|
// navigateur, clé de complétion, écoute réseau du moteur).
|
|
let system_rust = held_by_facade("LOKI_SYSTEM");
|
|
// LOKI_PROJECTS=rust : le hub des projets.
|
|
let projects_rust = held_by_facade("LOKI_PROJECTS");
|
|
// LOKI_MCP=rust : les serveurs MCP (le Go demande alors à la façade
|
|
// les outils MCP et leurs appels).
|
|
let mcp_rust = held_by_facade("LOKI_MCP");
|
|
// LOKI_UPDATE=rust : la mise à jour de Loki, en conteneur seulement
|
|
// (l'image se met à jour par Docker).
|
|
let update_rust = held_by_facade("LOKI_UPDATE")
|
|
&& loki_rs::sysupdate::usable(std::env::var("LOKI_CONTAINER").ok().as_deref());
|
|
let engine_rust = held_by_facade("LOKI_ENGINE")
|
|
&& match loki_rs::enginectl::usable(
|
|
std::env::var("LOKI_CONTAINER").ok().as_deref(),
|
|
std::env::var("LOKI_ENGINE_SUPERVISOR").ok().as_deref(),
|
|
) {
|
|
Ok(()) => true,
|
|
Err(why) => {
|
|
eprintln!("[loki-rs] pilotage du moteur tenu par le Go ({why})");
|
|
false
|
|
}
|
|
};
|
|
let mut env = vec![
|
|
(auth::INTERNAL_ENV.to_string(), token.clone()),
|
|
// Adresse de l'API interne du Go, héritée par ses enfants :
|
|
// « loki-rs engine serve » (LOKI_SERVE=rust) y lit la configuration.
|
|
("LOKI_GO_ADDR".to_string(), backend.to_string()),
|
|
];
|
|
let held = [
|
|
("LOKI_CHAT", chat_rust),
|
|
("LOKI_MODELS", models_rust),
|
|
("LOKI_DICTATE", dictate_rust),
|
|
("LOKI_SETTINGS", settings_rust),
|
|
("LOKI_FILES", files_rust),
|
|
("LOKI_PUSH", push_rust),
|
|
("LOKI_MEM", mem_rust),
|
|
("LOKI_PRESETS", presets_rust),
|
|
("LOKI_ENGINE", engine_rust),
|
|
("LOKI_SYSTEM", system_rust),
|
|
("LOKI_PROJECTS", projects_rust),
|
|
("LOKI_MCP", mcp_rust),
|
|
("LOKI_UPDATE", update_rust),
|
|
];
|
|
for (var, on) in held {
|
|
if on {
|
|
env.push((var.into(), "rust".into()));
|
|
}
|
|
}
|
|
if held.iter().any(|(_, on)| *on) {
|
|
env.push(("LOKI_FACADE_ADDR".into(), format!("127.0.0.1:{web_port}")));
|
|
}
|
|
let go_bin_path = go_bin.clone();
|
|
let sup = child::Supervisor::spawn(go_bin, go_port, env);
|
|
wait_for(backend, Duration::from_secs(30)).await;
|
|
let engine_paths = engine::Paths::from_env();
|
|
// Le Go peut garder la conversation malgré LOKI_CHAT=rust (réglage que
|
|
// la façade ne sait pas encore honorer) : sa décision fait foi.
|
|
let chat_rust = chat_rust
|
|
&& match go_keeps_chat(backend, &token).await {
|
|
Ok(None) => true,
|
|
Ok(Some(why)) => {
|
|
eprintln!("[loki-rs] conversation tenue par le Go ({why})");
|
|
false
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] conversation : {e}");
|
|
sup.shutdown(Duration::from_secs(10)).await;
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
let chat = if chat_rust {
|
|
match start_chat(backend, &token, &engine_paths.home).await {
|
|
Ok(c) => Some(c),
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] conversation : {e}");
|
|
sup.shutdown(Duration::from_secs(10)).await;
|
|
return ExitCode::FAILURE;
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let models = if models_rust {
|
|
Some(start_models(backend, &token, &engine_paths.home).await)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let dictate = dictate_rust.then(|| {
|
|
eprintln!("[loki-rs] dictée tenue par la façade (LOKI_DICTATE=rust)");
|
|
std::sync::Arc::new(loki_rs::dictate::Dictation::new(&engine_paths.home))
|
|
});
|
|
|
|
let files = if files_rust {
|
|
Some(start_files(backend, &token).await)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let addr = SocketAddr::from(([0, 0, 0, 0], web_port));
|
|
let listener = match tokio::net::TcpListener::bind(addr).await {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] écoute sur {addr} impossible : {e}");
|
|
sup.shutdown(Duration::from_secs(10)).await;
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
eprintln!("[loki-rs] façade : http://{addr} → {backend}");
|
|
let mem_home = engine_paths.home.clone();
|
|
// Le superviseur reçoit ce que le Go lui passait (rustSvcAction), et de
|
|
// quoi joindre l'API interne du Go pour « loki-rs engine serve ».
|
|
let engine_ctl = engine_rust.then(|| {
|
|
std::sync::Arc::new(loki_rs::enginectl::EngineCtl::new(
|
|
exe(),
|
|
vec![
|
|
(
|
|
"LOKI_GO_BIN".into(),
|
|
go_bin_path.to_string_lossy().into_owned(),
|
|
),
|
|
(
|
|
"LOKI_HOME".into(),
|
|
engine_paths.home.to_string_lossy().into_owned(),
|
|
),
|
|
("LOKI_SERVICE".into(), engine_paths.service.clone()),
|
|
("LOKI_GO_ADDR".into(), backend.to_string()),
|
|
(auth::INTERNAL_ENV.into(), token.clone()),
|
|
],
|
|
))
|
|
});
|
|
let cfg = proxy::Config {
|
|
backend,
|
|
internal_token: token,
|
|
host_env: auth::HostEnv::from_env(),
|
|
engine: engine_paths,
|
|
chat,
|
|
models,
|
|
dictate,
|
|
settings: settings_rust,
|
|
files,
|
|
push: push_rust.then(|| std::sync::Arc::new(loki_rs::push::Push::new())),
|
|
mem: mem_rust
|
|
.then(|| std::sync::Arc::new(loki_rs::memory::Memory::new(mem_home.clone()))),
|
|
presets: presets_rust,
|
|
engine_ctl,
|
|
system: system_rust.then(|| std::sync::Arc::new(loki_rs::system::System::new())),
|
|
projects: projects_rust,
|
|
update: update_rust,
|
|
mcp: mcp_rust.then(|| std::sync::Arc::new(loki_rs::mcp::Mcp::new(mem_home))),
|
|
};
|
|
let res = proxy::serve(listener, cfg, shutdown_signal()).await;
|
|
eprintln!("[loki-rs] arrêt demandé — arrêt du serveur Go");
|
|
sup.shutdown(Duration::from_secs(10)).await;
|
|
match res {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
eprintln!("[loki-rs] {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// `LOKI_CHAT=rust` : conversation tenue par la façade (Go par défaut).
|
|
fn chat_held_by_facade() -> bool {
|
|
std::env::var("LOKI_CHAT").is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
|
}
|
|
|
|
/// `LOKI_MODELS=rust` : routes des modèles tenues par la façade.
|
|
fn models_held_by_facade() -> bool {
|
|
held_by_facade("LOKI_MODELS")
|
|
}
|
|
|
|
/// `<VAR>=rust` : ce lot est tenu par la façade (le Go par défaut).
|
|
fn held_by_facade(var: &str) -> bool {
|
|
std::env::var(var).is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
|
}
|
|
|
|
/// Les `.part` d'un téléchargement tué ne reprendront jamais : vidés au
|
|
/// démarrage (le Go ne le fait plus quand la façade les tient).
|
|
async fn start_models(
|
|
backend: SocketAddr,
|
|
token: &str,
|
|
home: &std::path::Path,
|
|
) -> std::sync::Arc<loki_rs::models::Models> {
|
|
use loki_rs::{downloads, gguf, store};
|
|
let st = store::Store::new(backend, token.to_string());
|
|
let stored = gguf::parse_stored_model_dirs(
|
|
st.get(store::BK_STATE, gguf::STATE_MODEL_DIRS_KEY)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.as_deref(),
|
|
);
|
|
let dirs = gguf::model_dirs(
|
|
home,
|
|
&stored,
|
|
std::env::var(gguf::MODEL_DIRS_ENV).ok().as_deref(),
|
|
);
|
|
let _ = tokio::task::spawn_blocking(move || downloads::clean_stale_part_files(&dirs)).await;
|
|
eprintln!("[loki-rs] modèles tenus par la façade (LOKI_MODELS=rust)");
|
|
std::sync::Arc::new(loki_rs::models::Models::new())
|
|
}
|
|
|
|
/// Les envois interrompus (process arrêté en plein dépôt) ne reprendront
|
|
/// jamais : leurs `.part` sont vidés au démarrage (le Go ne le fait plus quand
|
|
/// la façade tient les fichiers).
|
|
async fn start_files(
|
|
backend: SocketAddr,
|
|
token: &str,
|
|
) -> std::sync::Arc<loki_rs::convfiles::ConvFiles> {
|
|
let st = loki_rs::store::Store::new(backend, token.to_string());
|
|
let root = st
|
|
.internal_json(
|
|
hyper::Method::POST,
|
|
"/internal/v1/conv/workspace",
|
|
Some(&serde_json::json!({})),
|
|
)
|
|
.await
|
|
.ok()
|
|
.and_then(|v| v.get("root").and_then(|r| r.as_str()).map(str::to_string));
|
|
if let Some(root) = root {
|
|
let _ = tokio::task::spawn_blocking(move || {
|
|
loki_rs::convfiles::clean_stale_upload_parts(&root)
|
|
})
|
|
.await;
|
|
}
|
|
eprintln!("[loki-rs] fichiers de la discussion tenus par la façade (LOKI_FILES=rust)");
|
|
std::sync::Arc::new(loki_rs::convfiles::ConvFiles::default())
|
|
}
|
|
|
|
/// Le Go garde-t-il la conversation ? (quelques essais : il vient de démarrer)
|
|
async fn go_keeps_chat(backend: SocketAddr, token: &str) -> Result<Option<String>, String> {
|
|
let store = loki_rs::store::Store::new(backend, token.to_string());
|
|
let mut last = String::new();
|
|
for _ in 0..5 {
|
|
match loki_rs::chat::generate::go_keeps_chat(&store).await {
|
|
Ok(v) => return Ok(v),
|
|
Err(e) => last = e,
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
}
|
|
Err(last)
|
|
}
|
|
|
|
/// Charge la discussion active et prépare le service des tours.
|
|
async fn start_chat(
|
|
backend: SocketAddr,
|
|
token: &str,
|
|
home: &std::path::Path,
|
|
) -> Result<std::sync::Arc<loki_rs::chat::generate::ChatService>, String> {
|
|
use loki_rs::chat::{conv::Conversation, generate::ChatService, host::ChatShared, sessions};
|
|
let store = loki_rs::store::Store::new(backend, token.to_string());
|
|
let s = sessions::Sessions::new(
|
|
store.clone(),
|
|
Conversation::new(),
|
|
sessions::GoSessionHooks(store.clone()),
|
|
);
|
|
s.load().await?;
|
|
let shared = ChatShared::for_sessions(&s, store, home.join("chatimg"));
|
|
eprintln!("[loki-rs] conversation tenue par la façade (LOKI_CHAT=rust)");
|
|
Ok(ChatService::new(
|
|
std::sync::Arc::new(s),
|
|
std::sync::Arc::new(shared),
|
|
))
|
|
}
|
|
|
|
fn internal_token() -> Result<String, getrandom::Error> {
|
|
let mut b = [0u8; 32];
|
|
getrandom::fill(&mut b)?;
|
|
Ok(hex::encode(b))
|
|
}
|
|
|
|
/// Attend que le Go accepte les connexions, sans bloquer au-delà de `max` :
|
|
/// la façade ouvre quand même et répond 502 en attendant.
|
|
async fn wait_for(addr: SocketAddr, max: Duration) {
|
|
let t0 = Instant::now();
|
|
while t0.elapsed() < max {
|
|
if tokio::net::TcpStream::connect(addr).await.is_ok() {
|
|
return;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
}
|
|
eprintln!("[loki-rs] le serveur Go ne répond pas encore — façade ouverte quand même");
|
|
}
|
|
|
|
async fn shutdown_signal() {
|
|
#[cfg(unix)]
|
|
{
|
|
use tokio::signal::unix::{SignalKind, signal};
|
|
match signal(SignalKind::terminate()) {
|
|
Ok(mut term) => {
|
|
tokio::select! {
|
|
_ = term.recv() => {}
|
|
_ = tokio::signal::ctrl_c() => {}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
}
|
|
}
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
}
|
|
}
|