mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Nouveau sys_service_container.go : supervision par fichier PID portée du mode utilisateur macOS (Setsid, SIGTERM sur le groupe puis SIGKILL, log fichier). sys_service_linux.go bascule automatiquement quand /run/systemd/system est absent ou que LOKI_CONTAINER=1 ; une install systemd classique est inchangée. Le tunnel ajean.link suit la même logique (uiServiceCtl / uiServiceActive). Sans ce repli, changer de modèle depuis l'UI (serviceAction restart) échouait sur un systemctl absent — le conteneur était inutilisable. Testé sans systemd : start / status / restart / stop, PID suivi, processus enfant arrêté avec le groupe. build linux+darwin+windows OK, go vet + go test verts.
142 lines
4.4 KiB
Go
142 lines
4.4 KiB
Go
//go:build linux
|
|
|
|
package loki
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// serviceAction wraps `systemctl <action> <svc>` with passwordless sudo where
|
|
// it makes sense, and prints a follow-up status check after start/restart.
|
|
// Sans systemd (conteneur Docker, LOKI_CONTAINER=1), on bascule sur la
|
|
// supervision par fichier PID (sys_service_container.go).
|
|
func serviceAction(action string) error {
|
|
if !systemdAvailable() {
|
|
return userSvcAction(action)
|
|
}
|
|
svc := serviceName()
|
|
needsRoot := action == "start" || action == "stop" || action == "restart" || action == "enable" || action == "disable"
|
|
args := []string{}
|
|
bin := "systemctl"
|
|
if needsRoot && os.Geteuid() != 0 {
|
|
bin = "sudo"
|
|
args = append(args, "-n", "systemctl")
|
|
}
|
|
args = append(args, action, svc)
|
|
cmd := exec.Command(bin, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
switch action {
|
|
case "start", "restart":
|
|
return checkStarted(svc)
|
|
case "stop":
|
|
fmt.Println(green("[ok]") + " arrêté")
|
|
case "enable":
|
|
fmt.Println(green("[ok]") + " démarrage auto activé")
|
|
case "disable":
|
|
fmt.Println(green("[ok]") + " démarrage auto désactivé")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unitState renvoie ActiveState et SubState de l'unité (« activating » /
|
|
// « auto-restart » par exemple).
|
|
func unitState(svc string) (active, sub string) {
|
|
out, _ := exec.Command("systemctl", "show", svc, "-p", "ActiveState", "-p", "SubState").Output()
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
k, v, ok := strings.Cut(strings.TrimSpace(line), "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch k {
|
|
case "ActiveState":
|
|
active = v
|
|
case "SubState":
|
|
sub = v
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// checkStarted attend que l'unité se stabilise, puis dit la vérité.
|
|
//
|
|
// ⚠️ « activating » n'est PAS un succès : avec Restart=on-failure, un moteur qui
|
|
// meurt au démarrage repasse en boucle par activating (SubState auto-restart).
|
|
// L'ancien test l'acceptait, et `loki start` répondait « [ok] activating » sur
|
|
// un service qui ne démarrerait jamais — d'où des rapports « loki start dit ok
|
|
// mais loki test ne répond pas ». On distingue donc le SubState, et on laisse
|
|
// au moteur le temps de charger un gros modèle (il reste en activating, mais
|
|
// PAS en auto-restart).
|
|
func checkStarted(svc string) error {
|
|
var state, sub string
|
|
for i := 0; i < 5; i++ {
|
|
time.Sleep(2 * time.Second)
|
|
state, sub = unitState(svc)
|
|
if state == "active" {
|
|
fmt.Printf("%s %s: actif\n", green("[ok]"), svc)
|
|
return nil
|
|
}
|
|
if state == "failed" || sub == "auto-restart" {
|
|
break // inutile d'attendre : il boucle sur un échec
|
|
}
|
|
}
|
|
if state == "activating" && sub != "auto-restart" {
|
|
// Chargement en cours (un gros .gguf prend des minutes) : légitime.
|
|
fmt.Printf("%s %s: démarrage en cours (chargement du modèle) — %s pour suivre\n",
|
|
green("[ok]"), svc, bold("loki logs"))
|
|
return nil
|
|
}
|
|
what := state
|
|
if sub == "auto-restart" {
|
|
what = "redémarre en boucle (le moteur meurt au lancement)"
|
|
}
|
|
fmt.Printf("%s %s: %s — derniers logs :\n", red("[ERREUR]"), svc, what)
|
|
fmt.Println("------------------------------------------------")
|
|
logs, _ := exec.Command("journalctl", "-u", svc, "-n", "20", "--no-pager").Output()
|
|
fmt.Print(string(logs))
|
|
fmt.Println("------------------------------------------------")
|
|
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger la configuration (BIN, MODEL…)\n")
|
|
return fmt.Errorf("service %s non démarré", svc)
|
|
}
|
|
|
|
func serviceLogs() error {
|
|
if !systemdAvailable() {
|
|
return userServiceLogs()
|
|
}
|
|
cmd := exec.Command("journalctl", "-u", serviceName(), "-n", "80", "-f")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// serviceIsActive reports whether the systemd unit is currently running.
|
|
func serviceIsActive() bool {
|
|
if !systemdAvailable() {
|
|
pid := readServicePID()
|
|
return pid > 0 && processAlive(pid)
|
|
}
|
|
out, _ := exec.Command("systemctl", "is-active", serviceName()).Output()
|
|
return strings.TrimSpace(string(out)) == "active"
|
|
}
|
|
|
|
// serviceLogTail renvoie les n dernières lignes du journal du service (pour
|
|
// l'UI web). Linux : journalctl.
|
|
func serviceLogTail(n int) string {
|
|
if !systemdAvailable() {
|
|
return tailFile(logFilePath(), n)
|
|
}
|
|
out, err := exec.Command("journalctl", "-u", serviceName(), "-n", strconv.Itoa(n), "--no-pager").CombinedOutput()
|
|
if err != nil && len(out) == 0 {
|
|
return "journalctl indisponible : " + err.Error()
|
|
}
|
|
return string(out)
|
|
}
|