A computer showing another brain lets its own box and Lima VM sleep
The Mac mini shows the cloud brain, whose bots work on the brain's own computer and borrow only this Mac's screen, browser and apps. Its local box still ran, and the Lima VM under it had grown to its whole 6 GB: the Mac never gets back memory a VM once used. - On a screen the box stops after 2 idle minutes, and the Lima VM too when nothing else runs in it. probe() no longer starts the VM there, and the volume holder and scheduled updates run on brains only. Becoming a brain again starts both (probe/ensure_running already wake the VM). - Screen mode is decided from the settings before the box loop's first look, which used to start the VM at launch. - gvisor-vm.sh: 4 GB instead of 6 (box capped at 2 GB), and the VM's ModemManager, udisks2 and multipathd are turned off. Verified on this Mac: box and VM stopped by themselves 2 minutes after the app started; the VM process (4.4 GB plus compressed pages) is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
This commit is contained in:
parent
22eaf031b0
commit
ec3fddf3ab
|
|
@ -738,7 +738,7 @@ AnyBot 是一個桌面聊天 App,介面和運作方式照 Grok Bot 重做。
|
|||
|
||||
- 預設是這台 Mac 的 Docker(OrbStack 或 Docker Desktop)。
|
||||
- 可以填 Docker 主機,例如 `ssh://me@server`(Tailscale 的機器名稱也行),Bot 的電腦就會在那台主機上;看畫面時 AnyBot 會自己開 SSH 通道。
|
||||
- **容器隔離**(設定 › 電腦 › 在哪裡執行):標準(runc:容器和 Docker 主機共用核心,Chromium 關掉自己的沙盒,容器是唯一那道牆)或強隔離(gVisor/runsc:電腦有自己的虛擬核心,權限全拿掉只留開機要的幾個,Chromium 的沙盒打開,兩道牆)。Docker 主機沒有 gVisor 時強隔離不能選;設定是強隔離、主機卻沒有 gVisor 時(例如換了 Docker 主機)先用標準開機並寫進紀錄,不會整台開不起來。那一列顯示電腦實際用的是哪種,和設定不同時提示按「更新」。Mac 上用 `deploy/mac/gvisor-vm.sh` 建一台自帶 Docker+gVisor 的 Lima 虛擬機(虛擬機沒開時 AnyBot 會自己開,例如 Mac 重開機後),`gvisor-migrate.sh` 把原本的檔案和登入搬過去(AnyBot 要先完全退出)。實測(Mac 上的 Lima VM):開機、畫面、VNC、無障礙編號、xdotool、截圖都正常;13 個 Chromium 程序都沒有 --no-sandbox,網頁程序 seccomp=2、有自己的命名空間;每一步 exec 55–69 ms、截圖 95 ms。
|
||||
- **容器隔離**(設定 › 電腦 › 在哪裡執行):標準(runc:容器和 Docker 主機共用核心,Chromium 關掉自己的沙盒,容器是唯一那道牆)或強隔離(gVisor/runsc:電腦有自己的虛擬核心,權限全拿掉只留開機要的幾個,Chromium 的沙盒打開,兩道牆)。Docker 主機沒有 gVisor 時強隔離不能選;設定是強隔離、主機卻沒有 gVisor 時(例如換了 Docker 主機)先用標準開機並寫進紀錄,不會整台開不起來。那一列顯示電腦實際用的是哪種,和設定不同時提示按「更新」。Mac 上用 `deploy/mac/gvisor-vm.sh` 建一台自帶 Docker+gVisor 的 Lima 虛擬機(4 GB;虛擬機沒開時 AnyBot 會自己開,例如 Mac 重開機後;虛擬機用過的記憶體 Mac 拿不回來,開越大就長越大,以前 6 GB 會整個長滿。腳本也關掉虛擬機用不到的數據機、磁碟自動掛載、多路徑儲存服務,約 70 MB),這台 AnyBot 是別的大腦的螢幕時(Bot 用的是大腦那邊的電腦,只借這台的畫面、瀏覽器和 App),自己的電腦閒置 2 分鐘就停下來,虛擬機裡沒有別的東西在跑就連虛擬機一起關掉,把記憶體還給 Mac;開機時看設定就知道自己是螢幕,不會先把虛擬機叫醒;變回大腦時兩個都會自己開回來(第一次多等 30–60 秒)。螢幕上也不跑「保住資料卷」的小容器和排程更新。`gvisor-migrate.sh` 把原本的檔案和登入搬過去(AnyBot 要先完全退出)。實測(Mac 上的 Lima VM):開機、畫面、VNC、無障礙編號、xdotool、截圖都正常;13 個 Chromium 程序都沒有 --no-sandbox,網頁程序 seccomp=2、有自己的命名空間;每一步 exec 55–69 ms、截圖 95 ms。
|
||||
- 換位置後會在新的地方重新建立;舊檔案可以先「儲存快照」,換過去再「從快照復原」。
|
||||
- **「網路流量改走這台 Mac」**(原版:網路流量可以改走你的本機電腦,「僅對新連線生效」,管理員可以關掉):打開後,Bot 的電腦之後執行的指令會經過這台 Mac 上的代理伺服器出去(例如要連公司內網或 VPN 才看得到的服務)。代理只聽這台 Mac 自己(127.0.0.1),用原版的埠 8791,被占用就往後找。設定頁顯示經過的連線數。Bot 的電腦在別的主機上時不能用。管理員可以用 `defaults write com.anybot.AnyBot AllowBoxTrafficViaThisMac false` 鎖住,這時顯示「🔒 由你的管理員設定,無法變更」。
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
set -euo pipefail
|
||||
VM=anybot-gvisor
|
||||
CPUS=4
|
||||
MEM=6 # GiB
|
||||
MEM=4 # GiB: the box is capped at 2 GB, Docker and the system take ~0.5 GB.
|
||||
# The Mac never gets back memory the VM once used, so a bigger VM
|
||||
# just grows bigger (it reached all of 6 GB).
|
||||
DISK=60 # GiB
|
||||
|
||||
if [ "${1:-}" = "--rm" ]; then
|
||||
|
|
@ -63,6 +65,8 @@ if d.get("runtimes", {}).get("runsc", {}).get("path") != "runsc":
|
|||
EOF
|
||||
)
|
||||
if [ "$CHANGED" = changed ]; then sudo systemctl restart docker; fi
|
||||
# Services a VM without modems, removable disks or SAN storage never uses (~70 MB).
|
||||
sudo systemctl disable --now ModemManager.service udisks2.service multipathd.service multipathd.socket >/dev/null 2>&1 || true
|
||||
# Prove the wall works: a container whose kernel really is gVisor.
|
||||
docker run --rm --runtime=runsc --entrypoint sh alpine:3.20 -c "grep -q gVisor /proc/version && uname -r" | sed "s/^/runsc 容器核心: /"
|
||||
'
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ impl App {
|
|||
/// Called once at start.
|
||||
pub(super) fn computers_start(&mut self) {
|
||||
if !self.demo {
|
||||
// A computer that will show another brain (mirror_boot, a moment later) says so now:
|
||||
// the box's first look would otherwise start the Lima VM it is about to put to sleep.
|
||||
let r = &self.state.remote;
|
||||
cloudbox::set_brain(r.brain_id.is_empty() || r.host_token.is_empty());
|
||||
cloudbox::start_background();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -741,9 +741,13 @@ fn wake_lima(wait: bool) -> Option<Result<(), String>> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ask Docker what state the box is in (changes nothing, but starts a Lima VM Docker lives in).
|
||||
/// Ask Docker what state the box is in (changes nothing, but starts a Lima VM Docker lives in —
|
||||
/// on a brain only: a computer showing another brain put its VM to sleep on purpose).
|
||||
pub fn probe() -> Run {
|
||||
if let Err(e) = dk(&["version", "--format", "{{.Server.Version}}"]) {
|
||||
if !IS_BRAIN.load(Ordering::SeqCst) && lima_vm(&docker_host()).is_some() {
|
||||
return Run::Asleep;
|
||||
}
|
||||
if wake_lima(false).is_some() {
|
||||
return Run::Starting("正在開 Docker 主機的虛擬機");
|
||||
}
|
||||
|
|
@ -1129,21 +1133,28 @@ pub fn start_background() {
|
|||
(s.last_used.elapsed().as_secs(), s.rebuilding)
|
||||
};
|
||||
if !rebuilding {
|
||||
if IS_BRAIN.load(Ordering::SeqCst) && matches!(probe(), Run::Asleep) {
|
||||
let brain = IS_BRAIN.load(Ordering::SeqCst);
|
||||
if brain && matches!(probe(), Run::Asleep) {
|
||||
log("Bot 的電腦不休眠:叫醒它。");
|
||||
let _ = ensure_running();
|
||||
}
|
||||
if !brain {
|
||||
sleep_on_a_screen(idle);
|
||||
}
|
||||
let quiet = idle >= QUIET_SECS && !is_busy() && !HELD_BY_PERSON.load(Ordering::SeqCst) && !viewer_open();
|
||||
let due = snapshot_time().map(|t| (chrono::Local::now() - t).num_seconds() >= SNAPSHOT_EVERY_SECS).unwrap_or(true);
|
||||
if quiet && due && run_state().is_running() {
|
||||
let _ = save_snapshot();
|
||||
}
|
||||
// (Only when a box exists on this Docker: the holder is its volume's seat-saver.)
|
||||
if matches!(probe(), Run::Asleep | Run::Running) {
|
||||
// (Only when a box exists on this Docker: the holder is its volume's seat-saver.
|
||||
// A screen leaves it stopped with the box.)
|
||||
if brain && matches!(probe(), Run::Asleep | Run::Running) {
|
||||
ensure_holder();
|
||||
}
|
||||
refresh();
|
||||
if brain {
|
||||
maybe_scheduled_update();
|
||||
}
|
||||
update_when_idle(idle);
|
||||
}
|
||||
}
|
||||
|
|
@ -1172,8 +1183,45 @@ fn update_when_idle(idle_secs: u64) {
|
|||
}
|
||||
}
|
||||
|
||||
/// How long a computer showing another brain keeps its own box after its last use.
|
||||
const SCREEN_SLEEP_SECS: u64 = 120;
|
||||
|
||||
/// This AnyBot shows another brain: the bots work on that brain's computer (and borrow only this
|
||||
/// one's screen, browser and apps), so this box — and on a Mac the Lima VM it runs in — only held
|
||||
/// memory: the VM grew to its whole 6 GB, which the Mac never gets back while it runs. Stop the
|
||||
/// box once nobody has used it for a while, and the VM when nothing else runs in it. Becoming a
|
||||
/// brain again starts both (probe and ensure_running wake the VM).
|
||||
fn sleep_on_a_screen(idle_secs: u64) {
|
||||
if IS_BRAIN.load(Ordering::SeqCst) || idle_secs < SCREEN_SLEEP_SECS || is_busy() || HELD_BY_PERSON.load(Ordering::SeqCst) || viewer_open() {
|
||||
return;
|
||||
}
|
||||
// Docker is down already (the VM sleeps), or nothing of ours runs: nothing to do.
|
||||
let Ok(running) = dk(&["ps", "--format", "{{.Names}}"]) else { return };
|
||||
let ours = [container().to_string(), holder_name()];
|
||||
let ours_running: Vec<&String> = ours.iter().filter(|n| running.lines().any(|l| l.trim() == n.as_str())).collect();
|
||||
let others = running.lines().map(str::trim).filter(|l| !l.is_empty() && !ours.iter().any(|n| n == l)).count();
|
||||
if ours_running.is_empty() && others > 0 {
|
||||
return;
|
||||
}
|
||||
for name in &ours_running {
|
||||
log(&format!("這台電腦現在是別的大腦的螢幕,Bot 用的是大腦那邊的電腦:先讓這台的 {name} 休息。"));
|
||||
let _ = dk_timeout(&["stop", "-t", "20", name.as_str()], 40_000);
|
||||
}
|
||||
set_run(Run::Asleep);
|
||||
if others > 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(vm) = lima_vm(&docker_host()) {
|
||||
log(&format!("虛擬機 {vm} 裡沒有別的東西在跑:關掉它,把記憶體還給 Mac(變回大腦時會自己開)。"));
|
||||
let out = Command::new("limactl").env("PATH", tool_path()).args(["stop", vm.as_str()]).stdin(Stdio::null()).output();
|
||||
if let Some(o) = out.ok().filter(|o| !o.status.success()) {
|
||||
log(&format!("關不了虛擬機 {vm}:{}", tail(String::from_utf8_lossy(&o.stderr).trim(), 300)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This AnyBot is the brain (its box is the bots' computer). A computer showing another brain
|
||||
/// doesn't use its own box, so it isn't kept awake there.
|
||||
/// doesn't use its own box, so it isn't kept awake there (sleep_on_a_screen).
|
||||
static IS_BRAIN: AtomicBool = AtomicBool::new(true);
|
||||
pub fn set_brain(on: bool) {
|
||||
IS_BRAIN.store(on, Ordering::SeqCst);
|
||||
|
|
|
|||
Loading…
Reference in New Issue