12 Commits

Author SHA1 Message Date
robcholz 4cb1162ca3 feat: fixed race condition, new version 2026-02-08 02:55:43 -05:00
robcholz 4c9f517107 doc: changed style 2026-02-08 02:54:52 -05:00
robcholz 15245fd245 fix: fixed the race condition brought by shutdown triggerred while vm input channel is not ready or closed 2026-02-08 02:51:35 -05:00
robcholz 1c5a464a68 doc: added zh-CN 2026-02-08 02:32:47 -05:00
robcholz 5659f2a538 fix: now project dir correctly mounted 2026-02-08 02:25:12 -05:00
robcholz 9cf33359f9 doc: updated README.md 2026-02-08 02:02:59 -05:00
robcholz a1056ba5cb doc: added readme and contributing.md 2026-02-08 01:53:51 -05:00
robcholz ad8fb139de feat: new version 2026-02-08 00:38:21 -05:00
robcholz 5e008c9471 fix: fixed the annoying dhcp conflict when having concurrent vm 2026-02-08 00:34:01 -05:00
robcholz 935e0cd566 fix: mise install fail will not fail the ssh 2026-02-08 00:32:29 -05:00
robcholz c53a6eff51 ci: added tests 2026-02-07 23:58:10 -05:00
robcholz 4dd9c31f74 refactor: now code and claude becomes default mounts 2026-02-07 23:50:25 -05:00
15 changed files with 506 additions and 35 deletions
+14
View File
@@ -6,6 +6,8 @@ on:
- "**"
paths-ignore:
- "README.md"
- "CONTRIBUTING.md"
- "README.zh.md"
- "docs/**"
- "install"
pull_request:
@@ -54,3 +56,15 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: cargo build
run: cargo build --locked
test:
name: Test
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- name: cargo test
run: cargo test --locked
+44
View File
@@ -0,0 +1,44 @@
# Contributing to Vibebox
Thanks for your interest in contributing! This guide keeps PRs small, reviewable, and consistent with the projects
development workflow.
**Prerequisites**
- macOS on Apple Silicon (required for the virtualization backend)
- Rust `1.91.1` or newer (see `Cargo.toml`)
**Getting Started**
1. Fork the repo and create a feature branch.
2. Build once to validate your toolchain:
```bash
cargo build --locked
```
**Development Commands**
- Format: `cargo fmt --all -- --check`
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
- Test: `cargo test --locked`
- Build: `cargo build --locked`
**Submitting Changes**
- Keep changes focused and scoped to one problem.
- Update or add tests when behavior changes.
- If you change user-facing behavior, update docs or help text.
- Avoid adding heavy dependencies without a clear reason.
**Reporting Issues**
Please include:
- macOS version and hardware (Apple Silicon model)
- Vibebox version (`vibebox --version`)
- Steps to reproduce
- Logs from `.vibebox/cli.log`, `.vibebox/vm_root.log` and `.vibebox/vm_manager.log`
**Security**
If you believe youve found a security issue, please avoid public disclosure. Open a private report via GitHub Security
Advisories instead.
Generated
+1 -1
View File
@@ -1238,7 +1238,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vibebox"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"block2",
"clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vibebox"
version = "0.2.0"
version = "0.2.2"
edition = "2024"
authors = ["Finn Sheng"]
description = "Ultrafast CLI on Apple Silicon macOS for fast, sandboxed development and LLM agents."
+158
View File
@@ -0,0 +1,158 @@
<p align="center">
<a href="https://vibebox.robcholz.com">
<picture>
<img src="docs/banner.png" alt="VibeBox logo">
</picture>
</a>
</p>
<p align="center">Your ultrafast open source AI sandbox.</p>
<p align="center">
<a href="https://crates.io/crates/vibebox">
<img alt="Crates.io" src="https://img.shields.io/crates/v/vibebox.svg">
</a>
<a href="https://github.com/robcholz/vibebox/blob/main/LICENSE">
<img alt="MIT licensed" src="https://img.shields.io/badge/license-MIT-blue.svg">
</a>
<a href="https://github.com/robcholz/vibebox/actions?query=workflow%3ACI+branch%3Amain">
<img alt="Build Status" src="https://github.com/robcholz/vibebox/workflows/CI/badge.svg">
</a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a>
</p>
[![OpenCode Terminal UI](docs/screenshot.png)](https://vibebox.robcholz.com)
---
### Installation
```bash
# YOLO
curl -fsSL https://raw.githubusercontent.com/robcholz/vibebox/main/install | bash
# Package managers
cargo install vibebox
# Or manually (bad)
curl -LO https://github.com/robcholz/vibebox/releases/download/latest/vibebox-macos-arm64.zip
unzip vibebox-macos-arm64.zip
mkdir -p ~/.local/bin
mv vibe ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
```
> [!TIP]
> We truly recommend you to use `YOLO` to install.
**Requirements**
- macOS on Apple Silicon (Vibebox uses Apple's virtualization APIs).
**First Run**
The first `vibebox` run downloads a Debian base image and provisions it. After that, per-project instances reuse the
cached base image for much faster startups.
### Documentation
**Quick Start**
```bash
cd /path/to/your/project
vibebox
```
On first run, Vibebox creates `vibebox.toml` in your project (if missing) and a `.vibebox/` directory for instance data.
**Configuration (`vibebox.toml`)**
`vibebox.toml` lives in your project root by default. You can override it with `vibebox -c path/to/vibebox.toml` or the
`VIBEBOX_CONFIG_PATH` env var, but the path must stay inside the project directory.
Default config (auto-created when missing):
```toml
[box]
cpu_count = 2
ram_mb = 2048
disk_gb = 5
mounts = [
"~/.codex:~/.codex:read-write",
"~/.claude:~/.claude:read-write",
]
[supervisor]
auto_shutdown_ms = 20000
```
`disk_gb` is only applied when the instance disk is first created. If you change it later, run `vibebox reset` to
recreate the disk.
**Mounts**
- Your project is mounted read-write at `~/<project-name>`, and the shell starts there.
- If a `.git` directory exists, it is masked with a tmpfs mount inside the VM to discourage accidental edits from the
guest.
- Extra mounts come from `box.mounts` with the format `host:guest[:read-only|read-write]`.
- Host paths support `~` expansion. Relative guest paths are treated as `/root/<path>`.
- Guest paths that use `~` are linked into `/home/<ssh-user>` for convenience. Run `vibebox explain` to see the resolved
host/guest mappings.
**CLI Commands**
```bash
vibebox # start or attach to the current project VM
vibebox list # list known project sessions
vibebox reset # delete .vibebox for this project and recreate on next run
vibebox purge-cache # delete the global cache (~/.cache/vibebox)
vibebox explain # show mounts and network info
```
**Inside the VM**
- Default SSH user: `vibecoder`
- Hostname: `vibebox`
- Base image provisioning installs: build tools, `git`, `curl`, `ripgrep`, `openssh-server`, and `sudo`.
- On first login, Vibebox installs `mise` and configures tools like `uv`, `node`, `@openai/codex`, and
`@anthropic-ai/claude-code` (best-effort).
- Shell aliases: `:help` and `:exit`.
**State & Cache**
- Project state lives in `.vibebox/` (instance disk, SSH keys, logs, manager socket/pid). `vibebox reset` removes it.
- Global cache lives in `~/.cache/vibebox` (base image + shared guest cache). `vibebox purge-cache` clears it.
- Session index lives in `~/.vibebox/sessions` and is shown by `vibebox list`.
### Contributing
If you're interested in contributing to VibeBox, please read our [contributing docs](CONTRIBUTING.md) before
submitting a pull request.
### Using VibeBox
Feel free to use, but remember to promote VibeBox as well!
### FAQ
#### How is this different from other Sandboxes?
Vibebox is built for fast, repeatable local sandboxes with minimal ceremony. Whats different here:
- Warm startup is typically under **6 seconds** on my M3, so you can jump back in quickly.
- One simple command — `vibebox` — drops you into the sandbox from your project.
- Configuration lives in `vibebox.toml`, where you can set CPU, RAM, disk size, and mounts.
### Special Thank
[vibe](https://github.com/lynaghk/vibe) by lynaghk.
And amazing Rust community, without your rich crates and fantastic toolchain like [crates.io](https://crates.io), this
wouldn't be possible!
---
**Follow me on X** [X.com](https://x.com/robcholz)
+156
View File
@@ -0,0 +1,156 @@
<p align="center">
<a href="https://vibebox.robcholz.com">
<picture>
<img src="docs/banner.png" alt="VibeBox logo">
</picture>
</a>
</p>
<p align="center">超高速、开源的 AI 沙盒。</p>
<p align="center">
<a href="https://crates.io/crates/vibebox">
<img alt="Crates.io" src="https://img.shields.io/crates/v/vibebox.svg">
</a>
<a href="https://github.com/robcholz/vibebox/blob/main/LICENSE">
<img alt="MIT licensed" src="https://img.shields.io/badge/license-MIT-blue.svg">
</a>
<a href="https://github.com/robcholz/vibebox/actions?query=workflow%3ACI+branch%3Amain">
<img alt="Build Status" src="https://github.com/robcholz/vibebox/workflows/CI/badge.svg">
</a>
</p>
<p align="center">
<a href="README.zh.md">简体中文</a> |
<a href="README.md">English</a>
</p>
[![OpenCode Terminal UI](docs/screenshot.png)](https://vibebox.robcholz.com)
---
### 安装
```bash
# YOLO:一键安装(推荐)
curl -fsSL https://raw.githubusercontent.com/robcholz/vibebox/main/install | bash
# Cargo
cargo install vibebox
# 或者手动安装(不推荐)
curl -LO https://github.com/robcholz/vibebox/releases/download/latest/vibebox-macos-arm64.zip
unzip vibebox-macos-arm64.zip
mkdir -p ~/.local/bin
mv vibe ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
```
> [!TIP]
> 强烈建议直接用 `YOLO` 方式安装,省事且更不容易踩坑。
**系统要求**
- Apple Silicon 的 macOSVibeBox 使用了 Apple 的虚拟化 API)。
**首次运行**
第一次执行 `vibebox` 会下载 Debian 基础镜像并完成初始化。之后每个项目的实例会复用缓存的基础镜像,
启动会快很多。
### 文档
**快速开始**
```bash
cd /path/to/your/project
vibebox
```
第一次运行时,如果项目目录里缺少配置,VibeBox 会自动创建 `vibebox.toml`(放在项目根目录),并创建
`.vibebox/` 用来保存实例数据。
**配置(`vibebox.toml`**
默认情况下,`vibebox.toml` 位于项目根目录。你可以用 `vibebox -c path/to/vibebox.toml` 或设置
`VIBEBOX_CONFIG_PATH` 环境变量来覆盖路径,但配置文件必须仍然位于项目目录内部。
默认配置(缺失时会自动生成):
```toml
[box]
cpu_count = 2
ram_mb = 2048
disk_gb = 5
mounts = [
"~/.codex:~/.codex:read-write",
"~/.claude:~/.claude:read-write",
]
[supervisor]
auto_shutdown_ms = 20000
```
注意:`disk_gb` 只在「首次创建实例磁盘」时生效。之后如果你改了它,需要运行 `vibebox reset` 重新创建磁盘。
**挂载(Mounts**
- 你的项目会以读写方式挂载到 `~/<project-name>`,并且 shell 会默认从那里启动。
- 如果项目里存在 `.git` 目录,VM 内会用 tmpfs 把它遮住,避免你在 guest 里误操作改到 Git 元数据。
- 额外挂载通过 `box.mounts` 配置,格式为 `host:guest[:read-only|read-write]`
- Host 路径支持 `~` 展开;guest 的相对路径会被视为 `/root/<path>`
- guest 路径如果用了 `~`,会为了方便被链接到 `/home/<ssh-user>` 下。你可以运行 `vibebox explain`
查看最终解析后的 host/guest 映射关系。
**CLI 命令**
```bash
vibebox # 启动或连接当前项目的 VM
vibebox list # 列出已知的项目会话
vibebox reset # 删除当前项目的 .vibebox,下一次运行会重新创建
vibebox purge-cache # 删除全局缓存(~/.cache/vibebox
vibebox explain # 显示挂载与网络信息
```
**在 VM 内部**
- 默认 SSH 用户:`vibecoder`
- 主机名:`vibebox`
- 基础镜像初始化会安装:构建工具、`git``curl``ripgrep``openssh-server``sudo`
- 首次登录时,VibeBox 会安装 `mise`,并尽力配置 `uv``node``@openai/codex`
`@anthropic-ai/claude-code` 等工具(best-effort,视网络和环境而定)
- Shell 里有两个别名:`:help``:exit`
**状态与缓存**
- 项目级状态在 `.vibebox/`(实例磁盘、SSH key、日志、manager socket/pid)。`vibebox reset` 会移除它。
- 全局缓存在 `~/.cache/vibebox`(基础镜像 + 共享 guest 缓存)。`vibebox purge-cache` 会清空它。
- 会话索引在 `~/.vibebox/sessions`,可以通过 `vibebox list` 查看。
### 参与贡献
如果你想参与贡献 VibeBox,请先阅读 [贡献指南](CONTRIBUTING.md),再提交 Pull Request。
### 使用 VibeBox
欢迎使用,也别忘了顺手帮 VibeBox 做点宣传!
### FAQ
#### 它和其它 Sandboxes 有什么不同?
Vibebox 追求的是:本地、可复现、启动快、流程简单。主要差异点:
- 在我的 M3 上,热启动通常 **6 秒以内**,可以非常快地回到工作状态。
- 一个命令——`vibebox`——直接把你带进沙盒(从你的项目目录启动)。
- 配置集中在 `vibebox.toml`,CPU / 内存 / 磁盘大小 / 挂载都能一眼看懂、随手改。
### 特别鸣谢
[vibe](https://github.com/lynaghk/vibe) by lynaghk。
以及 Rust 社区。没有你们丰富的 crates 生态和优秀的工具链(比如 [crates.io](https://crates.io)),
这个项目不可能这么顺利!Rust教。
---
**在 X 上关注我** [X.com](https://x.com/robcholz)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+4 -1
View File
@@ -67,7 +67,10 @@ fn default_auto_shutdown_ms() -> u64 {
}
fn default_mounts() -> Vec<String> {
Vec::new()
vec![
"~/.codex:~/.codex:read-write".into(),
"~/.claude:~/.claude:read-write".into(),
]
}
fn default_disk_gb() -> u64 {
+1 -1
View File
@@ -46,7 +46,7 @@ fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>, Box<dyn Error +
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("project");
let project_guest = format!("/root/{project_name}");
let project_guest = format!("~/{project_name}");
let project_host = display_path(cwd);
let mut rows = vec![tui::MountListRow {
host: project_host,
+2
View File
@@ -394,6 +394,7 @@ fn ssh_port_open(ip: &str) -> bool {
pub(crate) fn build_ssh_login_actions(
config: &Arc<Mutex<InstanceConfig>>,
project_name: &str,
project_guest_dir: &str,
guest_dir: &str,
key_name: &str,
home_links_script: &str,
@@ -409,6 +410,7 @@ pub(crate) fn build_ssh_login_actions(
.replace("__SSH_USER__", &ssh_user)
.replace("__SUDO_PASSWORD__", &sudo_password)
.replace("__PROJECT_NAME__", project_name)
.replace("__PROJECT_GUEST_DIR__", project_guest_dir)
.replace("__KEY_PATH__", &key_path)
.replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script())
.replace("__VIBEBOX_HOME_LINKS__", home_links_script);
+5
View File
@@ -43,6 +43,11 @@ systemctl restart ssh
# Set this env var so claude doesn't complain about running as root.'
echo "export IS_SANDBOX=1" >> .bashrc
# Ensure cloned instances generate unique machine-id on first boot.
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -sf /etc/machine-id /var/lib/dbus/machine-id
# Shutdown the VM when you logout
cat > .bash_logout <<EOF
systemctl poweroff
+51 -14
View File
@@ -3,6 +3,7 @@ set -eu
SSH_USER="__SSH_USER__"
PROJECT_NAME="__PROJECT_NAME__"
PROJECT_GUEST_DIR="__PROJECT_GUEST_DIR__"
KEY_PATH="__KEY_PATH__"
diag() { echo "[vibebox][diag] $*" >&2; }
@@ -49,7 +50,7 @@ dump_diag() {
}
# 1) tmpfs mount
TARGET="/root/${PROJECT_NAME}/.vibebox"
TARGET="${PROJECT_GUEST_DIR}/.vibebox"
if [ -d "$TARGET" ] && ! mountpoint -q "$TARGET"; then
mount -t tmpfs tmpfs "$TARGET"
fi
@@ -78,6 +79,21 @@ __VIBEBOX_SHELL_SCRIPT__
VIBEBOX_SHELL_EOF
chmod 644 /etc/profile.d/vibebox.sh
# Auto-cd into project for interactive shells
cat > /etc/profile.d/vibebox-project.sh <<'VIBEBOX_PROJECT_EOF'
case "$-" in
*i*)
project_home="${HOME}/__PROJECT_NAME__"
if [ "$USER" = "__SSH_USER__" ] && [ -d "$project_home" ]; then
cd "$project_home"
elif [ "$USER" = "__SSH_USER__" ] && [ -d "__PROJECT_GUEST_DIR__" ]; then
cd "__PROJECT_GUEST_DIR__"
fi
;;
esac
VIBEBOX_PROJECT_EOF
chmod 644 /etc/profile.d/vibebox-project.sh
if ! grep -q "vibebox-aliases" "${USER_HOME}/.bashrc" 2>/dev/null; then
{
echo ""
@@ -88,16 +104,22 @@ fi
# Install Mise
MISE_BIN="${USER_HOME}/.local/bin/mise"
if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then
curl https://mise.run | HOME="$USER_HOME" sh
fi
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc"
mise_warn() { echo "[mise] $*" >&2; }
mise_ok() { command -v mise >/dev/null 2>&1 || [ -x "$MISE_BIN" ]; }
mise_install() {
if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then
if ! curl https://mise.run | HOME="$USER_HOME" sh; then
mise_warn "mise install script failed (continuing)"
return 0
fi
fi
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc"
export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH"
export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH"
mkdir -p "${USER_HOME}/.config/mise"
mkdir -p "${USER_HOME}/.config/mise"
cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
[settings]
# Always use the venv created by uv, if available in directory
python.uv_venv_auto = true
@@ -111,12 +133,21 @@ cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
"npm:@anthropic-ai/claude-code" = "latest"
MISE
touch "${USER_HOME}/.config/mise/mise.lock"
if [ -x "$MISE_BIN" ]; then
HOME="$USER_HOME" "$MISE_BIN" install
else
HOME="$USER_HOME" mise install
fi
touch "${USER_HOME}/.config/mise/mise.lock"
if [ -x "$MISE_BIN" ]; then
if ! HOME="$USER_HOME" "$MISE_BIN" install; then
mise_warn "mise install failed (continuing)"
return 0
fi
else
if ! HOME="$USER_HOME" mise install; then
mise_warn "mise install failed (continuing)"
return 0
fi
fi
}
mise_install || true
# 3) start ssh (don't swallow failures)
# If ssh is already active, don't force start/restart.
@@ -184,5 +215,11 @@ if ! listens_ok; then
exit 1
fi
ip a
ip link
curl -s https://api.ipify.org ; echo
cat /etc/machine-id
echo VIBEBOX_SSH_READY
echo "VIBEBOX_IPV4=$ip"
+3 -10
View File
@@ -32,6 +32,7 @@ const DEBIAN_COMPRESSED_DISK_URL: &str = "https://cloud.debian.org/images/cloud/
const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
const SHARED_DIRECTORIES_TAG: &str = "shared";
pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
const BYTES_PER_MB: u64 = 1024 * 1024;
const DEFAULT_CPU_COUNT: usize = 2;
@@ -247,7 +248,8 @@ where
let mut directory_shares = Vec::new();
if !args.no_default_mounts {
login_actions.push(Send(format!("cd {project_name}")));
let project_guest_dir = PathBuf::from(PROJECT_GUEST_BASE).join(project_name);
login_actions.push(Send(format!("cd {}", project_guest_dir.display())));
// discourage read/write of .git folder from within the VM. note that this isn't secure, since the VM runs as root and could unmount this.
// I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
@@ -255,15 +257,6 @@ where
login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
}
directory_shares.push(
DirectoryShare::new(
project_root,
PathBuf::from("/root/").join(project_name),
false,
)
.expect("Project directory must exist"),
);
directory_shares.push(mise_directory_share);
}
+66 -7
View File
@@ -25,11 +25,12 @@ use crate::{
session_manager::{
GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
},
vm::{self, DirectoryShare, LoginAction, VmInput},
vm::{self, DirectoryShare, LoginAction, PROJECT_GUEST_BASE, VmInput},
};
const VM_MANAGER_LOCK_NAME: &str = "vm.lock";
const VM_MANAGER_LOG_NAME: &str = "vm_manager.log";
const HARD_SHUTDOWN_TIMEOUT_MS: u64 = 12_000;
pub fn ensure_manager(
raw_args: &[std::ffi::OsString],
@@ -200,6 +201,30 @@ fn cleanup_stale_manager(instance_dir: &Path) {
let _ = fs::remove_file(&pid_path);
}
fn inject_project_mount(
mounts: &mut Vec<String>,
project_root: &Path,
ssh_user: &str,
project_name: &str,
) {
let guest_tilde = format!("~/{project_name}");
let guest_home = format!("/home/{ssh_user}/{project_name}");
let guest_base = format!("{PROJECT_GUEST_BASE}/{project_name}");
let already_mapped = mounts.iter().any(|spec| {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() < 2 {
return false;
}
let guest = parts[1];
guest == guest_tilde || guest == guest_home || guest == guest_base
});
if already_mapped {
return;
}
let host = project_root.display();
mounts.insert(0, format!("{host}:{guest_tilde}:read-write"));
}
fn is_socket_path(path: &Path) -> bool {
fs::metadata(path)
.map(|meta| meta.file_type().is_socket())
@@ -252,7 +277,7 @@ fn rewrite_mount_spec(spec: &str, ssh_user: &str) -> (String, Option<HomeLink>)
return (spec.to_string(), None);
}
let root_base = "/usr/local/vibebox-mounts";
let root_base = PROJECT_GUEST_BASE;
let root_path = if rel.is_empty() {
root_base.to_string()
} else {
@@ -563,7 +588,7 @@ impl VmExecutor for RealVmExecutor {
fn run_manager_with(
project_root: &Path,
args: vm::VmArg,
mut args: vm::VmArg,
auto_shutdown_ms: u64,
executor: &dyn VmExecutor,
options: ManagerOptions,
@@ -602,8 +627,12 @@ fn run_manager_with(
.lock()
.map(|cfg| cfg.ssh_user_display())
.unwrap_or_else(|_| DEFAULT_SSH_USER.to_string());
if !args.no_default_mounts {
inject_project_mount(&mut args.mounts, project_root, &ssh_user, &project_name);
}
let (args, home_links_script) = prepare_mounts_and_links(args, &ssh_user);
let project_guest_dir = format!("{PROJECT_GUEST_BASE}/{project_name}");
let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME);
let extra_shares = vec![DirectoryShare::new(
instance_dir.clone(),
@@ -613,6 +642,7 @@ fn run_manager_with(
let extra_login_actions = build_ssh_login_actions(
&config,
&project_name,
&project_guest_dir,
ssh_guest_dir.as_str(),
"ssh_key",
&home_links_script,
@@ -692,7 +722,9 @@ fn manager_event_loop(
let mut ref_count: usize = 0;
let mut shutdown_deadline: Option<Instant> = None;
let mut shutdown_sent = false;
let mut hard_deadline: Option<Instant> = None;
let grace = Duration::from_millis(auto_shutdown_ms.max(1));
let hard_timeout = Duration::from_millis(HARD_SHUTDOWN_TIMEOUT_MS);
loop {
let timeout = match shutdown_deadline {
@@ -711,6 +743,7 @@ fn manager_event_loop(
);
shutdown_deadline = None;
shutdown_sent = false;
hard_deadline = None;
}
Ok(ManagerEvent::Dec(pid)) => {
ref_count = ref_count.saturating_sub(1);
@@ -736,12 +769,38 @@ fn manager_event_loop(
&& Instant::now() >= deadline
&& !shutdown_sent
{
let mut sent = false;
if let Some(tx) = vm_input_tx.lock().unwrap().clone() {
let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
if tx
.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()))
.is_ok()
{
sent = true;
} else {
tracing::warn!("shutdown command failed to send");
}
} else {
tracing::warn!("shutdown command deferred; vm input not ready");
}
tracing::info!("shutdown command sent");
shutdown_sent = true;
shutdown_deadline = None;
if sent {
tracing::info!("shutdown command sent");
shutdown_sent = true;
shutdown_deadline = None;
hard_deadline = Some(Instant::now() + hard_timeout);
} else {
shutdown_deadline = Some(Instant::now() + Duration::from_millis(500));
}
}
if ref_count == 0
&& shutdown_sent
&& let Some(deadline) = hard_deadline
&& Instant::now() >= deadline
{
tracing::warn!(
timeout_ms = HARD_SHUTDOWN_TIMEOUT_MS,
"force exiting: VM did not stop after shutdown timeout"
);
std::process::exit(1);
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,