mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
llama.cpp publie plusieurs versions par jour ; l'image de Loki ne se reconstruit qu'à une mise à jour de Loki. Le moteur y était donc figé à la date du dernier build, et le rattraper imposait un rebuild complet de 2,6 Go pour un composant qui en pèse 170 Mo. Le panneau « Moteur » ne le disait même pas : il annonçait « rien à mettre à jour ici ». Réglages → Moteur affiche maintenant la version qui tourne (bXXXXX et son commit, lus dans la bannière du binaire — jusqu'ici invisibles ailleurs que dans le journal du moteur) et la met à jour en un clic. Où le moteur est pris. Pas dans les releases GitHub de llama.cpp : elles ne contiennent AUCUN binaire CUDA pour Linux, et le mode « précompilé » retomberait sur Vulkan, donc sur une régression pour une carte NVIDIA. La seule distribution CUDA/Linux officielle et précompilée est l'image de conteneur — celle-là même dont l'image de Loki hérite. On lit son manifeste OCI et on ne télécharge que les couches qui portent /app, en descendant du sommet : le runtime CUDA (2 Go) et la base système sont déjà là. S'arrêter au binaire ne suffit pas — llama.cpp le range dans une couche et ses .so dans la précédente — d'où une règle d'arrêt sur « binaire + libggml-base + libllama », et un garde-fou de taille qui interdit de descendre jusqu'au runtime. Le moteur atterrit dans /data/engine/<version>/, donc sur le volume de données : il survit à un docker compose pull. La variante (CUDA, Vulkan, SYCL, MUSA, CPU) est déduite des backends ggml posés à côté du moteur courant — le conteneur ne sait pas de quelle image il vient, et faire retenir « server-cuda » à l'utilisateur serait un piège. Le risque, et ce qui le couvre. La mise à jour apporte llama.cpp, pas le runtime CUDA, qui reste celui de l'image : un llama.cpp compilé pour un CUDA plus récent ne chargerait pas son backend GPU. Le symptôme serait silencieux — tout marche, mais sur le processeur. Le nouveau moteur est donc lancé à blanc avant toute bascule ; il est refusé s'il ne démarre pas, ET s'il ne voit plus aucune carte alors que le moteur courant en voyait. Dans les deux cas le moteur courant n'est pas touché, et celui de l'image reste intact : « revenir au moteur de l'image » y ramène en un clic, sans réseau. Vérifié de bout en bout contre le vrai ghcr.io (166 Mo, 7 s, toutes les bibliothèques et leurs liens de version présents) et, pour les chemins d'échec, contre un faux registre. LOKI_OCI_REGISTRY permet de viser un miroir quand ghcr.io n'est pas joignable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
673 lines
21 KiB
Go
673 lines
21 KiB
Go
// web_llamacpp.go — pilotage du backend llama.cpp depuis l'UI web :
|
|
// statut (commit, retard, binaire, plan de build), vérification des mises à
|
|
// jour (git fetch), et jobs asynchrones install / update / rebuild dont la
|
|
// progression et les logs sont pollés par le client.
|
|
//
|
|
// GET /api/llamacpp → statut complet (dépôt, commit, binaire, plan)
|
|
// POST /api/llamacpp/check → git fetch + retard sur origin (réseau)
|
|
// POST /api/llamacpp/install → job d'installation {force?}
|
|
// POST /api/llamacpp/update → job de mise à jour {clean?}
|
|
// GET /api/llamacpp/job?from= → progression du job (phase, lignes, erreur)
|
|
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// lcJob est LE job llama.cpp en cours (un seul à la fois). Les lignes de log
|
|
// sont indexées de façon absolue (base = index de Lines[0]) pour que le client
|
|
// puisse poller « à partir de N » même quand on tronque le début.
|
|
type lcJob struct {
|
|
Action string // "install" | "update"
|
|
Phase string
|
|
Compiled int // fichiers compilés (progression du build)
|
|
Running bool
|
|
Err string
|
|
StartedAt int64
|
|
EndedAt int64
|
|
OldCommit string
|
|
NewCommit string
|
|
lines []string
|
|
base int
|
|
}
|
|
|
|
var (
|
|
lcMu sync.Mutex
|
|
lcCur *lcJob
|
|
)
|
|
|
|
const lcMaxLines = 4000
|
|
|
|
// lcAppend ajoute une ligne au log du job courant et met à jour la phase / le
|
|
// compteur de fichiers compilés à partir du contenu (mêmes heuristiques que le
|
|
// spinner du CLI). Appelée par le sink de build (emitBuildLine).
|
|
func lcAppend(line string) {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur == nil {
|
|
return
|
|
}
|
|
lcCur.lines = append(lcCur.lines, line)
|
|
if len(lcCur.lines) > lcMaxLines {
|
|
drop := len(lcCur.lines) - lcMaxLines
|
|
lcCur.lines = append([]string(nil), lcCur.lines[drop:]...)
|
|
lcCur.base += drop
|
|
}
|
|
if f := compiledFile(line); f != "" {
|
|
lcCur.Compiled++
|
|
lcCur.Phase = fmt.Sprintf("compilation… %d fichiers", lcCur.Compiled)
|
|
} else if p := phaseLabel(line); p != "" {
|
|
lcCur.Phase = p
|
|
}
|
|
lcSaveLine(line)
|
|
lcSave(false)
|
|
}
|
|
|
|
// lcPhase pose une phase explicite (étapes hors build : clone, fetch, service…)
|
|
// et la trace aussi dans le log.
|
|
func lcPhase(phase string) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Phase = phase
|
|
lcCur.lines = append(lcCur.lines, "▶ "+phase)
|
|
lcSaveLine("▶ " + phase)
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
// startLcJob démarre un job (install ou update) si aucun n'est en cours.
|
|
func startLcJob(action string, run func()) error {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur != nil && lcCur.Running {
|
|
return fmt.Errorf("un job %s est déjà en cours", lcCur.Action)
|
|
}
|
|
lcCur = &lcJob{Action: action, Running: true, Phase: "démarrage…", StartedAt: time.Now().Unix()}
|
|
lcResetLog()
|
|
lcSave(true)
|
|
setBuildSink(lcAppend)
|
|
go func() {
|
|
defer func() {
|
|
setBuildSink(nil)
|
|
lcMu.Lock()
|
|
lcCur.Running = false
|
|
lcCur.EndedAt = time.Now().Unix()
|
|
lcSave(true)
|
|
lcMu.Unlock()
|
|
}()
|
|
run()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func lcFail(err error) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Err = err.Error()
|
|
lcCur.Phase = "échec"
|
|
lcCur.lines = append(lcCur.lines, "✗ "+err.Error())
|
|
lcSaveLine("✗ " + err.Error())
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
func lcDone(msg string) {
|
|
lcMu.Lock()
|
|
if lcCur != nil {
|
|
lcCur.Phase = msg
|
|
lcCur.lines = append(lcCur.lines, "✓ "+msg)
|
|
lcSaveLine("✓ " + msg)
|
|
lcSave(true)
|
|
}
|
|
lcMu.Unlock()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleLlamacpp renvoie le statut complet du backend llama.cpp : dépôt,
|
|
// commit courant, retard connu (sans fetch réseau), binaire compilé et son
|
|
// usage dans config.env, plan de build détecté, et le job éventuel.
|
|
func handleLlamacpp(w http.ResponseWriter, r *http.Request) {
|
|
repo := llamacppRepoDir()
|
|
out := map[string]any{
|
|
"repo": repo,
|
|
"installed": isDir(filepath.Join(repo, ".git")),
|
|
}
|
|
if out["installed"].(bool) {
|
|
out["branch"] = gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
out["commit"] = gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
out["commit_date"] = gitOutput(repo, "log", "-1", "--format=%ci")
|
|
out["commit_msg"] = gitOutput(repo, "log", "-1", "--format=%s")
|
|
if br, _ := out["branch"].(string); br != "" && br != "HEAD" {
|
|
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+br); behind != "" {
|
|
n, _ := strconv.Atoi(behind)
|
|
out["behind"] = n
|
|
}
|
|
}
|
|
}
|
|
bin := llamaServerBin(repo)
|
|
out["bin"] = bin
|
|
cfgBin := ReadConfig()["BIN"]
|
|
out["config_bin"] = cfgBin
|
|
out["in_use"] = bin != "" && samePath(bin, cfgBin)
|
|
// Moteur livré par l'image Docker : il n'y a rien à installer, et l'image
|
|
// ne contient ni cmake ni compilateur. L'UI masque alors les trois modes,
|
|
// et le sélecteur par modèle propose « Image » au lieu de « Précompilé /
|
|
// Compilé », qui ne désignent rien ici.
|
|
out["provided"] = engineProvided()
|
|
out["provided_bin"] = providedEngineBin()
|
|
// Moteur téléchargé depuis l'image officielle llama.cpp (web_engine.go) : ni
|
|
// « précompilé », ni « compilé », ni « personnalisé ». Sans cette information,
|
|
// le bandeau d'état le rangeait dans « personnalisé », ce qui laisse croire à
|
|
// un bricolage alors que c'est la voie de mise à jour normale en conteneur.
|
|
out["engine"] = map[string]any{
|
|
"dir": engineDir(),
|
|
"in_use": engineOwns(cfgBin),
|
|
"tag": engineTagOf(cfgBin),
|
|
}
|
|
|
|
plan := detectBuildPlan()
|
|
out["plan"] = map[string]any{
|
|
"backend": plan.backend,
|
|
"arch": plan.cudaArch,
|
|
"jobs": plan.jobs,
|
|
}
|
|
// Mode « binaires précompilés » : installé ? utilisé par la config ?
|
|
pbTag, _ := prebuiltVersion()
|
|
pbBin := prebuiltServerBin()
|
|
out["prebuilt"] = map[string]any{
|
|
"tag": pbTag,
|
|
"bin": pbBin,
|
|
"dir": prebuiltDir(),
|
|
// Tout BIN vivant sous le dossier prebuilt EST le moteur précompilé, même
|
|
// s'il porte le numéro d'une release remplacée depuis (chemin versionné).
|
|
"in_use": pbBin != "" && prebuiltOwns(cfgBin),
|
|
}
|
|
out["backends_dir"] = filepath.Join(LokiHome(), "backends")
|
|
out["reco"] = recommendedMode(plan.backend)
|
|
out["job"] = lcJobSnapshot(0, false)
|
|
sendJSON(w, 200, out)
|
|
}
|
|
|
|
// providedEngineBin renvoie le moteur livré par l'image Docker, ou "" hors
|
|
// conteneur. L'image le déclare elle-même via LOKI_ENGINE_BIN (voir Dockerfile
|
|
// et docker-entrypoint.sh) : pas de chemin en dur ici, et n'importe quelle image
|
|
// de base convient tant qu'elle pose cette variable.
|
|
//
|
|
// Sans cette notion, l'UI proposait d'installer un moteur déjà présent et
|
|
// l'utilisateur tombait sur « outils manquants : cmake » — l'image ne contient
|
|
// volontairement ni cmake ni compilateur, puisqu'elle part de l'image officielle
|
|
// llama.cpp où llama-server est déjà compilé.
|
|
func providedEngineBin() string {
|
|
p := strings.TrimSpace(os.Getenv("LOKI_ENGINE_BIN"))
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
if st, err := os.Stat(prebuiltResolveBin(p)); err != nil || st.IsDir() {
|
|
return ""
|
|
}
|
|
return p
|
|
}
|
|
|
|
// engineProvided : le moteur vient-il de l'image ? (raccourci de lecture)
|
|
func engineProvided() bool { return providedEngineBin() != "" }
|
|
|
|
// refuseIfProvided coupe court aux jobs d'installation/compilation quand le
|
|
// moteur vient de l'image : rien à installer, et aucun compilateur disponible.
|
|
// Renvoie true si la requête a été refusée (l'appelant doit sortir).
|
|
func refuseIfProvided(w http.ResponseWriter) bool {
|
|
bin := providedEngineBin()
|
|
if bin == "" {
|
|
return false
|
|
}
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": "Le moteur est fourni par l'image Docker (" + bin +
|
|
") : il n'y a rien à compiler ici, et l'image ne contient ni cmake ni compilateur. " +
|
|
"Pour le mettre à jour, utilise « mettre à jour le moteur » dans Réglages → Moteur."})
|
|
return true
|
|
}
|
|
|
|
// samePath compare deux chemins en neutralisant séparateurs, symlinks et casse
|
|
// (Windows).
|
|
func samePath(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
norm := func(p string) string {
|
|
if real, err := filepath.EvalSymlinks(p); err == nil {
|
|
p = real
|
|
}
|
|
p = filepath.Clean(p)
|
|
return strings.ToLower(filepath.ToSlash(p))
|
|
}
|
|
return norm(a) == norm(b)
|
|
}
|
|
|
|
// handleLlamacppCheck fait un vrai git fetch puis renvoie le retard sur origin
|
|
// et le dernier commit distant. Synchrone (quelques secondes réseau).
|
|
func handleLlamacppCheck(w http.ResponseWriter, r *http.Request) {
|
|
repo := llamacppRepoDir()
|
|
if !isDir(filepath.Join(repo, ".git")) {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": "llama.cpp n'est pas installé"})
|
|
return
|
|
}
|
|
branch := gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
if branch == "" || branch == "HEAD" {
|
|
branch = "master"
|
|
}
|
|
if err := runStep("git fetch", repo, "git", "fetch", "origin", "--quiet"); err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": "git fetch a échoué : " + err.Error()})
|
|
return
|
|
}
|
|
behind := 0
|
|
if b := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); b != "" {
|
|
behind, _ = strconv.Atoi(b)
|
|
}
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"behind": behind,
|
|
"local": gitOutput(repo, "rev-parse", "--short", "HEAD"),
|
|
"remote": gitOutput(repo, "rev-parse", "--short", "origin/"+branch),
|
|
"remote_date": gitOutput(repo, "log", "-1", "--format=%ci", "origin/"+branch),
|
|
"remote_msg": gitOutput(repo, "log", "-1", "--format=%s", "origin/"+branch),
|
|
"branch": branch,
|
|
"has_binary": llamaServerBin(repo) != "",
|
|
"needs_rebuild": behind > 0 || llamaServerBin(repo) == "",
|
|
})
|
|
}
|
|
|
|
// handleLlamacppInstall lance le job d'installation (clone + build + BIN).
|
|
func handleLlamacppInstall(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Force bool `json:"force"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if err := startLcJob("install", func() { lcRunInstall(req.Force) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleLlamacppInstallCustom lance un job d'installation d'un backend CUSTOM
|
|
// (fork llama.cpp) depuis une URL de dépôt Git : cloné dans backends/<name> et
|
|
// compilé pour la machine, SANS toucher au BIN global. Il apparaît ensuite dans
|
|
// /api/backends et se choisit par modèle (éditeur de preset → Moteur).
|
|
func handleLlamacppInstallCustom(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Repo string `json:"repo"`
|
|
Name string `json:"name"`
|
|
Ref string `json:"ref"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if strings.TrimSpace(req.Repo) == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "URL du dépôt requise"})
|
|
return
|
|
}
|
|
if err := startLcJob("custom", func() { lcRunCustomInstall(req.Repo, req.Name, req.Ref) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// lcRunCustomInstall : corps du job d'install d'un backend custom (sans bascule
|
|
// de BIN — c'est volontaire, il se rattache par preset).
|
|
func lcRunCustomInstall(url, name, ref string) {
|
|
bin, err := installCustomBackend(url, name, ref, lcPhase)
|
|
if err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
lcAppend("binaire compilé : " + bin)
|
|
lcAppend("→ pour l'utiliser : édite un modèle → section Moteur → « backend détecté » et choisis-le.")
|
|
lcDone("backend custom installé")
|
|
}
|
|
|
|
// handleLlamacppUpdate lance le job de mise à jour (pull + rebuild + restart).
|
|
// {clean:true} force une recompilation from scratch même sans nouveau commit.
|
|
func handleLlamacppUpdate(w http.ResponseWriter, r *http.Request) {
|
|
if refuseIfProvided(w) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Clean bool `json:"clean"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if err := startLcJob("update", func() { lcRunUpdate(req.Clean) }); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleLlamacppPrebuiltCheck interroge la dernière release officielle de
|
|
// llama.cpp et la compare à la version précompilée installée. Synchrone.
|
|
func handleLlamacppPrebuiltCheck(w http.ResponseWriter, r *http.Request) {
|
|
tag, assets, err := fetchLlamaLatest()
|
|
if err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
main, _, label, _, err := pickPrebuilt(assets)
|
|
if err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
cur, _ := prebuiltVersion()
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"latest": tag,
|
|
"current": cur,
|
|
"variant": label,
|
|
"size_mb": main.Size / 1_000_000,
|
|
"update": cur != tag || prebuiltServerBin() == "",
|
|
})
|
|
}
|
|
|
|
// handleLlamacppPrebuilt lance le job de téléchargement / mise à jour des
|
|
// binaires précompilés officiels (pas de compilation).
|
|
func handleLlamacppPrebuilt(w http.ResponseWriter, r *http.Request) {
|
|
if err := startLcJob("prebuilt", lcRunPrebuilt); err != nil {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// lcRunPrebuilt : télécharge les binaires officiels, pointe BIN dessus, en
|
|
// stoppant le service pendant le remplacement (binaire verrouillé en cours
|
|
// d'exécution) puis en le relançant.
|
|
func lcRunPrebuilt() {
|
|
svcWasUp := serviceIsActive()
|
|
if svcWasUp {
|
|
lcPhase("arrêt du service le temps de l'installation…")
|
|
if err := serviceAction("stop"); err != nil {
|
|
lcAppend("[warn] impossible d'arrêter le service : " + err.Error())
|
|
}
|
|
}
|
|
bin, err := prebuiltInstall(lcAppend, lcPhase)
|
|
if err == nil {
|
|
if serr := SetConfigKey("BIN", bin); serr != nil {
|
|
err = fmt.Errorf("binaires installés mais échec d'écriture de BIN : %w", serr)
|
|
} else {
|
|
lcAppend("BIN mis à jour")
|
|
}
|
|
}
|
|
if svcWasUp {
|
|
lcPhase("redémarrage du service…")
|
|
if serr := serviceAction("start"); serr != nil {
|
|
lcAppend("[warn] redémarrage du service échoué : " + serr.Error())
|
|
}
|
|
}
|
|
if err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
tag, _ := prebuiltVersion()
|
|
lcDone("binaires précompilés installés (" + tag + ")")
|
|
}
|
|
|
|
// handleLlamacppUse bascule BIN entre deux versions DÉJÀ installées, sans
|
|
// rien recompiler : "fast" = binaires précompilés, "opt" = build local. Le
|
|
// service redémarre pour prendre le nouveau binaire. L'UI n'appelle ceci que
|
|
// quand la version cible existe déjà (sinon elle lance un job d'installation).
|
|
func handleLlamacppUse(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Mode string `json:"mode"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
var bin string
|
|
switch req.Mode {
|
|
case "fast":
|
|
bin = prebuiltServerBin()
|
|
case "opt":
|
|
bin = llamaServerBin(llamacppRepoDir())
|
|
default:
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "mode inconnu"})
|
|
return
|
|
}
|
|
if bin == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "cette version n'est pas installée"})
|
|
return
|
|
}
|
|
if err := SetConfigKey("BIN", bin); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
if serviceIsActive() {
|
|
_ = serviceAction("restart")
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "bin": bin})
|
|
}
|
|
|
|
// handleLlamacppJob renvoie l'état du job courant + les lignes de log depuis
|
|
// l'offset absolu ?from=N (le client mémorise `next` et enchaîne).
|
|
func handleLlamacppJob(w http.ResponseWriter, r *http.Request) {
|
|
from, _ := strconv.Atoi(r.URL.Query().Get("from"))
|
|
sendJSON(w, 200, lcJobSnapshot(from, true))
|
|
}
|
|
|
|
// lcJobSnapshot construit la vue JSON du job courant. withLines=false renvoie
|
|
// juste l'entête (imbriquée dans /api/llamacpp).
|
|
func lcJobSnapshot(from int, withLines bool) map[string]any {
|
|
lcMu.Lock()
|
|
defer lcMu.Unlock()
|
|
if lcCur == nil {
|
|
return map[string]any{"exists": false}
|
|
}
|
|
j := lcCur
|
|
out := map[string]any{
|
|
"exists": true,
|
|
"action": j.Action,
|
|
"running": j.Running,
|
|
"phase": j.Phase,
|
|
"compiled": j.Compiled,
|
|
"error": j.Err,
|
|
"started_at": j.StartedAt,
|
|
"ended_at": j.EndedAt,
|
|
"old": j.OldCommit,
|
|
"new": j.NewCommit,
|
|
}
|
|
if withLines {
|
|
start := from - j.base
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
if start > len(j.lines) {
|
|
start = len(j.lines)
|
|
}
|
|
out["lines"] = append([]string(nil), j.lines[start:]...)
|
|
out["next"] = j.base + len(j.lines)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Corps des jobs (miroir web de llamacppInstall / llamacppUpdate, sans stdout
|
|
// interactif : tout passe par lcPhase / le sink de build)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func lcRunInstall(force bool) {
|
|
repo := defaultRepoDir()
|
|
|
|
lcPhase("vérification des outils (git, cmake, compilateur)…")
|
|
if err := requireTools("git", "cmake"); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
if err := ensureCompiler(); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
ensureAccelerator()
|
|
|
|
if isDir(filepath.Join(repo, ".git")) {
|
|
if !force {
|
|
// Dépôt déjà là : on bascule sur une mise à jour (même intention).
|
|
lcPhase("dépôt déjà présent — bascule en mise à jour")
|
|
lcRunUpdate(false)
|
|
return
|
|
}
|
|
lcPhase("suppression du dépôt existant (--force)…")
|
|
if err := os.RemoveAll(repo); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(repo), 0o755); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
|
|
lcPhase("clone de llama.cpp…")
|
|
if err := runStep("git clone", "", "git", "clone", "--depth=1", llamacppRepoURL, repo); err != nil {
|
|
lcFail(fmt.Errorf("git clone a échoué : %w", err))
|
|
return
|
|
}
|
|
|
|
if !lcBuildAndSwitch(repo, true) {
|
|
return
|
|
}
|
|
lcDone("installation terminée")
|
|
}
|
|
|
|
func lcRunUpdate(clean bool) {
|
|
lcPhase("vérification des outils (git, cmake, compilateur)…")
|
|
if err := requireTools("git", "cmake"); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
if err := ensureCompiler(); err != nil {
|
|
lcFail(err)
|
|
return
|
|
}
|
|
ensureAccelerator()
|
|
|
|
repo := llamacppRepoDir()
|
|
if !isDir(filepath.Join(repo, ".git")) {
|
|
lcFail(fmt.Errorf("aucun dépôt llama.cpp (%s) — lance d'abord l'installation", repo))
|
|
return
|
|
}
|
|
|
|
oldCommit := gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
lcMu.Lock()
|
|
lcCur.OldCommit = oldCommit
|
|
lcMu.Unlock()
|
|
|
|
branch := gitOutput(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
|
if branch == "" || branch == "HEAD" {
|
|
branch = "master"
|
|
}
|
|
|
|
lcPhase("git fetch origin…")
|
|
if err := runStep("git fetch", repo, "git", "fetch", "origin", "--quiet"); err != nil {
|
|
lcFail(fmt.Errorf("git fetch a échoué : %w", err))
|
|
return
|
|
}
|
|
localRev := gitOutput(repo, "rev-parse", "HEAD")
|
|
remoteRev := gitOutput(repo, "rev-parse", "origin/"+branch)
|
|
if localRev != "" && localRev == remoteRev && !clean && llamaServerBin(repo) != "" {
|
|
lcDone("déjà à jour (" + oldCommit + ") — rien à faire")
|
|
return
|
|
}
|
|
if localRev != remoteRev {
|
|
lcPhase("git pull origin/" + branch + "…")
|
|
if err := runStep("git pull --ff-only", repo, "git", "pull", "--ff-only", "origin", branch); err != nil {
|
|
lcFail(fmt.Errorf("git pull a échoué (modifs locales ?) : %w", err))
|
|
return
|
|
}
|
|
}
|
|
newCommit := gitOutput(repo, "rev-parse", "--short", "HEAD")
|
|
lcMu.Lock()
|
|
lcCur.NewCommit = newCommit
|
|
lcMu.Unlock()
|
|
|
|
// Le binaire en cours d'exécution ne peut pas être réécrit → stop du service
|
|
// pendant le build, redémarrage après (même en échec).
|
|
svcWasUp := serviceIsActive()
|
|
if svcWasUp {
|
|
lcPhase("arrêt du service le temps du build…")
|
|
if err := serviceAction("stop"); err != nil {
|
|
lcAppend("[warn] impossible d'arrêter le service : " + err.Error())
|
|
}
|
|
}
|
|
|
|
ok := lcBuildAndSwitch(repo, clean)
|
|
if svcWasUp {
|
|
lcPhase("redémarrage du service…")
|
|
if err := serviceAction("start"); err != nil {
|
|
lcAppend("[warn] redémarrage du service échoué : " + err.Error())
|
|
}
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if oldCommit == newCommit {
|
|
lcDone("recompilé (" + newCommit + ")")
|
|
} else {
|
|
lcDone("mis à jour : " + oldCommit + " → " + newCommit)
|
|
}
|
|
}
|
|
|
|
// lcBuildAndSwitch détecte le plan, compile llama-server et pointe BIN dessus.
|
|
// Renvoie false (job en échec) si une étape casse.
|
|
func lcBuildAndSwitch(repo string, clean bool) bool {
|
|
plan := detectBuildPlan()
|
|
lcAppend(fmt.Sprintf("plan de build : backend=%s arch=%s jobs=%d", plan.backend, plan.cudaArch, plan.jobs))
|
|
lcPhase("configuration CMake…")
|
|
if err := buildLlamacpp(repo, plan, clean); err != nil {
|
|
lcAppendLogTail(filepath.Join(repo, "configure.log"), filepath.Join(repo, "build.log"))
|
|
lcFail(err)
|
|
return false
|
|
}
|
|
bin := llamaServerBin(repo)
|
|
if bin == "" {
|
|
lcFail(fmt.Errorf("build terminé mais binaire introuvable sous %s", filepath.Join(repo, "build")))
|
|
return false
|
|
}
|
|
lcAppend("binaire compilé : " + bin)
|
|
if err := SetConfigKey("BIN", bin); err != nil {
|
|
lcFail(fmt.Errorf("build ok mais échec d'écriture de BIN : %w", err))
|
|
return false
|
|
}
|
|
lcAppend("BIN mis à jour")
|
|
return true
|
|
}
|
|
|
|
// lcAppendLogTail remonte la fin des logs de build dans le job pour que
|
|
// l'erreur réelle soit visible dans l'UI sans aller chercher les fichiers.
|
|
func lcAppendLogTail(paths ...string) {
|
|
for _, p := range paths {
|
|
b, err := os.ReadFile(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
|
if len(lines) > 30 {
|
|
lines = lines[len(lines)-30:]
|
|
}
|
|
lcAppend("--- fin de " + filepath.Base(p) + " ---")
|
|
for _, l := range lines {
|
|
lcAppend(l)
|
|
}
|
|
}
|
|
}
|