From 92cce0f76768ab83497e1158673ab43d4d9e1fa3 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 5 Aug 2025 00:20:57 +0200 Subject: [PATCH] Add debugging utilities and improved package listing - Add support for debugging database tables with `--stats`, `--dump-all`, and `--dump-table` options. - Introduce CLI command `DebugDb` for inspecting catalog, obsoleted, and installed tables. - Enhance `List` command to include an `--all` flag for listing all available packages. - Update logging for database operations and package queries. - Add methods to dump and analyze table contents in `InstalledPackages` and `ImageCatalog`. --- libips/src/image/catalog.rs | 200 +++++++++++++++++++++++++++++++++ libips/src/image/installed.rs | 83 +++++++++++++- pkg6/src/main.rs | 203 +++++++++++++++++++++++++++++++++- 3 files changed, 483 insertions(+), 3 deletions(-) diff --git a/libips/src/image/catalog.rs b/libips/src/image/catalog.rs index 14bafaa..b74c35b 100644 --- a/libips/src/image/catalog.rs +++ b/libips/src/image/catalog.rs @@ -90,6 +90,206 @@ impl ImageCatalog { } } + /// Dump the contents of a specific table to stdout for debugging + pub fn dump_table(&self, table_name: &str) -> Result<()> { + // Open the database + let db = Database::open(&self.db_path) + .map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?; + + // Begin a read transaction + let tx = db.begin_read() + .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + + // Determine which table to dump + match table_name { + "catalog" => self.dump_catalog_table(&tx)?, + "obsoleted" => self.dump_obsoleted_table(&tx)?, + "installed" => self.dump_installed_table(&tx)?, + _ => return Err(CatalogError::Database(format!("Unknown table: {}", table_name))), + } + + Ok(()) + } + + /// Dump the contents of all tables to stdout for debugging + pub fn dump_all_tables(&self) -> Result<()> { + // Open the database + let db = Database::open(&self.db_path) + .map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?; + + // Begin a read transaction + let tx = db.begin_read() + .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + + println!("=== CATALOG TABLE ==="); + let _ = self.dump_catalog_table(&tx); + + println!("\n=== OBSOLETED TABLE ==="); + let _ = self.dump_obsoleted_table(&tx); + + println!("\n=== INSTALLED TABLE ==="); + let _ = self.dump_installed_table(&tx); + + Ok(()) + } + + /// Dump the contents of the catalog table + fn dump_catalog_table(&self, tx: &redb::ReadTransaction) -> Result<()> { + match tx.open_table(CATALOG_TABLE) { + Ok(table) => { + let mut count = 0; + for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? { + let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?; + let key_str = key.value(); + + // Try to deserialize the manifest + match serde_json::from_slice::(value.value()) { + Ok(manifest) => { + // Extract the publisher from the FMRI attribute + let publisher = manifest.attributes.iter() + .find(|attr| attr.key == "pkg.fmri") + .and_then(|attr| attr.values.get(0).cloned()) + .unwrap_or_else(|| "unknown".to_string()); + + println!("Key: {}", key_str); + println!(" FMRI: {}", publisher); + println!(" Attributes: {}", manifest.attributes.len()); + println!(" Files: {}", manifest.files.len()); + println!(" Directories: {}", manifest.directories.len()); + println!(" Dependencies: {}", manifest.dependencies.len()); + }, + Err(e) => { + println!("Key: {}", key_str); + println!(" Error deserializing manifest: {}", e); + } + } + count += 1; + } + println!("Total entries in catalog table: {}", count); + Ok(()) + }, + Err(e) => { + println!("Error opening catalog table: {}", e); + Err(CatalogError::Database(format!("Failed to open catalog table: {}", e))) + } + } + } + + /// Dump the contents of the obsoleted table + fn dump_obsoleted_table(&self, tx: &redb::ReadTransaction) -> Result<()> { + match tx.open_table(OBSOLETED_TABLE) { + Ok(table) => { + let mut count = 0; + for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? { + let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?; + let key_str = key.value(); + + println!("Key: {}", key_str); + count += 1; + } + println!("Total entries in obsoleted table: {}", count); + Ok(()) + }, + Err(e) => { + println!("Error opening obsoleted table: {}", e); + Err(CatalogError::Database(format!("Failed to open obsoleted table: {}", e))) + } + } + } + + /// Dump the contents of the installed table + fn dump_installed_table(&self, tx: &redb::ReadTransaction) -> Result<()> { + match tx.open_table(INSTALLED_TABLE) { + Ok(table) => { + let mut count = 0; + for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate installed table: {}", e)))? { + let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from installed table: {}", e)))?; + let key_str = key.value(); + + // Try to deserialize the manifest + match serde_json::from_slice::(value.value()) { + Ok(manifest) => { + // Extract the publisher from the FMRI attribute + let publisher = manifest.attributes.iter() + .find(|attr| attr.key == "pkg.fmri") + .and_then(|attr| attr.values.get(0).cloned()) + .unwrap_or_else(|| "unknown".to_string()); + + println!("Key: {}", key_str); + println!(" FMRI: {}", publisher); + println!(" Attributes: {}", manifest.attributes.len()); + println!(" Files: {}", manifest.files.len()); + println!(" Directories: {}", manifest.directories.len()); + println!(" Dependencies: {}", manifest.dependencies.len()); + }, + Err(e) => { + println!("Key: {}", key_str); + println!(" Error deserializing manifest: {}", e); + } + } + count += 1; + } + println!("Total entries in installed table: {}", count); + Ok(()) + }, + Err(e) => { + println!("Error opening installed table: {}", e); + Err(CatalogError::Database(format!("Failed to open installed table: {}", e))) + } + } + } + + /// Get database statistics + pub fn get_db_stats(&self) -> Result<()> { + // Open the database + let db = Database::open(&self.db_path) + .map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?; + + // Begin a read transaction + let tx = db.begin_read() + .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + + // Get table statistics + let mut catalog_count = 0; + let mut obsoleted_count = 0; + let mut installed_count = 0; + + // Count catalog entries + if let Ok(table) = tx.open_table(CATALOG_TABLE) { + for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? { + let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?; + catalog_count += 1; + } + } + + // Count obsoleted entries + if let Ok(table) = tx.open_table(OBSOLETED_TABLE) { + for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? { + let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?; + obsoleted_count += 1; + } + } + + // Count installed entries + if let Ok(table) = tx.open_table(INSTALLED_TABLE) { + for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate installed table: {}", e)))? { + let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from installed table: {}", e)))?; + installed_count += 1; + } + } + + // Print statistics + println!("Database path: {}", self.db_path.display()); + println!("Catalog directory: {}", self.catalog_dir.display()); + println!("Table statistics:"); + println!(" Catalog table: {} entries", catalog_count); + println!(" Obsoleted table: {} entries", obsoleted_count); + println!(" Installed table: {} entries", installed_count); + println!("Total entries: {}", catalog_count + obsoleted_count + installed_count); + + Ok(()) + } + /// Initialize the catalog database pub fn init_db(&self) -> Result<()> { // Create a parent directory if it doesn't exist diff --git a/libips/src/image/installed.rs b/libips/src/image/installed.rs index bf97774..3aa08f7 100644 --- a/libips/src/image/installed.rs +++ b/libips/src/image/installed.rs @@ -7,7 +7,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; use thiserror::Error; -use tracing::{info}; +use tracing::{info, warn}; /// Table definition for the installed packages database /// Key: full FMRI including publisher (pkg://publisher/stem@version) @@ -76,6 +76,87 @@ impl InstalledPackages { } } + /// Dump the contents of the installed table to stdout for debugging + pub fn dump_installed_table(&self) -> Result<()> { + // Open the database + let db = Database::open(&self.db_path) + .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; + + // Begin a read transaction + let tx = db.begin_read() + .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; + + // Open the installed table + match tx.open_table(INSTALLED_TABLE) { + Ok(table) => { + let mut count = 0; + for entry_result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { + let (key, value) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?; + let key_str = key.value(); + + // Try to deserialize the manifest + match serde_json::from_slice::(value.value()) { + Ok(manifest) => { + // Extract the publisher from the FMRI attribute + let publisher = manifest.attributes.iter() + .find(|attr| attr.key == "pkg.fmri") + .and_then(|attr| attr.values.get(0).cloned()) + .unwrap_or_else(|| "unknown".to_string()); + + println!("Key: {}", key_str); + println!(" FMRI: {}", publisher); + println!(" Attributes: {}", manifest.attributes.len()); + println!(" Files: {}", manifest.files.len()); + println!(" Directories: {}", manifest.directories.len()); + println!(" Dependencies: {}", manifest.dependencies.len()); + }, + Err(e) => { + println!("Key: {}", key_str); + println!(" Error deserializing manifest: {}", e); + } + } + count += 1; + } + println!("Total entries in installed table: {}", count); + Ok(()) + }, + Err(e) => { + println!("Error opening installed table: {}", e); + Err(InstalledError::Database(format!("Failed to open installed table: {}", e))) + } + } + } + + /// Get database statistics + pub fn get_db_stats(&self) -> Result<()> { + // Open the database + let db = Database::open(&self.db_path) + .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; + + // Begin a read transaction + let tx = db.begin_read() + .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; + + // Get table statistics + let mut installed_count = 0; + + // Count installed entries + if let Ok(table) = tx.open_table(INSTALLED_TABLE) { + for result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { + let _ = result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?; + installed_count += 1; + } + } + + // Print statistics + println!("Database path: {}", self.db_path.display()); + println!("Table statistics:"); + println!(" Installed table: {} entries", installed_count); + println!("Total entries: {}", installed_count); + + Ok(()) + } + /// Initialize the installed packages database pub fn init_db(&self) -> Result<()> { // Create a parent directory if it doesn't exist diff --git a/pkg6/src/main.rs b/pkg6/src/main.rs index d14dc3d..9796e75 100644 --- a/pkg6/src/main.rs +++ b/pkg6/src/main.rs @@ -228,6 +228,7 @@ enum Commands { /// List installed packages /// /// The list command displays information about installed packages. + /// By default, it lists only installed packages. Use the -a flag to list all available packages. List { /// Verbose output #[clap(short)] @@ -237,6 +238,10 @@ enum Commands { #[clap(short)] quiet: bool, + /// List all available packages, not just installed ones + #[clap(short)] + all: bool, + /// Output format (default: table) #[clap(short = 'o')] output_format: Option, @@ -422,6 +427,35 @@ enum Commands { #[clap(short = 't', long = "type", default_value = "full")] image_type: String, }, + + /// Debug database commands (hidden) + /// + /// These commands are for debugging purposes only and are not part of the public API. + /// They are used to inspect the contents of the redb databases for debugging purposes. + /// + /// Usage examples: + /// - Show database statistics: pkg6 debug-db --stats + /// - Dump all tables: pkg6 debug-db --dump-all + /// - Dump a specific table: pkg6 debug-db --dump-table catalog + /// + /// Available tables: + /// - catalog: Contains non-obsolete packages (in catalog.redb) + /// - obsoleted: Contains obsolete packages (in catalog.redb) + /// - installed: Contains installed packages (in installed.redb) + #[clap(hide = true)] + DebugDb { + /// Show database statistics + #[clap(long)] + stats: bool, + + /// Dump all tables + #[clap(long)] + dump_all: bool, + + /// Dump a specific table (catalog, obsoleted, installed) + #[clap(long)] + dump_table: Option, + }, } /// Determines the image path to use based on the provided argument and default rules @@ -478,6 +512,7 @@ fn main() -> Result<()> { // Print the command that was parsed match &cli.command { Commands::Publisher { .. } => eprintln!("MAIN: Publisher command detected"), + Commands::DebugDb { .. } => eprintln!("MAIN: Debug database command detected"), _ => eprintln!("MAIN: Other command detected: {:?}", cli.command), }; @@ -580,13 +615,84 @@ fn main() -> Result<()> { info!("Update completed successfully"); Ok(()) }, - Commands::List { verbose, quiet, output_format, pkg_fmri_patterns } => { + Commands::List { verbose, quiet, all, output_format, pkg_fmri_patterns } => { info!("Listing packages: {:?}", pkg_fmri_patterns); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); + debug!("All packages: {}", all); debug!("Output format: {:?}", output_format); - // Stub implementation + // Determine the image path using the -R argument or default rules + let image_path = determine_image_path(cli.image_path.clone()); + info!("Using image at: {}", image_path.display()); + + // Try to load the image from the determined path + let image = match libips::image::Image::load(&image_path) { + Ok(img) => img, + Err(e) => { + error!("Failed to load image from {}: {}", image_path.display(), e); + error!("Make sure the path points to a valid image or use pkg6 image-create first"); + return Err(e.into()); + } + }; + + // Convert pkg_fmri_patterns to a single pattern if provided + let pattern = if pkg_fmri_patterns.is_empty() { + None + } else { + // For simplicity, we'll just use the first pattern + // In a more complete implementation, we would handle multiple patterns + Some(pkg_fmri_patterns[0].as_str()) + }; + + if *all { + // List all available packages + info!("Listing all available packages"); + + match image.query_catalog(pattern) { + Ok(packages) => { + println!("PUBLISHER NAME VERSION STATE"); + println!("------------------------------------------------------------------------------------------------------------------------------------------------------"); + for pkg in packages { + let state = if image.is_package_installed(&pkg.fmri).unwrap_or(false) { + "installed" + } else { + "known" + }; + println!("{:<40} {:<40} {:<30} {}", + pkg.fmri.publisher.as_deref().unwrap_or("unknown"), + pkg.fmri.name, + pkg.fmri.version(), + state); + } + }, + Err(e) => { + error!("Failed to query catalog: {}", e); + return Err(e.into()); + } + } + } else { + // List only installed packages + info!("Listing installed packages"); + match image.query_installed_packages(pattern) { + Ok(packages) => { + println!("PUBLISHER NAME VERSION STATE"); + println!("------------------------------------------------------------------------------------------------------------------------------------------------------"); + for pkg in packages { + println!("{:<40} {:<40} {:<30} {}", + pkg.fmri.publisher.as_deref().unwrap_or("unknown"), + pkg.fmri.name, + pkg.fmri.version(), + "installed"); + } + }, + Err(e) => { + error!("Failed to query installed packages: {}", e); + return Err(e.into()); + } + } + } + info!("List completed successfully"); Ok(()) }, @@ -906,5 +1012,98 @@ fn main() -> Result<()> { Ok(()) }, + Commands::DebugDb { stats, dump_all, dump_table } => { + info!("Debug database command"); + debug!("Stats: {}", stats); + debug!("Dump all: {}", dump_all); + debug!("Dump table: {:?}", dump_table); + + // Determine the image path using the -R argument or default rules + let image_path = determine_image_path(cli.image_path.clone()); + info!("Using image at: {}", image_path.display()); + + // Try to load the image from the determined path + let image = match libips::image::Image::load(&image_path) { + Ok(img) => img, + Err(e) => { + error!("Failed to load image from {}: {}", image_path.display(), e); + error!("Make sure the path points to a valid image or use pkg6 image-create first"); + return Err(e.into()); + } + }; + + // Create a catalog object for the catalog.redb database + let catalog = libips::image::catalog::ImageCatalog::new( + image.catalog_dir(), + image.catalog_db_path() + ); + + // Create an installed packages object for the installed.redb database + let installed = libips::image::installed::InstalledPackages::new( + image.installed_db_path() + ); + + // Execute the requested debug command + if *stats { + info!("Showing database statistics"); + println!("=== CATALOG DATABASE ==="); + if let Err(e) = catalog.get_db_stats() { + error!("Failed to get catalog database statistics: {}", e); + return Err(Pkg6Error::Other(format!("Failed to get catalog database statistics: {}", e))); + } + + println!("\n=== INSTALLED DATABASE ==="); + if let Err(e) = installed.get_db_stats() { + error!("Failed to get installed database statistics: {}", e); + return Err(Pkg6Error::Other(format!("Failed to get installed database statistics: {}", e))); + } + } + + if *dump_all { + info!("Dumping all tables"); + println!("=== CATALOG DATABASE ==="); + if let Err(e) = catalog.dump_all_tables() { + error!("Failed to dump catalog database tables: {}", e); + return Err(Pkg6Error::Other(format!("Failed to dump catalog database tables: {}", e))); + } + + println!("\n=== INSTALLED DATABASE ==="); + if let Err(e) = installed.dump_installed_table() { + error!("Failed to dump installed database table: {}", e); + return Err(Pkg6Error::Other(format!("Failed to dump installed database table: {}", e))); + } + } + + if let Some(table_name) = dump_table { + info!("Dumping table: {}", table_name); + + // Determine which database to use based on the table name + match table_name.as_str() { + "installed" => { + // Use the installed packages database + println!("=== INSTALLED DATABASE ==="); + if let Err(e) = installed.dump_installed_table() { + error!("Failed to dump installed table: {}", e); + return Err(Pkg6Error::Other(format!("Failed to dump installed table: {}", e))); + } + }, + "catalog" | "obsoleted" => { + // Use the catalog database + println!("=== CATALOG DATABASE ==="); + if let Err(e) = catalog.dump_table(table_name) { + error!("Failed to dump table {}: {}", table_name, e); + return Err(Pkg6Error::Other(format!("Failed to dump table {}: {}", table_name, e))); + } + }, + _ => { + error!("Unknown table: {}", table_name); + return Err(Pkg6Error::Other(format!("Unknown table: {}. Available tables: catalog, obsoleted, installed", table_name))); + } + } + } + + info!("Debug database command completed successfully"); + Ok(()) + }, } }