From 2c4140a1a2d3d03cc821695b43afb93a9c323382 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 09:24:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(daemon):=20launchd=20plist=20+=20install/u?= =?UTF-8?q?ninstall=20=E8=84=9A=E6=9C=AC=EF=BC=88tsk=5FnqvOyIZkd1Dn?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供三个文件让 maestrod 作为 macOS User Agent 开机自启、崩溃自拉: - scripts/com.maestro.maestrod.plist — launchd plist 模板 KeepAlive=true(崩溃自拉)、RunAtLoad=true(登录即启) stdout+stderr 统一写入 ~/.maestro/maestrod.log ThrottleInterval=10s 防崩溃风暴;ProcessType=Background 占位符:__NODE__ / __PROJ_DIR__ / __HOME__ / __PATH__ - scripts/install-daemon.sh — 安装脚本 1. 检测 node(>= 20) 2. npm run build 构建项目 3. sed 替换占位符,生成真实 plist 并用 plutil 校验 4. launchctl bootstrap gui/$UID(回退 load -w)加载服务 - scripts/uninstall-daemon.sh — 卸载脚本 launchctl bootout / unload -w 卸载,删除 plist shellcheck 通过(两个脚本);现有 77 个单元测试全部通过。 Co-Authored-By: Claude Sonnet 4.6 --- scripts/com.maestro.maestrod.plist | 71 ++++++++++++++++++ scripts/install-daemon.sh | 113 +++++++++++++++++++++++++++++ scripts/uninstall-daemon.sh | 46 ++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 scripts/com.maestro.maestrod.plist create mode 100644 scripts/install-daemon.sh create mode 100644 scripts/uninstall-daemon.sh diff --git a/scripts/com.maestro.maestrod.plist b/scripts/com.maestro.maestrod.plist new file mode 100644 index 0000000..9061edf --- /dev/null +++ b/scripts/com.maestro.maestrod.plist @@ -0,0 +1,71 @@ + + + + + + + + Label + com.maestro.maestrod + + + ProgramArguments + + __NODE__ + __PROJ_DIR__/dist/daemon/index.js + + + + KeepAlive + + + + RunAtLoad + + + + StandardOutPath + __HOME__/.maestro/maestrod.log + StandardErrorPath + __HOME__/.maestro/maestrod.log + + + WorkingDirectory + __PROJ_DIR__ + + + EnvironmentVariables + + HOME + __HOME__ + PATH + __PATH__ + + + + ThrottleInterval + 10 + + + ProcessType + Background + + + diff --git a/scripts/install-daemon.sh b/scripts/install-daemon.sh new file mode 100644 index 0000000..394f32f --- /dev/null +++ b/scripts/install-daemon.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# install-daemon.sh — 安装 maestrod 为 launchd 用户 Agent +# +# 功能: +# 1. 检测 node 路径 +# 2. 构建项目(tsc → dist/) +# 3. 生成 ~/Library/LaunchAgents/com.maestro.maestrod.plist(填充绝对路径) +# 4. 通过 launchctl 加载服务(开机自启 + 崩溃自拉) +# +# 用法: +# bash scripts/install-daemon.sh +# +# 重装(更新 plist / 重启服务):重新执行本脚本即可。 +set -euo pipefail + +LABEL="com.maestro.maestrod" +PLIST_NAME="${LABEL}.plist" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJ_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +PLIST_TEMPLATE="${SCRIPT_DIR}/${PLIST_NAME}" +PLIST_DEST="${HOME}/Library/LaunchAgents/${PLIST_NAME}" +DATA_DIR="${HOME}/.maestro" + +# ── 1. 检测 Node.js ────────────────────────────────────────── +echo "==> 检测 Node.js…" +NODE_BIN="$(command -v node || true)" +if [[ -z "${NODE_BIN}" ]]; then + echo "错误:找不到 node,请先安装 Node.js >= 20(推荐 Homebrew:brew install node)" >&2 + exit 1 +fi +NODE_VER="$(node --version 2>&1)" +# 主版本号(去掉 "v" 和 ".x.y") +NODE_MAJOR="$(node --version 2>&1 | sed 's/^v//' | cut -d. -f1)" +if [[ "${NODE_MAJOR}" -lt 20 ]]; then + echo "错误:需要 Node.js >= 20,当前 ${NODE_VER}" >&2 + exit 1 +fi +echo " node: ${NODE_BIN} (${NODE_VER})" + +# node 所在目录,确保 launchd 进程能找到 node 和相关工具 +NODE_DIR="$(dirname "${NODE_BIN}")" +# 合并常用路径,去重 +DAEMON_PATH="${NODE_DIR}:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + +# ── 2. 构建项目 ─────────────────────────────────────────────── +echo "==> 构建项目(tsc)…" +cd "${PROJ_DIR}" +npm run build +DAEMON_ENTRY="${PROJ_DIR}/dist/daemon/index.js" +if [[ ! -f "${DAEMON_ENTRY}" ]]; then + echo "错误:构建产物不存在:${DAEMON_ENTRY}" >&2 + exit 1 +fi +echo " 入口文件: ${DAEMON_ENTRY}" + +# ── 3. 确保数据/日志目录存在 ───────────────────────────────── +echo "==> 创建数据目录 ${DATA_DIR}…" +mkdir -p "${DATA_DIR}" +mkdir -p "${HOME}/Library/LaunchAgents" + +# ── 4. 生成 plist(用 sed 填充占位符)──────────────────────── +echo "==> 生成 plist…" +if [[ ! -f "${PLIST_TEMPLATE}" ]]; then + echo "错误:找不到 plist 模板:${PLIST_TEMPLATE}" >&2 + exit 1 +fi +# 使用中间临时文件,避免 sed in-place 跨平台差异 +TMP_PLIST="$(mktemp /tmp/${LABEL}.XXXXXX.plist)" +trap 'rm -f "${TMP_PLIST}"' EXIT + +sed \ + -e "s|__NODE__|${NODE_BIN}|g" \ + -e "s|__PROJ_DIR__|${PROJ_DIR}|g" \ + -e "s|__HOME__|${HOME}|g" \ + -e "s|__PATH__|${DAEMON_PATH}|g" \ + "${PLIST_TEMPLATE}" > "${TMP_PLIST}" + +# 校验生成的 plist +plutil -lint "${TMP_PLIST}" || { echo "错误:生成的 plist 格式不合法" >&2; exit 1; } +cp "${TMP_PLIST}" "${PLIST_DEST}" +echo " 已写入: ${PLIST_DEST}" + +# ── 5. 加载(或重载)服务 ──────────────────────────────────── +echo "==> 加载 launchd 服务…" + +# 若服务已运行,先卸载再重载 +if launchctl list 2>/dev/null | grep -qF "${LABEL}"; then + echo " 检测到旧服务,先卸载…" + launchctl bootout "gui/${UID}" "${PLIST_DEST}" 2>/dev/null \ + || launchctl unload -w "${PLIST_DEST}" 2>/dev/null \ + || true + # 短暂等待彻底退出 + sleep 1 +fi + +# bootstrap 是 macOS 10.11+ 推荐 API;失败时回退到 load -w +if ! launchctl bootstrap "gui/${UID}" "${PLIST_DEST}" 2>/dev/null; then + launchctl load -w "${PLIST_DEST}" +fi + +echo "" +echo "✅ maestrod 安装完成" +echo "" +echo " 服务标识: ${LABEL}" +echo " 日志文件: ${DATA_DIR}/maestrod.log" +echo " API 地址: http://127.0.0.1:4517" +echo "" +echo " 常用命令:" +echo " 查看状态: launchctl list ${LABEL}" +echo " 实时日志: tail -f ${DATA_DIR}/maestrod.log" +echo " 停止服务: launchctl stop ${LABEL}" +echo " 启动服务: launchctl start ${LABEL}" +echo " 卸载: bash ${SCRIPT_DIR}/uninstall-daemon.sh" diff --git a/scripts/uninstall-daemon.sh b/scripts/uninstall-daemon.sh new file mode 100644 index 0000000..456b190 --- /dev/null +++ b/scripts/uninstall-daemon.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# uninstall-daemon.sh — 卸载 maestrod launchd 用户 Agent +# +# 功能: +# 1. 停止并卸载 launchd 服务 +# 2. 删除 ~/Library/LaunchAgents/com.maestro.maestrod.plist +# +# 注意:数据目录 ~/.maestro(SQLite 数据库、日志)不会被删除,需手动清理。 +# +# 用法: +# bash scripts/uninstall-daemon.sh +set -euo pipefail + +LABEL="com.maestro.maestrod" +PLIST_DEST="${HOME}/Library/LaunchAgents/${LABEL}.plist" + +echo "==> 卸载 maestrod (${LABEL})…" + +# ── 1. 停止并卸载服务 ───────────────────────────────────────── +if launchctl list 2>/dev/null | grep -qF "${LABEL}"; then + echo " 服务运行中,正在停止…" + # 优先使用 bootout(macOS 10.11+ 推荐),失败则回退 unload + if launchctl bootout "gui/${UID}" "${PLIST_DEST}" 2>/dev/null; then + echo " 已通过 bootout 卸载" + elif launchctl unload -w "${PLIST_DEST}" 2>/dev/null; then + echo " 已通过 unload 卸载" + else + echo "警告:无法正常卸载服务,将继续删除 plist 文件" >&2 + fi +else + echo " 服务未加载,跳过停止步骤" +fi + +# ── 2. 删除 plist ───────────────────────────────────────────── +if [[ -f "${PLIST_DEST}" ]]; then + rm -f "${PLIST_DEST}" + echo " 已删除: ${PLIST_DEST}" +else + echo " plist 不存在,跳过删除" +fi + +echo "" +echo "✅ maestrod 已卸载" +echo "" +echo " 注意:数据目录 ${HOME}/.maestro 未被删除(保留数据库和日志)。" +echo " 如需彻底清除:rm -rf ${HOME}/.maestro"