Skip to content

Commit 3e50b07

Browse files
committed
Improve robustness of camera, exit, and websocket handling
1 parent 50df784 commit 3e50b07

File tree

14 files changed

+368
-145
lines changed

14 files changed

+368
-145
lines changed

apps/desktop/src-tauri/src/camera.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ impl CameraPreviewManager {
227227
})
228228
.ok();
229229

230-
let rt = Runtime::new().expect("Failed to get Tokio runtime!");
230+
let rt = Runtime::new().context("Failed to create camera preview runtime")?;
231231
let (shutdown_complete_tx, shutdown_complete_rx) = oneshot::channel();
232232

233233
self.preview = Some(InitializedCameraPreview {
@@ -252,7 +252,7 @@ impl CameraPreviewManager {
252252
})
253253
.ok();
254254

255-
let _ = rt.block_on(drop_rx);
255+
let _ = rt.block_on(tokio::time::timeout(Duration::from_millis(250), drop_rx));
256256

257257
shutdown_complete_tx.send(()).ok();
258258
info!("DONE");
@@ -819,7 +819,7 @@ impl Renderer {
819819
async fn cleanup_for_shutdown(&mut self, window: &WebviewWindow) {
820820
info!("Camera preview shutdown requested. Cleaning up...");
821821

822-
let _ = self.device.poll(wgpu::PollType::Wait);
822+
let _ = self.device.poll(wgpu::PollType::Poll);
823823

824824
drop(std::mem::take(&mut self.texture));
825825
self.aspect_ratio = Cached::default();
@@ -832,11 +832,11 @@ impl Renderer {
832832
let _ = drop_tx.send(());
833833
})
834834
.ok();
835-
let _ = drop_rx.await;
835+
let _ = tokio::time::timeout(Duration::from_millis(250), drop_rx).await;
836836

837837
self.device.destroy();
838838

839-
tokio::time::sleep(Duration::from_millis(200)).await;
839+
tokio::time::sleep(Duration::from_millis(50)).await;
840840
}
841841

842842
fn reconfigure_gpu_surface(&mut self, window_width: u32, window_height: u32) {

apps/desktop/src-tauri/src/camera_legacy.rs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,19 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance
2828
converter
2929
}
3030
_ => {
31-
&mut converter
32-
.insert((
33-
frame.format(),
34-
ffmpeg::software::scaling::Context::get(
35-
frame.format(),
36-
frame.width(),
37-
frame.height(),
38-
Pixel::RGBA,
39-
1280,
40-
(1280.0 / (frame.width() as f64 / frame.height() as f64))
41-
as u32,
42-
ffmpeg::software::scaling::flag::Flags::FAST_BILINEAR,
43-
)
44-
.unwrap(),
45-
))
46-
.1
31+
let Ok(new_converter) = ffmpeg::software::scaling::Context::get(
32+
frame.format(),
33+
frame.width(),
34+
frame.height(),
35+
Pixel::RGBA,
36+
1280,
37+
(1280.0 / (frame.width() as f64 / frame.height() as f64)) as u32,
38+
ffmpeg::software::scaling::flag::Flags::FAST_BILINEAR,
39+
) else {
40+
continue;
41+
};
42+
43+
&mut converter.insert((frame.format(), new_converter)).1
4744
}
4845
};
4946

@@ -53,7 +50,9 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance
5350
converter.output().height,
5451
);
5552

56-
converter.run(&frame, &mut new_frame).unwrap();
53+
if converter.run(&frame, &mut new_frame).is_err() {
54+
continue;
55+
}
5756

5857
frame = new_frame;
5958
}

