mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-13 18:28:03 +02:00
macOS: support launchd (install/start/stop/logs) au lieu du code systemd hérité (#4) — non testé sur Mac. Isole le code Linux dans *_linux.go, ajoute *_darwin.go
This commit is contained in:
1 parent
2862a2f27a
commit
deb05f50ea
5 files changed
+309
-3
No files matched your search
@@ -0,0 +1,221 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const configTemplate = `# Configuration JEAN — édite-moi puis: jean restart
|
||||
# Le service systemd lit ce fichier et lance ton binaire llama.cpp.
|
||||
|
||||
# Chemins
|
||||
BIN="/usr/local/bin/llama-server"
|
||||
MODEL="/home/USER/models/your-model.gguf"
|
||||
|
||||
# Serveur
|
||||
PORT="8080"
|
||||
HOST="0.0.0.0"
|
||||
|
||||
# Inference
|
||||
CTX="32768"
|
||||
BATCH="2048"
|
||||
UBATCH="512"
|
||||
NGL="999"
|
||||
|
||||
# Args supplémentaires passés à llama-server
|
||||
EXTRA_ARGS=""
|
||||
`
|
||||
|
||||
const sudoersTemplate = `# Allow %s to manage the %s systemd unit without a password (installed by jean).
|
||||
%s ALL=(root) NOPASSWD: /bin/systemctl start %s, /bin/systemctl stop %s, /bin/systemctl restart %s, /bin/systemctl enable %s, /bin/systemctl disable %s
|
||||
`
|
||||
|
||||
const serviceUnitTemplate = `[Unit]
|
||||
Description=JEAN llama.cpp server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=%s
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
# Priorité CPU : on remonte le process pour qu'il ne soit pas dépriorisé face
|
||||
# aux tâches de fond (sampling/orchestration côté CPU pèsent sur le débit même
|
||||
# en inference GPU). Nice négatif + scheduling normal réactif.
|
||||
Nice=-10
|
||||
CPUSchedulingPolicy=other
|
||||
CPUAccounting=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
func cmdInstall(args []string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("jean install doit être exécuté en root (sudo jean install)")
|
||||
}
|
||||
targetUser := os.Getenv("SUDO_USER")
|
||||
if targetUser == "" {
|
||||
targetUser = "root"
|
||||
}
|
||||
for _, a := range args {
|
||||
if strings.HasPrefix(a, "--user=") {
|
||||
targetUser = strings.TrimPrefix(a, "--user=")
|
||||
}
|
||||
}
|
||||
u, err := user.Lookup(targetUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("utilisateur '%s' introuvable: %w", targetUser, err)
|
||||
}
|
||||
jeanHome := defaultJeanHome()
|
||||
if v := os.Getenv("JEAN_HOME"); v != "" {
|
||||
jeanHome = v
|
||||
}
|
||||
svc := serviceName()
|
||||
|
||||
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
|
||||
fmt.Printf(" JEAN_HOME = %s\n", jeanHome)
|
||||
fmt.Printf(" service = %s\n", svc)
|
||||
|
||||
// 1. Create directories
|
||||
for _, d := range []string{jeanHome, filepath.Join(jeanHome, "configs"), filepath.Join(jeanHome, "SKILLS")} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Drop a config.env if none exists
|
||||
conf := filepath.Join(jeanHome, "config.env")
|
||||
if _, err := os.Stat(conf); os.IsNotExist(err) {
|
||||
body := strings.ReplaceAll(configTemplate, "USER", targetUser)
|
||||
if err := os.WriteFile(conf, []byte(body), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s écrit %s\n", green("✓"), conf)
|
||||
}
|
||||
|
||||
// 3. Symlink current binary to /usr/local/bin/jean
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rp, e := filepath.EvalSymlinks(self); e == nil {
|
||||
self = rp
|
||||
}
|
||||
target := "/usr/local/bin/jean"
|
||||
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible (l'utilisateur a
|
||||
// posé le binaire dans /usr/local/bin/jean puis lancé `sudo jean install`),
|
||||
// ne surtout pas Remove+Symlink sur soi-même — ça effacerait le vrai binaire
|
||||
// et créerait /usr/local/bin/jean -> /usr/local/bin/jean (« Too many levels
|
||||
// of symbolic links »). On compare les chemins réels (résolus).
|
||||
alreadyInPlace := false
|
||||
if tp, e := filepath.EvalSymlinks(target); e == nil && tp == self {
|
||||
alreadyInPlace = true
|
||||
}
|
||||
if alreadyInPlace {
|
||||
fmt.Printf(" %s %s est déjà le binaire installé (aucun lien à créer)\n", green("✓"), target)
|
||||
} else {
|
||||
_ = os.Remove(target)
|
||||
if err := os.Symlink(self, target); err != nil {
|
||||
// fall back to copy if symlink fails (e.g. cross-fs)
|
||||
if data, err := os.ReadFile(self); err == nil {
|
||||
_ = os.WriteFile(target, data, 0o755)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
||||
}
|
||||
|
||||
// 4. Drop /etc/default/jean so root invocations resolve JEAN_HOME correctly.
|
||||
defaults := fmt.Sprintf("# Generated by jean install — racine des configs/skills/SKILLS\nJEAN_HOME=%s\n", jeanHome)
|
||||
if err := os.WriteFile("/etc/default/jean", []byte(defaults), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s /etc/default/jean\n", green("✓"))
|
||||
|
||||
// 5. Write the systemd unit (ExecStart = `jean serve`, no start.sh needed)
|
||||
unit := fmt.Sprintf(serviceUnitTemplate, targetUser, jeanHome, "/usr/local/bin/jean serve")
|
||||
unitPath := "/etc/systemd/system/" + svc + ".service"
|
||||
if err := os.WriteFile(unitPath, []byte(unit), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s %s\n", green("✓"), unitPath)
|
||||
|
||||
// 5. Sudoers drop-in
|
||||
sudoers := fmt.Sprintf(sudoersTemplate, targetUser, svc, targetUser, svc, svc, svc, svc, svc)
|
||||
sudoersPath := "/etc/sudoers.d/jean-" + svc
|
||||
if err := os.WriteFile(sudoersPath, []byte(sudoers), 0o440); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s %s\n", green("✓"), sudoersPath)
|
||||
|
||||
// 6. chown JEAN_HOME contents to target user
|
||||
chown(jeanHome, u)
|
||||
|
||||
// 7. systemd reload
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||
fmt.Printf("\nProchaines étapes :\n")
|
||||
fmt.Printf(" 1. édite la config : %s\n", bold("sudo -u "+targetUser+" jean edit"))
|
||||
fmt.Printf(" (renseigne BIN, MODEL, etc.)\n")
|
||||
fmt.Printf(" 2. démarre le service: %s\n", bold("sudo -u "+targetUser+" jean start"))
|
||||
fmt.Printf(" 3. UI web : %s\n", bold("sudo -u "+targetUser+" jean web"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdUninstall(args []string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("jean uninstall doit être exécuté en root")
|
||||
}
|
||||
svc := serviceName()
|
||||
keepData := false
|
||||
for _, a := range args {
|
||||
if a == "--purge" {
|
||||
keepData = false
|
||||
}
|
||||
if a == "--keep-data" {
|
||||
keepData = true
|
||||
}
|
||||
}
|
||||
_ = exec.Command("systemctl", "stop", svc).Run()
|
||||
_ = exec.Command("systemctl", "disable", svc).Run()
|
||||
for _, p := range []string{
|
||||
"/etc/systemd/system/" + svc + ".service",
|
||||
"/etc/sudoers.d/jean-" + svc,
|
||||
"/etc/default/jean",
|
||||
"/usr/local/bin/jean",
|
||||
} {
|
||||
if err := os.Remove(p); err == nil {
|
||||
fmt.Printf(" %s %s\n", green("✓"), p)
|
||||
}
|
||||
}
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
if !keepData {
|
||||
fmt.Println(dim("(données utilisateur conservées — supprime $JEAN_HOME manuellement si tu veux purger)"))
|
||||
}
|
||||
fmt.Println(green("[ok]") + " désinstallé")
|
||||
return nil
|
||||
}
|
||||
|
||||
// chown recursively changes ownership of path to the given user/group.
|
||||
func chown(path string, u *user.User) {
|
||||
var uid, gid int
|
||||
fmt.Sscanf(u.Uid, "%d", &uid)
|
||||
fmt.Sscanf(u.Gid, "%d", &gid)
|
||||
filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
|
||||
if err == nil {
|
||||
_ = os.Chown(p, uid, gid)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user