|
| 1 | +#!/usr/bin/env bash |
| 2 | +# 安装 macOS 日志轮转定时任务(launchd,无需 sudo)。 |
| 3 | +# |
| 4 | +# ./scripts/install-logrotate.sh # 渲染 + 安装 + 加载 |
| 5 | +# ./scripts/install-logrotate.sh --uninstall # 停止并移除 |
| 6 | +# ./scripts/install-logrotate.sh --now # 安装后立即跑一次轮转 |
| 7 | +# |
| 8 | +# 为什么不用 newsyslog: |
| 9 | +# macOS 的 newsyslog 只有 rename 式轮转(flags 无 copytruncate),而 launchd |
| 10 | +# 重定向给服务的 fd 仍指向旧 inode → 轮转后进程继续写已归档的文件,新路径 |
| 11 | +# 的文件直到重启才出现,中间日志全丢(实测丢过 21.7h)。详见 |
| 12 | +# scripts/rotate-logs.sh 顶部说明。 |
| 13 | +# |
| 14 | +# 模板 deploy/launchd/com.coding2api.logrotate.plist 用 __PROJECT_ROOT__ |
| 15 | +# 占位符;launchd 不展开环境变量,路径只能在安装时渲染成绝对路径。 |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 19 | +LABEL="com.coding2api.logrotate" |
| 20 | +TEMPLATE="$ROOT/deploy/launchd/$LABEL.plist" |
| 21 | +DEST="$HOME/Library/LaunchAgents/$LABEL.plist" |
| 22 | +DOMAIN="gui/$(id -u)" |
| 23 | + |
| 24 | +if [[ "${1:-}" == "--uninstall" ]]; then |
| 25 | + launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true |
| 26 | + rm -f "$DEST" |
| 27 | + echo "已卸载 $LABEL 并移除 $DEST" |
| 28 | + exit 0 |
| 29 | +fi |
| 30 | + |
| 31 | +if [[ ! -f "$TEMPLATE" ]]; then |
| 32 | + echo "找不到模板: $TEMPLATE" >&2 |
| 33 | + exit 1 |
| 34 | +fi |
| 35 | + |
| 36 | +mkdir -p "$(dirname "$DEST")" |
| 37 | +# LC_ALL=C:模板含中文注释,按字节替换占位符,避免 locale 改坏 UTF-8。 |
| 38 | +LC_ALL=C sed "s|__PROJECT_ROOT__|$ROOT|g" "$TEMPLATE" > "$DEST" |
| 39 | + |
| 40 | +if ! plutil -lint "$DEST" >/dev/null; then |
| 41 | + echo "渲染后的 plist 非法,已保留现场: $DEST" >&2 |
| 42 | + exit 1 |
| 43 | +fi |
| 44 | + |
| 45 | +# 先卸载旧的(未加载时 bootout 会失败,忽略),再加载新定义。 |
| 46 | +# bootout 是异步的:立即 bootstrap 会撞上「Input/output error」,小睡重试。 |
| 47 | +launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true |
| 48 | +loaded=1 |
| 49 | +for _ in 1 2 3 4 5; do |
| 50 | + sleep 1 |
| 51 | + if launchctl bootstrap "$DOMAIN" "$DEST" 2>/dev/null; then |
| 52 | + loaded=0 |
| 53 | + break |
| 54 | + fi |
| 55 | +done |
| 56 | +if [[ "$loaded" -ne 0 ]]; then |
| 57 | + echo "bootstrap 失败:launchd 迟迟未释放旧实例,稍后手动重跑本脚本" >&2 |
| 58 | + exit 1 |
| 59 | +fi |
| 60 | +echo "已安装: $DEST(每 6 小时按阈值截断式轮转)" |
| 61 | +echo "查看状态: launchctl print $DOMAIN/$LABEL | grep -E 'state|last exit'" |
| 62 | +echo "手动跑一次: launchctl kickstart $DOMAIN/$LABEL" |
| 63 | + |
| 64 | +if [[ "${1:-}" == "--now" ]]; then |
| 65 | + echo "== 立即执行一次 ==" |
| 66 | + "$ROOT/scripts/rotate-logs.sh" --dry-run |
| 67 | + echo "(上面是 dry-run;真要轮转请直接跑 ./scripts/rotate-logs.sh)" |
| 68 | +fi |
0 commit comments