apps/desktop/src-tauri/src/deeplink_actions.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ impl TryFrom<&Url> for DeepLinkAction {
8282
fn try_from(url: &Url) -> Result<Self, Self::Error> {
8383
#[cfg(target_os = "macos")]
8484
if url.scheme() == "file" {
85-
return Ok(Self::OpenEditor {
86-
project_path: url.to_file_path().unwrap(),
87-
});
85+
return url
86+
.to_file_path()
87+
.map(|project_path| Self::OpenEditor { project_path })
88+
.map_err(|_| ActionParseFromUrlError::Invalid);
8889
}
8990

9091
match url.domain() {

apps/desktop/src-tauri/src/editor_window.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,16 @@ impl<'de, R: Runtime> CommandArg<'de, R> for WindowEditorInstance {
159159
) -> Result<Self, tauri::ipc::InvokeError> {
160160
let window = Window::from_command(command)?;
161161

162-
let instances = window.state::<EditorInstances>();
162+
let Some(instances) = window.try_state::<EditorInstances>() else {
163+
return Err("editor instance registry unavailable".into());
164+
};
163165
let instance = futures::executor::block_on(instances.0.read());
164166

165-
Ok(Self(instance.get(window.label()).cloned().unwrap()))
167+
let Some(instance) = instance.get(window.label()).cloned() else {
168+
return Err("editor instance unavailable".into());
169+
};
170+
171+
Ok(Self(instance))
166172
}
167173
}
168174

apps/desktop/src-tauri/src/frame_ws.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
22
use std::time::Instant;
3+
use std::time::{SystemTime, UNIX_EPOCH};
34
use tokio::sync::{broadcast, watch};
45
use tokio_util::sync::CancellationToken;
56

@@ -179,7 +180,10 @@ pub async fn create_watch_frame_ws(
179180
Ok(()) => {
180181
TOTAL_BYTES_SENT.fetch_add(packed_len as u64, Ordering::Relaxed);
181182
TOTAL_FRAMES_SENT.fetch_add(1, Ordering::Relaxed);
182-
let now_ms = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64;
183+
let now_ms = SystemTime::now()
184+
.duration_since(UNIX_EPOCH)
185+
.map(|duration| duration.as_millis() as u64)
186+
.unwrap_or_default();
183187
let last_log = LAST_LOG_TIME.load(Ordering::Relaxed);
184188
if now_ms - last_log > 2000 {
185189
LAST_LOG_TIME.store(now_ms, Ordering::Relaxed);
@@ -214,12 +218,24 @@ pub async fn create_watch_frame_ws(
214218
.route("/", get(ws_handler))
215219
.with_state(frame_rx);
216220

217-
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
218-
let port = listener.local_addr().unwrap().port();
219-
tracing::info!("WebSocket server listening on port {}", port);
220-
221221
let cancel_token = CancellationToken::new();
222222
let cancel_token_child = cancel_token.child_token();
223+
let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
224+
Ok(listener) => listener,
225+
Err(err) => {
226+
tracing::error!("Failed to bind watch frame websocket listener: {err}");
227+
return (0, cancel_token_child);
228+
}
229+
};
230+
let port = match listener.local_addr() {
231+
Ok(addr) => addr.port(),
232+
Err(err) => {
233+
tracing::error!("Failed to read watch frame websocket listener address: {err}");
234+
return (0, cancel_token_child);
235+
}
236+
};
237+
tracing::info!("WebSocket server listening on port {}", port);
238+
223239
tokio::spawn(async move {
224240
let server = axum::serve(listener, router.into_make_service());
225241
tokio::select! {
@@ -314,12 +330,24 @@ pub async fn create_frame_ws(frame_tx: broadcast::Sender<WSFrame>) -> (u16, Canc
314330
.route("/", get(ws_handler))
315331
.with_state(frame_tx);
316332

317-
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
318-
let port = listener.local_addr().unwrap().port();
319-
tracing::info!("WebSocket server listening on port {}", port);
320-
321333
let cancel_token = CancellationToken::new();
322334
let cancel_token_child = cancel_token.child_token();
335+
let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
336+
Ok(listener) => listener,
337+
Err(err) => {
338+
tracing::error!("Failed to bind frame websocket listener: {err}");
339+
return (0, cancel_token_child);
340+
}
341+
};
342+
let port = match listener.local_addr() {
343+
Ok(addr) => addr.port(),
344+
Err(err) => {
345+
tracing::error!("Failed to read frame websocket listener address: {err}");
346+
return (0, cancel_token_child);
347+
}
348+
};
349+
tracing::info!("WebSocket server listening on port {}", port);
350+
323351
tokio::spawn(async move {
324352
let server = axum::serve(listener, router.into_make_service());
325353
tokio::select! {

apps/desktop/src-tauri/src/lib.rs

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,44 @@ impl AppExitState {
156156
}
157157
}
158158

159+
const APP_EXIT_STEP_TIMEOUT: Duration = Duration::from_millis(750);
160+
const APP_EXIT_CAMERA_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(1200);
161+
const APP_EXIT_TOTAL_TIMEOUT: Duration = Duration::from_secs(3);
162+
const APP_EXIT_FORCE_TIMEOUT: Duration = Duration::from_secs(8);
163+
164+
async fn await_exit_step<T, E, F>(name: &'static str, timeout: Duration, fut: F) -> Option<T>
165+
where
166+
E: std::fmt::Display,
167+
F: Future<Output = Result<T, E>>,
168+
{
169+
match tokio::time::timeout(timeout, fut).await {
170+
Ok(Ok(value)) => Some(value),
171+
Ok(Err(err)) => {
172+
warn!(step = name, error = %err, "Exit cleanup step failed");
173+
None
174+
}
175+
Err(_) => {
176+
warn!(
177+
step = name,
178+
timeout_ms = timeout.as_millis(),
179+
"Exit cleanup step timed out"
180+
);
181+
None
182+
}
183+
}
184+
}
185+
186+
fn spawn_exit_watchdog() {
187+
std::thread::spawn(move || {
188+
std::thread::sleep(APP_EXIT_FORCE_TIMEOUT);
189+
error!(
190+
timeout_ms = APP_EXIT_FORCE_TIMEOUT.as_millis(),
191+
"Forcing process exit after shutdown deadline"
192+
);
193+
std::process::exit(0);
194+
});
195+
}
196+
159197
fn now_millis() -> u64 {
160198
SystemTime::now()
161199
.duration_since(UNIX_EPOCH)
@@ -1016,8 +1054,16 @@ async fn cleanup_app_resources_for_exit(app: &AppHandle) {
10161054
)
10171055
};
10181056

1019-
let _ = mic_feed.ask(microphone::RemoveInput).await;
1020-
let _ = camera_feed.ask(feeds::camera::RemoveInput).await;
1057+
let _ = await_exit_step(
1058+
"remove_microphone_input",
1059+
APP_EXIT_STEP_TIMEOUT,
1060+
async move { mic_feed.ask(microphone::RemoveInput).await },
1061+
)
1062+
.await;
1063+
let _ = await_exit_step("remove_camera_input", APP_EXIT_STEP_TIMEOUT, async move {
1064+
camera_feed.ask(feeds::camera::RemoveInput).await
1065+
})
1066+
.await;
10211067

10221068
#[cfg(target_os = "macos")]
10231069
{
@@ -1028,7 +1074,12 @@ async fn cleanup_app_resources_for_exit(app: &AppHandle) {
10281074
}
10291075

10301076
if let Some(rx) = camera_shutdown {
1031-
let _ = tokio::time::timeout(Duration::from_secs(2), rx).await;
1077+
let _ = await_exit_step(
1078+
"camera_preview_shutdown",
1079+
APP_EXIT_CAMERA_SHUTDOWN_TIMEOUT,
1080+
rx,
1081+
)
1082+
.await;
10321083
}
10331084
}
10341085

@@ -1037,7 +1088,18 @@ pub async fn request_app_exit(app: AppHandle) {
10371088
return;
10381089
}
10391090

1040-
cleanup_app_resources_for_exit(&app).await;
1091+
spawn_exit_watchdog();
1092+
1093+
if tokio::time::timeout(APP_EXIT_TOTAL_TIMEOUT, cleanup_app_resources_for_exit(&app))
1094+
.await
1095+
.is_err()
1096+
{
1097+
error!(
1098+
timeout_ms = APP_EXIT_TOTAL_TIMEOUT.as_millis(),
1099+
"Timed out while cleaning up app resources for exit"
1100+
);
1101+
}
1102+
10411103
app.exit(0);
10421104
}
10431105

@@ -3394,7 +3456,9 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
33943456

33953457
audio_meter::spawn_event_emitter(app.clone(), mic_samples_rx);
33963458

3397-
tray::create_tray(&app).unwrap();
3459+
if let Err(err) = tray::create_tray(&app) {
3460+
error!("Failed to create tray: {err}");
3461+
}
33983462

33993463
RequestStartRecording::listen_any_spawn(&app, async |event, app| {
34003464
let settings = RecordingSettingsStore::get(&app)
@@ -3492,13 +3556,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
34923556
let _ = camera_window.hide();
34933557
}
34943558

3495-
for (id, overlay_window) in app.webview_windows() {
3496-
if let Ok(CapWindowId::TargetSelectOverlay { .. }) =
3497-
CapWindowId::from_str(&id)
3498-
{
3499-
let _ = overlay_window.hide();
3500-
}
3501-
}
3559+
close_target_select_overlays(app);
35023560

35033561
let app = app.clone();
35043562
tokio::spawn(async move {
@@ -3532,13 +3590,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
35323590
CapWindowId::Main => {
35333591
let app = app.clone();
35343592

3535-
for (id, window) in app.webview_windows() {
3536-
if let Ok(CapWindowId::TargetSelectOverlay { .. }) =
3537-
CapWindowId::from_str(&id)
3538-
{
3539-
let _ = window.hide();
3540-
}
3541-
}
3593+
close_target_select_overlays(&app);
35423594

35433595
if let Some(camera) = CapWindowId::Camera.get(&app) {
35443596
let _ = camera.hide();
@@ -3777,16 +3829,18 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
37773829
});
37783830
}
37793831
tauri::RunEvent::Exit => {
3832+
if !_handle.state::<AppExitState>().begin() {
3833+
return;
3834+
}
3835+
37803836
let handle = _handle.clone();
3781-
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3782-
let _ = tauri::async_runtime::block_on(async {
3783-
tokio::time::timeout(
3784-
Duration::from_secs(2),
3785-
cleanup_app_resources_for_exit(&handle),
3786-
)
3787-
.await
3788-
});
3789-
}));
3837+
tokio::spawn(async move {
3838+
let _ = tokio::time::timeout(
3839+
Duration::from_secs(2),
3840+
cleanup_app_resources_for_exit(&handle),
3841+
)
3842+
.await;
3843+
});
37903844
}
37913845
_ => {}
37923846
});
@@ -3817,6 +3871,17 @@ fn restore_main_windows_if_no_editors(app: &AppHandle) {
38173871
}
38183872
}
38193873

3874+
fn close_target_select_overlays(app: &AppHandle) {
3875+
let focus_manager = app.state::<target_select_overlay::WindowFocusManager>();
3876+
3877+
for (label, window) in app.webview_windows() {
3878+
if let Ok(CapWindowId::TargetSelectOverlay { display_id }) = CapWindowId::from_str(&label) {
3879+
let _ = window.hide();
3880+
focus_manager.destroy(&display_id, app.global_shortcut());
3881+
}
3882+
}
3883+
}
3884+
38203885
#[cfg(target_os = "windows")]
38213886
fn reopen_main_window(app: &AppHandle) {
38223887
if let Some(main) = CapWindowId::Main.get(app) {

0 commit comments

Comments
 (0)