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:
nathaninline committed 2026-07-25 11:30:02 +02:00
1 parent 89cbb50fcf
commit a1b0cc0c58
4 files changed
+158 -26

No files matched your search

+29
View File
@@ -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)
}
}
+102 -1
View File
@@ -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
}
+22
View File
@@ -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)
}
+5 -25
View File
@@ -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) {