// SPDX-License-Identifier: MPL-2.0 use std::process::exit; use vsc::VscLaunchConfig; use super::{ build::create_base_and_cached_build, util::{is_tdx_enabled, DEFAULT_TARGET_RELPATH}, }; use crate::{ config::{scheme::ActionChoice, Config}, error::Errno, error_msg, util::{get_kernel_crate, get_target_directory}, warn_msg, }; pub fn execute_run_command(config: &Config, gdb_server_args: Option<&str>) { let cargo_target_directory = get_target_directory(); let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH); let target_info = get_kernel_crate(); let mut config = config.clone(); let _vsc_launch_file = if let Some(gdb_server_str) = gdb_server_args { adapt_for_gdb_server(&mut config, gdb_server_str) } else { None }; let default_bundle_directory = osdk_output_directory.join(&target_info.name); let bundle = create_base_and_cached_build( target_info, default_bundle_directory, &osdk_output_directory, &cargo_target_directory, &config, ActionChoice::Run, &[], ); bundle.run(&config, ActionChoice::Run); } fn adapt_for_gdb_server(config: &mut Config, gdb_server_str: &str) -> Option { let gdb_server_args = GdbServerArgs::from_str(gdb_server_str); // Add GDB server arguments to QEMU. let qemu_gdb_args = { let gdb_stub_addr = gdb_server_args.host_addr.as_str(); match gdb::stub_type_of(gdb_stub_addr) { gdb::StubAddrType::Unix => { format!( " -chardev socket,path={},server=on,wait=off,id=gdb0 -gdb chardev:gdb0", gdb_stub_addr ) } gdb::StubAddrType::Tcp => { format!( " -gdb tcp:{}", gdb::tcp_addr_util::format_tcp_addr(gdb_stub_addr) ) } } }; config.run.qemu.args += &qemu_gdb_args; if gdb_server_args.wait_client { config.run.qemu.args += " -S"; } if is_tdx_enabled() { let target = "-object tdx-guest,"; if let Some(pos) = config.run.qemu.args.find(target) { let insert_pos = pos + target.len(); config.run.qemu.args.insert_str(insert_pos, "debug=on,"); } else { warn_msg!( "TDX is enabled, but the TDX guest object is not found in the QEMU arguments" ); } } // Ensure debug info added when debugging in the release profile. if config.run.build.profile.contains("release") { config .run .build .override_configs .push(format!("profile.{}.debug=true", config.run.build.profile)); } gdb_server_args.vsc_launch_file.then(|| { vsc::check_gdb_config(&gdb_server_args); let profile = super::util::profile_name_adapter(&config.run.build.profile); vsc::VscLaunchConfig::new(profile, &gdb_server_args.host_addr) }) } struct GdbServerArgs { host_addr: String, wait_client: bool, vsc_launch_file: bool, } impl GdbServerArgs { fn from_str(args: &str) -> Self { let mut host_addr = ".osdk-gdb-socket".to_string(); let mut wait_client = false; let mut vsc_launch_file = false; for arg in args.split(",") { let kv = arg.split('=').collect::>(); match kv.as_slice() { ["addr", addr] => host_addr = addr.to_string(), ["wait-client"] => wait_client = true, ["vscode"] => vsc_launch_file = true, _ => { error_msg!("Invalid GDB server argument: {}", arg); exit(Errno::Cli as _); } } } GdbServerArgs { host_addr, wait_client, vsc_launch_file, } } } mod gdb { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StubAddrType { Unix, // Unix Domain Socket Tcp, // IP_ADDR:PORT } pub fn stub_type_of(stub: &str) -> StubAddrType { if stub.split(':').next_back().unwrap().parse::().is_ok() { return StubAddrType::Tcp; } StubAddrType::Unix } pub mod tcp_addr_util { use crate::{error::Errno, error_msg}; use std::process::exit; fn strip_tcp_prefix(addr: &str) -> &str { addr.strip_prefix("tcp:").unwrap_or(addr) } fn parse_tcp_addr(addr: &str) -> (&str, u16) { let addr = strip_tcp_prefix(addr); if !addr.contains(':') { error_msg!("Ambiguous GDB server address, use '[IP]:PORT' format"); exit(Errno::ParseMetadata as _); } let mut iter = addr.split(':'); let host = iter.next().unwrap(); let port = iter.next().unwrap().parse().unwrap(); (host, port) } pub fn format_tcp_addr(tcp_addr: &str) -> String { let (host, port) = parse_tcp_addr(tcp_addr); format!("{}:{}", host, port) } } } mod vsc { use crate::{ commands::util::bin_file_name, util::{get_cargo_metadata, get_kernel_crate}, }; use serde_json::{from_str, Value}; use std::{ fs::{read_to_string, write as write_file}, path::Path, }; use super::{gdb, GdbServerArgs}; const VSC_DIR: &str = ".vscode"; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] struct Existence { vsc_dir: bool, launch_file: bool, } fn workspace_root() -> String { get_cargo_metadata(None::<&str>, None::<&[&str]>).unwrap()["workspace_root"] .as_str() .unwrap() .to_owned() } #[derive(Debug, Default)] pub struct VscLaunchConfig { existence: Existence, backup_launch_path: Option, } impl VscLaunchConfig { pub fn new(profile: &str, addr: &str) -> Self { let workspace = workspace_root(); let workspace = Path::new(&workspace); let launch_file_path = workspace.join(VSC_DIR).join("launch.json"); let existence = Existence { vsc_dir: workspace.join(VSC_DIR).exists(), launch_file: launch_file_path.exists(), }; let backup_launch_path = existence.launch_file.then(|| { let backup_launch_path = launch_file_path.with_extension("bak"); std::fs::copy(&launch_file_path, &backup_launch_path).unwrap(); backup_launch_path.to_string_lossy().to_string() }); if !existence.vsc_dir { std::fs::create_dir(workspace.join(VSC_DIR)).unwrap(); } generate_vsc_launch_file(workspace, profile, addr).unwrap(); VscLaunchConfig { existence, backup_launch_path, } } } impl Drop for VscLaunchConfig { fn drop(&mut self) { // remove generated files if !self.existence.vsc_dir { std::fs::remove_dir_all(Path::new(&workspace_root()).join(VSC_DIR)).unwrap(); return; } if !self.existence.launch_file { std::fs::remove_file( Path::new(&workspace_root()) .join(VSC_DIR) .join("launch.json"), ) .unwrap(); return; } // restore backup launch file if let Some(backup_launch_path) = &self.backup_launch_path { std::fs::copy( backup_launch_path, Path::new(&workspace_root()) .join(VSC_DIR) .join("launch.json"), ) .unwrap(); std::fs::remove_file(backup_launch_path).unwrap(); } } } /// Exit if the QEMU GDB server configuration is not valid pub fn check_gdb_config(args: &GdbServerArgs) { use crate::{error::Errno, error_msg}; use std::process::exit; // check GDB server address let gdb_stub_addr = args.host_addr.as_str(); if gdb_stub_addr.is_empty() { error_msg!("GDB server address is required to generate a VSCode launch file"); exit(Errno::ParseMetadata as _); } if gdb::stub_type_of(gdb_stub_addr) != gdb::StubAddrType::Tcp { error_msg!( "Non-TCP GDB server address is not supported under '--gdb-server vscode' currently" ); exit(Errno::ParseMetadata as _); } } fn generate_vsc_launch_file( workspace: impl AsRef, profile: &str, addr: &str, ) -> Result<(), std::io::Error> { let contents = include_str!("launch.json.template") .replace("#PROFILE#", profile) .replace("#CRATE_NAME#", &get_kernel_crate().name) .replace("#BIN_NAME#", &bin_file_name()) .replace( "#ADDR_PORT#", gdb::tcp_addr_util::format_tcp_addr(addr).trim_start_matches(':'), ); let original_items: Option = { let launch_file_path = workspace.as_ref().join(VSC_DIR).join("launch.json"); let src_path = if launch_file_path.exists() { launch_file_path } else { launch_file_path.with_extension("bak") }; src_path .exists() .then(|| from_str(&read_to_string(&src_path).unwrap()).unwrap()) }; let contents = if let Some(mut original_items) = original_items { let items: Value = from_str(&contents)?; for item in items["configurations"].as_array().unwrap() { if original_items["configurations"] .as_array() .unwrap() .iter() .any(|x| x["name"].as_str() == item["name"].as_str()) { println!("[{}/launch.json]{} already exists", VSC_DIR, item["name"]); // no override for configurations with the same name continue; } original_items["configurations"] .as_array_mut() .unwrap() .push(item.clone()); } serde_json::to_string_pretty(&original_items).unwrap() } else { contents }; let launch_file_path = workspace.as_ref().join(VSC_DIR).join("launch.json"); write_file(launch_file_path, contents) } }