//go:build darwin package loki import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" ) // sys_service_darwin.go — gestion du service via launchd (LaunchDaemon), équivalent // macOS de sys_service_linux.go (systemd). ⚠️ NON TESTÉ sur un vrai Mac : jusqu'ici // le support macOS était absent (le code systemd/Linux était utilisé par erreur, // cf. issue #4). Implémentation prudente basée sur launchctl load/unload/list. // launchdLabel dérive le label launchd du service (ex. "com.loki.loki"), en // conservant le préfixe hérité « com.loki. » tant que c'est LUI qui est // réellement installé : le plist d'un daemon déjà chargé ne se renomme pas tout // seul, et viser le mauvais label reviendrait à ne plus voir le service. func launchdLabel(svc string) string { modern := "com.loki." + svc if _, err := os.Stat("/Library/LaunchDaemons/" + modern + ".plist"); err == nil { return modern } if legacy := "com.loki." + svc; fileExists("/Library/LaunchDaemons/" + legacy + ".plist") { return legacy } return modern } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil } // launchdPlistPath : chemin du LaunchDaemon (domaine système, exécuté par root // puis abaissé à l'utilisateur cible via la clé UserName du plist). func launchdPlistPath(svc string) string { return "/Library/LaunchDaemons/" + launchdLabel(svc) + ".plist" } // launchdLogPath : sortie standard/erreur du service, sous LOKI_HOME (accessible // en écriture par l'utilisateur du service après le chown de l'installation). func launchdLogPath() string { return filepath.Join(LokiHome(), serviceName()+".log") } // serviceActionOS mappe start/stop/restart/enable/disable sur launchctl. `load -w` // (re)active le service ET le rend persistant au boot ; `unload -w` le désactive. func serviceActionOS(action string) error { svc := serviceName() plist := launchdPlistPath(svc) // Pas de LaunchDaemon installé — cas normal d'un Mac de bureau, où Loki tourne // via Loki.app sans jamais passer par `sudo loki install` : on gère alors le // service comme sous Windows, avec un `loki serve` détaché suivi par un fichier // PID. Aucun droit root requis. Sans ça, `start` échouait sur un plist absent // et l'UI affichait éternellement « service arrêté ». if _, err := os.Stat(plist); err != nil { return userSvcAction(action) } run := func(args ...string) error { bin, a := "launchctl", args if os.Geteuid() != 0 { bin, a = "sudo", append([]string{"-n", "launchctl"}, args...) } cmd := exec.Command(bin, a...) cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr return cmd.Run() } switch action { case "start", "enable": if err := run("load", "-w", plist); err != nil { return err } return checkStarted(svc) case "stop", "disable": if err := run("unload", "-w", plist); err != nil { return err } fmt.Println(green("[ok]") + " arrêté") return nil case "restart": _ = run("unload", plist) // best-effort : peut ne pas être chargé if err := run("load", "-w", plist); err != nil { return err } return checkStarted(svc) case "status": if serviceIsActive() { fmt.Printf("%s %s: actif (launchd)\n", green("[ok]"), svc) } else { fmt.Printf("%s %s: arrêté\n", yellow("[info]"), svc) } fmt.Printf(" logs : %s\n", launchdLogPath()) return nil } return fmt.Errorf("action inconnue: %s", action) } func checkStarted(svc string) error { time.Sleep(2 * time.Second) if serviceIsActive() { fmt.Printf("%s %s: actif\n", green("[ok]"), svc) return nil } fmt.Printf("%s %s: non démarré — derniers logs :\n", red("[ERREUR]"), svc) fmt.Println("------------------------------------------------") if b, err := os.ReadFile(launchdLogPath()); err == nil { lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n") if len(lines) > 20 { lines = lines[len(lines)-20:] } fmt.Println(strings.Join(lines, "\n")) } fmt.Println("------------------------------------------------") fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger config.env\n") return fmt.Errorf("service %s non démarré", svc) } func serviceLogs() error { // tail -f du fichier de log défini dans le plist (StandardOut/ErrorPath). cmd := exec.Command("tail", "-n", "80", "-f", launchdLogPath()) cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr return cmd.Run() } // serviceIsActive : le service est chargé ET possède un PID courant. `launchctl // list