Release camera on lock and re-establish connection
- Add entropy/release.rs: try_release_camera() (macOS: killall VDCAssistant/AppleCameraAssistant) - open_camera_with_retry: on lock-like errors, call release then retry - try_reconnect: attempt release before reconnecting - GET/POST /release-camera: release then test_camera(), return JSON - Export try_release_camera from lib; update startup message Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b26cd18579
commit
9cf6ccfd28
|
|
@ -13,6 +13,7 @@ use nokhwa::{
|
|||
};
|
||||
|
||||
use super::config::{CameraConfig, CameraListItem};
|
||||
use super::release;
|
||||
|
||||
/// Retry configuration for camera operations
|
||||
pub const MAX_RETRIES: u32 = 5;
|
||||
|
|
@ -105,13 +106,19 @@ pub fn configure_for_thermal_noise(camera: &mut Camera) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Open camera with retry logic. Attempts to reconnect on failure.
|
||||
fn is_lock_like_error(err: &str) -> bool {
|
||||
let lower = err.to_lowercase();
|
||||
lower.contains("lock") || lower.contains("rejected") || lower.contains("in use") || lower.contains("busy")
|
||||
}
|
||||
|
||||
/// Open camera with retry logic. On lock-like errors, tries to release the camera (e.g. macOS daemons) then retries.
|
||||
pub fn open_camera_with_retry(config: &CameraConfig) -> Result<Camera, String> {
|
||||
let index = CameraIndex::Index(config.index);
|
||||
let format = requested_format(config);
|
||||
|
||||
let mut released = false;
|
||||
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
match Camera::new(index.clone(), format.clone()) {
|
||||
let open_stream_err = match Camera::new(index.clone(), format.clone()) {
|
||||
Ok(mut camera) => {
|
||||
match camera.open_stream() {
|
||||
Ok(_) => {
|
||||
|
|
@ -121,31 +128,38 @@ pub fn open_camera_with_retry(config: &CameraConfig) -> Result<Camera, String> {
|
|||
}
|
||||
return Ok(camera);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[camera] open_stream failed (attempt {}): {}", attempt, e);
|
||||
if attempt < MAX_RETRIES {
|
||||
thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
Err(e) => Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[camera] Camera::new failed (attempt {}): {}", attempt, e);
|
||||
if attempt < MAX_RETRIES {
|
||||
thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
|
||||
}
|
||||
Err(e) => Some(e.to_string()),
|
||||
};
|
||||
|
||||
if let Some(ref err_str) = open_stream_err {
|
||||
eprintln!("[camera] open failed (attempt {}): {}", attempt, err_str);
|
||||
if !released && is_lock_like_error(err_str) {
|
||||
eprintln!("[camera] lock-like error, attempting to release camera...");
|
||||
release::try_release_camera();
|
||||
released = true;
|
||||
thread::sleep(Duration::from_millis(RECONNECT_DELAY_MS));
|
||||
}
|
||||
}
|
||||
if attempt < MAX_RETRIES {
|
||||
thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Err(format!("Failed to open camera index {} after {} attempts", config.index, MAX_RETRIES))
|
||||
}
|
||||
|
||||
/// Try to recover camera connection. Returns new Camera if successful.
|
||||
/// Try to recover camera connection. Attempts to release the camera first if the error looks like a lock, then retries.
|
||||
pub fn try_reconnect(config: &CameraConfig, error: &str) -> Option<Camera> {
|
||||
eprintln!("[camera] error: {}, attempting reconnect...", error);
|
||||
if is_lock_like_error(error) {
|
||||
eprintln!("[camera] attempting to release camera before reconnect...");
|
||||
release::try_release_camera();
|
||||
}
|
||||
thread::sleep(Duration::from_millis(RECONNECT_DELAY_MS));
|
||||
|
||||
|
||||
match open_camera_with_retry(config) {
|
||||
Ok(camera) => {
|
||||
eprintln!("[camera] successfully reconnected");
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ mod config;
|
|||
mod live;
|
||||
mod camera;
|
||||
mod extract;
|
||||
mod release;
|
||||
|
||||
pub use config::{CameraConfig, CameraListItem};
|
||||
pub use live::{extract_entropy, fill_entropy, stream_entropy};
|
||||
pub use camera::{list_cameras, test_camera, open_camera_with_retry, try_reconnect};
|
||||
pub use release::try_release_camera;
|
||||
pub use extract::{
|
||||
extract_raw_lsb, spawn_raw_lsb_stream, CHUNK_SIZE,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
//! Best-effort release of camera hold by system daemons (e.g. macOS).
|
||||
//! Allows the app to re-establish connection without requiring a manual script.
|
||||
|
||||
/// Try to free the camera so it can be opened by this process.
|
||||
///
|
||||
/// On macOS: kills VDCAssistant and AppleCameraAssistant if present
|
||||
/// (best-effort; may require sudo for full effect — see scripts/release-camera.sh).
|
||||
/// On other platforms: no-op, returns true.
|
||||
pub fn try_release_camera() -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
for process in &["VDCAssistant", "AppleCameraAssistant"] {
|
||||
let _ = std::process::Command::new("killall").arg(process).status();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,8 @@ pub mod tools;
|
|||
pub use entropy::{
|
||||
extract_entropy, fill_entropy, stream_entropy,
|
||||
list_cameras, spawn_raw_lsb_stream,
|
||||
test_camera, extract_raw_lsb, CameraConfig, CameraListItem, CHUNK_SIZE,
|
||||
test_camera, extract_raw_lsb, try_release_camera,
|
||||
CameraConfig, CameraListItem, CHUNK_SIZE,
|
||||
};
|
||||
|
||||
pub use tools::{
|
||||
|
|
|
|||
43
src/main.rs
43
src/main.rs
|
|
@ -12,7 +12,7 @@ use axum::{
|
|||
Router,
|
||||
};
|
||||
use camera_trng::{
|
||||
extract_entropy, list_cameras, stream_entropy, test_camera,
|
||||
extract_entropy, list_cameras, stream_entropy, test_camera, try_release_camera,
|
||||
CameraConfig, CHUNK_SIZE,
|
||||
};
|
||||
mod qrng_handlers;
|
||||
|
|
@ -84,7 +84,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
);
|
||||
if e.contains("Lock Rejected") || e.contains("lock") {
|
||||
eprintln!(
|
||||
" → To release camera: ./scripts/release-camera.sh then restart."
|
||||
" → App will try to release the camera on next request, or hit GET/POST /release-camera"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.route("/8ball", get(get_eightball))
|
||||
.route("/tools", get(tools_page))
|
||||
.route("/health", get(health))
|
||||
.route("/release-camera", get(release_camera).post(release_camera))
|
||||
.route("/.well-known/mcp.json", get(mcp_wellknown))
|
||||
.route("/.well-known/skill.md", get(get_skill_md))
|
||||
.route("/docs", get(get_docs))
|
||||
|
|
@ -143,6 +144,44 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
|
||||
async fn tools_page() -> Html<&'static str> { Html(TOOLS_HTML) }
|
||||
|
||||
/// Release camera hold (e.g. macOS daemons) and re-establish connection.
|
||||
/// GET or POST /release-camera. Returns JSON with ok, released, and camera info or error.
|
||||
async fn release_camera() -> Response {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
try_release_camera();
|
||||
std::thread::sleep(std::time::Duration::from_millis(800));
|
||||
match test_camera(&CameraConfig::from_env()) {
|
||||
Ok((w, h, frame_size)) => Ok::<_, String>((w, h, frame_size)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok((w, h, frame_size))) => {
|
||||
let body = serde_json::json!({
|
||||
"ok": true,
|
||||
"released": true,
|
||||
"camera": { "width": w, "height": h, "frame_size": frame_size },
|
||||
});
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(body.to_string()))
|
||||
.unwrap()
|
||||
}
|
||||
Ok(Err(e)) => (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(serde_json::json!({
|
||||
"ok": false,
|
||||
"released": true,
|
||||
"error": e,
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Health check: lightweight probe — confirms the server is running.
|
||||
/// Actual camera availability is tested on every /random request.
|
||||
async fn health() -> Response {
|
||||
|
|
|
|||
Loading…
Reference in New Issue