☰
mo单实例设计解析:为什么第二次运行mo不会启动新服务器而是追加文件
2026/10/16 2:35:53 网站建设 项目流程

【免费下载链接】mo

mo is a Markdown viewer that opens .md files in a browser.

项目地址:https://gitcode.com/gh_mirrors/mo7/mo
点击查看免费下载

mo 是一款把 Markdown 文件直接在浏览器中打开的 Markdown viewer。它的核心设计是单实例:默认只在6275端口运行一个服务器进程,第二次运行 mo 时不会启动新服务器,而是把新文件追加到已存在的会话里。本文将带你读懂这套"单服务器、多文件"(Single Server, Multiple Files)机制的实现思路:mo 如何探测到已有服务、如何通过 HTTP 接口追加文件、两个 mo 同时启动时如何避免冲突,以及会话备份如何让你在重启后不丢文件。

一、先看懂场景:连续两次运行 mo 会发生什么

假设你已经安装好 mo,在项目里执行:

$ mo README.md # 第一次:后台启动 mo 服务器,并打开浏览器 mo: serving at http://localhost:6275 (pid 12345) $ mo CHANGELOG.md # 第二次:不会新开服务器,而是把文件加进去 mo: added 1 item(s) to http://localhost:6275

第二次运行 mo 时,浏览器里会多出一个CHANGELOG.md的侧边栏条目,而终端立刻返回、不会阻塞。这正是 mo 官方帮助里明确描述的"Single Server, Multiple Files"行为:

如果相同端口上已有 mo 服务器在运行,后续的 mo 调用会把文件加入现有会话,而不是启动一个新的。

这种设计对新手非常友好:

  • 🧭浏览器入口唯一:不管打开多少个文件,都集中在http://localhost:6275一个地址,不用来回切换标签页里多个不同端口的服务
  • 📝会话天然累积:写文档、写笔记时可以随手mo xxx.md,所有文件自动汇总到同一侧边栏
  • 🔄配合会话恢复:服务器重启后,之前打开的文件列表会自动恢复(下文详述)

💡 如果你确实想要一个完全独立的会话,只需要换一个端口:mo draft.md -p 6276。

二、"追加文件"是如何实现的:探测 + HTTP 接口

mo 的命令行部分本质上是一个"客户端":它先探测 6275 端口上有没有一个 mo 服务器在跑,再决定"追加"还是"新建"。核心逻辑集中在 cmd/root.go 的run函数中。

第一步:用健康检查接口探测服务器是否存活

