AI 内容提示:本文章使用 Claude Code 辅助编写
现象
使用 Cursor Remote SSH 远程打开 Ubuntu 服务器上的代码项目时,侧边栏 Source Control 一直显示 "scanning folder for git repositories",大部分插件不工作。等 2~10 分钟后自己恢复。不是每次都出现。
原因
Cursor 自带的 agent 扩展(cursor-agent-exec、cursor-agent-host、cursor-mcp)在扩展宿主启动时,会检查沙箱是否可用。检查方式是用 execFileSync 同步执行:
~/.cursor-server/bin/linux-x64/<版本>/resources/helpers/cursorsandbox --policy <策略文件> --preflight-only -- /bin/true问题出在四处:
- 策略的
cwd取的是扩展宿主的process.cwd(),也就是<$HOME>,不是工作区项目目录。cursorsandbox用 12 个线程遍历整个 home,约 95 万个 inode,包括~/.m2、~/.gradle、pnpm store 和~/code下的node_modules。 /home在 sda4 上,这是一块 5400 转的 SMR 机械盘。目录缓存热的时候,遍历约 4 秒;缓存冷的时候要 2~10 分钟。execFileSync会阻塞扩展宿主的主线程。阻塞期间,同一宿主里的 git、GitLens 等扩展全部停住。cursorsandbox运行在 AppArmor profilecursor_sandbox_remote下,模式是 enforce。这个 profile 不允许接收 unconfined 进程发来的信号。扩展宿主到 15 秒超时后发出 SIGTERM 和 SIGKILL,两个信号都被拒绝。execFileSync只能一直等,直到cursorsandbox自己遍历完退出。所以代码里的 15 秒超时没有生效。
遍历结束后,检查照样失败,报错是 loopback: failed to look up lo interface。这台机器上沙箱一直不可用,这几分钟的等待没有任何收益。
同一个 profile 还拒绝了 cursorsandbox 创建 unix dgram socket。查网卡时通常要先建一个 socket,这条拒绝可能就是 lo 查找失败的原因。这一点还没验证。
排查过程
/proc/pressure/io的 full avg10 约 71%~78%,CPU 和内存压力为 0。iostat显示 sda 利用率 87%~97%,读等待约 48 ms。确认瓶颈在磁盘。- 对比
/proc/diskstats里各分区的增量,11-09 那次读请求全在 sda4(/home),每秒约 425 次 4 KB 随机读。 pidstat -d找到读盘进程cursorsandbox --preflight-only。它的 12 个线程都处于 D 状态,wchan是wait_on_buffer。ls -l /proc/<pid>/fd显示它正在打开 home 下的各个目录。~/.cursor/sandbox-policies/下的策略文件里,"cwd":"<$HOME>"。- 扩展宿主的子进程
git、gk退出后变成僵尸,没人回收;git blame卡在往 stdout 写数据。vscode.git/Git.log只有一行。这说明扩展宿主的主循环停了。 - 在扩展代码里找到
isSandboxHelperSupported,确认它调用execFileSync,策略 cwd 取process.cwd()。 - 00:08
cursorsandbox退出。同一秒,日志打出Preflight timed out after 15000ms;0.7 秒后 Git.log 出现Initial repository scan started,IO 压力降到 1%。扩展宿主从 00:00 停到 00:08,共约 8 分钟。 - 00:10 又卡住一次。执行
pkill -f 'cursorsandbox.*--preflight-only',报“权限不够”;切到 root 执行kill -9,同样报“权限不够”。进程的Seccomp为 0,当前 shell 的 AppArmor 标签是unconfined,排除了这两方面的限制。 ps -o pid,label,args -p <pid>显示进程标签是cursor_sandbox_remote (enforce)。grep -rl cursorsandbox /etc/apparmor.d/找到对应的文件/etc/apparmor.d/cursor-sandbox-remote。dmesg里每次 kill 都有一条对应的拒绝日志。其中 pid 26692(comm="MainThread")是扩展宿主,它在 2577.377 s 先后发出 term 和 kill,都被拒绝:[2562.372144] apparmor="DENIED" operation="create" class="net" profile="cursor_sandbox_remote" pid=28145 comm="cursorsandbox" family="unix" sock_type="dgram" ... [2577.377795] apparmor="DENIED" operation="signal" class="signal" profile="cursor_sandbox_remote" pid=26692 comm="MainThread" requested_mask="receive" denied_mask="receive" signal=term peer="unconfined" [2577.377812] apparmor="DENIED" operation="signal" class="signal" profile="cursor_sandbox_remote" pid=26692 comm="MainThread" requested_mask="receive" denied_mask="receive" signal=kill peer="unconfined"第一条是这次检查里
cursorsandbox的第一条日志,和扩展宿主发信号正好隔了 15.005 秒,对上了代码里的 15 秒超时。
怎么判断是同一个问题
查看 ~/.cursor-server/data/logs/<会话>/exthost*/remoteexthost.log。如果 [sandboxPreflight] Running preflight 和 [primeSandboxHelperSupport] Preflight timed out after 15000ms 之间隔了几分钟,就是这个问题。
卡住时也可以直接看进程和 AppArmor 日志:
ps -eo pid,etime,label,args | grep '[c]ursorsandbox.*preflight-only'
sudo dmesg | grep 'profile="cursor_sandbox_remote"' | tail缓解办法
卡住时结束进程
结束这个进程,扩展宿主会立即恢复。结果和等它跑完一样,Cursor 都会按“沙箱不可用”处理。
直接 pkill 无效,原因见上文:AppArmor 拦截了所有发给它的信号,root 也不例外。要先把 profile 切到 complain 模式,这个模式只记日志、不拦截:
sudo apparmor_parser -r -C /etc/apparmor.d/cursor-sandbox-remote
sudo pkill -9 -f '[c]ursorsandbox.*--preflight-only'
# 恢复 enforce
sudo apparmor_parser -r /etc/apparmor.d/cursor-sandbox-remote如果切到 complain 后仍然报“权限不够”,就用 apparmor_parser -R 卸载 profile 再杀。卸载后,原来受它约束的进程会变成 unconfined。杀完用 -r 重新加载。
匹配模式要写成 [c]ursorsandbox。pkill -f 只会排除 pkill 自己,不会排除外层的 sudo。sudo 的命令行里也有 cursorsandbox,不加方括号就会把 sudo 一起杀掉,终端里会出现 killed sudo pkill ...。
长期办法
(1)让 profile 接收信号
在 /etc/apparmor.d/cursor-sandbox-remote 的 profile cursor_sandbox_remote 块里加一行:
signal (receive) peer=unconfined,然后执行 sudo apparmor_parser -r /etc/apparmor.d/cursor-sandbox-remote。改完后,扩展宿主的 15 秒超时能结束检查进程,每次最多卡 15 秒,手动 pkill 也能直接用。
这个文件不在 Cursor 的版本目录下,升级时不会随版本目录一起替换。它是不是由 Cursor 安装脚本写入、会不会被重写,还没确认。可以用 dpkg -S /etc/apparmor.d/cursor-sandbox-remote 查它属于哪个包。
(2)禁用沙箱程序
把 resources/helpers/cursorsandbox 改名,Cursor 找不到它就会跳过检查。这台机器上沙箱本来就不可用,功能上没有损失。
(3)换服务端启动目录
在 bin/cursor-server 里 cd 到一个空目录,再启动 node。扩展宿主继承这个目录,检查只扫空目录。
后两种办法都要改 ~/.cursor-server/bin/linux-x64/<版本> 下的文件。Cursor 升级后会换一个新的版本目录,需要重新改。
Google 查到的设置 "git.autoRepositoryDetection": false 对这个问题无效。卡住时 git 扩展本身没有在运行,这个设置还来不及生效。