mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 21:48:47 +02:00
Suite du renommage. Principe directeur : le code s'adapte a ce qui est DEJA installe sur la machine, plutot que d'exiger une migration privilegiee qui pourrait echouer et laisser un serveur sans service. Services (sys_unitname.go). Une mise a jour remplace le binaire, pas les unites systemd : un serveur deja installe tourne encore sous jean.service et jean-link.service. serviceName() et linkServiceName() resolvent donc le nom a l'usage — l'unite qui existe reellement — au lieu de supposer "ajean*". Sans ca, le premier systemctl restart apres MAJ echouerait, et sur un serveur distant ca veut dire l'acces coupe sans terminal pour reparer. Meme logique pour le label launchd, ou le prefixe com.jean. est conserve tant que c'est lui qui est charge. Le fichier PID du worker de lien est repris au passage (jean-link.pid -> ajean-link.pid) : c'est lui qui dit si un worker tourne, et sans reprise la premiere version renommee en aurait demarre un second, donnant deux tunnels concurrents vers le relais. Binaire. installedExePath vise desormais ajean(.exe), et l'installation pose un alias "jean" a cote — lien symbolique sous Unix, copie sous Windows ou les liens demandent des droits qu'on n'a pas toujours. La commande d'hier reste tapable : on ne controle ni les alias shell, ni les cron, ni les scripts de deploiement, ni les tutos deja ecrits. /etc/default/ajean est ecrit avec AJEAN_HOME ET JEAN_HOME, et l'ancien /etc/default/jean est laisse en place plutot que supprime : des scripts d'utilisateurs le sourcent. Releases. Les workflows produisent ajean-<os>-<arch> puis en copient un jeu jean-<os>-<arch> identique, une fois les binaires macOS recuperes. Les deux jeux sont dans SHA256SUMS. Ces copies sont indispensables : le parc installe ne sait chercher que jean-*, il ne pourrait donc meme pas atteindre la version qui apprend a lire ajean-*. A retirer quand le parc aura bascule. Le bundle devient AJEAN.app, avec le zip publie sous les deux noms pour ne pas casser les liens de telechargement deja diffuses. La detection du mode application se basant sur /Contents/MacOS et non sur le nom du bundle, elle n'est pas affectee.
206 lines
6.4 KiB
Go
206 lines
6.4 KiB
Go
//go:build darwin
|
|
|
|
package ajean
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// sys_install_darwin.go — installation macOS via launchd (LaunchDaemon), équivalent
|
|
// de sys_install_linux.go (systemd). ⚠️ NON TESTÉ sur un vrai Mac : le support macOS
|
|
// était totalement absent (le code systemd/Linux tournait par erreur et échouait
|
|
// sur « systemctl: command not found » / « /etc/default/jean », cf. issue #4).
|
|
// Implémentation prudente ; à valider sur une machine Apple.
|
|
|
|
const configTemplate = `# Configuration JEAN — édite-moi puis: jean restart
|
|
# Le service launchd lit ce fichier et lance ton binaire llama.cpp.
|
|
|
|
# Chemins
|
|
BIN="/usr/local/bin/llama-server"
|
|
MODEL="/Users/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=""
|
|
`
|
|
|
|
// launchdPlistTemplate — champs formatés : Label, UserName, WorkingDirectory,
|
|
// JEAN_HOME, StandardOutPath, StandardErrorPath. KeepAlive/SuccessfulExit=false
|
|
// ≈ Restart=on-failure ; RunAtLoad relance au boot une fois chargé avec `-w`.
|
|
const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key><string>%s</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/local/bin/jean</string>
|
|
<string>serve</string>
|
|
</array>
|
|
<key>UserName</key><string>%s</string>
|
|
<key>WorkingDirectory</key><string>%s</string>
|
|
<key>EnvironmentVariables</key>
|
|
<dict><key>JEAN_HOME</key><string>%s</string></dict>
|
|
<key>RunAtLoad</key><true/>
|
|
<key>KeepAlive</key>
|
|
<dict><key>SuccessfulExit</key><false/></dict>
|
|
<key>StandardOutPath</key><string>%s</string>
|
|
<key>StandardErrorPath</key><string>%s</string>
|
|
</dict>
|
|
</plist>
|
|
`
|
|
|
|
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)
|
|
}
|
|
ajeanHome := defaultAjeanHome()
|
|
if v := os.Getenv("JEAN_HOME"); v != "" {
|
|
ajeanHome = v
|
|
}
|
|
svc := serviceName()
|
|
|
|
fmt.Printf("Installation pour utilisateur %s\n", cyan(targetUser))
|
|
fmt.Printf(" JEAN_HOME = %s\n", ajeanHome)
|
|
fmt.Printf(" service = %s (launchd)\n", svc)
|
|
|
|
// 1. Répertoires
|
|
for _, d := range []string{ajeanHome, filepath.Join(ajeanHome, "configs"), filepath.Join(ajeanHome, "SKILLS")} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 2. config.env si absent
|
|
conf := filepath.Join(ajeanHome, "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 idempotent vers /usr/local/bin/jean — même garde-fou que Linux
|
|
// (issue #5 : ne pas se lier sur soi-même si on tourne déjà depuis la cible).
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rp, e := filepath.EvalSymlinks(self); e == nil {
|
|
self = rp
|
|
}
|
|
target := "/usr/local/bin/jean"
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
if data, err := os.ReadFile(self); err == nil {
|
|
_ = os.WriteFile(target, data, 0o755)
|
|
}
|
|
}
|
|
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
|
}
|
|
|
|
// 4. /etc/default/ajean pour que les invocations CLI résolvent AJEAN_HOME.
|
|
// Les deux clés sont écrites : des scripts d'utilisateurs sourcent ce fichier
|
|
// et lisent encore $JEAN_HOME.
|
|
_ = os.MkdirAll("/etc/default", 0o755)
|
|
defaults := fmt.Sprintf("# Generated by ajean install — racine des configs/memoire/presets\nAJEAN_HOME=%s\nJEAN_HOME=%s\n", ajeanHome, ajeanHome)
|
|
if err := os.WriteFile("/etc/default/ajean", []byte(defaults), 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
|
|
|
|
// 5. LaunchDaemon plist (le log va sous JEAN_HOME, accessible au user cible)
|
|
logPath := filepath.Join(ajeanHome, svc+".log")
|
|
plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), targetUser, ajeanHome, ajeanHome, logPath, logPath)
|
|
plistPath := launchdPlistPath(svc)
|
|
if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(" %s %s\n", green("✓"), plistPath)
|
|
|
|
// 6. chown JEAN_HOME au user cible
|
|
chown(ajeanHome, u)
|
|
|
|
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 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()
|
|
_ = exec.Command("launchctl", "unload", "-w", launchdPlistPath(svc)).Run()
|
|
for _, p := range []string{
|
|
launchdPlistPath(svc),
|
|
"/etc/default/ajean",
|
|
installedExePath(),
|
|
legacyExePath(),
|
|
} {
|
|
if err := os.Remove(p); err == nil {
|
|
fmt.Printf(" %s %s\n", green("✓"), p)
|
|
}
|
|
}
|
|
// On ne supprime jamais JEAN_HOME automatiquement (modèles, configs).
|
|
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
|
|
})
|
|
}
|