diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9a0904b..c2570100 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: cargo version export DISK_IMG="${{ github.workspace }}/disk-aarch64.img" export VM_CONFIGS="$(pwd)/configs/vms/nimbos-aarch64-qemu-smp1.toml" - ./axvisor.sh run --plat ${{ matrix.plat }} --vmconfigs $VM_CONFIGS --features fs,ept-level-4 --arceos-args DISK_IMG=$DISK_IMG,BUS=mmio,BLK=y,MEM=8g + ./scripts/auto_interrupt.sh ./axvisor.sh run --plat ${{ matrix.plat }} --vmconfigs $VM_CONFIGS --features fs,ept-level-4 --arceos-args DISK_IMG=$DISK_IMG,BUS=mmio,BLK=y,MEM=8g,LOG=info aarch64-generic-phytiumpi: runs-on: [self-hosted, linux, phytiumpi] @@ -135,4 +135,4 @@ jobs: cargo version export DISK_IMG="${{ github.workspace }}/disk-x86_64.img" export VM_CONFIGS="$(pwd)/configs/vms/nimbos-x86_64-qemu-smp1.toml" - ./axvisor.sh run --plat ${{ matrix.plat }} --vmconfigs $VM_CONFIGS --features fs --arceos-args DISK_IMG=$DISK_IMG,BLK=y + ./scripts/auto_interrupt.sh ./axvisor.sh run --plat ${{ matrix.plat }} --vmconfigs $VM_CONFIGS --features fs --arceos-args DISK_IMG=$DISK_IMG,BLK=y,LOG=info diff --git a/Cargo.lock b/Cargo.lock index 199f307f..fc23d8f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1026,6 +1026,7 @@ dependencies = [ "fdt-parser", "kernel_guard", "kspin", + "lazy_static", "lazyinit", "log", "memory_addr", diff --git a/Cargo.toml b/Cargo.toml index ce33f192..bb6d8296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ lazyinit = "0.2" log = "=0.4.21" spin = "0.9" timer_list = "0.1.0" +lazy_static = { version = "1.5", default-features = false, features = ["spin_no_std"] } # System dependent modules provided by ArceOS. axstd = {git = "https://github.com/arceos-hypervisor/arceos.git", tag = "hv-0.3.1", features = [ diff --git a/README.md b/README.md index 469de2cd..c3327739 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ Currently, AxVisor has been verified in scenarios with the following systems as - [NimbOS](https://github.com/equation314/nimbos) - Linux +## Shell Management + +AxVisor provides an interactive shell interface for managing virtual machines and file operations. + +For detailed information about shell features, commands, and usage, see: [Shell模块介绍.md](doc/Shell模块介绍.md) + # Build and Run After AxVisor starts, it loads and starts the guest based on the information in the guest configuration file. Currently, AxVisor supports loading guest images from a FAT32 file system and also supports binding guest images to the hypervisor image through static compilation (using include_bytes). diff --git a/configs/vms/arceos-aarch64-e2000-smp2.toml b/configs/vms/arceos-aarch64-e2000-smp2.toml index fffae98d..0a39e493 100644 --- a/configs/vms/arceos-aarch64-e2000-smp2.toml +++ b/configs/vms/arceos-aarch64-e2000-smp2.toml @@ -23,7 +23,7 @@ image_location = "memory" # The load address of the kernel image. kernel_load_addr = 0x20_2008_0000 ## The file path of the kernel image. -kernel_path = "/path/to/arceos_aarch64-dyn_smp1.bin" +kernel_path = "/guest/arceos/arceos_aarch64-dyn_smp2.bin" ## The file path of the device tree blob (DTB). dtb_load_addr = 0x20_2000_0000 #dtb_path = "/path/to/axvisor/configs/vms/arceos-aarch64-e2000_smp2.dtb" diff --git a/configs/vms_bkp/arceos-aarch64.toml b/configs/vms_bkp/arceos-aarch64.toml index c75f7f49..7936825e 100644 --- a/configs/vms_bkp/arceos-aarch64.toml +++ b/configs/vms_bkp/arceos-aarch64.toml @@ -20,9 +20,9 @@ phys_cpu_sets = [2] entry_point = 0x4020_0000 # The location of image: "memory" | "fs". # Load from file system. -image_location = "memory" +image_location = "fs" # The file path of the kernel image. -kernel_path = "path/to/kernel" +kernel_path = "helloworld_aarch64-qemu-virt.bin" # The load address of the kernel image. kernel_load_addr = 0x4020_0000 ## Load from memory diff --git "a/doc/Shell\346\250\241\345\235\227\344\273\213\347\273\215.md" "b/doc/Shell\346\250\241\345\235\227\344\273\213\347\273\215.md" new file mode 100644 index 00000000..2fb7afa9 --- /dev/null +++ "b/doc/Shell\346\250\241\345\235\227\344\273\213\347\273\215.md" @@ -0,0 +1,379 @@ +# AxVisor Shell 模块详细介绍 + +## 概述 + +AxVisor Shell 模块是 AxVisor 虚拟化管理器中的一个重要组件,为用户提供了一个功能丰富的交互式命令行界面。该模块基于 Rust 语言实现,具有完整的命令解析、历史记录、终端控制和虚拟机管理功能。 + +``` +┌─────────────────────────────────────────────┐ +│ Shell Interface Layer │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Interactive │ │ Command CLI │ │ +│ │ Shell │ │ Parser │ │ +│ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────┤ +│ VM Management Facade │ +│ ┌─────────────┐ ┌──────────────────┐ │ +│ │ Controller │ │ Query & Monitor │ │ +│ └─────────────┘ └──────────────────┘ │ +├─────────────────────────────────────────────┤ +│ Existing VMM Components │ +│ VMList │ VCpu │ IVC │ Timer │ Config │ │ +└─────────────────────────────────────────────┘ +``` + +## 模块架构 + +### 目录结构 +``` +src/shell/ +├── mod.rs # 主模块,实现交互式shell界面 +└── command/ + ├── mod.rs # 命令框架和解析器 + ├── base.rs # 基础Unix命令实现 + ├── vm.rs # 虚拟机管理命令 + └── history.rs # 命令历史记录管理 +``` + +## 核心组件 + +### 1. 交互式Shell界面 (mod.rs) + +#### 主要功能 +- **实时字符输入处理**: 支持逐字符读取和处理用户输入 +- **光标控制**: 支持左右箭头键移动光标位置 +- **行编辑功能**: 支持删除、插入字符等基本编辑操作 +- **历史记录导航**: 通过上下箭头键浏览命令历史 +- **转义序列处理**: 支持部分ANSI转义序列和特殊键处理 + +#### 关键特性 +```rust +const MAX_LINE_LEN: usize = 256; // 最大命令行长度 + +enum InputState { + Normal, // 正常输入状态 + Escape, // ESC键按下状态 + EscapeSeq, // 转义序列处理状态 +} +``` + +#### 支持的按键操作 +- **回车键 (CR/LF)**: 执行当前命令 +- **退格键 (BS/DEL)**: 删除光标前的字符 +- **ESC序列**: 处理箭头键和功能键 +- **上/下箭头**: 浏览命令历史 +- **左/右箭头**: 移动光标位置 + +### 2. 命令框架和解析器 (command/mod.rs) + +#### 命令树结构 +采用基于树状结构的命令系统,支持主命令和子命令的层次化组织: + +```rust +#[derive(Debug, Clone)] +pub struct CommandNode { + handler: Option, // 命令处理函数 + subcommands: BTreeMap, // 子命令映射 + description: &'static str, // 命令描述 + usage: Option<&'static str>, // 使用说明 + log_level: log::LevelFilter, // 日志级别 + options: Vec, // 命令选项 + flags: Vec, // 命令标志 +} +``` + +#### 命令解析功能 +- **智能分词**: 支持引号包围的参数和转义字符 +- **选项解析**: 支持短选项(-x)和长选项(--option) +- **参数验证**: 自动验证必需选项和参数格式 +- **错误处理**: 详细的错误信息和使用提示 + +#### 解析错误类型 +```rust +pub enum ParseError { + UnknownCommand(String), // 未知命令 + UnknownOption(String), // 未知选项 + MissingValue(String), // 缺少参数值 + MissingRequiredOption(String), // 缺少必需选项 + NoHandler(String), // 没有处理函数 +} +``` + +### 3. 基础Unix命令 (command/base.rs) + +实现了部分Unix风格命令,包括: + +#### 文件系统操作命令 +- **ls**: 列出目录内容,支持 `-l`(详细信息) 和 `-a`(显示隐藏文件) 选项 +- **cat**: 显示文件内容,支持多文件连接输出 +- **mkdir**: 创建目录,支持 `-p`(创建父目录) 选项 +- **rm**: 删除文件和目录,支持 `-r`(递归)、`-f`(强制)、`-d`(删除空目录) 选项 +- **cp**: 复制文件和目录,支持 `-r`(递归复制) 选项 +- **mv**: 移动/重命名文件和目录 +- **touch**: 创建空文件 + +#### 系统信息命令 +- **pwd**: 显示当前工作目录 +- **cd**: 切换目录 +- **uname**: 显示系统信息,支持 `-a`(全部信息)、`-s`(内核名)、`-m`(架构) 选项 +- **echo**: 输出文本,支持 `-n`(不换行) 选项和文件重定向 + +#### 系统控制命令 +- **exit**: 退出shell,支持指定退出码 +- **log**: 控制日志级别 (off/error/warn/info/debug/trace) **有计划实现** + +#### 文件权限显示 +实现了完整的Unix风格文件权限显示: +```rust +fn file_type_to_char(ty: FileType) -> char { + match ty { + is_dir() => 'd', + is_file() => '-', + is_symlink() => 'l', + is_char_device() => 'c', + is_block_device() => 'b', + is_socket() => 's', + is_fifo() => 'p', + _ => '?' + } +} +``` + +### 4. 虚拟机管理命令 (command/vm.rs) + +提供完整的虚拟机生命周期管理功能: + +#### 主要子命令 +- **vm create**: 从配置文件创建虚拟机 +- **vm start**: 启动虚拟机(支持单个或全部) +- **vm stop**: 停止虚拟机,支持强制停止 +- **vm restart**: 重启虚拟机 +- **vm delete**: 删除虚拟机,支持数据保留选项 +- **vm list**: 列出虚拟机,支持JSON格式输出 +- **vm show**: 显示虚拟机详细信息 +- **vm status**: 显示虚拟机状态,支持实时监控 + +#### 功能特性 +```rust +// 虚拟机状态显示 +let state = if vm.running() { + "🟢 running" +} else if vm.shutting_down() { + "🟡 stopping" +} else { + "🔴 stopped" +}; +``` + +#### 详细信息显示 +- **配置信息**: BSP/AP入口点、中断模式、直通设备、模拟设备 +- **资源统计**: 内存区域、VCPU数量、设备数量 +- **运行状态**: VCPU状态分布、CPU亲和性设置 + +#### 支持的选项和标志 +- `--all`: 显示所有虚拟机(包括已停止的) +- `--format json`: JSON格式输出 +- `--config`: 显示配置信息 +- `--stats`: 显示统计信息 +- `--force`: 强制操作 +- `--detach`: 后台运行 +- `--watch`: 实时监控 + +### 5. 命令历史管理 (command/history.rs) + +#### 核心功能 +```rust +pub struct CommandHistory { + history: Vec, // 历史命令列表 + current_index: usize, // 当前索引位置 + max_size: usize, // 最大历史记录数 +} +``` + +#### 关键特性 +- **去重处理**: 避免连续重复命令 +- **循环缓冲**: 超出最大容量时自动删除最旧记录 +- **导航功能**: 支持前进/后退浏览 +- **空命令过滤**: 自动忽略空白命令 + +#### 终端控制 +```rust +pub fn clear_line_and_redraw( + stdout: &mut dyn Write, + prompt: &str, + content: &str, + cursor_pos: usize, +) { + write!(stdout, "\r"); // 回到行首 + write!(stdout, "\x1b[2K"); // 清除整行 + write!(stdout, "{}{}", prompt, content); // 重绘内容 + // 调整光标位置 + if cursor_pos < content.len() { + write!(stdout, "\x1b[{}D", content.len() - cursor_pos); + } +} +``` + +## 内置命令 + +### 系统级内置命令 +- **help**: 显示可用命令列表 +- **help ``**: 显示特定命令的详细帮助 +- **clear**: 清屏 (发送ANSI清屏序列) +- **exit/quit**: 退出shell + +### 命令提示符 +```rust +fn print_prompt() { + print!("axvisor:{}$ ", current_directory); +} +``` + +## 扩展性 + +### 添加新命令 + +1. 在对应的模块中实现命令处理函数 +2. 定义命令节点和选项/标志 +3. 在 `build_command_tree()` 中注册命令 + +### 命令定义示例 + +```rust +tree.insert( + "mycommand".to_string(), + CommandNode::new("My custom command") + .with_handler(my_command_handler) + .with_usage("mycommand [OPTIONS] ") + .with_option( + OptionDef::new("config", "Config file path") + .with_short('c') + .with_long("config") + .required() + ) + .with_flag( + FlagDef::new("verbose", "Verbose output") + .with_short('v') + .with_long("verbose") + ), +); +``` + +# 使用说明 + +## 启用Shell功能 + +AxVisor Shell模块需要启用特定的feature才能使用: + +### 必需的Features + +编译时需要启用 `fs` feature 以及对应的文件系统类型: + +#### 基础Shell功能 + +```bash +# 启用Shell基础功能 +--features fs +``` + +#### 文件系统支持 + +根据使用的文件系统类型选择对应的feature,默认为`fatfs`: + +```bash +# FAT32文件系统支持 +./axvisor.sh run --features fs --arceos-features "fatfs" + +# EXT4文件系统支持 +./axvisor.sh run --arceos-features "fs,ext4fs" + +``` + +#### 完整示例 + +```bash +# 使用FAT32文件系统运行AxVisor Shell +./axvisor.sh run --plat aarch64-generic --vmconfigs configs/vms/nimbos-aarch64-qemu-smp1.toml --features fs,ept-level-4 --arceos-features fatfs --arceos-args DISK_IMG=disk-aarch64.img,BUS=mmio,BLK=y,MEM=8g + +# 使用EXT4文件系统运行AxVisor Shell +./axvisor.sh run --plat aarch64-generic --vmconfigs configs/vms/nimbos-aarch64-qemu-smp1.toml --features fs,ept-level-4 --arceos-features ext4fs --arceos-args DISK_IMG=disk-aarch64.img,BUS=mmio,BLK=y,MEM=8g +``` + +### 配置说明 + +Shell模块通过条件编译控制: +```rust +#[cfg(feature = "fs")] +mod shell; + +#[cfg(feature = "fs")] +shell::console_init(); +``` + +只有当启用 `fs` feature时,Shell模块才会被编译和启动。 + +## 快速开始 + +启动AxVisor后会自动进入Shell界面: +``` +axvisor:/$ +``` + +### 基本操作 +- `help` - 查看所有命令 +- `help ` - 查看特定命令帮助 +- `clear` - 清屏 +- `exit` - 退出 + +### 键盘快捷键 +- **上/下箭头**: 浏览命令历史 +- **左/右箭头**: 移动光标 +- **退格键**: 删除字符 + +## 常用命令 + +### 文件操作 +```bash +ls -la # 列出文件(详细信息+隐藏文件) +cat file.txt # 查看文件内容 +mkdir -p dir/subdir # 创建目录 +cp -r source dest # 复制文件/目录 +mv old new # 移动/重命名 +rm -rf path # 删除文件/目录 +touch file.txt # 创建空文件 +``` + +### 虚拟机管理 +```bash +vm list -a # 列出所有虚拟机 +vm create config.toml # 创建虚拟机 +vm start 1 # 启动VM(ID=1) +vm stop -f 1 # 强制停止VM +vm status 1 # 查看VM状态 +vm show -c 1 # 查看VM配置 +``` + +### 系统信息 +```bash +pwd # 当前目录 +uname -a # 系统信息 +``` + +## 典型工作流 + +```bash +# 1. 检查环境 +ls -la + +# 2. 创建并启动虚拟机 +vm create linux.toml +vm start 1 + +# 3. 监控状态 +vm status 1 + +# 4. 停止虚拟机 +vm stop 1 +``` + +更多详细信息请使用 `help ` 查看具体命令的使用方法。 diff --git a/scripts/auto_interrupt.sh b/scripts/auto_interrupt.sh new file mode 100755 index 00000000..2b09847b --- /dev/null +++ b/scripts/auto_interrupt.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +if [ $# -eq 0 ]; then + echo "Usage: $0 [arguments...]" + echo "Example: $0 ostool run uboot" + exit 1 +fi + +COMMAND=("$@") +echo "Executing command: ${COMMAND[*]}" + +"${COMMAND[@]}" 2>&1 | while IFS= read -r line; do + echo "$line" + + if [[ "$line" == *"[OK] Default guest initialized"* ]]; then + echo "Completion signal detected, exiting..." + + sleep 2 + + echo "Safely finding and killing QEMU processes..." + + # Get current script and parent process PIDs to avoid killing them + SCRIPT_PID=$$ + PARENT_PID=$PPID + + echo "Current script PID: $SCRIPT_PID" + echo "Parent process PID: $PARENT_PID" + + # Find QEMU processes, but exclude script-related processes + pgrep -f "qemu" 2>/dev/null | while read pid; do + # Check if it's a script-related process + if [ "$pid" != "$SCRIPT_PID" ] && [ "$pid" != "$PARENT_PID" ]; then + # Further check process command line + CMD=$(ps -p "$pid" -o cmd --no-headers 2>/dev/null) + if [[ "$CMD" == *"qemu-system"* ]]; then + echo "kill -9 $pid (QEMU system process)" + kill -9 "$pid" 2>/dev/null || true + else + echo "Skipping process $pid (not a QEMU system process): $CMD" + fi + else + echo "Skipping script-related process: $pid" + fi + done + + exit 0 + fi +done + +echo "Done" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f5fa4ce0..351ace51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #[macro_use] extern crate log; + #[macro_use] extern crate alloc; @@ -17,6 +18,9 @@ extern crate axplat_aarch64_roc_rk3568_pc; #[cfg(feature = "plat-x86-qemu-q35")] extern crate axplat_x86_qemu_q35; +#[cfg(feature = "fs")] +mod shell; + mod hal; mod logo; mod task; @@ -33,5 +37,8 @@ fn main() { vmm::init(); vmm::start(); - info!("VMM shutdown"); + info!("[OK] Default guest initialized"); + + #[cfg(feature = "fs")] + shell::console_init(); } diff --git a/src/shell/command/base.rs b/src/shell/command/base.rs new file mode 100644 index 00000000..754f3d69 --- /dev/null +++ b/src/shell/command/base.rs @@ -0,0 +1,749 @@ +use std::collections::BTreeMap; +use std::fs::{self, File, FileType}; +use std::io::{self, Read, Write}; +use std::string::{String, ToString}; +use std::vec::Vec; +use std::{print, println}; + +use crate::shell::command::{CommandNode, FlagDef, ParsedCommand}; + +macro_rules! print_err { + ($cmd: literal, $msg: expr) => { + println!("{}: {}", $cmd, $msg); + }; + ($cmd: literal, $arg: expr, $err: expr) => { + println!("{}: {}: {}", $cmd, $arg, $err); + }; +} + +// Helper function: split whitespace +fn split_whitespace(s: &str) -> (&str, &str) { + let s = s.trim(); + if let Some(pos) = s.find(char::is_whitespace) { + let (first, rest) = s.split_at(pos); + (first, rest.trim()) + } else { + (s, "") + } +} + +fn do_ls(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let show_long = cmd.flags.get("long").unwrap_or(&false); + let show_all = cmd.flags.get("all").unwrap_or(&false); + + let _current_dir = std::env::current_dir().unwrap(); + + fn show_entry_info(path: &str, entry: &str, show_long: bool) -> io::Result<()> { + if show_long { + let metadata = fs::metadata(path)?; + let size = metadata.len(); + let file_type = metadata.file_type(); + let file_type_char = file_type_to_char(file_type); + let rwx = file_perm_to_rwx(metadata.permissions().mode()); + let rwx = unsafe { core::str::from_utf8_unchecked(&rwx) }; + println!("{}{} {:>8} {}", file_type_char, rwx, size, entry); + } else { + println!("{}", entry); + } + Ok(()) + } + + fn list_one(name: &str, print_name: bool, show_long: bool, show_all: bool) -> io::Result<()> { + let is_dir = fs::metadata(name)?.is_dir(); + if !is_dir { + return show_entry_info(name, name, show_long); + } + + if print_name { + println!("{}:", name); + } + + let mut entries = fs::read_dir(name)? + .filter_map(|e| e.ok()) + .map(|e| e.file_name()) + .filter(|name| show_all || !name.starts_with('.')) + .collect::>(); + entries.sort(); + + for entry in entries { + let path = format!("{name}/{entry}"); + if let Err(e) = show_entry_info(&path, &entry, show_long) { + print_err!("ls", path, e); + } + } + Ok(()) + } + + let targets = if args.is_empty() { + vec![".".to_string()] + } else { + args.clone() + }; + + for (i, name) in targets.iter().enumerate() { + if i > 0 { + println!(); + } + if let Err(e) = list_one(name, targets.len() > 1, *show_long, *show_all) { + print_err!("ls", name, e); + } + } +} + +fn do_cat(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + if args.is_empty() { + print_err!("cat", "no file specified"); + return; + } + + fn cat_one(fname: &str) -> io::Result<()> { + let mut buf = [0; 1024]; + let mut file = File::open(fname)?; + loop { + let n = file.read(&mut buf)?; + if n > 0 { + io::stdout().write_all(&buf[..n])?; + } else { + return Ok(()); + } + } + } + + for fname in args { + if let Err(e) = cat_one(fname) { + print_err!("cat", fname, e); + } + } +} + +fn do_echo(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let no_newline = cmd.flags.get("no-newline").unwrap_or(&false); + + let args_str = args.join(" "); + + fn echo_file(fname: &str, text_list: &[&str]) -> io::Result<()> { + let mut file = File::create(fname)?; + for text in text_list { + file.write_all(text.as_bytes())?; + } + Ok(()) + } + + if let Some(pos) = args_str.rfind('>') { + let text_before = args_str[..pos].trim(); + let (fname, text_after) = split_whitespace(&args_str[pos + 1..]); + if fname.is_empty() { + print_err!("echo", "no file specified"); + return; + }; + + let text_list = [ + text_before, + if !text_after.is_empty() { " " } else { "" }, + text_after, + if !no_newline { "\n" } else { "" }, + ]; + if let Err(e) = echo_file(fname, &text_list) { + print_err!("echo", fname, e); + } + } else if *no_newline { + print!("{}", args_str); + } else { + println!("{}", args_str); + } +} + +fn do_mkdir(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let create_parents = cmd.flags.get("parents").unwrap_or(&false); + + if args.is_empty() { + print_err!("mkdir", "missing operand"); + return; + } + + fn mkdir_one(path: &str, create_parents: bool) -> io::Result<()> { + if create_parents { + fs::create_dir_all(path) + } else { + fs::create_dir(path) + } + } + + for path in args { + if let Err(e) = mkdir_one(path, *create_parents) { + print_err!("mkdir", format_args!("cannot create directory '{path}'"), e); + } + } +} + +fn do_rm(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let rm_dir = cmd.flags.get("dir").unwrap_or(&false); + let recursive = cmd.flags.get("recursive").unwrap_or(&false); + let force = cmd.flags.get("force").unwrap_or(&false); + + if args.is_empty() { + print_err!("rm", "missing operand"); + return; + } + + fn rm_one(path: &str, rm_dir: bool, recursive: bool, force: bool) -> io::Result<()> { + let metadata = fs::metadata(path); + + if force && metadata.is_err() { + return Ok(()); // Ignore non-existent files when in force mode + } + + let metadata = metadata?; + + if metadata.is_dir() { + if recursive { + remove_dir_recursive(path, force) + } else if rm_dir { + fs::remove_dir(path) + } else { + Err(io::Error::Unsupported) + } + } else { + fs::remove_file(path) + } + } + + for path in args { + if let Err(e) = rm_one(path, *rm_dir, *recursive, *force) + && !force + { + print_err!("rm", format_args!("cannot remove '{path}'"), e); + } + } +} + +// Implementation of recursively deleting directories (manual recursion) +fn remove_dir_recursive(path: &str, _force: bool) -> io::Result<()> { + // Read directory contents + let entries = fs::read_dir(path)?; + + // Remove all child items + for entry_result in entries { + let entry = entry_result?; + let entry_path = format!("{}/{}", path, entry.file_name()); + let metadata = entry.file_type(); + + if metadata.is_dir() { + // Recursively delete subdirectory + remove_dir_recursive(&entry_path, _force)?; + } else { + // Delete file + fs::remove_file(&entry_path)?; + } + } + + // Delete empty directory + fs::remove_dir(path) +} + +fn do_cd(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + let target = if args.is_empty() { + "/" + } else if args.len() == 1 { + &args[0] + } else { + print_err!("cd", "too many arguments"); + return; + }; + + if let Err(e) = std::env::set_current_dir(target) { + print_err!("cd", target, e); + } +} + +fn do_pwd(cmd: &ParsedCommand) { + let _logical = cmd.flags.get("logical").unwrap_or(&false); + + let pwd = std::env::current_dir().unwrap(); + println!("{}", pwd); +} + +fn do_uname(cmd: &ParsedCommand) { + let show_all = cmd.flags.get("all").unwrap_or(&false); + let show_kernel = cmd.flags.get("kernel-name").unwrap_or(&false); + let show_arch = cmd.flags.get("machine").unwrap_or(&false); + + let arch = option_env!("AX_ARCH").unwrap_or(""); + let platform = option_env!("AX_PLATFORM").unwrap_or(""); + let smp = match option_env!("AX_SMP") { + None | Some("1") => "", + _ => " SMP", + }; + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("0.1.0"); + + if *show_all { + println!( + "ArceOS {ver}{smp} {arch} {plat}", + ver = version, + smp = smp, + arch = arch, + plat = platform, + ); + } else if *show_kernel { + println!("ArceOS"); + } else if *show_arch { + println!("{}", arch); + } else { + println!( + "ArceOS {ver}{smp} {arch} {plat}", + ver = version, + smp = smp, + arch = arch, + plat = platform, + ); + } +} + +fn do_exit(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let exit_code = if args.is_empty() { + 0 + } else { + args[0].parse::().unwrap_or(0) + }; + + println!("Bye~"); + std::process::exit(exit_code); +} + +fn do_log(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + if args.is_empty() { + println!("Current log level: {:?}", log::max_level()); + return; + } + + match args[0].as_str() { + "on" | "enable" => log::set_max_level(log::LevelFilter::Info), + "off" | "disable" => log::set_max_level(log::LevelFilter::Off), + "error" => log::set_max_level(log::LevelFilter::Error), + "warn" => log::set_max_level(log::LevelFilter::Warn), + "info" => log::set_max_level(log::LevelFilter::Info), + "debug" => log::set_max_level(log::LevelFilter::Debug), + "trace" => log::set_max_level(log::LevelFilter::Trace), + level => { + println!("Unknown log level: {}", level); + println!("Available levels: off, error, warn, info, debug, trace"); + return; + } + } + println!("Log level set to: {:?}", log::max_level()); +} + +fn do_mv(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + if args.len() < 2 { + print_err!("mv", "missing operand"); + return; + } + + // If only two arguments, handle single file/dir move + if args.len() == 2 { + let source = &args[0]; + let dest = &args[1]; + + // Check if destination exists and is a directory + if let Ok(dest_meta) = fs::metadata(dest) + && dest_meta.is_dir() + { + // Move source into destination directory + let mut file_dir = fs::read_dir(dest).unwrap(); + let source_name = match file_dir.next() { + Some(name) => { + let dir_name = name.expect("Failed to read directory"); + let file = dir_name.file_name(); + format!("{dest}/{file}") + } + None => { + print_err!("mv", format_args!("invalid source path '{source}'")); + return; + } + }; + let dest_path = format!("{dest}/{source_name}"); + if let Err(e) = move_file_or_dir(source, &dest_path) { + print_err!( + "mv", + format_args!("cannot move '{source}' to '{dest_path}'"), + e + ); + } + return; + } + + // Direct rename/move + if let Err(e) = move_file_or_dir(source, dest) { + print_err!("mv", format_args!("cannot move '{source}' to '{dest}'"), e); + } + } else { + // Multiple sources - destination must be a directory + let dest = &args[args.len() - 1]; + let sources = &args[..args.len() - 1]; + + // Check if destination is a directory + match fs::metadata(dest) { + Ok(meta) if meta.is_dir() => { + // Move each source into destination directory + for source in sources { + let mut file_dir = fs::read_dir(source).unwrap(); + let source_name = match file_dir.next() { + Some(name) => { + let dir_name = name.expect("Failed to read directory"); + let file = dir_name.file_name(); + format!("{dest}/{file}") + } + None => { + print_err!("mv", format_args!("invalid source path '{source}'")); + return; + } + }; + let dest_path = format!("{dest}/{source_name}"); + if let Err(e) = move_file_or_dir(source, &dest_path) { + print_err!( + "mv", + format_args!("cannot move '{source}' to '{dest_path}'"), + e + ); + } + } + } + Ok(_) => { + print_err!("mv", format_args!("target '{dest}' is not a directory")); + } + Err(e) => { + print_err!("mv", format_args!("cannot access '{dest}'"), e); + } + } + } +} + +// Helper function to move file or directory (handles cross-filesystem moves) +fn move_file_or_dir(source: &str, dest: &str) -> io::Result<()> { + // Try simple rename first (works within same filesystem) + match fs::rename(source, dest) { + Ok(()) => Ok(()), + Err(_) => { + // If rename fails, try copy + delete (for cross-filesystem moves) + let src_meta = fs::metadata(source)?; + + if src_meta.is_dir() { + // For directories, use recursive copy then remove + copy_dir_recursive(source, dest)?; + remove_dir_recursive(source, false)?; + } else { + // For files, copy then remove + copy_file(source, dest)?; + fs::remove_file(source)?; + } + Ok(()) + } + } +} + +fn do_touch(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + if args.is_empty() { + print_err!("touch", "missing operand"); + return; + } + + for filename in args { + if let Err(e) = File::create(filename) { + print_err!("touch", filename, e); + } + } +} + +fn do_cp(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let recursive = cmd.flags.get("recursive").unwrap_or(&false); + + if args.len() < 2 { + print_err!("cp", "missing operand"); + return; + } + + let source = &args[0]; + let dest = &args[1]; + + // Check if source file/directory exists + let src_metadata = match fs::metadata(source) { + Ok(metadata) => metadata, + Err(e) => { + print_err!("cp", format_args!("cannot access '{source}'"), e); + return; + } + }; + + let result = if src_metadata.is_dir() { + if *recursive { + copy_dir_recursive(source, dest) + } else { + Err(io::Error::Unsupported) + } + } else { + copy_file(source, dest) + }; + + if let Err(e) = result { + print_err!("cp", format_args!("cannot copy '{source}' to '{dest}'"), e); + } +} + +// Manually implement file copy +fn copy_file(src: &str, dst: &str) -> io::Result<()> { + let mut src_file = File::open(src)?; + let mut dst_file = File::create(dst)?; + + let mut buffer = [0; 4096]; + loop { + let bytes_read = src_file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + dst_file.write_all(&buffer[..bytes_read])?; + } + Ok(()) +} + +// Recursively copy directory +fn copy_dir_recursive(src: &str, dst: &str) -> io::Result<()> { + // Create target directory + fs::create_dir(dst)?; + + // Read source directory contents + let entries = fs::read_dir(src)?; + + for entry_result in entries { + let entry = entry_result?; + let file_name = entry.file_name(); + let src_path = format!("{src}/{file_name}"); + let dst_path = format!("{dst}/{file_name}"); + + let metadata = entry.file_type(); + if metadata.is_dir() { + copy_dir_recursive(&src_path, &dst_path)?; + } else { + copy_file(&src_path, &dst_path)?; + } + } + Ok(()) +} + +fn file_type_to_char(ty: FileType) -> char { + if ty.is_char_device() { + 'c' + } else if ty.is_block_device() { + 'b' + } else if ty.is_socket() { + 's' + } else if ty.is_fifo() { + 'p' + } else if ty.is_symlink() { + 'l' + } else if ty.is_dir() { + 'd' + } else if ty.is_file() { + '-' + } else { + '?' + } +} + +#[rustfmt::skip] +const fn file_perm_to_rwx(mode: u32) -> [u8; 9] { + let mut perm = [b'-'; 9]; + macro_rules! set { + ($bit:literal, $rwx:literal) => { + if mode & (1 << $bit) != 0 { + perm[8 - $bit] = $rwx + } + }; + } + + set!(2, b'r'); set!(1, b'w'); set!(0, b'x'); + set!(5, b'r'); set!(4, b'w'); set!(3, b'x'); + set!(8, b'r'); set!(7, b'w'); set!(6, b'x'); + perm +} + +pub fn build_base_cmd(tree: &mut BTreeMap) { + // ls Command + tree.insert( + "ls".to_string(), + CommandNode::new("List directory contents") + .with_handler(do_ls) + .with_usage("ls [OPTIONS] [DIRECTORY...]") + .with_flag( + FlagDef::new("long", "Use long listing format") + .with_short('l') + .with_long("long"), + ) + .with_flag( + FlagDef::new("all", "Show hidden files") + .with_short('a') + .with_long("all"), + ), + ); + + // cat Command + tree.insert( + "cat".to_string(), + CommandNode::new("Display file contents") + .with_handler(do_cat) + .with_usage("cat [FILE2...]"), + ); + + // echo Command + tree.insert( + "echo".to_string(), + CommandNode::new("Display text") + .with_handler(do_echo) + .with_usage("echo [OPTIONS] [TEXT...]") + .with_flag( + FlagDef::new("no-newline", "Do not output trailing newline") + .with_short('n') + .with_long("no-newline"), + ), + ); + + // mkdir Command + tree.insert( + "mkdir".to_string(), + CommandNode::new("Create directories") + .with_handler(do_mkdir) + .with_usage("mkdir [OPTIONS] [DIRECTORY2...]") + .with_flag( + FlagDef::new("parents", "Create parent directories as needed") + .with_short('p') + .with_long("parents"), + ), + ); + + // rm Command + tree.insert( + "rm".to_string(), + CommandNode::new("Remove files and directories") + .with_handler(do_rm) + .with_usage("rm [OPTIONS] [FILE2...]") + .with_flag( + FlagDef::new("dir", "Remove empty directories") + .with_short('d') + .with_long("dir"), + ) + .with_flag( + FlagDef::new("recursive", "Remove directories recursively") + .with_short('r') + .with_long("recursive"), + ) + .with_flag( + FlagDef::new("force", "Force removal, ignore nonexistent files") + .with_short('f') + .with_long("force"), + ), + ); + + // cd Command + tree.insert( + "cd".to_string(), + CommandNode::new("Change directory") + .with_handler(do_cd) + .with_usage("cd [DIRECTORY]"), + ); + + // pwd Command + tree.insert( + "pwd".to_string(), + CommandNode::new("Print working directory") + .with_handler(do_pwd) + .with_usage("pwd [OPTIONS]") + .with_flag( + FlagDef::new("logical", "Use logical path") + .with_short('L') + .with_long("logical"), + ), + ); + + // uname Command + tree.insert( + "uname".to_string(), + CommandNode::new("System information") + .with_handler(do_uname) + .with_usage("uname [OPTIONS]") + .with_flag( + FlagDef::new("all", "Show all information") + .with_short('a') + .with_long("all"), + ) + .with_flag( + FlagDef::new("kernel-name", "Show kernel name") + .with_short('s') + .with_long("kernel-name"), + ) + .with_flag( + FlagDef::new("machine", "Show machine architecture") + .with_short('m') + .with_long("machine"), + ), + ); + + // exit Command + tree.insert( + "exit".to_string(), + CommandNode::new("Exit the shell") + .with_handler(do_exit) + .with_usage("exit [EXIT_CODE]"), + ); + + // log Command + tree.insert( + "log".to_string(), + CommandNode::new("Change log level") + .with_handler(do_log) + .with_usage("log [LEVEL]"), + ); + + // touch Command + tree.insert( + "touch".to_string(), + CommandNode::new("Create empty files") + .with_handler(do_touch) + .with_usage("touch [FILE2...]"), + ); + + // cp Command + tree.insert( + "cp".to_string(), + CommandNode::new("Copy files") + .with_handler(do_cp) + .with_usage("cp [OPTIONS] ") + .with_flag( + FlagDef::new("recursive", "Copy directories recursively") + .with_short('r') + .with_long("recursive"), + ), + ); + + // mv Command + tree.insert( + "mv".to_string(), + CommandNode::new("Move/rename files") + .with_handler(do_mv) + .with_usage("mv | mv [SOURCE2...] "), + ); +} diff --git a/src/shell/command/history.rs b/src/shell/command/history.rs new file mode 100644 index 00000000..9c13fc83 --- /dev/null +++ b/src/shell/command/history.rs @@ -0,0 +1,68 @@ +use std::io::prelude::*; +use std::{string::String, vec::Vec}; + +pub struct CommandHistory { + history: Vec, + current_index: usize, + max_size: usize, +} + +impl CommandHistory { + pub fn new(max_size: usize) -> Self { + Self { + history: Vec::new(), + current_index: 0, + max_size, + } + } + + pub fn add_command(&mut self, cmd: String) { + if !cmd.trim().is_empty() && self.history.last() != Some(&cmd) { + if self.history.len() >= self.max_size { + self.history.remove(0); + } + self.history.push(cmd); + } + self.current_index = self.history.len(); + } + + #[allow(dead_code)] + pub fn previous(&mut self) -> Option<&String> { + if self.current_index > 0 { + self.current_index -= 1; + self.history.get(self.current_index) + } else { + None + } + } + + #[allow(dead_code)] + pub fn next(&mut self) -> Option<&String> { + if self.current_index < self.history.len() { + self.current_index += 1; + if self.current_index < self.history.len() { + self.history.get(self.current_index) + } else { + None + } + } else { + None + } + } +} + +#[allow(unused_must_use)] +pub fn clear_line_and_redraw( + stdout: &mut dyn Write, + prompt: &str, + content: &str, + cursor_pos: usize, +) { + write!(stdout, "\r"); + write!(stdout, "\x1b[2K"); + write!(stdout, "{prompt}{content}"); + if cursor_pos < content.len() { + write!(stdout, "\x1b[{}D", content.len() - cursor_pos); + } + stdout.flush(); +} diff --git a/src/shell/command/mod.rs b/src/shell/command/mod.rs new file mode 100644 index 00000000..434dc5a2 --- /dev/null +++ b/src/shell/command/mod.rs @@ -0,0 +1,562 @@ +mod base; +mod history; +mod vm; + +pub use base::*; +pub use history::*; +pub use vm::*; + +use std::io::prelude::*; +use std::string::String; +use std::vec::Vec; +use std::{collections::BTreeMap, string::ToString}; +use std::{print, println}; + +lazy_static::lazy_static! { + pub static ref COMMAND_TREE: BTreeMap = build_command_tree(); +} + +#[derive(Debug, Clone)] +pub struct CommandNode { + handler: Option, + subcommands: BTreeMap, + description: &'static str, + usage: Option<&'static str>, + #[allow(dead_code)] + log_level: log::LevelFilter, + options: Vec, + flags: Vec, +} + +#[derive(Debug, Clone)] +pub struct OptionDef { + name: &'static str, + short: Option, + long: Option<&'static str>, + description: &'static str, + required: bool, +} + +#[derive(Debug, Clone)] +pub struct FlagDef { + name: &'static str, + short: Option, + long: Option<&'static str>, + description: &'static str, +} + +#[derive(Debug, Clone)] +pub struct ParsedCommand { + pub command_path: Vec, + pub options: BTreeMap, + pub flags: BTreeMap, + pub positional_args: Vec, +} + +#[derive(Debug)] +pub enum ParseError { + UnknownCommand(String), + UnknownOption(String), + MissingValue(String), + MissingRequiredOption(String), + NoHandler(String), +} + +impl CommandNode { + pub fn new(description: &'static str) -> Self { + Self { + handler: None, + subcommands: BTreeMap::new(), + description, + usage: None, + log_level: log::LevelFilter::Off, + options: Vec::new(), + flags: Vec::new(), + } + } + + pub fn with_handler(mut self, handler: fn(&ParsedCommand)) -> Self { + self.handler = Some(handler); + self + } + + pub fn with_usage(mut self, usage: &'static str) -> Self { + self.usage = Some(usage); + self + } + + #[allow(dead_code)] + pub fn with_log_level(mut self, level: log::LevelFilter) -> Self { + self.log_level = level; + self + } + + pub fn with_option(mut self, option: OptionDef) -> Self { + self.options.push(option); + self + } + + pub fn with_flag(mut self, flag: FlagDef) -> Self { + self.flags.push(flag); + self + } + + pub fn add_subcommand>(mut self, name: S, node: CommandNode) -> Self { + self.subcommands.insert(name.into(), node); + self + } +} + +impl OptionDef { + pub fn new(name: &'static str, description: &'static str) -> Self { + Self { + name, + short: None, + long: None, + description, + required: false, + } + } + + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + pub fn with_long(mut self, long: &'static str) -> Self { + self.long = Some(long); + self + } + + #[allow(dead_code)] + pub fn required(mut self) -> Self { + self.required = true; + self + } +} + +impl FlagDef { + pub fn new(name: &'static str, description: &'static str) -> Self { + Self { + name, + short: None, + long: None, + description, + } + } + + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + pub fn with_long(mut self, long: &'static str) -> Self { + self.long = Some(long); + self + } +} + +// Command Parser +pub struct CommandParser; + +impl CommandParser { + pub fn parse(input: &str) -> Result { + let tokens = Self::tokenize(input); + if tokens.is_empty() { + return Err(ParseError::UnknownCommand("empty command".to_string())); + } + + // Find the command path + let (command_path, command_node, remaining_tokens) = Self::find_command(&tokens)?; + + // Parse the arguments + let (options, flags, positional_args) = Self::parse_args(remaining_tokens, command_node)?; + + // Validate required options + Self::validate_required_options(command_node, &options)?; + + Ok(ParsedCommand { + command_path, + options, + flags, + positional_args, + }) + } + + fn tokenize(input: &str) -> Vec { + let mut tokens = Vec::new(); + let mut current_token = String::new(); + let mut in_quotes = false; + let mut escape_next = false; + + for ch in input.chars() { + if escape_next { + current_token.push(ch); + escape_next = false; + } else if ch == '\\' { + escape_next = true; + } else if ch == '"' { + in_quotes = !in_quotes; + } else if ch.is_whitespace() && !in_quotes { + if !current_token.is_empty() { + tokens.push(current_token.clone()); + current_token.clear(); + } + } else { + current_token.push(ch); + } + } + + if !current_token.is_empty() { + tokens.push(current_token); + } + + tokens + } + + fn find_command( + tokens: &[String], + ) -> Result<(Vec, &CommandNode, &[String]), ParseError> { + let mut current_node = COMMAND_TREE + .get(&tokens[0]) + .ok_or_else(|| ParseError::UnknownCommand(tokens[0].clone()))?; + + let mut command_path = vec![tokens[0].clone()]; + let mut token_index = 1; + + // Traverse to find the deepest command node + while token_index < tokens.len() { + if let Some(subcommand) = current_node.subcommands.get(&tokens[token_index]) { + current_node = subcommand; + command_path.push(tokens[token_index].clone()); + token_index += 1; + } else { + break; + } + } + + Ok((command_path, current_node, &tokens[token_index..])) + } + + #[allow(clippy::type_complexity)] + fn parse_args( + tokens: &[String], + command_node: &CommandNode, + ) -> Result< + ( + BTreeMap, + BTreeMap, + Vec, + ), + ParseError, + > { + let mut options = BTreeMap::new(); + let mut flags = BTreeMap::new(); + let mut positional_args = Vec::new(); + let mut i = 0; + + while i < tokens.len() { + let token = &tokens[i]; + + if let Some(name) = token.strip_prefix("--") { + // Long options/flags + if let Some(eq_pos) = name.find('=') { + // --option=value format + let (opt_name, value) = name.split_at(eq_pos); + let value = &value[1..]; // Skip '=' + if Self::is_option(opt_name, command_node) { + options.insert(opt_name.to_string(), value.to_string()); + } else { + return Err(ParseError::UnknownOption(format!("--{opt_name}"))); + } + } else if Self::is_flag(name, command_node) { + flags.insert(name.to_string(), true); + } else if Self::is_option(name, command_node) { + // --option value format + if i + 1 >= tokens.len() { + return Err(ParseError::MissingValue(format!("--{name}"))); + } + options.insert(name.to_string(), tokens[i + 1].clone()); + i += 1; // Skip value + } else { + return Err(ParseError::UnknownOption(format!("--{name}"))); + } + } else if token.starts_with('-') && token.len() > 1 { + // Short options/flags + let chars: Vec = token[1..].chars().collect(); + for (j, &ch) in chars.iter().enumerate() { + if Self::is_short_flag(ch, command_node) { + flags.insert( + Self::get_flag_name_by_short(ch, command_node) + .unwrap() + .to_string(), + true, + ); + } else if Self::is_short_option(ch, command_node) { + let opt_name = Self::get_option_name_by_short(ch, command_node).unwrap(); + if j == chars.len() - 1 && i + 1 < tokens.len() { + // Last character and there is a next token as value + options.insert(opt_name.to_string(), tokens[i + 1].clone()); + i += 1; // Skip value + } else { + return Err(ParseError::MissingValue(format!("-{ch}"))); + } + } else { + return Err(ParseError::UnknownOption(format!("-{ch}"))); + } + } + } else { + // Positional arguments + positional_args.push(token.clone()); + } + i += 1; + } + + Ok((options, flags, positional_args)) + } + + fn is_option(name: &str, node: &CommandNode) -> bool { + node.options + .iter() + .any(|opt| (opt.long == Some(name)) || opt.name == name) + } + + fn is_flag(name: &str, node: &CommandNode) -> bool { + node.flags + .iter() + .any(|flag| (flag.long == Some(name)) || flag.name == name) + } + + fn is_short_option(ch: char, node: &CommandNode) -> bool { + node.options.iter().any(|opt| opt.short == Some(ch)) + } + + fn is_short_flag(ch: char, node: &CommandNode) -> bool { + node.flags.iter().any(|flag| flag.short == Some(ch)) + } + + fn get_option_name_by_short(ch: char, node: &CommandNode) -> Option<&str> { + node.options + .iter() + .find(|opt| opt.short == Some(ch)) + .map(|opt| opt.name) + } + + fn get_flag_name_by_short(ch: char, node: &CommandNode) -> Option<&str> { + node.flags + .iter() + .find(|flag| flag.short == Some(ch)) + .map(|flag| flag.name) + } + + fn validate_required_options( + node: &CommandNode, + options: &BTreeMap, + ) -> Result<(), ParseError> { + for option in &node.options { + if option.required && !options.contains_key(option.name) { + return Err(ParseError::MissingRequiredOption(option.name.to_string())); + } + } + Ok(()) + } +} + +// Command execution function +pub fn execute_command(input: &str) -> Result<(), ParseError> { + let parsed = CommandParser::parse(input)?; + + // Find the corresponding command node + let mut current_node = COMMAND_TREE.get(&parsed.command_path[0]).unwrap(); + for cmd in &parsed.command_path[1..] { + current_node = current_node.subcommands.get(cmd).unwrap(); + } + + // Execute the command + if let Some(handler) = current_node.handler { + handler(&parsed); + Ok(()) + } else { + Err(ParseError::NoHandler(parsed.command_path.join(" "))) + } +} + +// Build command tree +fn build_command_tree() -> BTreeMap { + let mut tree = BTreeMap::new(); + + build_base_cmd(&mut tree); + build_vm_cmd(&mut tree); + + tree +} + +// Helper function: Display command help +pub fn show_help(command_path: &[String]) -> Result<(), ParseError> { + let mut current_node = COMMAND_TREE + .get(&command_path[0]) + .ok_or_else(|| ParseError::UnknownCommand(command_path[0].clone()))?; + + for cmd in &command_path[1..] { + current_node = current_node + .subcommands + .get(cmd) + .ok_or_else(|| ParseError::UnknownCommand(cmd.clone()))?; + } + + println!("Command: {}", command_path.join(" ")); + println!("Description: {}", current_node.description); + + if let Some(usage) = current_node.usage { + println!("Usage: {}", usage); + } + + if !current_node.options.is_empty() { + println!("\nOptions:"); + for option in ¤t_node.options { + let mut opt_str = String::new(); + if let Some(short) = option.short { + opt_str.push_str(&format!("-{short}")); + } + if let Some(long) = option.long { + if !opt_str.is_empty() { + opt_str.push_str(", "); + } + opt_str.push_str(&format!("--{long}")); + } + if opt_str.is_empty() { + opt_str = option.name.to_string(); + } + + let required_str = if option.required { " (required)" } else { "" }; + println!(" {:<20} {}{}", opt_str, option.description, required_str); + } + } + + if !current_node.flags.is_empty() { + println!("\nFlags:"); + for flag in ¤t_node.flags { + let mut flag_str = String::new(); + if let Some(short) = flag.short { + flag_str.push_str(&format!("-{short}")); + } + if let Some(long) = flag.long { + if !flag_str.is_empty() { + flag_str.push_str(", "); + } + flag_str.push_str(&format!("--{long}")); + } + if flag_str.is_empty() { + flag_str = flag.name.to_string(); + } + + println!(" {:<20} {}", flag_str, flag.description); + } + } + + if !current_node.subcommands.is_empty() { + println!("\nSubcommands:"); + for (name, node) in ¤t_node.subcommands { + println!(" {:<20} {}", name, node.description); + } + } + + Ok(()) +} + +pub fn print_prompt() { + print!("axvisor:{}$ ", std::env::current_dir().unwrap()); + std::io::stdout().flush().unwrap(); +} + +pub fn run_cmd_bytes(cmd_bytes: &[u8]) { + match str::from_utf8(cmd_bytes) { + Ok(cmd_str) => { + let trimmed = cmd_str.trim(); + if trimmed.is_empty() { + return; + } + + match execute_command(trimmed) { + Ok(_) => { + // Command executed successfully + } + Err(ParseError::UnknownCommand(cmd)) => { + println!("Error: Unknown command '{}'", cmd); + println!("Type 'help' to see available commands"); + } + Err(ParseError::UnknownOption(opt)) => { + println!("Error: Unknown option '{}'", opt); + } + Err(ParseError::MissingValue(opt)) => { + println!("Error: Option '{}' is missing a value", opt); + } + Err(ParseError::MissingRequiredOption(opt)) => { + println!("Error: Missing required option '{}'", opt); + } + Err(ParseError::NoHandler(cmd)) => { + println!("Error: Command '{}' has no handler function", cmd); + } + } + } + Err(_) => { + println!("Error: Input contains invalid UTF-8 characters"); + } + } +} + +// Built-in command handler +pub fn handle_builtin_commands(input: &str) -> bool { + match input.trim() { + "help" => { + show_available_commands(); + true + } + "exit" | "quit" => { + println!("Goodbye!"); + std::process::exit(0); + } + "clear" => { + print!("\x1b[2J\x1b[H"); // ANSI clear screen sequence + std::io::stdout().flush().unwrap(); + true + } + _ if input.starts_with("help ") => { + let cmd_parts: Vec = input[5..] + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + if let Err(e) = show_help(&cmd_parts) { + println!("Error: {:?}", e); + } + true + } + _ => false, + } +} + +pub fn show_available_commands() { + println!("ArceOS Shell - Available Commands:"); + println!(); + + // Display all top-level commands + for (name, node) in COMMAND_TREE.iter() { + println!(" {:<15} {}", name, node.description); + + // Display subcommands + if !node.subcommands.is_empty() { + for (sub_name, sub_node) in &node.subcommands { + println!(" {:<13} {}", sub_name, sub_node.description); + } + } + } + + println!(); + println!("Built-in Commands:"); + println!(" help Show help information"); + println!(" help Show help for a specific command"); + println!(" clear Clear the screen"); + println!(" exit/quit Exit the shell"); + println!(); + println!("Tip: Use 'help ' to see detailed usage of a command"); +} diff --git a/src/shell/command/vm.rs b/src/shell/command/vm.rs new file mode 100644 index 00000000..3f6ba782 --- /dev/null +++ b/src/shell/command/vm.rs @@ -0,0 +1,821 @@ +use std::{ + collections::btree_map::BTreeMap, + fs::read_to_string, + println, + string::{String, ToString}, + vec::Vec, +}; + +use crate::{ + shell::command::{CommandNode, FlagDef, OptionDef, ParsedCommand}, + vmm::{ + add_running_vm_count, config::init_guest_vm, get_running_vm_count, vcpus, vm_list, with_vm, + }, +}; + +fn vm_help(_cmd: &ParsedCommand) { + println!("VM - virtual machine management"); + println!("Most commonly used vm commands:"); + println!(" create Create a new virtual machine"); + println!(" start Start a virtual machine"); + println!(" stop Stop a virtual machine"); + println!(" restart Restart a virtual machine"); + println!(" delete Delete a virtual machine"); + println!(" list Show virtual machine lists"); + println!(" show Show virtual machine details"); + println!(" status Show virtual machine status"); + println!(); + println!("Use 'vm --help' for more information on a specific command."); +} + +fn vm_create(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + + println!("Positional args: {:?}", args); + + if args.is_empty() { + println!("Error: No VM configuration file specified"); + println!("Usage: vm create [CONFIG_FILE]"); + return; + } + + let initial_vm_count = vm_list::get_vm_list().len(); + + let mut processed_count = 0; + for config_path in args.iter() { + println!("Creating VM from config: {}", config_path); + + match read_to_string(config_path) { + Ok(raw_cfg) => match init_guest_vm(&raw_cfg) { + Ok(_) => { + println!("✓ Successfully created VM from config: {}", config_path); + processed_count += 1; + } + Err(_) => { + println!( + "✗ Failed to create VM from {}: Configuration error or panic occurred", + config_path + ); + } + }, + Err(e) => { + println!("✗ Failed to read config file {}: {:?}", config_path, e); + } + } + } + + // Check the actual number of VMs created + let final_vm_count = vm_list::get_vm_list().len(); + let created_count = final_vm_count - initial_vm_count; + + if created_count > 0 { + println!("Successfully created {} VM(s)", created_count); + } else if processed_count > 0 { + println!( + "Processed {} config file(s) but no VMs were actually created", + processed_count + ); + } +} + +fn vm_start(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let detach = cmd.flags.get("detach").unwrap_or(&false); + + if args.is_empty() { + // start all VMs + info!("VMM starting, booting all VMs..."); + let mut started_count = 0; + + for vm in vm_list::get_vm_list() { + // Set up primary virtual CPU before starting + vcpus::setup_vm_primary_vcpu(vm.clone()); + + match vm.boot() { + Ok(_) => { + vcpus::notify_primary_vcpu(vm.id()); + add_running_vm_count(1); + println!("✓ VM[{}] started successfully", vm.id()); + started_count += 1; + } + Err(err) => { + println!("✗ VM[{}] failed to start: {:?}", vm.id(), err); + } + } + } + println!("Started {} VM(s)", started_count); + } else { + // Start specified VMs + for vm_name in args { + // Try to parse as VM ID or lookup VM name + if let Ok(vm_id) = vm_name.parse::() { + start_vm_by_id(vm_id); + } else { + println!("Error: VM name lookup not implemented. Use VM ID instead."); + println!("Available VMs:"); + vm_list_simple(); + } + } + } + + if *detach { + println!("VMs started in background mode"); + } +} + +fn start_vm_by_id(vm_id: usize) { + // Set up primary virtual CPU before starting + match with_vm(vm_id, |vm| { + vcpus::setup_vm_primary_vcpu(vm.clone()); + vm.boot() + }) { + Some(Ok(_)) => { + vcpus::notify_primary_vcpu(vm_id); + add_running_vm_count(1); + println!("✓ VM[{}] started successfully", vm_id); + } + Some(Err(err)) => { + println!("✗ VM[{}] failed to start: {:?}", vm_id, err); + } + None => { + println!("✗ VM[{}] not found", vm_id); + } + } +} + +fn vm_stop(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let force = cmd.flags.get("force").unwrap_or(&false); + + if args.is_empty() { + println!("Error: No VM specified"); + println!("Usage: vm stop [OPTIONS] "); + return; + } + + for vm_name in args { + if let Ok(vm_id) = vm_name.parse::() { + stop_vm_by_id(vm_id, *force); + } else { + println!("Error: Invalid VM ID: {}", vm_name); + } + } +} + +fn stop_vm_by_id(vm_id: usize, force: bool) { + match with_vm(vm_id, |vm| { + if force { + println!("Force stopping VM[{}]...", vm_id); + // Force shutdown, directly call shutdown + vm.shutdown() + } else { + println!("Stopping VM[{}]...", vm_id); + vm.shutdown() + } + }) { + Some(Ok(_)) => { + println!("✓ VM[{}] stopped successfully", vm_id); + } + Some(Err(err)) => { + println!("✗ Failed to stop VM[{}]: {:?}", vm_id, err); + } + None => { + println!("✗ VM[{}] not found", vm_id); + } + } +} + +fn vm_restart(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let force = cmd.flags.get("force").unwrap_or(&false); + + if args.is_empty() { + println!("Error: No VM specified"); + println!("Usage: vm restart [OPTIONS] "); + return; + } + + for vm_name in args { + if let Ok(vm_id) = vm_name.parse::() { + restart_vm_by_id(vm_id, *force); + } else { + println!("Error: Invalid VM ID: {}", vm_name); + } + } +} + +fn restart_vm_by_id(vm_id: usize, force: bool) { + println!("Restarting VM[{}]...", vm_id); + + // First stop the virtual machine + stop_vm_by_id(vm_id, force); + + // Wait for a period to ensure complete shutdown + // In actual implementation, more complex state checking may be needed + + // Restart the virtual machine + start_vm_by_id(vm_id); +} + +fn vm_delete(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let force = cmd.flags.get("force").unwrap_or(&false); + let keep_data = cmd.flags.get("keep-data").unwrap_or(&false); + + if args.is_empty() { + println!("Error: No VM specified"); + println!("Usage: vm delete [OPTIONS] "); + return; + } + + let vm_name = &args[0]; + + if let Ok(vm_id) = vm_name.parse::() { + if !force { + println!( + "Are you sure you want to delete VM[{}]? (This operation cannot be undone)", + vm_id + ); + println!("Use --force to skip confirmation"); + return; + } + + delete_vm_by_id(vm_id, *keep_data); + } else { + println!("Error: Invalid VM ID: {}", vm_name); + } +} + +fn delete_vm_by_id(vm_id: usize, keep_data: bool) { + // First ensure VM is stopped + with_vm(vm_id, |vm| vm.shutdown()).unwrap_or(Ok(())).ok(); + + // Remove VM from global list + match crate::vmm::vm_list::remove_vm(vm_id) { + Some(_) => { + if keep_data { + println!("✓ VM[{}] deleted (data preserved)", vm_id); + } else { + println!("✓ VM[{}] deleted completely", vm_id); + // Here all VM-related data files should be cleaned up + } + } + None => { + println!("✗ VM[{}] not found", vm_id); + } + } +} + +fn vm_list_simple() { + let vms = vm_list::get_vm_list(); + println!("ID NAME STATE VCPU MEMORY"); + println!("---- ----------- ------- ---- ------"); + for vm in vms { + let state = if vm.running() { + "running" + } else if vm.shutting_down() { + "stopping" + } else { + "stopped" + }; + + // Calculate total memory size + let total_memory: usize = vm.memory_regions().iter().map(|region| region.size()).sum(); + + println!( + "{:<4} {:<11} {:<7} {:<4} {}MB", + vm.id(), + vm.with_config(|cfg| cfg.name()), + state, + vm.vcpu_num(), + total_memory / (1024 * 1024) // Convert to MB + ); + } +} + +fn vm_list(cmd: &ParsedCommand) { + let show_all = cmd.flags.get("all").unwrap_or(&false); + let binding = "table".to_string(); + let format = cmd.options.get("format").unwrap_or(&binding); + + let vms = vm_list::get_vm_list(); + + if format == "json" { + println!("{{"); + println!(" \"vms\": ["); + for (i, vm) in vms.iter().enumerate() { + let state = if vm.running() { + "running" + } else if vm.shutting_down() { + "stopping" + } else { + "stopped" + }; + + let total_memory: usize = vm.memory_regions().iter().map(|region| region.size()).sum(); + + println!(" {{"); + println!(" \"id\": {},", vm.id()); + println!(" \"name\": \"{}\",", vm.with_config(|cfg| cfg.name())); + println!(" \"state\": \"{}\",", state); + println!(" \"vcpu\": {},", vm.vcpu_num()); + println!(" \"memory\": \"{}MB\",", total_memory / (1024 * 1024)); + println!( + " \"interrupt_mode\": \"{:?}\"", + vm.with_config(|cfg| cfg.interrupt_mode()) + ); + + if i < vms.len() - 1 { + println!(" }},"); + } else { + println!(" }}"); + } + } + println!(" ]"); + println!("}}"); + } else { + println!("Virtual Machines:"); + if vms.is_empty() { + println!("No virtual machines found."); + return; + } + + // Count running VMs before filtering + let running_count = vms.iter().filter(|vm| vm.running()).count(); + let total_count = vms.len(); + + // Filter displayed VMs + let display_vms: Vec<_> = if *show_all { + vms + } else { + vms.into_iter().filter(|vm| vm.running()).collect() + }; + + if display_vms.is_empty() && !*show_all { + println!("No running virtual machines found."); + println!("Use --all to show all VMs including stopped ones."); + return; + } + + println!("ID NAME STATE VCPU MEMORY"); + println!("---- ----------- ------- ---- ------"); + for vm in display_vms { + let state = if vm.running() { + "🟢 running" + } else if vm.shutting_down() { + "🟡 stopping" + } else { + "🔴 stopped" + }; + + let total_memory: usize = vm.memory_regions().iter().map(|region| region.size()).sum(); + + println!( + "{:<4} {:<11} {:<9} {:<4} {:<8}", + vm.id(), + vm.with_config(|cfg| cfg.name()), + state, + vm.vcpu_num(), + format!("{}MB", total_memory / (1024 * 1024)) + ); + } + + if !show_all && running_count < total_count { + println!( + "\nShowing {} running VMs. Use --all to show all {} VMs.", + running_count, total_count + ); + } + } +} + +fn vm_show(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let show_config = cmd.flags.get("config").unwrap_or(&false); + let show_stats = cmd.flags.get("stats").unwrap_or(&false); + + if args.is_empty() { + println!("Error: No VM specified"); + println!("Usage: vm show [OPTIONS] "); + return; + } + + let vm_name = &args[0]; + if let Ok(vm_id) = vm_name.parse::() { + show_vm_details(vm_id, *show_config, *show_stats); + } else { + println!("Error: Invalid VM ID: {}", vm_name); + } +} + +/// Show detailed information about a specific VM. +fn show_vm_details(vm_id: usize, show_config: bool, show_stats: bool) { + match with_vm(vm_id, |vm| { + let state = if vm.running() { + "🟢 running" + } else if vm.shutting_down() { + "🟡 stopping" + } else { + "🔴 stopped" + }; + + println!("VM Details: {}", vm_id); + println!(" ID: {}", vm.id()); + println!(" Name: {}", vm.with_config(|cfg| cfg.name())); + println!(" State: {}", state); + println!(" VCPUs: {}", vm.vcpu_num()); + + // show VCPU information + println!(" VCPU List:"); + for (i, vcpu) in vm.vcpu_list().iter().enumerate() { + if let Some(phys_cpu_set) = vcpu.phys_cpu_set() { + println!(" VCPU[{}]: CPU affinity mask = {:#x}", i, phys_cpu_set); + } else { + println!(" VCPU[{}]: No CPU affinity set", i); + } + } + + if show_config { + println!(); + println!("Configuration:"); + vm.with_config(|cfg| { + println!(" BSP Entry: {:#x}", cfg.bsp_entry().as_usize()); + println!(" AP Entry: {:#x}", cfg.ap_entry().as_usize()); + println!(" Interrupt Mode: {:?}", cfg.interrupt_mode()); + + // show passthrough devices + if !cfg.pass_through_devices().is_empty() { + println!(" Passthrough Devices:"); + for device in cfg.pass_through_devices() { + println!( + " {}: GPA[{:#x}~{:#x}] -> HPA[{:#x}~{:#x}]", + device.name, + device.base_gpa, + device.base_gpa + device.length, + device.base_hpa, + device.base_hpa + device.length + ); + } + } + + // show emulated devices + if !cfg.emu_devices().is_empty() { + println!(" Emulated Devices:"); + for device in cfg.emu_devices() { + println!(" {:?}", device); + } + } + }); + } + + if show_stats { + println!(); + println!("Statistics:"); + println!(" EPT Root: {:#x}", vm.ept_root().as_usize()); + println!( + " Device Count: {}", + vm.get_devices().iter_mmio_dev().count() + ); + + let mut vcpu_states = BTreeMap::new(); + for vcpu in vm.vcpu_list() { + let state_key = match vcpu.state() { + axvcpu::VCpuState::Free => "Free", + axvcpu::VCpuState::Running => "Running", + axvcpu::VCpuState::Blocked => "Blocked", + axvcpu::VCpuState::Invalid => "Invalid", + axvcpu::VCpuState::Created => "Created", + axvcpu::VCpuState::Ready => "Ready", + }; + *vcpu_states.entry(state_key).or_insert(0) += 1; + } + + println!(" VCPU States:"); + for (state, count) in vcpu_states { + println!(" {}: {}", state, count); + } + } + }) { + Some(_) => {} + None => { + println!("✗ VM[{}] not found", vm_id); + } + } +} + +fn vm_status(cmd: &ParsedCommand) { + let args = &cmd.positional_args; + let watch = cmd.flags.get("watch").unwrap_or(&false); + + if args.is_empty() { + // show all VM status + show_all_vm_status(*watch); + return; + } + + let vm_name = &args[0]; + if let Ok(vm_id) = vm_name.parse::() { + show_vm_status(vm_id, *watch); + } else { + println!("Error: Invalid VM ID: {}", vm_name); + } +} + +/// Show status of a specific VM. +fn show_vm_status(vm_id: usize, watch: bool) { + if watch { + println!("Watching VM[{}] status (press Ctrl+C to stop):", vm_id); + // TODO: add real-time status information + } + + match with_vm(vm_id, |vm| { + let state = if vm.running() { + "🟢 running" + } else if vm.shutting_down() { + "🟡 stopping" + } else { + "🔴 stopped" + }; + + println!("Virtual machine status for VM[{}]:", vm_id); + println!(" ID: {}", vm.id()); + println!(" Name: {}", vm.with_config(|cfg| cfg.name())); + println!(" State: {}", state); + println!(" VCPUs: {}", vm.vcpu_num()); + + // Calculate total memory + let total_memory: usize = vm.memory_regions().iter().map(|region| region.size()).sum(); + + println!(" Total Memory: {}MB", total_memory / (1024 * 1024)); + + // Show memory region details + println!(" Memory Regions:"); + for (i, region) in vm.memory_regions().iter().enumerate() { + println!( + " Region[{}]: GPA[{:#x}~{:#x}] Size={}KB", + i, + region.gpa, + region.gpa + region.size(), + region.size() / 1024 + ); + } + + println!(" VCPU Details:"); + for vcpu in vm.vcpu_list() { + let vcpu_state = match vcpu.state() { + axvcpu::VCpuState::Free => "Free", + axvcpu::VCpuState::Running => "Running", + axvcpu::VCpuState::Blocked => "Blocked", + axvcpu::VCpuState::Invalid => "Invalid", + axvcpu::VCpuState::Created => "Created", + axvcpu::VCpuState::Ready => "Ready", + }; + + if let Some(phys_cpu_set) = vcpu.phys_cpu_set() { + println!( + " VCPU[{}]: {} (CPU affinity: {:#x})", + vcpu.id(), + vcpu_state, + phys_cpu_set + ); + } else { + println!(" VCPU[{}]: {} (No CPU affinity)", vcpu.id(), vcpu_state); + } + } + + // show device information + let mmio_dev_count = vm.get_devices().iter_mmio_dev().count(); + println!(" Devices: {} MMIO devices", mmio_dev_count); + + // TODO: add more real-time status information + // println!(" Network: connected/disconnected"); + // println!(" Uptime: {} seconds", uptime); + }) { + Some(_) => {} + None => { + println!("✗ VM[{}] not found", vm_id); + } + } +} + +/// Show status of all VMs in a summary format. +fn show_all_vm_status(watch: bool) { + if watch { + println!("Watching all VMs status (press Ctrl+C to stop):"); + } + + let vms = vm_list::get_vm_list(); + if vms.is_empty() { + println!("No virtual machines found."); + return; + } + + println!("System Status:"); + println!(" Total VMs: {}", vms.len()); + println!(" Running VMs: {}", get_running_vm_count()); + + let mut running_count = 0; + let mut stopping_count = 0; + let mut stopped_count = 0; + let mut total_vcpus = 0; + let mut total_memory = 0; + + for vm in &vms { + if vm.running() { + running_count += 1; + } else if vm.shutting_down() { + stopping_count += 1; + } else { + stopped_count += 1; + } + + total_vcpus += vm.vcpu_num(); + total_memory += vm + .memory_regions() + .iter() + .map(|region| region.size()) + .sum::(); + } + + println!(" Total VCPUs: {}", total_vcpus); + println!(" Total Memory: {}MB", total_memory / (1024 * 1024)); + println!(); + + println!("VM Status Overview:"); + println!(" 🟢 Running: {}", running_count); + println!(" 🟡 Stopping: {}", stopping_count); + println!(" 🔴 Stopped: {}", stopped_count); + println!(); + + println!("Individual VM Status:"); + for vm in vms { + let state_icon = if vm.running() { + "🟢" + } else if vm.shutting_down() { + "🟡" + } else { + "🔴" + }; + + let vm_memory: usize = vm.memory_regions().iter().map(|region| region.size()).sum(); + + println!( + " {} VM[{}] {} ({} VCPUs, {}MB)", + state_icon, + vm.id(), + vm.with_config(|cfg| cfg.name()), + vm.vcpu_num(), + vm_memory / (1024 * 1024), + ); + + if vm.running() { + let mut vcpu_summary = BTreeMap::new(); + for vcpu in vm.vcpu_list() { + let state = match vcpu.state() { + axvcpu::VCpuState::Free => "Free", + axvcpu::VCpuState::Running => "Running", + axvcpu::VCpuState::Blocked => "Blocked", + axvcpu::VCpuState::Invalid => "Invalid", + axvcpu::VCpuState::Created => "Created", + axvcpu::VCpuState::Ready => "Ready", + }; + *vcpu_summary.entry(state).or_insert(0) += 1; + } + + let summary_str: Vec = vcpu_summary + .into_iter() + .map(|(state, count)| format!("{state}:{count}")) + .collect(); + + if !summary_str.is_empty() { + println!(" VCPUs: {}", summary_str.join(", ")); + } + } + } +} + +/// Build the VM command tree and register it. +pub fn build_vm_cmd(tree: &mut BTreeMap) { + let create_cmd = CommandNode::new("Create a new virtual machine") + .with_handler(vm_create) + .with_usage("vm create [OPTIONS] ...") + .with_option( + OptionDef::new("name", "Virtual machine name") + .with_short('n') + .with_long("name"), + ) + .with_option( + OptionDef::new("cpu", "Number of CPU cores") + .with_short('c') + .with_long("cpu"), + ) + .with_option( + OptionDef::new("memory", "Amount of memory") + .with_short('m') + .with_long("memory"), + ) + .with_flag( + FlagDef::new("force", "Force creation without confirmation") + .with_short('f') + .with_long("force"), + ); + + let start_cmd = CommandNode::new("Start a virtual machine") + .with_handler(vm_start) + .with_usage("vm start [OPTIONS] [VM_ID...]") + .with_flag( + FlagDef::new("detach", "Start in background") + .with_short('d') + .with_long("detach"), + ) + .with_flag( + FlagDef::new("console", "Attach to console") + .with_short('c') + .with_long("console"), + ); + + let stop_cmd = CommandNode::new("Stop a virtual machine") + .with_handler(vm_stop) + .with_usage("vm stop [OPTIONS] ...") + .with_flag( + FlagDef::new("force", "Force stop") + .with_short('f') + .with_long("force"), + ) + .with_flag( + FlagDef::new("graceful", "Graceful shutdown") + .with_short('g') + .with_long("graceful"), + ); + + let restart_cmd = CommandNode::new("Restart a virtual machine") + .with_handler(vm_restart) + .with_usage("vm restart [OPTIONS] ...") + .with_flag( + FlagDef::new("force", "Force restart") + .with_short('f') + .with_long("force"), + ); + + let delete_cmd = CommandNode::new("Delete a virtual machine") + .with_handler(vm_delete) + .with_usage("vm delete [OPTIONS] ") + .with_flag( + FlagDef::new("force", "Skip confirmation") + .with_short('f') + .with_long("force"), + ) + .with_flag(FlagDef::new("keep-data", "Keep VM data").with_long("keep-data")); + + let list_cmd = CommandNode::new("Show virtual machine lists") + .with_handler(vm_list) + .with_usage("vm list [OPTIONS]") + .with_flag( + FlagDef::new("all", "Show all VMs including stopped ones") + .with_short('a') + .with_long("all"), + ) + .with_option(OptionDef::new("format", "Output format (table, json)").with_long("format")); + + let show_cmd = CommandNode::new("Show virtual machine details") + .with_handler(vm_show) + .with_usage("vm show [OPTIONS] ") + .with_flag( + FlagDef::new("config", "Show configuration") + .with_short('c') + .with_long("config"), + ) + .with_flag( + FlagDef::new("stats", "Show statistics") + .with_short('s') + .with_long("stats"), + ); + + let status_cmd = CommandNode::new("Show virtual machine status") + .with_handler(vm_status) + .with_usage("vm status [OPTIONS] [VM_ID]") + .with_flag( + FlagDef::new("watch", "Watch status changes") + .with_short('w') + .with_long("watch"), + ); + + // main VM command + let vm_node = CommandNode::new("Virtual machine management") + .with_handler(vm_help) + .with_usage("vm [options] [args...]") + .add_subcommand( + "help", + CommandNode::new("Show VM help").with_handler(vm_help), + ) + .add_subcommand("create", create_cmd) + .add_subcommand("start", start_cmd) + .add_subcommand("stop", stop_cmd) + .add_subcommand("restart", restart_cmd) + .add_subcommand("delete", delete_cmd) + .add_subcommand("list", list_cmd) + .add_subcommand("show", show_cmd) + .add_subcommand("status", status_cmd); + + tree.insert("vm".to_string(), vm_node); +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs new file mode 100644 index 00000000..5af82752 --- /dev/null +++ b/src/shell/mod.rs @@ -0,0 +1,209 @@ +mod command; + +use std::io::prelude::*; +use std::println; +use std::string::ToString; + +use crate::shell::command::{ + CommandHistory, clear_line_and_redraw, handle_builtin_commands, print_prompt, run_cmd_bytes, +}; + +const LF: u8 = b'\n'; +const CR: u8 = b'\r'; +const DL: u8 = b'\x7f'; +const BS: u8 = b'\x08'; +const ESC: u8 = 0x1b; // ESC key + +const MAX_LINE_LEN: usize = 256; + +// Initialize the console shell. +pub fn console_init() { + let mut stdin = std::io::stdin(); + let mut stdout = std::io::stdout(); + let mut history = CommandHistory::new(100); + + let mut buf = [0; MAX_LINE_LEN]; + let mut cursor = 0; // cursor position in buffer + let mut line_len = 0; // actual length of current line + + enum InputState { + Normal, + Escape, + EscapeSeq, + } + + let mut input_state = InputState::Normal; + + println!("Welcome to AxVisor Shell!"); + println!("Type 'help' to see available commands"); + println!("Use UP/DOWN arrows to navigate command history"); + println!(); + + print_prompt(); + + loop { + let mut temp_buf = [0u8; 1]; + + let ch = match stdin.read(&mut temp_buf) { + Ok(1) => temp_buf[0], + _ => { + continue; + } + }; + + match input_state { + InputState::Normal => { + match ch { + CR | LF => { + println!(); + if line_len > 0 { + let cmd_str = std::str::from_utf8(&buf[..line_len]).unwrap_or(""); + + // Add to history + history.add_command(cmd_str.to_string()); + + // Execute command + if !handle_builtin_commands(cmd_str) { + run_cmd_bytes(&buf[..line_len]); + } + + // reset buffer + buf[..line_len].fill(0); + cursor = 0; + line_len = 0; + } + print_prompt(); + } + BS | DL => { + // backspace: delete character before cursor / DEL key: delete character at cursor + if cursor > 0 { + // move characters after cursor forward + for i in cursor..line_len { + buf[i - 1] = buf[i]; + } + cursor -= 1; + line_len -= 1; + if line_len < buf.len() { + buf[line_len] = 0; + } + + let current_content = + std::str::from_utf8(&buf[..line_len]).unwrap_or(""); + let prompt = format!("axvisor:{}$ ", &std::env::current_dir().unwrap()); + clear_line_and_redraw(&mut stdout, &prompt, current_content, cursor); + } + } + ESC => { + input_state = InputState::Escape; + } + 0..=31 => { + // ignore other control characters + } + c => { + // insert character + if line_len < MAX_LINE_LEN - 1 { + // move characters after cursor backward to make space for new character + for i in (cursor..line_len).rev() { + buf[i + 1] = buf[i]; + } + buf[cursor] = c; + cursor += 1; + line_len += 1; + + let current_content = + std::str::from_utf8(&buf[..line_len]).unwrap_or(""); + let prompt = format!("axvisor:{}$ ", &std::env::current_dir().unwrap()); + clear_line_and_redraw(&mut stdout, &prompt, current_content, cursor); + } + } + } + } + InputState::Escape => match ch { + b'[' => { + input_state = InputState::EscapeSeq; + } + _ => { + input_state = InputState::Normal; + } + }, + InputState::EscapeSeq => { + match ch { + b'A' => { + // UP arrow - previous command + if let Some(prev_cmd) = history.previous() { + // clear current buffer + buf[..line_len].fill(0); + + let cmd_bytes = prev_cmd.as_bytes(); + let copy_len = cmd_bytes.len().min(MAX_LINE_LEN - 1); + buf[..copy_len].copy_from_slice(&cmd_bytes[..copy_len]); + cursor = copy_len; + line_len = copy_len; + + let prompt = format!("axvisor:{}$ ", &std::env::current_dir().unwrap()); + clear_line_and_redraw(&mut stdout, &prompt, prev_cmd, cursor); + } + input_state = InputState::Normal; + } + b'B' => { + // DOWN arrow - next command + match history.next() { + Some(next_cmd) => { + // clear current buffer + buf[..line_len].fill(0); + + let cmd_bytes = next_cmd.as_bytes(); + let copy_len = cmd_bytes.len().min(MAX_LINE_LEN - 1); + buf[..copy_len].copy_from_slice(&cmd_bytes[..copy_len]); + cursor = copy_len; + line_len = copy_len; + + let prompt = + format!("axvisor:{}$ ", &std::env::current_dir().unwrap()); + clear_line_and_redraw(&mut stdout, &prompt, next_cmd, cursor); + } + None => { + // clear current line + buf[..line_len].fill(0); + cursor = 0; + line_len = 0; + let prompt = + format!("axvisor:{}$ ", &std::env::current_dir().unwrap()); + clear_line_and_redraw(&mut stdout, &prompt, "", cursor); + } + } + input_state = InputState::Normal; + } + b'C' => { + // RIGHT arrow - move cursor right + if cursor < line_len { + cursor += 1; + stdout.write_all(b"\x1b[C").ok(); + stdout.flush().ok(); + } + input_state = InputState::Normal; + } + b'D' => { + // LEFT arrow - move cursor left + if cursor > 0 { + cursor -= 1; + stdout.write_all(b"\x1b[D").ok(); + stdout.flush().ok(); + } + input_state = InputState::Normal; + } + b'3' => { + // check if this is Delete key sequence (ESC[3~) + // need to read next character to confirm + input_state = InputState::Normal; + // can add additional state to handle complete Delete sequence + } + _ => { + // ignore other escape sequences + input_state = InputState::Normal; + } + } + } + } + } +} diff --git a/src/vmm/config.rs b/src/vmm/config.rs index 06afc5ca..10a0d93b 100644 --- a/src/vmm/config.rs +++ b/src/vmm/config.rs @@ -1,4 +1,5 @@ use axaddrspace::GuestPhysAddr; +use axerrno::AxResult; use axvm::{ VMMemoryRegion, config::{AxVMConfig, AxVMCrateConfig, VmMemMappingType}, @@ -20,14 +21,7 @@ pub mod config { /// Default static VM configs. Used when no VM config is provided. pub fn default_static_vm_configs() -> Vec<&'static str> { - vec![ - #[cfg(target_arch = "x86_64")] - core::include_str!("../../configs/vms/nimbos-x86_64-qemu-smp1.toml"), - #[cfg(target_arch = "aarch64")] - core::include_str!("../../configs/vms/nimbos-aarch64-qemu-smp1.toml"), - #[cfg(target_arch = "riscv64")] - core::include_str!("../../configs/vms/nimbos-riscv64-qemu-smp1.toml"), - ] + vec![] } /// Read VM configs from filesystem @@ -93,53 +87,61 @@ pub fn init_guest_vms() { } for raw_cfg_str in gvm_raw_configs { - let vm_create_config = - AxVMCrateConfig::from_toml(&raw_cfg_str).expect("Failed to resolve VM config"); - - if let Some(linux) = super::images::get_image_header(&vm_create_config) { - debug!( - "VM[{}] Linux header: {:#x?}", - vm_create_config.base.id, linux - ); + if let Err(e) = init_guest_vm(&raw_cfg_str) { + error!("Failed to initialize guest VM: {:?}", e); } + } +} + +pub fn init_guest_vm(raw_cfg: &str) -> AxResult { + let vm_create_config = + AxVMCrateConfig::from_toml(raw_cfg).expect("Failed to resolve VM config"); - #[cfg(target_arch = "aarch64")] - let mut vm_config = AxVMConfig::from(vm_create_config.clone()); + if let Some(linux) = super::images::get_image_header(&vm_create_config) { + debug!( + "VM[{}] Linux header: {:#x?}", + vm_create_config.base.id, linux + ); + } + + #[cfg(target_arch = "aarch64")] + let mut vm_config = AxVMConfig::from(vm_create_config.clone()); - #[cfg(not(target_arch = "aarch64"))] - let vm_config = AxVMConfig::from(vm_create_config.clone()); + #[cfg(not(target_arch = "aarch64"))] + let vm_config = AxVMConfig::from(vm_create_config.clone()); - // Handle FDT-related operations for aarch64 - #[cfg(target_arch = "aarch64")] - handle_fdt_operations(&mut vm_config, &vm_create_config); + // Handle FDT-related operations for aarch64 + #[cfg(target_arch = "aarch64")] + handle_fdt_operations(&mut vm_config, &vm_create_config); - // info!("after parse_vm_interrupt, crate VM[{}] with config: {:#?}", vm_config.id(), vm_config); - info!("Creating VM[{}] {:?}", vm_config.id(), vm_config.name()); + // info!("after parse_vm_interrupt, crate VM[{}] with config: {:#?}", vm_config.id(), vm_config); + info!("Creating VM[{}] {:?}", vm_config.id(), vm_config.name()); - // Create VM. - let vm = VM::new(vm_config).expect("Failed to create VM"); - push_vm(vm.clone()); + // Create VM. + let vm = VM::new(vm_config).expect("Failed to create VM"); + push_vm(vm.clone()); - vm_alloc_memorys(&vm_create_config, &vm); + vm_alloc_memorys(&vm_create_config, &vm); - let main_mem = vm - .memory_regions() - .first() - .cloned() - .expect("VM must have at least one memory region"); + let main_mem = vm + .memory_regions() + .first() + .cloned() + .expect("VM must have at least one memory region"); - config_guest_address(&vm, &main_mem); + config_guest_address(&vm, &main_mem); - // Load corresponding images for VM. - info!("VM[{}] created success, loading images...", vm.id()); + // Load corresponding images for VM. + info!("VM[{}] created success, loading images...", vm.id()); - let mut loader = ImageLoader::new(main_mem, vm_create_config, vm.clone()); - loader.load().expect("Failed to load VM images"); + let mut loader = ImageLoader::new(main_mem, vm_create_config, vm.clone()); + loader.load().expect("Failed to load VM images"); - if let Err(e) = vm.init() { - panic!("VM[{}] setup failed: {:?}", vm.id(), e); - } + if let Err(e) = vm.init() { + panic!("VM[{}] setup failed: {:?}", vm.id(), e); } + + Ok(()) } fn config_guest_address(vm: &VM, main_memory: &VMMemoryRegion) { diff --git a/src/vmm/mod.rs b/src/vmm/mod.rs index 0c228bad..50597e50 100644 --- a/src/vmm/mod.rs +++ b/src/vmm/mod.rs @@ -1,10 +1,11 @@ -mod config; mod hvc; -mod images; mod ivc; + +pub mod config; +pub mod images; pub mod timer; -mod vcpus; -mod vm_list; +pub mod vcpus; +pub mod vm_list; #[cfg(target_arch = "aarch64")] pub mod fdt; @@ -127,3 +128,11 @@ pub fn with_vm_and_vcpu_on_pcpu( // with_vm_and_vcpu_on_pcpu(vm_id, vcpu_id, f); // })) } + +pub fn get_running_vm_count() -> usize { + RUNNING_VM_COUNT.load(Ordering::Acquire) +} + +pub fn add_running_vm_count(count: usize) { + RUNNING_VM_COUNT.fetch_add(count, Ordering::Release); +}