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`.
This commit is contained in:
Till Wegmueller 2025-08-05 00:20:57 +02:00
parent 0ec1c1928a
commit 92cce0f767
No known key found for this signature in database
3 changed files with 483 additions and 3 deletions

View file

@ -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::<Manifest>(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::<Manifest>(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 /// Initialize the catalog database
pub fn init_db(&self) -> Result<()> { pub fn init_db(&self) -> Result<()> {
// Create a parent directory if it doesn't exist // Create a parent directory if it doesn't exist

View file

@ -7,7 +7,7 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
use tracing::{info}; use tracing::{info, warn};
/// Table definition for the installed packages database /// Table definition for the installed packages database
/// Key: full FMRI including publisher (pkg://publisher/stem@version) /// 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::<Manifest>(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 /// Initialize the installed packages database
pub fn init_db(&self) -> Result<()> { pub fn init_db(&self) -> Result<()> {
// Create a parent directory if it doesn't exist // Create a parent directory if it doesn't exist

View file

@ -228,6 +228,7 @@ enum Commands {
/// List installed packages /// List installed packages
/// ///
/// The list command displays information about 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 { List {
/// Verbose output /// Verbose output
#[clap(short)] #[clap(short)]
@ -237,6 +238,10 @@ enum Commands {
#[clap(short)] #[clap(short)]
quiet: bool, quiet: bool,
/// List all available packages, not just installed ones
#[clap(short)]
all: bool,
/// Output format (default: table) /// Output format (default: table)
#[clap(short = 'o')] #[clap(short = 'o')]
output_format: Option<String>, output_format: Option<String>,
@ -422,6 +427,35 @@ enum Commands {
#[clap(short = 't', long = "type", default_value = "full")] #[clap(short = 't', long = "type", default_value = "full")]
image_type: String, 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<String>,
},
} }
/// Determines the image path to use based on the provided argument and default rules /// 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 // Print the command that was parsed
match &cli.command { match &cli.command {
Commands::Publisher { .. } => eprintln!("MAIN: Publisher command detected"), Commands::Publisher { .. } => eprintln!("MAIN: Publisher command detected"),
Commands::DebugDb { .. } => eprintln!("MAIN: Debug database command detected"),
_ => eprintln!("MAIN: Other command detected: {:?}", cli.command), _ => eprintln!("MAIN: Other command detected: {:?}", cli.command),
}; };
@ -580,13 +615,84 @@ fn main() -> Result<()> {
info!("Update completed successfully"); info!("Update completed successfully");
Ok(()) 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); info!("Listing packages: {:?}", pkg_fmri_patterns);
debug!("Verbose: {}", verbose); debug!("Verbose: {}", verbose);
debug!("Quiet: {}", quiet); debug!("Quiet: {}", quiet);
debug!("All packages: {}", all);
debug!("Output format: {:?}", output_format); 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"); info!("List completed successfully");
Ok(()) Ok(())
}, },
@ -906,5 +1012,98 @@ fn main() -> Result<()> {
Ok(()) 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(())
},
} }
} }