"""Génération des fichiers de lancement : ecosystem PM2 (.cjs) et plists launchd.""" import json import plistlib import shlex def _mem(v): if not v: return None if isinstance(v, str): return v return "%dM" % (int(v) // (1024 * 1024)) def _home(path, ctx): """PM2/launchd n'expandent pas « ~ » : chemin absolu obligatoire.""" if isinstance(path, str) and path.startswith("~"): return ctx["HOME"] + path[1:] return path def ecosystem(m, ctx): """Retourne le contenu JS d'un ecosystem.config.cjs pour les processus PM2 (l'exposition publique est une route Caddy sur la passerelle du MacLustr Tunnel, plus aucun processus ngrok local).""" from .manifest import render apps = [] for p in m["processes"]: if p.get("manager", "pm2") != "pm2": continue a = { "name": p["name"], "script": _home(render(p["script"], ctx), ctx), "cwd": _home(render(p.get("cwd") or m["dir"], ctx), ctx), "exec_mode": "fork", "instances": 1, "autorestart": p.get("autorestart", True), "merge_logs": True, "time": True, "env": render(p.get("env") or {}, ctx), } if p.get("args"): args = p["args"] a["args"] = " ".join(shlex.quote(str(x)) for x in render(args, ctx)) if isinstance(args, list) else render(args, ctx) interp = p.get("interpreter") if interp and interp not in ("none", "N/A"): a["interpreter"] = render(interp, ctx) elif interp in ("none",) or (p["script"].endswith((".sh", ".mjs", ".js", ".py", ".cjs")) is False): a["interpreter"] = "none" if p.get("cron_restart"): a["cron_restart"] = p["cron_restart"] if p.get("max_memory_restart"): a["max_memory_restart"] = _mem(p["max_memory_restart"]) if p.get("restart_delay"): a["restart_delay"] = p["restart_delay"] if p.get("node_args"): a["node_args"] = p["node_args"] apps.append(a) head = "// généré par maclustr-dispatch — ne pas éditer à la main (app: %s, nœud: %s)\n" % (m["app"], ctx.get("NODE")) return head + "module.exports = { apps: %s };\n" % json.dumps(apps, indent=2, ensure_ascii=False) def plist(item, ctx): from .manifest import render d = { "Label": item["label"], "ProgramArguments": render(item["program_arguments"], ctx), "RunAtLoad": item.get("run_at_load", True), "KeepAlive": item.get("keep_alive", True), "StandardOutPath": render(item.get("stdout") or "{{HOME}}/Library/Logs/%s.log" % item["label"], ctx), "StandardErrorPath": render(item.get("stderr") or "{{HOME}}/Library/Logs/%s.err.log" % item["label"], ctx), } if item.get("working_directory"): d["WorkingDirectory"] = _home(render(item["working_directory"], ctx), ctx) env = dict(item.get("env") or {}) env.setdefault("HOME", ctx["HOME"]) env.setdefault("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin") d["EnvironmentVariables"] = render(env, ctx) if item.get("start_interval"): d["StartInterval"] = int(item["start_interval"]) if item.get("throttle_interval"): d["ThrottleInterval"] = int(item["throttle_interval"]) return plistlib.dumps(d).decode()