一、背景:为什么我要折腾 eDiary 云同步
我平时比较喜欢用 eDiary 记录一些日常安排、问题处理过程、技术排查笔记之类的内容。
最近把 Windows 换成了 MacBook,刚好发现 eDiary 也有 macOS 版本,于是就想着把原来 Windows 上的日记本迁移到 Mac 上继续使用。
后来又想到,我办公电脑上也装了 eDiary,用来记录每天的工作安排和问题处理笔记;家里的 Mac 也想继续查看和编辑这些内容。如果两台 Mac 各写各的,时间一长就会变成两份日记本,后续查看和整理都不方便。
所以我就产生了一个想法:把两台 Mac 上的 eDiary 日记本整理成一份,然后通过 iCloud Drive 做同步。
我的预期效果很简单:
下班前在办公 Mac 上退出 eDiary,日记本同步到 iCloud;回到家后,在个人 Mac 上打开 eDiary,可以继续编辑,也可以查看历史笔记。
这样遇到类似问题时,就不用重新翻聊天记录、翻命令历史,直接查自己之前写过的笔记就行。好记性不如烂笔头,这个习惯对我来说还是挺有用的。
当然,这里也有一个前提:如果日记里涉及公司内部信息、服务器信息、账号信息、故障细节等敏感内容,一定要做好脱敏,不要把敏感数据同步到不该同步的地方。
一开始我以为这个事情会很简单:把 eDiary 的 .edf 文件放到 iCloud Drive,然后让两台 Mac 都访问同一份文件就好了。
结果真正配置完后发现,eDiary 编辑完退出后,iCloud 监听到了文件变化,但上传一直卡在 0%。后面排查日志时,发现 fileproviderd 在处理这个 .edf 文件时遇到了 flock 相关阻塞,导致同步任务没有继续推进。
最后我做了一个相对保守的方案:
- eDiary 的
.edf文件放到 iCloud Drive; - eDiary 本地数据目录通过软链接指向 iCloud Drive 中的
.edf文件; - eDiary 退出后,通过 LaunchAgent 检测状态变化;
- 检测到 eDiary 从运行变为退出后,自动重启
fileproviderd; - 打开 eDiary 前,通过自制启动器触发 iCloud 下载,并等待文件稳定;
- 最后做成一个
Cloud eDiary.app,像普通应用一样从启动台打开。
这篇文章记录的就是整个迁移、排查和自动化改造过程。
说明:这不是官方推荐方案,而是我基于自己使用场景做的个人 workaround。适合单人、单端编辑、退出后同步、另一端再打开的场景,不适合多端同时编辑。
二、整体方案设计
最终的整体链路大概是这样:
#mermaid-svg-5oX6q4oWHaSeU04v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5oX6q4oWHaSeU04v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5oX6q4oWHaSeU04v .error-icon{fill:#552222;}#mermaid-svg-5oX6q4oWHaSeU04v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5oX6q4oWHaSeU04v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5oX6q4oWHaSeU04v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5oX6q4oWHaSeU04v .marker.cross{stroke:#333333;}#mermaid-svg-5oX6q4oWHaSeU04v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5oX6q4oWHaSeU04v p{margin:0;}#mermaid-svg-5oX6q4oWHaSeU04v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5oX6q4oWHaSeU04v .cluster-label text{fill:#333;}#mermaid-svg-5oX6q4oWHaSeU04v .cluster-label span{color:#333;}#mermaid-svg-5oX6q4oWHaSeU04v .cluster-label span p{background-color:transparent;}#mermaid-svg-5oX6q4oWHaSeU04v .label text,#mermaid-svg-5oX6q4oWHaSeU04v span{fill:#333;color:#333;}#mermaid-svg-5oX6q4oWHaSeU04v .node rect,#mermaid-svg-5oX6q4oWHaSeU04v .node circle,#mermaid-svg-5oX6q4oWHaSeU04v .node ellipse,#mermaid-svg-5oX6q4oWHaSeU04v .node polygon,#mermaid-svg-5oX6q4oWHaSeU04v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5oX6q4oWHaSeU04v .rough-node .label text,#mermaid-svg-5oX6q4oWHaSeU04v .node .label text,#mermaid-svg-5oX6q4oWHaSeU04v .image-shape .label,#mermaid-svg-5oX6q4oWHaSeU04v .icon-shape .label{text-anchor:middle;}#mermaid-svg-5oX6q4oWHaSeU04v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5oX6q4oWHaSeU04v .rough-node .label,#mermaid-svg-5oX6q4oWHaSeU04v .node .label,#mermaid-svg-5oX6q4oWHaSeU04v .image-shape .label,#mermaid-svg-5oX6q4oWHaSeU04v .icon-shape .label{text-align:center;}#mermaid-svg-5oX6q4oWHaSeU04v .node.clickable{cursor:pointer;}#mermaid-svg-5oX6q4oWHaSeU04v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5oX6q4oWHaSeU04v .arrowheadPath{fill:#333333;}#mermaid-svg-5oX6q4oWHaSeU04v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5oX6q4oWHaSeU04v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5oX6q4oWHaSeU04v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5oX6q4oWHaSeU04v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5oX6q4oWHaSeU04v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5oX6q4oWHaSeU04v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5oX6q4oWHaSeU04v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5oX6q4oWHaSeU04v .cluster text{fill:#333;}#mermaid-svg-5oX6q4oWHaSeU04v .cluster span{color:#333;}#mermaid-svg-5oX6q4oWHaSeU04v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5oX6q4oWHaSeU04v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5oX6q4oWHaSeU04v rect.text{fill:none;stroke-width:0;}#mermaid-svg-5oX6q4oWHaSeU04v .icon-shape,#mermaid-svg-5oX6q4oWHaSeU04v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5oX6q4oWHaSeU04v .icon-shape p,#mermaid-svg-5oX6q4oWHaSeU04v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5oX6q4oWHaSeU04v .icon-shape .label rect,#mermaid-svg-5oX6q4oWHaSeU04v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5oX6q4oWHaSeU04v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5oX6q4oWHaSeU04v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5oX6q4oWHaSeU04v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 编辑后退出
检测退出
重新扫描上传
启动前下载
继续编辑
办公 Mac
LaunchAgent
fileproviderd
iCloud Drive
家用 Mac
eDiary
这个方案的核心不是"让 eDiary 支持云同步",而是绕过 eDiary 本身没有多端同步的问题,把它的日记数据文件交给 iCloud Drive 同步。
但这里要先说清楚一点:
iCloud Drive 是文件同步服务,不是数据库级同步工具。
如果两台 Mac 同时打开同一份 .edf 文件,或者两边都修改了内容,就可能出现冲突、覆盖、同步失败,甚至文件损坏。
所以我给自己定的使用规则是:
- 办公 Mac 编辑时,家用 Mac 不打开;
- 办公 Mac 下班前退出 eDiary;
- 等 iCloud 同步完成;
- 回家后再从家用 Mac 打开;
- 反过来也一样。
说白了,就是把 eDiary 当成一个"单人单端编辑"的日记本,只是通过 iCloud 做文件流转。
三、迁移 eDiary 日记数据
eDiary 启动后,会创建一个 .edf 日记本文件。我的文件名是:
text
我的日记本.edf
如果不清楚日记文件在哪里,可以在 eDiary 里查看本地文件路径。我的排查方式主要有两个:
- 打开 eDiary,通过「账号」→「本地」→「文件路径」查看;
- 或者通过「设置」→「安全」→「备份文件夹」找到备份目录,再到上一级目录找
.edf文件。
我这里记录一下常见路径。
Windows 上我看到的路径类似:
text
C:\Program Files (x86)\eDiary\Users\Administrator\我的日记本.edf
macOS 上我这里的路径是:
bash
$HOME/Library/Application Support/eDiary/Data/我的日记本.edf
不同 eDiary 版本、不同安装方式、不同用户名下,路径可能会有差异。实际操作时,建议以 eDiary 内显示的文件路径为准。
迁移本质上就是迁移这个 .edf 文件,但操作前一定要注意:
- 先关闭 eDiary;
- 备份原来的
.edf文件; - 再替换或迁移文件;
- 替换后再打开 eDiary 验证内容是否正常。
例如在 macOS 上,可以先做一份备份:
bash
EDIR="$HOME/Library/Application Support/eDiary/Data"
cp "$EDIR/我的日记本.edf" "$EDIR/我的日记本.edf.bak.$(date +%Y%m%d%H%M%S)"
这个备份很重要,尤其是你后面要做软链接、同步、脚本自动化时,手里有一份原始备份,心里会稳很多。
四、使用 iCloud Drive + 软链接实现基础同步
macOS 上 iCloud Drive 的本地路径通常是:
bash
$HOME/Library/Mobile Documents/com~apple~CloudDocs
不清楚路径的话,也可以在访达里打开 iCloud Drive,然后右键选择"在终端中打开",再执行:
bash
pwd
我的思路是:
- 把
我的日记本.edf放到 iCloud Drive; - 删除或备份 eDiary 默认数据目录下的原
.edf文件; - 在 eDiary 默认数据目录中创建软链接;
- 让 eDiary 以为自己还在访问原路径,实际上访问的是 iCloud Drive 里的文件。
可以先准备变量:
bash
ICLOUD_FILE="$HOME/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf"
EDIARY_DIR="$HOME/Library/Application Support/eDiary/Data"
LOCAL_FILE="$EDIARY_DIR/我的日记本.edf"
操作前先关闭 eDiary,然后备份本地文件:
bash
mkdir -p "$EDIARY_DIR"
if [ -f "$LOCAL_FILE" ] && [ ! -L "$LOCAL_FILE" ]; then
cp "$LOCAL_FILE" "$LOCAL_FILE.bak.$(date +%Y%m%d%H%M%S)"
fi
如果 iCloud Drive 里还没有日记文件,可以先复制过去:
bash
cp "$LOCAL_FILE" "$ICLOUD_FILE"
然后把本地文件改成软链接:
bash
rm -f "$LOCAL_FILE"
ln -s "$ICLOUD_FILE" "$LOCAL_FILE"
可以用下面命令确认软链接是否创建成功:
bash
ls -l "$LOCAL_FILE"
正常会看到类似这样的结果:
text
我的日记本.edf -> /Users/xxx/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf
软链接可以粗略理解为"让一个路径指向另一个真实文件"。它和 Windows 普通快捷方式不完全一样,因为程序访问软链接时,通常会直接访问到目标文件。
这样配置后,eDiary 打开的还是它默认目录下的 我的日记本.edf,但真实数据文件已经放在 iCloud Drive 里了。
注意:两台 Mac 必须登录同一个 Apple ID,并且都开启 iCloud Drive。更重要的是,不要两台 Mac 同时打开同一份
.edf文件。
五、同步失败:iCloud 卡在 0% 的排查过程
基础同步配置完成后,我原本以为事情就结束了。
结果实际测试时发现:
- eDiary 可以正常打开这份云端
.edf文件; - 编辑后,本地文件内容确实发生了变化;
- iCloud 也能监听到文件变化;
- 但是上传进度一直卡在 0%。
这就有点难受了。
下面是我的排查过程。
1. 查看文件占用情况
先定义一下文件路径,后续命令会更清晰:
bash
FILE="$HOME/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf"
查看文件占用:
bash
lsof "$FILE"
我当时看到的现象类似:
text
fileprovi 4341 user 22w REG 1,15 7586907 28940470 /Users/user/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf
eDiary 5477 user 5u REG 1,15 7586907 28940470 /Users/user/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf
这里的 fileprovi 是进程名被截断后的显示,实际对应的是 fileproviderd,也就是 macOS 里和 FileProvider / iCloud Drive 文件同步相关的进程。
eDiary 打开时占用 .edf 文件是正常的,因为它本来就要读写日记文件。
比较关键的是,eDiary 退出后,我发现 fileproviderd 仍然在处理这个文件。单看 lsof 还不能直接说明它异常,只能说明 fileproviderd 确实参与了这个文件的同步处理。真正的问题还需要结合后面的 iCloud 事件和日志判断。
2. 查看文件扩展属性
接着看了一下扩展属性:
bash
xattr -l "$FILE"
输出类似:
text
com.apple.decmpfs: fpmc
com.apple.macl:
com.apple.provenance:
这里简单记录一下我的理解:
com.apple.decmpfs:macOS 文件压缩相关的扩展属性;com.apple.macl:和 macOS 访问控制、隐私授权相关;com.apple.provenance:和文件来源、创建或修改来源追踪相关。
这些属性本身不能证明同步失败。它们更像是 macOS 文件系统或 iCloud 处理文件时的伴随状态。
所以这一轮排查只能说明:文件上存在一些 macOS 扩展属性,但它们不是本次同步失败的直接证据。
3. 检查是否产生临时文件或锁文件
接着看 iCloud Drive 目录下有没有明显的临时文件、锁文件:
bash
ls -la "$HOME/Library/Mobile Documents/com~apple~CloudDocs"
我重点看了有没有类似这些文件:
text
.lock
.wal
.shm
.tmp
结果没有看到明显的锁文件或临时文件。
不过这只能排除一部分可能,不代表文件没有被锁。因为文件锁不一定会表现为一个肉眼可见的 .lock 文件,也可能是进程对文件句柄加锁。
4. 观察 iCloud 同步事件
然后使用 brctl monitor 观察 iCloud Drive 的同步状态:
bash
brctl monitor com.apple.CloudDocs | grep -i "我的日记本\|edf\|upload\|error\|stalled\|conflict"
我看到类似输出:
text
o /com~apple~CloudDocs/我的日记本.edf ↑ 7.6 MB (7586907) 0.0%
这个信息很关键。
它说明 iCloud 确实观察到了 我的日记本.edf 发生变化,并且进入了上传流程,但是进度卡在了 0.0%。
也就是说,问题不是"iCloud 完全没发现文件变化",而是"发现变化后,上传没有继续推进"。
注意:
brctl是 macOS 上用于 iCloud 相关诊断的工具,不同 macOS 版本输出可能不完全一样,这里仅作为排查参考。
5. 检查 inode 是否变化
我又看了一下文件的 inode 信息:
bash
stat "$FILE"
输出类似:
text
16777231 28940470 -rw-r--r-- 1 user staff 0 7586907 "Jul 8 16:25:14 2026" "Jul 8 16:25:12 2026" "Jul 8 16:25:12 2026" "Jun 29 18:21:50 2026" 4096 14824 0x40 /Users/user/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf
从我的观察来看,eDiary 编辑文件后,这个 .edf 文件的 inode 没有变化,更像是对原文件进行原地修改,而不是写入一个新文件再替换旧文件。
这点对理解后面的同步卡住有帮助。
有些应用保存文件时,会先写临时文件,再 rename 替换原文件;而 eDiary 这里更像是直接在原 .edf 文件上修改。对于 iCloud 这种文件同步服务来说,这类单文件容器的原地修改,遇到锁或同步时序问题时,就比较容易出现卡住、冲突或上传失败。
6. 查看 fileproviderd 日志
为了进一步确认,我开始看 fileproviderd 的日志:
bash
log stream --style compact --info --predicate 'process == "fileproviderd"'
关键日志如下:
text
2026-07-09 10:08:27.751 I fileproviderd[27690:d9d82e] [com.apple.FileProvider:com.apple.CloudDocs.iCloudDriveFileProvider/3{34}2]
trigger: itemUpdatedInFSSnapshot(... diffs: content|mtime|evictable)
2026-07-09 10:08:27.794 I fileproviderd[27690:d9d82e] [com.apple.FileProvider:com.apple.CloudDocs.iCloudDriveFileProvider/3{34}2]
reconciliation update: ... fields:content|mtime ...
2026-07-09 10:08:27.806 E fileproviderd[27690:d9d82e] [com.apple.FileProvider:com.apple.CloudDocs.iCloudDriveFileProvider/3{34}2]
done executing <FS1 fetch-content(docID(133)) why:itemChangedRemotely ... error:<NSError: libfssync.FileTreeError<libfssync.VFSItem> 13 "itemIsFlockedCanNotPropagate" >>
这几行日志基本说明了几个信息:
- fileproviderd 已经检测到
.edf文件发生变化; - 变化字段包括
content和mtime,也就是内容和修改时间; - FileProvider 开始做本地文件状态和 iCloud 状态的核对;
- 在处理文件内容时,遇到了
itemIsFlockedCanNotPropagate。
我对这里的理解是:
日志显示 FileProvider 在处理该文件内容时遇到了 flock 相关阻塞,导致本次同步无法继续推进。
所以这里可以比较谨慎地得出一个结论:
eDiary 对 .edf 文件进行原地写入后,iCloud 已经检测到了文件变化,但 fileproviderd 在处理该文件上传任务时遇到了 flock 相关阻塞,最终表现为 iCloud 上传进度卡在 0%。
7. 临时解决方法:重启 fileproviderd
当时我尝试重启 fileproviderd:
bash
killall fileproviderd
重启后,fileproviderd 会重新启动,并重新扫描 iCloud Drive 当前文件状态。此时 .edf 文件已经处于 eDiary 退出后的稳定状态,上传任务就能重新推进,最终同步成功。
这个方法对我当前场景有效,但要强调一下:
这不是 Apple 官方针对 eDiary 的修复方案,只是我在个人使用场景下验证可用的 workaround。
执行前建议:
- 确认 eDiary 已经关闭;
- 确认
.edf文件已经保存完成; - 不要在其他 iCloud 文件正在大量同步时频繁执行;
- 最好保留
.edf文件备份。
因为 fileproviderd 不只服务 eDiary,也和 iCloud Drive 里的其他文件同步有关,直接 killall 可能会影响其他正在进行的 iCloud 文件同步任务。
六、自动化方案:eDiary 退出后自动刷新 iCloud 同步
手动执行 killall fileproviderd 虽然有效,但每次写完日记都手动敲一遍,显然不现实。
我的需求是:
- eDiary 运行时,不干预;
- eDiary 退出后,等待几秒让文件落盘;
- 确认 eDiary 不再占用
.edf文件; - 再重启 fileproviderd;
- 让 iCloud 重新扫描并上传稳定后的
.edf文件。
macOS 上可以通过 LaunchAgent 创建一个用户级后台任务,粗略理解有点像 Linux 里的 crontab,不过 LaunchAgent 是 launchd 体系下的机制,功能更丰富。
1. 创建触发脚本
先创建目录:
bash
mkdir -p "$HOME/scripts" "$HOME/.cache" "$HOME/Library/Logs"
创建脚本:
bash
cat > "$HOME/scripts/ediary-icloud-exit-trigger.sh" <<'EOF'
#!/bin/bash
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
APP_NAME="eDiary"
FILE="$HOME/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf"
STATE_FILE="$HOME/.cache/ediary-last-state"
LOG_FILE="$HOME/Library/Logs/ediary-icloud-trigger.log"
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$LOG_FILE")"
now() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
echo "[$(now)] $*" >> "$LOG_FILE"
}
set_state() {
echo "$1" > "$STATE_FILE"
}
get_state() {
cat "$STATE_FILE" 2>/dev/null || echo "stopped"
}
ediary_running() {
pgrep -x "$APP_NAME" >/dev/null 2>&1
}
ediary_holds_file() {
lsof "$FILE" 2>/dev/null | grep -q "$APP_NAME"
}
log "Check eDiary state."
# 如果日记文件不存在,不做任何处理
if [ ! -e "$FILE" ]; then
log "Diary file does not exist, skip. FILE=$FILE"
set_state "stopped"
exit 0
fi
# eDiary 正在运行,记录状态后退出
if ediary_running; then
log "eDiary is running."
set_state "running"
exit 0
fi
LAST_STATE="$(get_state)"
# 只有从 running -> stopped 时才触发
if [ "$LAST_STATE" = "running" ]; then
log "Detected eDiary exited."
# 给 eDiary 和文件系统一点落盘时间
sleep 5
# 再确认 eDiary 是否重新启动
if ediary_running; then
log "eDiary started again, skip fileproviderd restart."
set_state "running"
exit 0
fi
# 再确认 eDiary 是否仍占用文件
if ediary_holds_file; then
log "eDiary still holds diary file, skip fileproviderd restart."
set_state "running"
exit 0
fi
log "Restarting fileproviderd to refresh iCloud upload state."
killall fileproviderd 2>/dev/null || true
sleep 3
log "fileproviderd restart triggered. Current file stat:"
stat "$FILE" >> "$LOG_FILE" 2>&1 || true
set_state "stopped"
exit 0
fi
set_state "stopped"
exit 0
EOF
chmod +x "$HOME/scripts/ediary-icloud-exit-trigger.sh"
这个脚本的关键点是:
- 用状态文件记录上一次 eDiary 状态;
- 只有检测到
running -> stopped时才触发; - eDiary 运行中不处理;
- eDiary 退出后等待 5 秒;
- 如果 eDiary 仍然占用文件,就跳过;
- 最后才重启 fileproviderd。
这样可以避免每 30 秒无脑重启 fileproviderd。
2. 创建 LaunchAgent
这里建议用变量生成 plist,避免手动替换 /Users/你的用户名:
bash
PLIST="$HOME/Library/LaunchAgents/com.xyz.ediary-icloud-exit-trigger.plist"
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.xyz.ediary-icloud-exit-trigger</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$HOME/scripts/ediary-icloud-exit-trigger.sh</string>
</array>
<!-- 每 30 秒启动一次任务 -->
<key>StartInterval</key>
<integer>30</integer>
<!-- 加载时立即运行一次 -->
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/ediary-icloud-trigger.stdout.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/ediary-icloud-trigger.stderr.log</string>
</dict>
</plist>
EOF
LaunchAgent并不是每30s都重启fileproviderd,而是先记录eDiary状态,只有检测到eDiary从running变为stopped时,才等待文件落盘并触发刷新。
3. 加载任务
bash
launchctl bootstrap "gui/$(id -u)" "$PLIST"
launchctl enable "gui/$(id -u)/com.xyz.ediary-icloud-exit-trigger"
查看状态:
bash
launchctl print "gui/$(id -u)/com.xyz.ediary-icloud-exit-trigger"
查看日志:
bash
tail -f "$HOME/Library/Logs/ediary-icloud-trigger.log"
4. 修改后重新加载
如果后续修改了脚本,一般不需要重载 LaunchAgent。
但如果修改了 plist,建议先卸载再加载:
bash
launchctl bootout "gui/$(id -u)" "$PLIST" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST"
launchctl enable "gui/$(id -u)/com.xyz.ediary-icloud-exit-trigger"
停止并卸载:
bash
launchctl bootout "gui/$(id -u)" "$PLIST"
5. 验证方式
验证步骤:
- 打开 eDiary;
- 编辑一条日记;
- 退出 eDiary;
- 查看脚本日志;
- 到 iCloud.com 或另一台 Mac 上确认
我的日记本.edf修改时间是否更新。
正常日志类似:
text
[2026-07-08 23:00:23] Detected eDiary exited.
[2026-07-08 23:00:28] Restarting fileproviderd to refresh iCloud upload state.
[2026-07-08 23:00:31] fileproviderd restart triggered. Current file stat:
七、启动前同步:打开 eDiary 前先确认 iCloud 文件已就绪
前面解决的是"办公 Mac 退出后上传"的问题。
但回到家打开 eDiary 前,我还是会有点担心:
家用 Mac 本地这份 iCloud 文件到底是不是最新的?
手动打开访达检查也可以,但既然已经折腾到这一步了,那就顺手做一个"保守启动脚本"。
核心思路:
- 打开 eDiary 前,先触发 iCloud 下载;
- 检查文件是否存在;
- 检查是否还是 iCloud 占位文件;
- 检查文件大小是否异常;
- 等待文件大小和修改时间稳定;
- 如果文件状态稳定,再打开 eDiary;
- 如果 eDiary 已经运行,则禁止重复启动,避免文件冲突。
这里用到的命令是:
bash
brctl download "$HOME/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf"
不过要注意:
brctl download更准确地说是请求/触发 iCloud 下载文件,不等于强一致同步,也不能 100% 保证当前文件一定就是云端最新版本。
所以我后面又加了文件稳定性检查。
创建保守启动脚本
bash
cat > "$HOME/scripts/open-ediary-with-icloud-sync.sh" <<'EOF'
#!/bin/bash
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
APP_NAME="eDiary"
FILE="$HOME/Library/Mobile Documents/com~apple~CloudDocs/我的日记本.edf"
LOG_FILE="$HOME/Library/Logs/ediary-icloud-open.log"
MIN_SIZE=1024
WAIT_SECONDS=30
STABLE_SECONDS=3
mkdir -p "$(dirname "$LOG_FILE")"
now() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
echo "[$(now)] $*" >> "$LOG_FILE"
}
fail() {
log "启动失败:$*"
echo "启动失败:$*"
echo "日志文件:$LOG_FILE"
exit 1
}
file_size() {
stat -f%z "$FILE" 2>/dev/null || echo 0
}
file_mtime() {
stat -f%m "$FILE" 2>/dev/null || echo 0
}
is_placeholder_file() {
local dir
local base
dir="$(dirname "$FILE")"
base="$(basename "$FILE")"
# 常见 iCloud 占位文件形式
if [ -e "$dir/.$base.icloud" ]; then
return 0
fi
case "$FILE" in
*.icloud)
return 0
;;
esac
return 1
}
fileprovider_holds_file() {
lsof "$FILE" 2>/dev/null | grep -q "fileprovi"
}
log "========== Preparing to open eDiary =========="
# 1. eDiary 已运行时禁止重复启动
if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
fail "eDiary 已经在运行,禁止重复启动,避免文件冲突。"
fi
# 2. brctl 必须存在
if ! command -v brctl >/dev/null 2>&1; then
fail "brctl 命令不存在,无法触发 iCloud 下载。"
fi
# 3. 日记文件必须存在
if [ ! -e "$FILE" ]; then
fail "日记文件不存在:$FILE"
fi
log "Before download stat:"
stat "$FILE" >> "$LOG_FILE" 2>&1 || true
# 4. 请求 iCloud 下载/刷新文件
log "Running brctl download."
if brctl download "$FILE" >> "$LOG_FILE" 2>&1; then
log "brctl download command executed."
else
fail "brctl download 命令执行失败。"
fi
# 5. 等待文件稳定
log "Waiting for diary file to become ready. max wait: ${WAIT_SECONDS}s"
last_size=""
last_mtime=""
stable_count=0
i=0
while [ "$i" -lt "$WAIT_SECONDS" ]; do
i=$((i + 1))
if [ ! -e "$FILE" ]; then
log "Waiting: file does not exist. ${i}/${WAIT_SECONDS}"
stable_count=0
sleep 1
continue
fi
if is_placeholder_file; then
log "Waiting: file still looks like iCloud placeholder. ${i}/${WAIT_SECONDS}"
stable_count=0
sleep 1
continue
fi
size="$(file_size)"
mtime="$(file_mtime)"
if [ "$size" -lt "$MIN_SIZE" ]; then
log "Waiting: file size too small: $size. ${i}/${WAIT_SECONDS}"
stable_count=0
sleep 1
continue
fi
if fileprovider_holds_file; then
log "Waiting: fileproviderd still holds diary file. ${i}/${WAIT_SECONDS}"
stable_count=0
sleep 1
continue
fi
if [ "$size" = "$last_size" ] && [ "$mtime" = "$last_mtime" ]; then
stable_count=$((stable_count + 1))
else
stable_count=0
last_size="$size"
last_mtime="$mtime"
fi
log "Check: size=$size mtime=$mtime stable_count=$stable_count/${STABLE_SECONDS}"
if [ "$stable_count" -ge "$STABLE_SECONDS" ]; then
log "Diary file looks ready."
break
fi
sleep 1
done
# 6. 最终检查
if [ ! -e "$FILE" ]; then
fail "同步后日记文件不存在。"
fi
if is_placeholder_file; then
fail "同步后仍检测到 iCloud 占位文件,文件可能没有完整下载。"
fi
SIZE="$(file_size)"
if [ "$SIZE" -lt "$MIN_SIZE" ]; then
fail "同步后文件大小异常:$SIZE"
fi
if fileprovider_holds_file; then
log "Final lsof:"
lsof "$FILE" >> "$LOG_FILE" 2>&1 || true
fail "fileproviderd 在等待 ${WAIT_SECONDS}s 后仍占用日记文件,暂不启动 eDiary。"
fi
if [ "$stable_count" -lt "$STABLE_SECONDS" ]; then
fail "文件状态在 ${WAIT_SECONDS}s 内没有稳定下来,暂不启动 eDiary。"
fi
log "After download stat:"
stat "$FILE" >> "$LOG_FILE" 2>&1 || true
# 7. 启动 eDiary
log "Opening eDiary."
if open -a "$APP_NAME" >> "$LOG_FILE" 2>&1; then
log "eDiary launch command executed."
exit 0
else
fail "open -a eDiary 执行失败。"
fi
EOF
chmod +x "$HOME/scripts/open-ediary-with-icloud-sync.sh"
这个脚本整体偏保守。
它不会直接假设 iCloud 文件一定已经准备好,而是通过文件存在、占位文件、文件大小、修改时间稳定性等条件做一个基础判断。
这样虽然会让启动慢几十秒,但对我来说可以接受。毕竟日记本这种东西,数据安全比启动速度更重要。
八、制作 Cloud eDiary.app 启动器
启动脚本可以直接放桌面双击运行,但看起来总感觉不太像一个正常应用。
所以我又想把它做成一个 .app,放到启动台里,像普通应用一样点击打开。
这个 Cloud eDiary.app 本质上不是一个完整的 macOS 应用,它只是一个本地启动器,核心作用是执行前面的 Shell 脚本。
1. 创建 App 目录结构
bash
APP="$HOME/Applications/Cloud eDiary.app"
mkdir -p "$APP/Contents/MacOS"
mkdir -p "$APP/Contents/Resources"
2. 复制 eDiary 图标
直接沿用 eDiary 的图标。
先找到图标:
bash
find "/Applications/eDiary.app/Contents/Resources" -iname "*.icns" -print
我这里结果是:
text
/Applications/eDiary.app/Contents/Resources/eDiary.icns
复制图标:
bash
cp "/Applications/eDiary.app/Contents/Resources/eDiary.icns" "$APP/Contents/Resources/CloudEdiary.icns"
3. 写入 Info.plist
bash
cat > "$APP/Contents/Info.plist" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Cloud eDiary</string>
<key>CFBundleDisplayName</key>
<string>Cloud eDiary</string>
<key>CFBundleIdentifier</key>
<string>com.xyz.open-ediary</string>
<key>CFBundleIconFile</key>
<string>CloudEdiary.icns</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>launcher</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
EOF
其中:
CFBundleExecutable指向后面编译出来的launcher;CFBundleIconFile指向图标文件;LSUIElement设置为true后,这个启动器本身不会作为常规窗口应用显示在 Dock 里。
4. 编写 C launcher
先生成 C 源码:
bash
cat > /tmp/cloud_ediary_launcher.c <<'EOF'
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
const char *home = getenv("HOME");
if (home == NULL) {
return 1;
}
char script[4096];
snprintf(script, sizeof(script), "%s/scripts/open-ediary-with-icloud-sync.sh", home);
execl("/bin/bash", "bash", script, (char *)NULL);
return 1;
}
EOF
这个 launcher 做的事情很简单:
- 获取当前用户的
$HOME; - 拼出启动脚本路径;
- 用
/bin/bash执行脚本。
5. 编译 launcher
如果是 Apple Silicon,可以编译为 arm64:
bash
APP="$HOME/Applications/Cloud eDiary.app"
xcrun clang -arch arm64 -O2 \
-o "$APP/Contents/MacOS/launcher" \
/tmp/cloud_ediary_launcher.c
chmod +x "$APP/Contents/MacOS/launcher"
检查架构:
bash
file "$APP/Contents/MacOS/launcher"
lipo -info "$APP/Contents/MacOS/launcher"
输出类似:
text
/Users/user/Applications/Cloud eDiary.app/Contents/MacOS/launcher: Mach-O 64-bit executable arm64
Non-fat file: /Users/user/Applications/Cloud eDiary.app/Contents/MacOS/launcher is architecture: arm64
这样 Finder 会把它识别为 Apple Silicon 原生可执行文件,不需要通过 Rosetta 运行。
如果是 Intel Mac,不要直接照抄
-arch arm64。可以改成-arch x86_64,或者去掉-arch参数,让 clang 按当前机器架构编译。
6. 本地签名
执行本地 ad-hoc 签名:
bash
codesign --force --deep --sign - "$APP"
验证:
bash
codesign --verify --deep --strict --verbose=2 "$APP"
成功时会看到类似:
text
valid on disk
satisfies its Designated Requirement
这里的 --sign - 是本地 ad-hoc 签名,适合自己本机使用,不是正规开发者证书签名,也不适合拿去分发。
7. 刷新 LaunchServices / Finder 图标缓存
bash
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
"$LSREGISTER" -f "$APP"
touch "$APP"
killall Finder
执行后,启动台或 Finder 中的图标识别会更正常。
8. 授权 Cloud eDiary
如果启动器访问脚本、iCloud Drive 或日志文件时遇到权限问题,可以到这里添加权限:
text
系统设置 → 隐私与安全性 → 完全磁盘访问权限
添加:
text
$HOME/Applications/Cloud eDiary.app
然后打开开关。
不同 macOS 版本权限策略可能略有差异。如果没有权限问题,也不一定必须添加完全磁盘访问权限。但如果脚本访问 iCloud Drive、日志目录或应用路径失败,这里是一个需要检查的位置。
到这里,Cloud eDiary.app 就可以像普通应用一样,从启动台点击打开了。
九、使用注意事项和风险
这个方案目前在我的使用场景下是可用的,但它不是完美方案。实际使用时需要注意这些点。
1. 不要两台 Mac 同时打开同一份 .edf
这是最重要的一点。
.edf 本质上是 eDiary 的数据文件,不是为多端并发编辑设计的。两台 Mac 同时打开,很容易出现同步冲突、覆盖,甚至文件损坏。
2. iCloud Drive 不是数据库同步工具
iCloud Drive 更适合普通文件同步,不适合承担数据库级强一致同步。
所以这个方案适合:
text
单人使用 → 单端编辑 → 退出后同步 → 另一端再打开
不适合:
text
多端同时打开 → 多端同时修改 → 期待自动合并
3. 修改前一定要备份 .edf
尤其是第一次迁移、第一次创建软链接、第一次调试脚本前,都建议备份。
例如:
bash
cp "$FILE" "$FILE.bak.$(date +%Y%m%d%H%M%S)"
4. 定期保留离线备份
iCloud 能解决同步问题,但不能完全替代备份。
建议定期把 .edf 文件复制到其他安全位置,例如移动硬盘、本地备份目录,或者其他不自动覆盖的备份空间。
5. 公司敏感信息要脱敏
如果日记里有公司服务器、账号、内部域名、故障细节、数据库信息等内容,最好做脱敏处理。
笔记是为了帮助自己复盘,不是为了把敏感信息带出安全边界。
6. killall fileproviderd 有影响范围
fileproviderd 不只处理 eDiary,也和 iCloud Drive 其他文件同步有关。
频繁重启可能影响其他正在上传或下载的 iCloud 文件,所以脚本里我只在 eDiary 从运行变成退出时触发,而不是每 30 秒无脑执行。
7. 不同 macOS 版本行为可能不同
brctl、fileproviderd、FileProvider 日志输出,在不同 macOS 版本中可能会有差异。
本文是基于我当前环境排查得到的结果,不代表所有 macOS 版本都完全一致。
8. 如果出现冲突文件,先人工确认再合并
如果 iCloud 后续生成了冲突副本,不要急着删除。
建议先复制备份,再打开对比,确认哪份内容更新,最后再手动合并。
十、总结
这次折腾的初衷其实很简单:我希望自己的日记和技术笔记能在办公 Mac 和家用 Mac 之间流转得更顺畅。
我一直觉得,好记性不如烂笔头。
虽然现在有 AI,可以随时问问题,但每次问 AI 本身也需要组织上下文,还要从一大堆回答里筛选自己真正需要的信息。很多时候,还是翻自己之前写过的笔记更直接。
这套方案最终实现了我想要的效果:
- 办公 Mac 写完日记后退出 eDiary;
- LaunchAgent 自动触发同步刷新;
- 回到家后通过 Cloud eDiary 启动器打开;
- 启动前先尽量确认 iCloud 文件已经就绪;
- 然后继续编辑同一份日记本。
它不是完美的云笔记方案,也不是官方推荐方案,本质上是一个基于 eDiary + iCloud Drive + Shell + LaunchAgent 的个人 workaround。
但对我来说,它已经够实用了。
虽然启动时可能慢几十秒,但换来的好处是:不用每次手动确认文件有没有同步,也不用维护两份日记本。对于一个喜欢写笔记、写排查过程的人来说,这点等待时间还是值得的。
后续如果再遇到同步冲突、文件锁或者 iCloud 异常,我再继续优化。折腾嘛,本来就是一边使用,一边修修补补。