mo 通过 probeServer 向GET /_/api/status发起一次带超时的 HTTP 请求,并校验响应 JSON 中是否包含version字段——这样即使端口被别的程序占用,也不会误认为是 mo 实例。注意两个不同的超时设置(见 cmd/root.go#L40-L52):

场景超时含义
probeTimeoutFast500ms"没服务器"是常态(比如第一次启动),快速失败
probeTimeoutDefault2s预期服务器应该在跑(比如mo不带参数时只打开浏览器)

第二步:服务器活着 → 通过 API 逐个追加文件

探测成功后,mo 把每个文件以POST /_/api/groups/{group}/files的形式发给服务器(实现见 postFiles),服务器端在内存状态里登记该文件并开始监听它的变化(live-reload),完整的路由表可以在 internal/server/server.go#L1640-L1655 看到。

服务器端 State.AddFile 还有一个细节:如果文件已经存在于同组中会直接返回已有条目,天然幂等——你重复执行mo README.md不会造成重复条目。

第三步:服务器没活着 → 派生一个后台子进程

探测失败时,mo 走 startBackground 路径:

  1. 把要打开的文件清单写成一个临时 JSON 恢复文件(WriteRestoreFile)
  2. 用--foreground --no-open --restore参数把自己再次执行一次(spawnNewProcess),子进程成为真正的常驻服务器
  3. 父进程立刻退出并把子进程 detach 掉,所以终端不会被占住;随后用 waitForReady 轮询确认服务器就绪,再打开浏览器

也就是说,常驻服务器其实是由"父进程"临时拉起的前台子进程,这样既方便调试,也让--restart这类操作可以优雅地用"旧进程退出 + 新进程继承"的方式完成。

三、两个 mo 同时启动怎么办?靠 PID 校验防"双开"

如果你极快连按两次回车(或者两个终端同时执行mo),两个父进程可能都探测不到服务器,于是各自派生子进程去监听 6275 端口——此时只有一个人的net.Listen能成功,另一个子进程会因端口冲突而退出。

mo 用一套精巧的判定来兜底(waitForReady 的注释直接点明了这一点):

  • 轮询/​_/api/status时,把响应里报告的 PID与自己派生的子进程 PID比对
  • PID 一致 → 服务器是自己拉起来的,正常继续
  • PID 不一致 → 说明端口被另一个 mo 实例抢走了,返回errServerConflict
  • 随后走 addToRunningServer:不报错,而是把自己要开的文件追加给获胜的那个服务器
mo: another mo server is already running at http://localhost:6275 (pid 999); added 1 item(s) to it

另外,如果子进程死掉后 1 秒内(deadChildGrace)端口上出现了新的 mo 服务,也会按"并发竞争输家"处理而不是直接报错。这套"输了就加入"的策略保证了无论时序如何交错,结果总是唯一的服务器 + 完整的文件列表,这正是单实例设计的可靠性关键。

四、会话备份与恢复:关掉 mo 再打开,文件不丢

单实例服务器跑在内存里,进程一停状态就没了。mo 的解法是自动会话备份(internal/backup/backup.go):

  • 每次文件增删改都会标记状态"脏",服务器内部用 1 秒防抖后把全部组、文件路径、watch 模式和上传内容快照到XDG_STATE_HOME/mo/backup/mo-6275.json(防抖循环见 State.EnableBackup)
  • 下次新服务器启动时,mo 会Load这份备份,用 mergeGroups 把旧会话与新命令行参数合并(旧文件在前、新文件在后),并跳过已经不存在于磁盘上的文件(filterValidRestoreData)
$ mo README.md CHANGELOG.md # 启动两个文件 $ mo --shutdown # 关闭服务器 $ mo # 自动恢复 README.md 和 CHANGELOG.md $ mo TODO.md # 恢复旧会话 + 追加 TODO.md

配合--target(-t)分组,你可以在同一个单实例服务器里维护多个互不干扰的"工作区",每个组有独立 URL 和侧边栏,例如mo spec.md -t design打开http://localhost:6275/design:

日常运维也只需三个命令:mo --status查看运行中的服务器、mo --shutdown停止、mo --restart保留会话重启(实现见 doShutdown / doRestart)。日志统一落在XDG_STATE_HOME/mo/log/mo-6275.log(internal/logfile/logfile.go),--status就是靠扫描这个日志目录里的mo-{端口}.log来发现所有端口上的实例的(discoverPorts)。

五、核心文件导读:想深入源码从哪里看

模块路径说明
命令入口与单实例调度cmd/root.go探测 → 追加 / 新建的完整决策流
服务器探测cmd/root.goprobeServer健康检查与 mo 实例校验
后台派生与就绪等待cmd/root.gostartBackground与 PID 竞争判定
会话备份读写internal/backup/backup.go原子写入(临时文件 + rename)的备份保存
内存状态与备份循环internal/server/server.goRestoreData快照与 1 秒防抖保存
API 路由表internal/server/server.go追加文件、分组、shutdown 等全部端点
日志与端口发现internal/logfile/logfile.go按端口命名的轮转日志目录

六、小结:单实例设计的三个关键点

  1. 🎯一个端口一个会话:mo 把"6275 端口"当作会话的唯一标识,第二次运行先探测、后追加,浏览器入口始终统一;
  2. 🤝并发不冲突:PID 校验 + "输了就加入"(addToRunningServer)让同时启动的多个 mo 自动收敛为一个服务器;
  3. 💾状态可恢复:后台防抖备份 + 启动时合并恢复,让"单实例 + 内存态"的服务器依然拥有持久化的用户体验。

理解了这套机制,你就能自如地在 mo 里追加文件、用-t分组、用-p开独立会话、用--status / --shutdown / --restart管理后台服务器——这正是 README.md 中"Single server, multiple files"一节想传达的全部。

【免费下载链接】mo

mo is a Markdown viewer that opens .md files in a browser.

项目地址:https://gitcode.com/gh_mirrors/mo7/mo
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询