{ fn default_client_pki_dir() -> PathBuf { if let Some(home) = std::env::var_os("HOME") { return PathBuf::from(home).join(".config/lesavka/pki"); } std::env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join(".config/lesavka/pki") } fn install_client_pki_bundle(bundle: &Path) -> Result { let target = default_client_pki_dir(); let scratch = std::env::temp_dir().join(format!( "lesavka-client-pki-{}", SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() )); std::fs::create_dir_all(&scratch) .map_err(|err| format!("could not create extraction folder: {err}"))?; let extract = Command::new("tar") .arg("-xzf") .arg(bundle) .arg("-C") .arg(&scratch) .status() .map_err(|err| format!("tar is unavailable: {err}"))?; if !extract.success() { let _ = std::fs::remove_dir_all(&scratch); return Err(format!("could not extract {}", bundle.display())); } for item in ["ca.crt", "client.crt", "client.key"] { if !scratch.join(item).is_file() { let _ = std::fs::remove_dir_all(&scratch); return Err(format!("bundle is missing {item}")); } } std::fs::create_dir_all(&target) .map_err(|err| format!("could not create {}: {err}", target.display()))?; for item in ["ca.crt", "client.crt", "client.key"] { std::fs::copy(scratch.join(item), target.join(item)) .map_err(|err| format!("could not install {item}: {err}"))?; } tighten_client_key_permissions(&target.join("client.key")); let _ = std::fs::remove_dir_all(&scratch); Ok(target) } #[cfg(unix)] fn tighten_client_key_permissions(path: &Path) { use std::os::unix::fs::PermissionsExt; if let Ok(mut permissions) = std::fs::metadata(path).map(|metadata| metadata.permissions()) { permissions.set_mode(0o600); let _ = std::fs::set_permissions(path, permissions); } } #[cfg(not(unix))] fn tighten_client_key_permissions(_path: &Path) {} { let widgets = widgets.clone(); let window = window.clone(); let certs_button = widgets.certs_button.clone(); certs_button.connect_clicked(move |_| { let chooser = gtk::FileChooserNative::new( Some("Choose Lesavka Client TLS Bundle"), Some(&window), gtk::FileChooserAction::Open, Some("Install"), Some("Cancel"), ); chooser.set_modal(true); let widgets = widgets.clone(); chooser.connect_response(move |dialog, response| { if response == gtk::ResponseType::Accept { if let Some(bundle) = dialog.file().and_then(|file| file.path()) { widgets.status_label.set_text(&format!( "Installing Lesavka client TLS bundle from {}...", bundle.display() )); match install_client_pki_bundle(&bundle) { Ok(target) => widgets.status_label.set_text(&format!( "Client TLS certs installed at {}. Reconnect the relay to use them.", target.display() )), Err(err) => widgets .status_label .set_text(&format!("Client TLS bundle install failed: {err}")), } } else { widgets .status_label .set_text("Selected TLS bundle did not provide a filesystem path."); } } dialog.destroy(); }); chooser.show(); }); } { let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let clipboard_tx = clipboard_tx.clone(); widgets.clipboard_button.connect_clicked(move |_| { if child_proc.borrow().is_none() { widgets .status_label .set_text("Start the relay before sending clipboard text."); return; } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); let Some(display) = gtk::gdk::Display::default() else { widgets .status_label .set_text("No desktop clipboard is available in this session."); return; }; widgets .status_label .set_text("Reading the local clipboard and preparing remote paste..."); let clipboard = display.clipboard(); let clipboard_tx = clipboard_tx.clone(); clipboard.read_text_async(None::<>k::gio::Cancellable>, move |result| match result { Ok(Some(text)) => { let text = text.trim_end_matches(['\r', '\n']).to_string(); if text.is_empty() { let _ = clipboard_tx .send(ClipboardMessage::Finished(Err("clipboard is empty".to_string()))); return; } let clipboard_tx = clipboard_tx.clone(); std::thread::spawn(move || { let result = send_clipboard_text_to_remote(&server_addr, &text) .map_err(|err| err.to_string()); let _ = clipboard_tx.send(ClipboardMessage::Finished(result)); }); } Ok(None) => { let _ = clipboard_tx .send(ClipboardMessage::Finished(Err("clipboard is empty".to_string()))); } Err(err) => { let _ = clipboard_tx.send(ClipboardMessage::Finished(Err(format!( "clipboard read failed: {err}" )))); } }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let widgets_for_click = widgets.clone(); widgets.usb_recover_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_for_click.status_label.set_text( "Recover USB 1/3: sending gadget reset request to relay host...", ); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}")); let _ = tx.send(result); }); let widgets = widgets_for_click.clone(); glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() { Ok(Ok(())) => { widgets.status_label.set_text( "Recover USB 2/3: relay acknowledged reset. Recover USB 3/3: waiting for USB/UAC/UVC chips to settle.", ); glib::ControlFlow::Break } Ok(Err(err)) => { widgets .status_label .set_text(&format!("Recover USB failed: {err}")); glib::ControlFlow::Break } Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(std::sync::mpsc::TryRecvError::Disconnected) => { widgets.status_label.set_text( "Recover USB failed: relay stopped responding before completion.", ); glib::ControlFlow::Break } }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let widgets_for_click = widgets.clone(); widgets.uac_recover_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_for_click .status_label .set_text("Recover UAC 1/3: sending gadget reset request to relay host..."); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}")); let _ = tx.send(result); }); let widgets = widgets_for_click.clone(); glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() { Ok(Ok(())) => { widgets.status_label.set_text( "Recover UAC 2/3: relay acknowledged reset. Recover UAC 3/3: waiting for UAC chip to settle.", ); glib::ControlFlow::Break } Ok(Err(err)) => { widgets .status_label .set_text(&format!("Recover UAC failed: {err}")); glib::ControlFlow::Break } Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(std::sync::mpsc::TryRecvError::Disconnected) => { widgets.status_label.set_text( "Recover UAC failed: relay stopped responding before completion.", ); glib::ControlFlow::Break } }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let widgets_for_click = widgets.clone(); widgets.uvc_recover_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_for_click .status_label .set_text("Recover UVC 1/3: sending gadget reset request to relay host..."); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}")); let _ = tx.send(result); }); let widgets = widgets_for_click.clone(); glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() { Ok(Ok(())) => { widgets.status_label.set_text( "Recover UVC 2/3: relay acknowledged reset. Recover UVC 3/3: waiting for UVC chip to settle.", ); glib::ControlFlow::Break } Ok(Err(err)) => { widgets .status_label .set_text(&format!("Recover UVC failed: {err}")); glib::ControlFlow::Break } Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(std::sync::mpsc::TryRecvError::Disconnected) => { widgets.status_label.set_text( "Recover UVC failed: relay stopped responding before completion.", ); glib::ControlFlow::Break } }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let calibration_tx = calibration_tx.clone(); let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight); widgets.calibration_default_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets .status_label .set_text("Calibration 1/2: restoring saved upstream A/V default..."); calibration_request_in_flight.set(true); request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| { restore_default_calibration(server_addr) }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let calibration_tx = calibration_tx.clone(); let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight); widgets.calibration_factory_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets .status_label .set_text("Calibration 1/2: restoring factory MJPEG upstream A/V baseline..."); calibration_request_in_flight.set(true); request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| { restore_factory_calibration(server_addr) }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let calibration_tx = calibration_tx.clone(); let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight); widgets.calibration_minus_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets .status_label .set_text("Calibration 1/2: nudging upstream audio 5 ms earlier..."); calibration_request_in_flight.set(true); request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| { nudge_audio_calibration(server_addr, -5_000) }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let calibration_tx = calibration_tx.clone(); let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight); widgets.calibration_plus_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets .status_label .set_text("Calibration 1/2: nudging upstream audio 5 ms later..."); calibration_request_in_flight.set(true); request_calibration_command(calibration_tx.clone(), server_addr, |server_addr| { nudge_audio_calibration(server_addr, 5_000) }); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let calibration_tx = calibration_tx.clone(); let calibration_request_in_flight = Rc::clone(&calibration_request_in_flight); widgets.calibration_blind_button.connect_clicked(move |_| { let Some(sample) = widgets.diagnostics_log.borrow().latest().cloned() else { widgets.status_label.set_text( "Blind calibration needs a live upstream camera and microphone sample first.", ); return; }; let camera = sample.upstream_camera; let microphone = sample.upstream_microphone; if !camera.connected || !microphone.connected { widgets.status_label.set_text( "Blind calibration refused: upstream camera and microphone are not both live.", ); return; } let delivery_delta_ms = microphone.latest_delivery_age_ms - camera.latest_delivery_age_ms; let delivery_skew_ms = delivery_delta_ms.abs(); let enqueue_skew_ms = (microphone.latest_enqueue_age_ms - camera.latest_enqueue_age_ms).abs(); if camera.queue_depth >= 28 || microphone.queue_depth >= 14 || delivery_skew_ms > 80.0 { widgets.status_label.set_text( "Blind calibration refused: live queues are too backed up to make a safe timing estimate. Use the test rig or fix queue churn first.", ); return; } let audio_delta_us = (-(delivery_delta_ms as f64) * 500.0) .round() .clamp(-10_000.0, 10_000.0) as i64; let note = format!( "blind estimate from live telemetry: mic-camera delivery delta {delivery_delta_ms:+.1}ms, enqueue skew {enqueue_skew_ms:.1}ms; applying half-step audio delta {:+.1}ms", audio_delta_us as f64 / 1000.0 ); let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets .status_label .set_text("Calibration 1/2: applying blind upstream A/V estimate..."); calibration_request_in_flight.set(true); request_calibration_command(calibration_tx.clone(), server_addr, move |server_addr| { blind_calibration_estimate( server_addr, audio_delta_us, delivery_skew_ms, enqueue_skew_ms, ¬e, ) }); }); } { let widgets = widgets.clone(); widgets.calibration_rig_button.connect_clicked(move |_| { widgets.status_label.set_text( "Rig calibration wizard is queued for the 0.16.0 test-equipment phase; for now the manual Tethys sync battery remains the measured-default path.", ); }); } { let widgets = widgets.clone(); widgets.diagnostics_copy_button.connect_clicked(move |_| { if let Err(err) = copy_plain_text(&widgets.diagnostics_rendered_text.borrow()) { widgets .status_label .set_text(&format!("Could not copy the diagnostics report: {err}")); } else { widgets .status_label .set_text("Diagnostics report copied to the local clipboard."); } }); } { let app = app.clone(); let widgets = widgets.clone(); let diagnostics_popout = Rc::clone(&diagnostics_popout); widgets.diagnostics_popout_button.connect_clicked(move |_| { open_diagnostics_popout( &app, &diagnostics_popout, &widgets.diagnostics_popout_label, &widgets.diagnostics_popout_scroll, &widgets.diagnostics_rendered_text, ); widgets .status_label .set_text("Diagnostics report moved into its own window."); }); } { let widgets = widgets.clone(); widgets.console_level_combo.connect_changed(move |combo| { let level = combo .active_id() .as_deref() .and_then(ConsoleLogLevel::from_id) .unwrap_or_default(); *widgets.session_log_level.borrow_mut() = level; widgets.status_label.set_text(&format!( "Console now shows {} relay logs and higher.", level.label() )); }); } { let widgets = widgets.clone(); widgets.console_copy_button.connect_clicked(move |_| { if let Err(err) = copy_session_log(&widgets.session_log_buffer) { widgets .status_label .set_text(&format!("Could not copy the session log: {err}")); } else { widgets .status_label .set_text("Session log copied to the local clipboard."); } }); } { let app = app.clone(); let widgets = widgets.clone(); let log_popout = Rc::clone(&log_popout); widgets.console_popout_button.connect_clicked(move |_| { open_session_log_popout(&app, &log_popout, &widgets.session_log_buffer); widgets .status_label .set_text("Session log moved into its own window."); }); } }