mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
La façade (rust/loki-rs/src/backends) sert le statut du moteur llama.cpp (dépôt git, binaire, moteur de l'image, précompilé, moteur téléchargé, plan de build et conseil), la vérification git, la dernière release et l'installation des binaires précompilés (extraction avec liens, VERSION, ménage), la bascule fast/opt, les backends custom et leur désinstallation, et les GPU vus par un moteur (caches mémoire et disque). Elle détient LE job des moteurs (state/build_job + loki-build.log, repris au démarrage). Les jobs que le Go fait encore tourner (compilation depuis les sources, moteur de l'image, Strata) lui rapportent chaque étape par /internal/facade/v1/build-job/* ; le Go garde le garde de l'optimiseur et ne restaure plus de job quand la façade tient le lot. Correction du Go : prebuiltVersionFormat lisait le format comme 3e champ du marqueur VERSION ; hors CUDA Windows (« b300 fmt2 ») il n'y en a que deux, et chaque vérification retéléchargeait la release au lieu de dire « déjà à jour ». Le port suit. Parité : parity_backends_test.go (Linux) → tests/parity_backends, rejoué par tests/backends.rs (fausses machines dans le PATH, faux GitHub, dépôt git reproductible) ; TestFacadeBackends pour les étapes transmises. Mutations : 55 tuées sur 57, deux inobservables sous Linux (cudart Windows, garde d'un avertissement). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
699 lines
22 KiB
Go
699 lines
22 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) {
|
|
if facadeBackends() {
|
|
facadeJobNote("line", map[string]any{"line": line})
|
|
return
|
|
}
|
|
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) {
|
|
if facadeBackends() {
|
|
facadeJobNote("phase", map[string]any{"phase": phase})
|
|
return
|
|
}
|
|
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 {
|
|
// Compiler, installer ou changer de moteur l'arrête et le relance : pas pendant
|
|
// une optimisation (backend_tune_lock.go).
|
|
if err := tuneGuard(); err != nil {
|
|
return err
|
|
}
|
|
if facadeBackends() { // le job est tenu par la façade (facade_backends.go)
|
|
return facadeJobStart(action, run)
|
|
}
|
|
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) {
|
|
if facadeBackends() {
|
|
facadeJobNote("fail", map[string]any{"error": err.Error()})
|
|
return
|
|
}
|
|
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) {
|
|
if facadeBackends() {
|
|
facadeJobNote("done", map[string]any{"msg": msg})
|
|
return
|
|
}
|
|
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) {
|
|
if tuneDenyHTTP(w) {
|
|
return
|
|
}
|
|
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 {
|
|
if facadeBackends() {
|
|
return facadeJobSnapshot(from, withLines)
|
|
}
|
|
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")
|
|
lcSetCommit("old", oldCommit)
|
|
|
|
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")
|
|
lcSetCommit("new", newCommit)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|