mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
macOS : JEAN_HOME utilisateur, RAM detectee, PATH Homebrew depuis le Finder
- /etc/jean n'est pas ecrivable par une app lancee depuis le Finder -> repli sur ~/Library/Application Support/jean (sauf sudo jean install) - /api/ram lisait /proc/meminfo uniquement : ajout sysctl+vm_stat (macOS) et GlobalMemoryStatusEx (Windows, qui ne remontait rien non plus) - PATH herite du Finder complete avec Homebrew/MacPorts : sans ca cmake, git et brew sont invisibles pour la compilation du backend
This commit is contained in:
1 parent
89cbb50fcf
commit
a1b0cc0c58
4 files changed
+158
-26
No files matched your search
@@ -30,7 +30,36 @@ func setupConsole() bool {
|
||||
}
|
||||
// .../Jean.app/Contents/MacOS/jean
|
||||
if strings.HasSuffix(filepath.Dir(exe), "/Contents/MacOS") {
|
||||
fixFinderPath()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fixFinderPath complète le PATH famélique hérité du Finder. Une app lancée par
|
||||
// LaunchServices reçoit /usr/bin:/bin:/usr/sbin:/sbin — pas les dossiers de
|
||||
// Homebrew : sans ça, Jean ne voit ni `brew`, ni `cmake`, ni `git`, et la
|
||||
// compilation d'un backend échoue alors que les outils sont bien installés.
|
||||
func fixFinderPath() {
|
||||
extra := []string{
|
||||
"/opt/homebrew/bin", "/opt/homebrew/sbin", // Homebrew Apple Silicon
|
||||
"/usr/local/bin", "/usr/local/sbin", // Homebrew Intel
|
||||
"/opt/local/bin", // MacPorts
|
||||
}
|
||||
path := os.Getenv("PATH")
|
||||
have := map[string]bool{}
|
||||
for _, p := range strings.Split(path, ":") {
|
||||
have[p] = true
|
||||
}
|
||||
var add []string
|
||||
for _, p := range extra {
|
||||
if !have[p] {
|
||||
if fi, err := os.Stat(p); err == nil && fi.IsDir() {
|
||||
add = append(add, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(add) > 0 {
|
||||
_ = os.Setenv("PATH", strings.Join(add, ":")+":"+path)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,41 @@ import (
|
||||
|
||||
// defaultJeanHome is the data root when neither $JEAN_HOME nor /etc/default/jean
|
||||
// override it.
|
||||
func defaultJeanHome() string { return "/etc/jean" }
|
||||
//
|
||||
// Linux : /etc/jean (machine de prod, service systemd lancé par root).
|
||||
// macOS : /etc/jean n'est PAS écrivable par une app lancée depuis le Finder
|
||||
// (« mkdir /etc/jean: permission denied »), et un Mac est une machine de bureau,
|
||||
// pas un serveur — on retombe donc sur le dossier utilisateur standard, sauf si
|
||||
// un `sudo jean install` a déjà créé /etc/jean à notre nom.
|
||||
func defaultJeanHome() string {
|
||||
if runtime.GOOS != "darwin" || os.Geteuid() == 0 {
|
||||
return "/etc/jean"
|
||||
}
|
||||
if isWritableDir("/etc/jean") {
|
||||
return "/etc/jean" // installé par `sudo jean install` puis chown à l'utilisateur
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, "Library", "Application Support", "jean")
|
||||
}
|
||||
return "/etc/jean"
|
||||
}
|
||||
|
||||
// isWritableDir teste qu'un dossier existe ET qu'on peut y écrire (le seul test
|
||||
// fiable : les permissions POSIX seules ignorent ACL, SIP, volumes read-only).
|
||||
func isWritableDir(dir string) bool {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil || !fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
probe := filepath.Join(dir, ".jean-write-test")
|
||||
f, err := os.OpenFile(probe, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
f.Close()
|
||||
_ = os.Remove(probe)
|
||||
return true
|
||||
}
|
||||
|
||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "nano" }
|
||||
@@ -194,3 +228,70 @@ func execServer(bin string, args []string) error {
|
||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "/bin/bash", "-c", command)
|
||||
}
|
||||
|
||||
// ramUsageMB renvoie (utilisée, totale) en Mo pour /api/ram. Linux : /proc/meminfo.
|
||||
// macOS : sysctl pour le total, vm_stat pour ce qui est réellement libre (pages
|
||||
// free + inactive + speculative ; le reste — wired, active, compressé — est
|
||||
// considéré occupé, comme le fait le Moniteur d'activité).
|
||||
func ramUsageMB() (used, total int) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
total = int(totalRAMGB() * 1024)
|
||||
if total == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
out, err := exec.Command("vm_stat").Output()
|
||||
if err != nil {
|
||||
return 0, total
|
||||
}
|
||||
pageSize := 4096
|
||||
freePages := 0
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.HasPrefix(line, "Mach Virtual Memory Statistics") {
|
||||
// « … (page size of 16384 bytes) » — l'Apple Silicon n'est pas en 4 Ko.
|
||||
if i := strings.Index(line, "page size of "); i >= 0 {
|
||||
if v, err := strconv.Atoi(strings.Fields(line[i+len("page size of "):])[0]); err == nil {
|
||||
pageSize = v
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(k) {
|
||||
case "Pages free", "Pages inactive", "Pages speculative":
|
||||
if n, err := strconv.Atoi(strings.Trim(strings.TrimSpace(v), ".")); err == nil {
|
||||
freePages += n
|
||||
}
|
||||
}
|
||||
}
|
||||
freeMB := freePages * pageSize / (1024 * 1024)
|
||||
if freeMB > total {
|
||||
freeMB = total
|
||||
}
|
||||
return total - freeMB, total
|
||||
}
|
||||
b, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
var totalKB, availKB int
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 2 {
|
||||
continue
|
||||
}
|
||||
v, _ := strconv.Atoi(f[1]) // kB
|
||||
switch f[0] {
|
||||
case "MemTotal:":
|
||||
totalKB = v
|
||||
case "MemAvailable:":
|
||||
availKB = v
|
||||
}
|
||||
}
|
||||
if totalKB == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return (totalKB - availKB) / 1024, totalKB / 1024
|
||||
}
|
||||
@@ -368,3 +368,25 @@ func cudaPathEnv(toolkitDir string) []string {
|
||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||
return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command))
|
||||
}
|
||||
|
||||
// ramUsageMB renvoie (utilisée, totale) en Mo pour /api/ram (GlobalMemoryStatusEx).
|
||||
func ramUsageMB() (used, total int) {
|
||||
var m struct {
|
||||
dwLength uint32
|
||||
dwMemoryLoad uint32
|
||||
ullTotalPhys uint64
|
||||
ullAvailPhys uint64
|
||||
ullTotalPageFile uint64
|
||||
ullAvailPageFile uint64
|
||||
ullTotalVirtual uint64
|
||||
ullAvailVirtual uint64
|
||||
ullAvailExtendedVirtual uint64
|
||||
}
|
||||
m.dwLength = uint32(unsafe.Sizeof(m))
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
if r, _, _ := kernel32.NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&m))); r == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
const mb = 1024 * 1024
|
||||
return int((m.ullTotalPhys - m.ullAvailPhys) / mb), int(m.ullTotalPhys / mb)
|
||||
}
|
||||
@@ -71,32 +71,12 @@ func handleVram(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// handleRam renvoie la RAM système {used, total} en Mo (mêmes unités que
|
||||
// /api/vram → l'UI divise par 1024 pour des Gio). Lit /proc/meminfo (Linux, la
|
||||
// box de prod). Sur un OS sans /proc, renvoie total=0 → l'UI masque le bloc.
|
||||
// /api/vram → l'UI divise par 1024 pour des Gio). La mesure est spécifique à
|
||||
// l'OS (ramUsageMB : /proc/meminfo sous Linux, sysctl+vm_stat sur macOS,
|
||||
// GlobalMemoryStatusEx sous Windows). total=0 → l'UI masque le bloc.
|
||||
func handleRam(w http.ResponseWriter, r *http.Request) {
|
||||
out := map[string]any{"used": 0, "total": 0}
|
||||
b, err := os.ReadFile("/proc/meminfo")
|
||||
if err == nil {
|
||||
var totalKB, availKB int
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 2 {
|
||||
continue
|
||||
}
|
||||
v, _ := strconv.Atoi(f[1]) // valeur en kB
|
||||
switch f[0] {
|
||||
case "MemTotal:":
|
||||
totalKB = v
|
||||
case "MemAvailable:":
|
||||
availKB = v
|
||||
}
|
||||
}
|
||||
if totalKB > 0 {
|
||||
out["total"] = totalKB / 1024
|
||||
out["used"] = (totalKB - availKB) / 1024
|
||||
}
|
||||
}
|
||||
sendJSON(w, 200, out)
|
||||
used, total := ramUsageMB()
|
||||
sendJSON(w, 200, map[string]any{"used": used, "total": total})
|
||||
}
|
||||
|
||||
func handleConfigEnv(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user