mirror of
https://github.com/robcholz/vibebox.git
synced 2026-04-01 00:10:15 +02:00
feat: add an actual config file.
This commit is contained in:
@@ -11,27 +11,25 @@ use tracing_subscriber::EnvFilter;
|
||||
use vibebox::tui::{AppState, VmInfo};
|
||||
use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager};
|
||||
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 30000;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_tracing();
|
||||
color_eyre::install()?;
|
||||
|
||||
let raw_args: Vec<OsString> = env::args_os().collect();
|
||||
|
||||
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
|
||||
let config = config::load_config(&cwd);
|
||||
|
||||
if env::var("VIBEBOX_VM_MANAGER").as_deref() == Ok("1") {
|
||||
tracing::info!("starting vm manager mode");
|
||||
// TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
|
||||
let args = vm::VmArg {
|
||||
cpu_count: 2,
|
||||
ram_bytes: 2048 * 1024 * 1024,
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: Vec::new(),
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
tracing::info!(auto_shutdown_ms, "vm manager config");
|
||||
if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) {
|
||||
tracing::error!(error = %err, "vm manager exited");
|
||||
@@ -42,12 +40,11 @@ fn main() -> Result<()> {
|
||||
|
||||
vm::ensure_signed();
|
||||
|
||||
// TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
|
||||
let vm_args = vm::VmArg {
|
||||
cpu_count: 2,
|
||||
ram_bytes: 2048 * 1024 * 1024,
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: Vec::new(),
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
|
||||
let vm_info = VmInfo {
|
||||
@@ -55,15 +52,10 @@ fn main() -> Result<()> {
|
||||
max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
|
||||
cpu_cores: vm_args.cpu_count,
|
||||
};
|
||||
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
|
||||
let auto_shutdown_ms = config::load_config(&cwd)
|
||||
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?
|
||||
.auto_shutdown_ms
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
if let Ok(manager) = SessionManager::new() {
|
||||
if let Err(err) = manager.update_global_sessions(&cwd) {
|
||||
tracing::warn!(error = %err, "failed to update global session list");
|
||||
tracing::warn!(error = %err, "failed to update a global session list");
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("failed to initialize session manager");
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::{
|
||||
use color_eyre::Result;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use vibebox::{instance, vm, vm_manager};
|
||||
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000;
|
||||
use vibebox::{config, instance, vm, vm_manager};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_tracing();
|
||||
@@ -16,20 +14,17 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::info!("starting vm supervisor");
|
||||
let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let config = config::load_config(&cwd);
|
||||
let instance_dir = instance::ensure_instance_dir(&cwd)
|
||||
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
|
||||
let _ = instance::touch_last_active(&instance_dir);
|
||||
// TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
|
||||
let args = vm::VmArg {
|
||||
cpu_count: 2,
|
||||
ram_bytes: 2048 * 1024 * 1024,
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: Vec::new(),
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
|
||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||
tracing::info!(auto_shutdown_ms, "vm supervisor config");
|
||||
|
||||
let result = vm_manager::run_manager(args, auto_shutdown_ms);
|
||||
|
||||
212
src/config.rs
212
src/config.rs
@@ -3,13 +3,72 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vm::DirectoryShare;
|
||||
|
||||
pub const CONFIG_FILENAME: &str = "vibebox.toml";
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
pub auto_shutdown_ms: Option<u64>,
|
||||
const DEFAULT_CPU_COUNT: usize = 2;
|
||||
const DEFAULT_RAM_MB: u64 = 2048;
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "box")]
|
||||
pub box_cfg: BoxConfig,
|
||||
pub supervisor: SupervisorConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
box_cfg: BoxConfig::default(),
|
||||
supervisor: SupervisorConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BoxConfig {
|
||||
pub cpu_count: usize,
|
||||
pub ram_mb: u64,
|
||||
pub mounts: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for BoxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cpu_count: default_cpu_count(),
|
||||
ram_mb: default_ram_mb(),
|
||||
mounts: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SupervisorConfig {
|
||||
pub auto_shutdown_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for SupervisorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_shutdown_ms: default_auto_shutdown_ms(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cpu_count() -> usize {
|
||||
DEFAULT_CPU_COUNT
|
||||
}
|
||||
|
||||
fn default_ram_mb() -> u64 {
|
||||
DEFAULT_RAM_MB
|
||||
}
|
||||
|
||||
fn default_auto_shutdown_ms() -> u64 {
|
||||
DEFAULT_AUTO_SHUTDOWN_MS
|
||||
}
|
||||
|
||||
pub fn config_path(project_root: &Path) -> PathBuf {
|
||||
@@ -19,20 +78,147 @@ pub fn config_path(project_root: &Path) -> PathBuf {
|
||||
pub fn ensure_config_file(project_root: &Path) -> Result<PathBuf, io::Error> {
|
||||
let path = config_path(project_root);
|
||||
if !path.exists() {
|
||||
fs::write(&path, "")?;
|
||||
let default_config = Config::default();
|
||||
let contents = toml::to_string_pretty(&default_config).unwrap_or_default();
|
||||
fs::write(&path, contents)?;
|
||||
tracing::info!(path = %path.display(), "created vibebox config");
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn load_config(
|
||||
project_root: &Path,
|
||||
) -> Result<ProjectConfig, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let path = ensure_config_file(project_root)?;
|
||||
let raw = fs::read_to_string(&path)?;
|
||||
pub fn load_config(project_root: &Path) -> Config {
|
||||
let path = match ensure_config_file(project_root) {
|
||||
Ok(path) => path,
|
||||
Err(err) => die(&format!("failed to create config: {err}")),
|
||||
};
|
||||
let raw = match fs::read_to_string(&path) {
|
||||
Ok(raw) => raw,
|
||||
Err(err) => die(&format!("failed to read config: {err}")),
|
||||
};
|
||||
let trimmed = raw.trim();
|
||||
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(ProjectConfig::default());
|
||||
if trimmed.is_empty() {
|
||||
die(&format!(
|
||||
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
Ok(toml::from_str::<ProjectConfig>(&raw)?)
|
||||
|
||||
let value: toml::Value = match toml::from_str(trimmed) {
|
||||
Ok(value) => value,
|
||||
Err(err) => die(&format!("invalid config: {err}")),
|
||||
};
|
||||
let schema_errors = validate_schema(&value);
|
||||
if !schema_errors.is_empty() {
|
||||
let message = format!(
|
||||
"config file ({}) is missing or invalid fields:\n- {}",
|
||||
path.display(),
|
||||
schema_errors.join("\n- ")
|
||||
);
|
||||
die(&message);
|
||||
}
|
||||
|
||||
let config: Config = match toml::from_str(trimmed) {
|
||||
Ok(config) => config,
|
||||
Err(err) => die(&format!("invalid config: {err}")),
|
||||
};
|
||||
validate_or_exit(&config);
|
||||
config
|
||||
}
|
||||
|
||||
fn validate_schema(value: &toml::Value) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
let root = match value.as_table() {
|
||||
Some(table) => table,
|
||||
None => {
|
||||
errors.push("config must be a table".to_string());
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
match root.get("box") {
|
||||
None => errors.push("missing [box] table".to_string()),
|
||||
Some(value) => match value.as_table() {
|
||||
Some(table) => {
|
||||
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
|
||||
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
|
||||
validate_string_array(
|
||||
table,
|
||||
"mounts",
|
||||
"[box].mounts (array of strings)",
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
None => errors.push("[box] must be a table".to_string()),
|
||||
},
|
||||
}
|
||||
|
||||
match root.get("supervisor") {
|
||||
None => errors.push("missing [supervisor] table".to_string()),
|
||||
Some(value) => match value.as_table() {
|
||||
Some(table) => {
|
||||
validate_int(
|
||||
table,
|
||||
"auto_shutdown_ms",
|
||||
"[supervisor].auto_shutdown_ms (integer)",
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
None => errors.push("[supervisor] must be a table".to_string()),
|
||||
},
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn validate_int(table: &toml::value::Table, key: &str, label: &str, errors: &mut Vec<String>) {
|
||||
match table.get(key) {
|
||||
None => errors.push(format!("missing {label}")),
|
||||
Some(value) => {
|
||||
if value.as_integer().is_none() {
|
||||
errors.push(format!("invalid {label}: expected integer"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_string_array(
|
||||
table: &toml::value::Table,
|
||||
key: &str,
|
||||
label: &str,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
match table.get(key) {
|
||||
None => errors.push(format!("missing {label}")),
|
||||
Some(value) => match value.as_array() {
|
||||
Some(values) => {
|
||||
if values.iter().any(|value| !value.is_str()) {
|
||||
errors.push(format!("invalid {label}: expected array of strings"));
|
||||
}
|
||||
}
|
||||
None => errors.push(format!("invalid {label}: expected array of strings")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_or_exit(config: &Config) {
|
||||
if config.box_cfg.cpu_count == 0 {
|
||||
die("box.cpu_count must be >= 1");
|
||||
}
|
||||
if config.box_cfg.ram_mb == 0 {
|
||||
die("box.ram_mb must be >= 1");
|
||||
}
|
||||
if config.supervisor.auto_shutdown_ms == 0 {
|
||||
die("supervisor.auto_shutdown_ms must be >= 1");
|
||||
}
|
||||
for spec in &config.box_cfg.mounts {
|
||||
if let Err(err) = DirectoryShare::from_mount_spec(spec) {
|
||||
die(&format!("invalid mount spec '{spec}': {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn die(message: &str) -> ! {
|
||||
eprintln!("[vibebox] {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[box]
|
||||
cpu_count = 2
|
||||
ram_mb = 2048
|
||||
mounts = []
|
||||
|
||||
[supervisor]
|
||||
auto_shutdown_ms = 20000
|
||||
|
||||
Reference in New Issue
Block a user