Prepare public release v0.1.0
This commit is contained in:
@@ -117,9 +117,7 @@ fn get_real_username() -> String {
|
||||
std::process::Command::new("whoami")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
String::from_utf8(output.stdout).ok()
|
||||
})
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
})
|
||||
@@ -130,7 +128,11 @@ async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logging
|
||||
let log_level = if cli.verbose { Level::DEBUG } else { Level::INFO };
|
||||
let log_level = if cli.verbose {
|
||||
Level::DEBUG
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
FmtSubscriber::builder()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
@@ -140,30 +142,22 @@ async fn main() -> Result<()> {
|
||||
let config = Config::load_or_default();
|
||||
|
||||
match cli.command {
|
||||
Commands::Capture { output, count, device } => {
|
||||
cmd_capture(&config, &output, count, device.as_deref()).await
|
||||
}
|
||||
Commands::Detect { image, scores, output } => {
|
||||
cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await
|
||||
}
|
||||
Commands::Status { camera, daemon } => {
|
||||
cmd_status(&config, camera, daemon).await
|
||||
}
|
||||
Commands::Enroll { label } => {
|
||||
cmd_enroll(&config, &label).await
|
||||
}
|
||||
Commands::List => {
|
||||
cmd_list(&config).await
|
||||
}
|
||||
Commands::Remove { label, all } => {
|
||||
cmd_remove(&config, label.as_deref(), all).await
|
||||
}
|
||||
Commands::Test { verbose, debug } => {
|
||||
cmd_test(&config, verbose, debug).await
|
||||
}
|
||||
Commands::Config { json, set } => {
|
||||
cmd_config(&config, json, set.as_deref()).await
|
||||
}
|
||||
Commands::Capture {
|
||||
output,
|
||||
count,
|
||||
device,
|
||||
} => cmd_capture(&config, &output, count, device.as_deref()).await,
|
||||
Commands::Detect {
|
||||
image,
|
||||
scores,
|
||||
output,
|
||||
} => cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await,
|
||||
Commands::Status { camera, daemon } => cmd_status(&config, camera, daemon).await,
|
||||
Commands::Enroll { label } => cmd_enroll(&config, &label).await,
|
||||
Commands::List => cmd_list(&config).await,
|
||||
Commands::Remove { label, all } => cmd_remove(&config, label.as_deref(), all).await,
|
||||
Commands::Test { verbose, debug } => cmd_test(&config, verbose, debug).await,
|
||||
Commands::Config { json, set } => cmd_config(&config, json, set.as_deref()).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,12 +205,18 @@ async fn cmd_capture(
|
||||
|
||||
for i in 0..count {
|
||||
let frame = camera.capture_frame()?;
|
||||
|
||||
|
||||
match frame.format {
|
||||
PixelFormat::Grey => {
|
||||
let filename = format!("{}/frame_{:03}.png", output, i);
|
||||
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data) {
|
||||
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data)
|
||||
{
|
||||
img.save(&filename).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
info!("Saved frame {} (Permissions: Grey)", filename);
|
||||
}
|
||||
}
|
||||
@@ -227,9 +227,18 @@ async fn cmd_capture(
|
||||
for chunk in frame.data.chunks_exact(2) {
|
||||
gray_data.push(chunk[0]); // Y component
|
||||
}
|
||||
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, gray_data) {
|
||||
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
info!("Saved frame {} (converted from YUYV to grayscale)", filename);
|
||||
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, gray_data)
|
||||
{
|
||||
img.save(&filename).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
info!(
|
||||
"Saved frame {} (converted from YUYV to grayscale)",
|
||||
filename
|
||||
);
|
||||
}
|
||||
}
|
||||
PixelFormat::Mjpeg => {
|
||||
@@ -238,7 +247,12 @@ async fn cmd_capture(
|
||||
match image::load_from_memory(&frame.data) {
|
||||
Ok(img) => {
|
||||
let gray = img.to_luma8();
|
||||
gray.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
gray.save(&filename).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
info!("Saved frame {} (decoded from MJPEG)", filename);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -249,7 +263,11 @@ async fn cmd_capture(
|
||||
_ => {
|
||||
let filename = format!("{}/frame_{:03}.raw", output, i);
|
||||
std::fs::write(&filename, &frame.data)?;
|
||||
info!("Saved frame {} ({} bytes - Unknown format)", filename, frame.data.len());
|
||||
info!(
|
||||
"Saved frame {} ({} bytes - Unknown format)",
|
||||
filename,
|
||||
frame.data.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,26 +312,30 @@ async fn cmd_detect(
|
||||
info!("Saving annotated image to: {}", out_path);
|
||||
// Convert to RGB for drawing
|
||||
let mut rgb_img = img.to_rgb8();
|
||||
|
||||
|
||||
// Draw bounding box (red color)
|
||||
draw_bounding_box(&mut rgb_img, x, y, w, h, [255, 0, 0]);
|
||||
|
||||
rgb_img.save(out_path)
|
||||
.map_err(|e| linux_hello_common::Error::Io(
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
))?;
|
||||
|
||||
rgb_img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Annotated image saved to: {}", out_path);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("No face detected");
|
||||
|
||||
|
||||
if let Some(out_path) = output {
|
||||
// Save original image without annotations
|
||||
img.save(out_path)
|
||||
.map_err(|e| linux_hello_common::Error::Io(
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
))?;
|
||||
img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Image saved to: {} (no face detected)", out_path);
|
||||
}
|
||||
}
|
||||
@@ -329,23 +351,16 @@ async fn cmd_detect(
|
||||
}
|
||||
|
||||
/// Draw a bounding box on an RGB image
|
||||
fn draw_bounding_box(
|
||||
img: &mut image::RgbImage,
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
color: [u8; 3],
|
||||
) {
|
||||
fn draw_bounding_box(img: &mut image::RgbImage, x: u32, y: u32, w: u32, h: u32, color: [u8; 3]) {
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
let thickness = 2u32;
|
||||
|
||||
|
||||
// Clamp coordinates to image bounds
|
||||
let x1 = x.min(img_width.saturating_sub(1));
|
||||
let y1 = y.min(img_height.saturating_sub(1));
|
||||
let x2 = (x + w).min(img_width.saturating_sub(1));
|
||||
let y2 = (y + h).min(img_height.saturating_sub(1));
|
||||
|
||||
|
||||
// Draw horizontal lines (top and bottom)
|
||||
for t in 0..thickness {
|
||||
// Top line
|
||||
@@ -353,7 +368,7 @@ fn draw_bounding_box(
|
||||
for px in x1..=x2 {
|
||||
img.put_pixel(px, top_y, image::Rgb(color));
|
||||
}
|
||||
|
||||
|
||||
// Bottom line
|
||||
let bottom_y = y2.saturating_sub(t);
|
||||
if bottom_y >= y1 {
|
||||
@@ -362,7 +377,7 @@ fn draw_bounding_box(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw vertical lines (left and right)
|
||||
for t in 0..thickness {
|
||||
// Left line
|
||||
@@ -370,7 +385,7 @@ fn draw_bounding_box(
|
||||
for py in y1..=y2 {
|
||||
img.put_pixel(left_x, py, image::Rgb(color));
|
||||
}
|
||||
|
||||
|
||||
// Right line
|
||||
let right_x = x2.saturating_sub(t);
|
||||
if right_x >= x1 {
|
||||
@@ -425,28 +440,42 @@ async fn cmd_status(config: &Config, show_camera: bool, show_daemon: bool) -> Re
|
||||
|
||||
println!("\nConfiguration:");
|
||||
println!(" Detection model: {}", config.detection.model);
|
||||
println!(" Anti-spoofing: {}", if config.anti_spoofing.enabled { "enabled" } else { "disabled" });
|
||||
println!(" TPM: {}", if config.tpm.enabled { "enabled" } else { "disabled" });
|
||||
println!(
|
||||
" Anti-spoofing: {}",
|
||||
if config.anti_spoofing.enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
" TPM: {}",
|
||||
if config.tpm.enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
|
||||
use linux_hello_daemon::auth::AuthService;
|
||||
|
||||
|
||||
info!("Starting enrollment with label: {}", label);
|
||||
|
||||
|
||||
// Get real user (handles sudo)
|
||||
let user = get_real_username();
|
||||
|
||||
|
||||
println!("Enrolling user: {}", user);
|
||||
println!("Label: {}", label);
|
||||
println!("Please look at the camera...");
|
||||
|
||||
|
||||
// Create auth service
|
||||
let auth_service = AuthService::new(config.clone());
|
||||
auth_service.initialize()?;
|
||||
|
||||
|
||||
// Enroll with 5 frames
|
||||
match auth_service.enroll(&user, label, 5).await {
|
||||
Ok(()) => {
|
||||
@@ -462,16 +491,16 @@ async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
|
||||
|
||||
async fn cmd_list(_config: &Config) -> Result<()> {
|
||||
use linux_hello_common::TemplateStore;
|
||||
|
||||
|
||||
let store = TemplateStore::new(TemplateStore::default_path());
|
||||
|
||||
|
||||
let users = store.list_users()?;
|
||||
|
||||
|
||||
if users.is_empty() {
|
||||
println!("No enrolled users");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
println!("Enrolled users:");
|
||||
for user in users {
|
||||
let templates = store.list_templates(&user)?;
|
||||
@@ -481,28 +510,27 @@ async fn cmd_list(_config: &Config) -> Result<()> {
|
||||
println!(" {}:", user);
|
||||
for label in templates {
|
||||
if let Ok(template) = store.load(&user, &label) {
|
||||
println!(" - {} (enrolled: {}, frames: {})",
|
||||
label,
|
||||
template.enrolled_at,
|
||||
template.frame_count
|
||||
println!(
|
||||
" - {} (enrolled: {}, frames: {})",
|
||||
label, template.enrolled_at, template.frame_count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<()> {
|
||||
use linux_hello_common::TemplateStore;
|
||||
|
||||
|
||||
let user = std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
|
||||
|
||||
let store = TemplateStore::new(TemplateStore::default_path());
|
||||
|
||||
|
||||
if all {
|
||||
store.remove_all(&user)?;
|
||||
println!("Removed all templates for user: {}", user);
|
||||
@@ -515,23 +543,23 @@ async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<
|
||||
"No label specified".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_test(config: &Config, verbose: bool, _debug: bool) -> Result<()> {
|
||||
use linux_hello_daemon::auth::AuthService;
|
||||
|
||||
|
||||
// Get real user (handles sudo)
|
||||
let user = get_real_username();
|
||||
|
||||
|
||||
println!("Testing authentication for user: {}", user);
|
||||
println!("Please look at the camera...");
|
||||
|
||||
|
||||
// Create auth service
|
||||
let auth_service = AuthService::new(config.clone());
|
||||
auth_service.initialize()?;
|
||||
|
||||
|
||||
match auth_service.authenticate(&user).await {
|
||||
Ok(true) => {
|
||||
println!("✓ Authentication successful!");
|
||||
@@ -560,62 +588,77 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
|
||||
"Invalid format. Use key=value".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
let key = parts[0].trim();
|
||||
let value = parts[1].trim();
|
||||
|
||||
|
||||
let mut new_config = config.clone();
|
||||
|
||||
|
||||
// Update configuration based on key
|
||||
match key {
|
||||
"general.log_level" => new_config.general.log_level = value.to_string(),
|
||||
"general.timeout_seconds" => {
|
||||
new_config.general.timeout_seconds = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid timeout value".to_string()))?;
|
||||
new_config.general.timeout_seconds = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid timeout value".to_string())
|
||||
})?;
|
||||
}
|
||||
"camera.device" => new_config.camera.device = value.to_string(),
|
||||
"camera.ir_emitter" => new_config.camera.ir_emitter = value.to_string(),
|
||||
"camera.fps" => {
|
||||
new_config.camera.fps = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid fps value".to_string()))?;
|
||||
new_config.camera.fps = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid fps value".to_string())
|
||||
})?;
|
||||
}
|
||||
"detection.model" => new_config.detection.model = value.to_string(),
|
||||
"detection.min_face_size" => {
|
||||
new_config.detection.min_face_size = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid min_face_size value".to_string()))?;
|
||||
new_config.detection.min_face_size = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid min_face_size value".to_string())
|
||||
})?;
|
||||
}
|
||||
"detection.confidence_threshold" => {
|
||||
new_config.detection.confidence_threshold = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid confidence_threshold value".to_string()))?;
|
||||
new_config.detection.confidence_threshold = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config(
|
||||
"Invalid confidence_threshold value".to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"embedding.model" => new_config.embedding.model = value.to_string(),
|
||||
"embedding.distance_threshold" => {
|
||||
new_config.embedding.distance_threshold = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid distance_threshold value".to_string()))?;
|
||||
new_config.embedding.distance_threshold = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config(
|
||||
"Invalid distance_threshold value".to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"anti_spoofing.enabled" => {
|
||||
new_config.anti_spoofing.enabled = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
|
||||
new_config.anti_spoofing.enabled = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid boolean value".to_string())
|
||||
})?;
|
||||
}
|
||||
"anti_spoofing.depth_check" => {
|
||||
new_config.anti_spoofing.depth_check = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
|
||||
new_config.anti_spoofing.depth_check = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid boolean value".to_string())
|
||||
})?;
|
||||
}
|
||||
"anti_spoofing.liveness_model" => {
|
||||
new_config.anti_spoofing.liveness_model = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
|
||||
new_config.anti_spoofing.liveness_model = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid boolean value".to_string())
|
||||
})?;
|
||||
}
|
||||
"anti_spoofing.min_score" => {
|
||||
new_config.anti_spoofing.min_score = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid min_score value".to_string()))?;
|
||||
new_config.anti_spoofing.min_score = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid min_score value".to_string())
|
||||
})?;
|
||||
}
|
||||
"tpm.enabled" => {
|
||||
new_config.tpm.enabled = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
|
||||
new_config.tpm.enabled = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid boolean value".to_string())
|
||||
})?;
|
||||
}
|
||||
"tpm.pcr_binding" => {
|
||||
new_config.tpm.pcr_binding = value.parse()
|
||||
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
|
||||
new_config.tpm.pcr_binding = value.parse().map_err(|_| {
|
||||
linux_hello_common::Error::Config("Invalid boolean value".to_string())
|
||||
})?;
|
||||
}
|
||||
_ => {
|
||||
return Err(linux_hello_common::Error::Config(format!(
|
||||
@@ -624,7 +667,7 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Save configuration
|
||||
let config_path = "/etc/linux-hello/config.toml";
|
||||
match new_config.save(config_path) {
|
||||
@@ -634,10 +677,14 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
|
||||
let user_config_path = dirs_config_path();
|
||||
std::fs::create_dir_all(user_config_path.parent().unwrap())?;
|
||||
new_config.save(&user_config_path)?;
|
||||
println!("Configuration saved to {} (no write access to {})", user_config_path.display(), config_path);
|
||||
println!(
|
||||
"Configuration saved to {} (no write access to {})",
|
||||
user_config_path.display(),
|
||||
config_path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user