fix(server): survive unattached HID gadget startup
This commit is contained in:
parent
b8e43cac6f
commit
c65fcd1137
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.34"
|
version = "0.11.35"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -182,7 +182,8 @@ pub fn build_launcher_view(
|
|||||||
let operations = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
let operations = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||||
operations.set_size_request(OPERATIONS_RAIL_WIDTH, -1);
|
operations.set_size_request(OPERATIONS_RAIL_WIDTH, -1);
|
||||||
operations.set_hexpand(false);
|
operations.set_hexpand(false);
|
||||||
operations.set_vexpand(true);
|
operations.set_vexpand(false);
|
||||||
|
operations.set_valign(gtk::Align::Start);
|
||||||
content.append(&operations);
|
content.append(&operations);
|
||||||
|
|
||||||
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
@ -320,14 +321,14 @@ pub fn build_launcher_view(
|
|||||||
|
|
||||||
let (preview_panel, preview_body) = build_panel("Device Testing");
|
let (preview_panel, preview_body) = build_panel("Device Testing");
|
||||||
preview_panel.set_hexpand(true);
|
preview_panel.set_hexpand(true);
|
||||||
preview_panel.set_vexpand(false);
|
preview_panel.set_vexpand(true);
|
||||||
preview_panel.set_valign(gtk::Align::Fill);
|
preview_panel.set_valign(gtk::Align::Fill);
|
||||||
preview_body.set_vexpand(false);
|
preview_body.set_vexpand(true);
|
||||||
preview_body.set_spacing(8);
|
preview_body.set_spacing(8);
|
||||||
let testing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let testing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
testing_row.set_hexpand(true);
|
testing_row.set_hexpand(true);
|
||||||
testing_row.set_vexpand(false);
|
testing_row.set_vexpand(true);
|
||||||
testing_row.set_valign(gtk::Align::Start);
|
testing_row.set_valign(gtk::Align::Fill);
|
||||||
let camera_preview = gtk::Picture::new();
|
let camera_preview = gtk::Picture::new();
|
||||||
camera_preview.set_can_shrink(false);
|
camera_preview.set_can_shrink(false);
|
||||||
camera_preview.set_hexpand(false);
|
camera_preview.set_hexpand(false);
|
||||||
@ -363,15 +364,15 @@ pub fn build_launcher_view(
|
|||||||
camera_preview_shell.append(&camera_preview_frame);
|
camera_preview_shell.append(&camera_preview_frame);
|
||||||
let webcam_group = build_subgroup("Webcam Preview");
|
let webcam_group = build_subgroup("Webcam Preview");
|
||||||
webcam_group.set_hexpand(true);
|
webcam_group.set_hexpand(true);
|
||||||
webcam_group.set_vexpand(false);
|
webcam_group.set_vexpand(true);
|
||||||
webcam_group.set_valign(gtk::Align::Start);
|
webcam_group.set_valign(gtk::Align::Fill);
|
||||||
webcam_group.append(&camera_preview_shell);
|
webcam_group.append(&camera_preview_shell);
|
||||||
testing_row.append(&webcam_group);
|
testing_row.append(&webcam_group);
|
||||||
|
|
||||||
let playback_group = build_subgroup("Mic Playback");
|
let playback_group = build_subgroup("Mic Playback");
|
||||||
playback_group.set_hexpand(false);
|
playback_group.set_hexpand(false);
|
||||||
playback_group.set_vexpand(false);
|
playback_group.set_vexpand(true);
|
||||||
playback_group.set_valign(gtk::Align::Start);
|
playback_group.set_valign(gtk::Align::Fill);
|
||||||
playback_group.set_size_request(72, -1);
|
playback_group.set_size_request(72, -1);
|
||||||
let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||||
playback_body.set_halign(gtk::Align::Center);
|
playback_body.set_halign(gtk::Align::Center);
|
||||||
@ -512,7 +513,8 @@ pub fn build_launcher_view(
|
|||||||
let diagnostics_scroll = gtk::ScrolledWindow::builder()
|
let diagnostics_scroll = gtk::ScrolledWindow::builder()
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.vexpand(false)
|
.vexpand(false)
|
||||||
.min_content_height(190)
|
.min_content_height(150)
|
||||||
|
.max_content_height(150)
|
||||||
.child(&diagnostics_shell)
|
.child(&diagnostics_shell)
|
||||||
.build();
|
.build();
|
||||||
diagnostics_body.append(&diagnostics_toolbar);
|
diagnostics_body.append(&diagnostics_toolbar);
|
||||||
@ -520,7 +522,7 @@ pub fn build_launcher_view(
|
|||||||
operations.append(&diagnostics_panel);
|
operations.append(&diagnostics_panel);
|
||||||
|
|
||||||
let (console_panel, console_body) = build_panel("Session Console");
|
let (console_panel, console_body) = build_panel("Session Console");
|
||||||
console_panel.set_vexpand(true);
|
console_panel.set_vexpand(false);
|
||||||
let console_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let console_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
console_toolbar.set_homogeneous(true);
|
console_toolbar.set_homogeneous(true);
|
||||||
let console_copy_button = gtk::Button::with_label("Copy Log");
|
let console_copy_button = gtk::Button::with_label("Copy Log");
|
||||||
@ -550,8 +552,9 @@ pub fn build_launcher_view(
|
|||||||
session_log_view.set_wrap_mode(gtk::WrapMode::WordChar);
|
session_log_view.set_wrap_mode(gtk::WrapMode::WordChar);
|
||||||
let log_scroll = gtk::ScrolledWindow::builder()
|
let log_scroll = gtk::ScrolledWindow::builder()
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.vexpand(true)
|
.vexpand(false)
|
||||||
.min_content_height(220)
|
.min_content_height(150)
|
||||||
|
.max_content_height(150)
|
||||||
.child(&session_log_view)
|
.child(&session_log_view)
|
||||||
.build();
|
.build();
|
||||||
console_body.append(&console_toolbar);
|
console_body.append(&console_toolbar);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.34"
|
version = "0.11.35"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn banner_includes_version() {
|
fn banner_includes_version() {
|
||||||
assert_eq!(banner("0.11.34"), "lesavka-common CLI (v0.11.34)");
|
assert_eq!(banner("0.11.35"), "lesavka-common CLI (v0.11.35)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.34"
|
version = "0.11.35"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -51,8 +51,8 @@ fn live_keyboard_report_delay() -> Duration {
|
|||||||
|
|
||||||
/*──────────────── Handler ───────────────────*/
|
/*──────────────── Handler ───────────────────*/
|
||||||
struct Handler {
|
struct Handler {
|
||||||
kb: Arc<Mutex<tokio::fs::File>>,
|
kb: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
ms: Arc<Mutex<tokio::fs::File>>,
|
ms: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
gadget: UsbGadget,
|
gadget: UsbGadget,
|
||||||
did_cycle: Arc<AtomicBool>,
|
did_cycle: Arc<AtomicBool>,
|
||||||
camera_rt: Arc<CameraRuntime>,
|
camera_rt: Arc<CameraRuntime>,
|
||||||
@ -91,10 +91,16 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
info!("🛠️ opening HID endpoints …");
|
info!("🛠️ opening HID endpoints …");
|
||||||
}
|
}
|
||||||
let kb = runtime_support::open_with_retry(&hid_endpoint(0)).await?;
|
let kb_path = hid_endpoint(0);
|
||||||
let ms = runtime_support::open_with_retry(&hid_endpoint(1)).await?;
|
let ms_path = hid_endpoint(1);
|
||||||
|
let kb = runtime_support::open_hid_if_ready(&kb_path).await?;
|
||||||
|
let ms = runtime_support::open_hid_if_ready(&ms_path).await?;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
info!("✅ HID endpoints ready");
|
if kb.is_some() && ms.is_some() {
|
||||||
|
info!("✅ HID endpoints ready");
|
||||||
|
} else {
|
||||||
|
warn!("⌛ HID endpoints are not ready; relay will keep running and open them lazily");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
kb: Arc::new(Mutex::new(kb)),
|
kb: Arc::new(Mutex::new(kb)),
|
||||||
@ -108,8 +114,8 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn reopen_hid(&self) -> anyhow::Result<()> {
|
async fn reopen_hid(&self) -> anyhow::Result<()> {
|
||||||
let kb_new = runtime_support::open_with_retry(&hid_endpoint(0)).await?;
|
let kb_new = runtime_support::open_hid_if_ready(&hid_endpoint(0)).await?;
|
||||||
let ms_new = runtime_support::open_with_retry(&hid_endpoint(1)).await?;
|
let ms_new = runtime_support::open_hid_if_ready(&hid_endpoint(1)).await?;
|
||||||
*self.kb.lock().await = kb_new;
|
*self.kb.lock().await = kb_new;
|
||||||
*self.ms.lock().await = ms_new;
|
*self.ms.lock().await = ms_new;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -311,7 +317,7 @@ impl Handler {
|
|||||||
) -> Result<Response<PasteReply>, Status> {
|
) -> Result<Response<PasteReply>, Status> {
|
||||||
let req = req.into_inner();
|
let req = req.into_inner();
|
||||||
let text = paste::decrypt(&req).map_err(|e| Status::unauthenticated(format!("{e}")))?;
|
let text = paste::decrypt(&req).map_err(|e| Status::unauthenticated(format!("{e}")))?;
|
||||||
if let Err(e) = paste::type_text(self.kb.as_ref(), &text).await {
|
if let Err(e) = paste::type_text(&self.kb, &hid_endpoint(0), &text).await {
|
||||||
return Ok(Response::new(PasteReply {
|
return Ok(Response::new(PasteReply {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: format!("{e}"),
|
error: format!("{e}"),
|
||||||
@ -528,6 +534,8 @@ impl Relay for Handler {
|
|||||||
let (tx, rx) = tokio::sync::mpsc::channel(32);
|
let (tx, rx) = tokio::sync::mpsc::channel(32);
|
||||||
let kb = self.kb.clone();
|
let kb = self.kb.clone();
|
||||||
let ms = self.ms.clone();
|
let ms = self.ms.clone();
|
||||||
|
let kb_path = hid_endpoint(0);
|
||||||
|
let ms_path = hid_endpoint(1);
|
||||||
let gadget = self.gadget.clone();
|
let gadget = self.gadget.clone();
|
||||||
let did_cycle = self.did_cycle.clone();
|
let did_cycle = self.did_cycle.clone();
|
||||||
let session_lease = self.capture_power.acquire_session().await;
|
let session_lease = self.capture_power.acquire_session().await;
|
||||||
@ -537,7 +545,7 @@ impl Relay for Handler {
|
|||||||
let _session_lease = session_lease;
|
let _session_lease = session_lease;
|
||||||
let mut s = req.into_inner();
|
let mut s = req.into_inner();
|
||||||
while let Some(pkt) = s.next().await.transpose()? {
|
while let Some(pkt) = s.next().await.transpose()? {
|
||||||
if let Err(e) = runtime_support::write_hid_report(&kb, &pkt.data).await {
|
if let Err(e) = runtime_support::write_hid_report(&kb, &kb_path, &pkt.data).await {
|
||||||
if e.raw_os_error() == Some(libc::EAGAIN) {
|
if e.raw_os_error() == Some(libc::EAGAIN) {
|
||||||
debug!(rpc_id, "⌨️ write would block (dropped)");
|
debug!(rpc_id, "⌨️ write would block (dropped)");
|
||||||
} else {
|
} else {
|
||||||
@ -547,6 +555,8 @@ impl Relay for Handler {
|
|||||||
gadget.clone(),
|
gadget.clone(),
|
||||||
kb.clone(),
|
kb.clone(),
|
||||||
ms.clone(),
|
ms.clone(),
|
||||||
|
kb_path.clone(),
|
||||||
|
ms_path.clone(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -573,6 +583,8 @@ impl Relay for Handler {
|
|||||||
let (tx, rx) = tokio::sync::mpsc::channel(1024);
|
let (tx, rx) = tokio::sync::mpsc::channel(1024);
|
||||||
let ms = self.ms.clone();
|
let ms = self.ms.clone();
|
||||||
let kb = self.kb.clone();
|
let kb = self.kb.clone();
|
||||||
|
let kb_path = hid_endpoint(0);
|
||||||
|
let ms_path = hid_endpoint(1);
|
||||||
let gadget = self.gadget.clone();
|
let gadget = self.gadget.clone();
|
||||||
let did_cycle = self.did_cycle.clone();
|
let did_cycle = self.did_cycle.clone();
|
||||||
let session_lease = self.capture_power.acquire_session().await;
|
let session_lease = self.capture_power.acquire_session().await;
|
||||||
@ -581,7 +593,7 @@ impl Relay for Handler {
|
|||||||
let _session_lease = session_lease;
|
let _session_lease = session_lease;
|
||||||
let mut s = req.into_inner();
|
let mut s = req.into_inner();
|
||||||
while let Some(pkt) = s.next().await.transpose()? {
|
while let Some(pkt) = s.next().await.transpose()? {
|
||||||
if let Err(e) = runtime_support::write_hid_report(&ms, &pkt.data).await {
|
if let Err(e) = runtime_support::write_hid_report(&ms, &ms_path, &pkt.data).await {
|
||||||
if e.raw_os_error() == Some(libc::EAGAIN) {
|
if e.raw_os_error() == Some(libc::EAGAIN) {
|
||||||
debug!(rpc_id, "🖱️ write would block (dropped)");
|
debug!(rpc_id, "🖱️ write would block (dropped)");
|
||||||
} else {
|
} else {
|
||||||
@ -591,6 +603,8 @@ impl Relay for Handler {
|
|||||||
gadget.clone(),
|
gadget.clone(),
|
||||||
kb.clone(),
|
kb.clone(),
|
||||||
ms.clone(),
|
ms.clone(),
|
||||||
|
kb_path.clone(),
|
||||||
|
ms_path.clone(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -753,7 +767,7 @@ impl Relay for Handler {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut s = req.into_inner();
|
let mut s = req.into_inner();
|
||||||
while let Some(pkt) = s.next().await.transpose()? {
|
while let Some(pkt) = s.next().await.transpose()? {
|
||||||
let _ = runtime_support::write_hid_report(&kb, &pkt.data).await;
|
let _ = runtime_support::write_hid_report(&kb, &hid_endpoint(0), &pkt.data).await;
|
||||||
tx.send(Ok(pkt)).await.ok();
|
tx.send(Ok(pkt)).await.ok();
|
||||||
if !report_delay.is_zero() {
|
if !report_delay.is_zero() {
|
||||||
tokio::time::sleep(report_delay).await;
|
tokio::time::sleep(report_delay).await;
|
||||||
@ -775,7 +789,7 @@ impl Relay for Handler {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut s = req.into_inner();
|
let mut s = req.into_inner();
|
||||||
while let Some(pkt) = s.next().await.transpose()? {
|
while let Some(pkt) = s.next().await.transpose()? {
|
||||||
let _ = runtime_support::write_hid_report(&ms, &pkt.data).await;
|
let _ = runtime_support::write_hid_report(&ms, &hid_endpoint(1), &pkt.data).await;
|
||||||
tx.send(Ok(pkt)).await.ok();
|
tx.send(Ok(pkt)).await.ok();
|
||||||
}
|
}
|
||||||
Ok::<(), Status>(())
|
Ok::<(), Status>(())
|
||||||
|
|||||||
@ -4,11 +4,10 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chacha20poly1305::aead::{Aead, KeyInit};
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
||||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::runtime_support;
|
||||||
use lesavka_common::hid::{append_char_reports, char_to_usage};
|
use lesavka_common::hid::{append_char_reports, char_to_usage};
|
||||||
use lesavka_common::lesavka::PasteRequest;
|
use lesavka_common::lesavka::PasteRequest;
|
||||||
use lesavka_common::paste::decode_shared_key;
|
use lesavka_common::paste::decode_shared_key;
|
||||||
@ -41,7 +40,11 @@ pub fn decrypt(req: &PasteRequest) -> Result<String> {
|
|||||||
/// supported character up to the configured maximum.
|
/// supported character up to the configured maximum.
|
||||||
/// Why: paste injection must rate-limit itself so slower hosts do not drop
|
/// Why: paste injection must rate-limit itself so slower hosts do not drop
|
||||||
/// HID reports under bursty clipboard loads.
|
/// HID reports under bursty clipboard loads.
|
||||||
pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
pub async fn type_text(
|
||||||
|
kb: &Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
|
hid_path: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let max = std::env::var("LESAVKA_PASTE_MAX")
|
let max = std::env::var("LESAVKA_PASTE_MAX")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse::<usize>().ok())
|
.and_then(|v| v.parse::<usize>().ok())
|
||||||
@ -59,12 +62,11 @@ pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut kb = kb.lock().await;
|
|
||||||
for c in text.chars().take(max) {
|
for c in text.chars().take(max) {
|
||||||
let mut reports = Vec::with_capacity(4);
|
let mut reports = Vec::with_capacity(4);
|
||||||
if append_char_reports(&mut reports, c) {
|
if append_char_reports(&mut reports, c) {
|
||||||
for report in reports {
|
for report in reports {
|
||||||
kb.write_all(&report).await?;
|
runtime_support::write_hid_report(kb, hid_path, &report).await?;
|
||||||
if delay_ms > 0 {
|
if delay_ms > 0 {
|
||||||
tokio::time::sleep(delay).await;
|
tokio::time::sleep(delay).await;
|
||||||
}
|
}
|
||||||
@ -128,6 +130,7 @@ mod tests {
|
|||||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
use lesavka_common::lesavka::PasteRequest;
|
use lesavka_common::lesavka::PasteRequest;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
use std::sync::Arc;
|
||||||
use temp_env::with_var;
|
use temp_env::with_var;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use tokio::fs::{File, OpenOptions};
|
use tokio::fs::{File, OpenOptions};
|
||||||
@ -183,8 +186,10 @@ mod tests {
|
|||||||
.open(&path)
|
.open(&path)
|
||||||
.await
|
.await
|
||||||
.expect("open temp file");
|
.expect("open temp file");
|
||||||
let kb = Mutex::new(file);
|
let kb = Arc::new(Mutex::new(Some(file)));
|
||||||
type_text(&kb, "A!?").await.expect("type text");
|
type_text(&kb, path.to_str().unwrap(), "A!?")
|
||||||
|
.await
|
||||||
|
.expect("type text");
|
||||||
|
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
let mut file = File::open(&path).await.expect("reopen temp file");
|
let mut file = File::open(&path).await.expect("reopen temp file");
|
||||||
@ -218,8 +223,8 @@ mod tests {
|
|||||||
.open(&path)
|
.open(&path)
|
||||||
.await
|
.await
|
||||||
.expect("open temp file");
|
.expect("open temp file");
|
||||||
let kb = Mutex::new(file);
|
let kb = Arc::new(Mutex::new(Some(file)));
|
||||||
let err = type_text(&kb, "pw🙂")
|
let err = type_text(&kb, path.to_str().unwrap(), "pw🙂")
|
||||||
.await
|
.await
|
||||||
.expect_err("unsupported char should fail");
|
.expect_err("unsupported char should fail");
|
||||||
assert!(err.to_string().contains("unsupported character"));
|
assert!(err.to_string().contains("unsupported character"));
|
||||||
|
|||||||
@ -66,10 +66,7 @@ pub fn init_tracing() -> anyhow::Result<WorkerGuard> {
|
|||||||
/// must wait for readiness instead of failing the whole process immediately.
|
/// must wait for readiness instead of failing the whole process immediately.
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
||||||
OpenOptions::new()
|
open_hid_file(path)
|
||||||
.write(true)
|
|
||||||
.custom_flags(libc::O_NONBLOCK)
|
|
||||||
.open(path)
|
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("opening {path}"))
|
.with_context(|| format!("opening {path}"))
|
||||||
}
|
}
|
||||||
@ -77,18 +74,16 @@ pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
||||||
for attempt in 1..=200 {
|
for attempt in 1..=200 {
|
||||||
match OpenOptions::new()
|
match open_hid_file(path).await {
|
||||||
.write(true)
|
|
||||||
.custom_flags(libc::O_NONBLOCK)
|
|
||||||
.open(path)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
info!("✅ {path} opened on attempt #{attempt}");
|
info!("✅ {path} opened on attempt #{attempt}");
|
||||||
return Ok(file);
|
return Ok(file);
|
||||||
}
|
}
|
||||||
Err(error) if error.raw_os_error() == Some(libc::EBUSY) => {
|
Err(error)
|
||||||
trace!("⏳ {path} busy… retry #{attempt}");
|
if hid_endpoint_open_is_temporarily_unavailable(error.raw_os_error())
|
||||||
|
|| error.raw_os_error() == Some(libc::EBUSY) =>
|
||||||
|
{
|
||||||
|
trace!("⏳ {path} unavailable ({error})… retry #{attempt}");
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
}
|
}
|
||||||
Err(error) => return Err(error).with_context(|| format!("opening {path}")),
|
Err(error) => return Err(error).with_context(|| format!("opening {path}")),
|
||||||
@ -98,6 +93,36 @@ pub async fn open_with_retry(path: &str) -> anyhow::Result<tokio::fs::File> {
|
|||||||
Err(anyhow::anyhow!("timeout waiting for {path}"))
|
Err(anyhow::anyhow!("timeout waiting for {path}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn open_hid_file(path: &str) -> std::io::Result<tokio::fs::File> {
|
||||||
|
OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.custom_flags(libc::O_NONBLOCK)
|
||||||
|
.open(path)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_hid_if_ready(path: &str) -> anyhow::Result<Option<tokio::fs::File>> {
|
||||||
|
match open_hid_file(path).await {
|
||||||
|
Ok(file) => {
|
||||||
|
info!("✅ {path} opened");
|
||||||
|
Ok(Some(file))
|
||||||
|
}
|
||||||
|
Err(error) if hid_endpoint_open_is_temporarily_unavailable(error.raw_os_error()) => {
|
||||||
|
warn!("⌛ {path} is not ready yet ({error}); relay will retry lazily");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(error) => Err(error).with_context(|| format!("opening {path}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn hid_endpoint_open_is_temporarily_unavailable(code: Option<i32>) -> bool {
|
||||||
|
matches!(
|
||||||
|
code,
|
||||||
|
Some(libc::ENOENT) | Some(libc::ENODEV) | Some(libc::ENXIO)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check whether gadget auto-recovery is enabled.
|
/// Check whether gadget auto-recovery is enabled.
|
||||||
///
|
///
|
||||||
/// Inputs: none.
|
/// Inputs: none.
|
||||||
@ -120,7 +145,7 @@ pub fn should_recover_hid_error(code: Option<i32>) -> bool {
|
|||||||
matches!(
|
matches!(
|
||||||
code,
|
code,
|
||||||
Some(libc::ENOTCONN) | Some(libc::ESHUTDOWN) | Some(libc::EPIPE)
|
Some(libc::ENOTCONN) | Some(libc::ESHUTDOWN) | Some(libc::EPIPE)
|
||||||
)
|
) || hid_endpoint_open_is_temporarily_unavailable(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recover the HID endpoints after a transport failure.
|
/// Recover the HID endpoints after a transport failure.
|
||||||
@ -134,8 +159,10 @@ pub fn should_recover_hid_error(code: Option<i32>) -> bool {
|
|||||||
pub async fn recover_hid_if_needed(
|
pub async fn recover_hid_if_needed(
|
||||||
err: &std::io::Error,
|
err: &std::io::Error,
|
||||||
gadget: UsbGadget,
|
gadget: UsbGadget,
|
||||||
kb: Arc<Mutex<tokio::fs::File>>,
|
kb: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
ms: Arc<Mutex<tokio::fs::File>>,
|
ms: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
|
_kb_path: String,
|
||||||
|
_ms_path: String,
|
||||||
did_cycle: Arc<AtomicBool>,
|
did_cycle: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let code = err.raw_os_error();
|
let code = err.raw_os_error();
|
||||||
@ -166,8 +193,10 @@ pub async fn recover_hid_if_needed(
|
|||||||
pub async fn recover_hid_if_needed(
|
pub async fn recover_hid_if_needed(
|
||||||
err: &std::io::Error,
|
err: &std::io::Error,
|
||||||
gadget: UsbGadget,
|
gadget: UsbGadget,
|
||||||
kb: Arc<Mutex<tokio::fs::File>>,
|
kb: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
ms: Arc<Mutex<tokio::fs::File>>,
|
ms: Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
|
kb_path: String,
|
||||||
|
ms_path: String,
|
||||||
did_cycle: Arc<AtomicBool>,
|
did_cycle: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let code = err.raw_os_error();
|
let code = err.raw_os_error();
|
||||||
@ -198,8 +227,8 @@ pub async fn recover_hid_if_needed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(error) = async {
|
if let Err(error) = async {
|
||||||
let kb_new = open_with_retry("/dev/hidg0").await?;
|
let kb_new = open_hid_if_ready(&kb_path).await?;
|
||||||
let ms_new = open_with_retry("/dev/hidg1").await?;
|
let ms_new = open_hid_if_ready(&ms_path).await?;
|
||||||
*kb.lock().await = kb_new;
|
*kb.lock().await = kb_new;
|
||||||
*ms.lock().await = ms_new;
|
*ms.lock().await = ms_new;
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
@ -496,16 +525,28 @@ pub fn next_stream_id() -> u64 {
|
|||||||
/// stalls without blocking the stream task indefinitely.
|
/// stalls without blocking the stream task indefinitely.
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
pub async fn write_hid_report(
|
pub async fn write_hid_report(
|
||||||
dev: &Arc<Mutex<tokio::fs::File>>,
|
dev: &Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
|
path: &str,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let mut file = dev.lock().await;
|
let mut file = dev.lock().await;
|
||||||
file.write_all(data).await
|
if file.is_none() {
|
||||||
|
*file = Some(open_hid_file(path).await?);
|
||||||
|
}
|
||||||
|
if let Some(file) = file.as_mut() {
|
||||||
|
file.write_all(data).await
|
||||||
|
} else {
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotConnected,
|
||||||
|
"HID endpoint is not open",
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
pub async fn write_hid_report(
|
pub async fn write_hid_report(
|
||||||
dev: &Arc<Mutex<tokio::fs::File>>,
|
dev: &Arc<Mutex<Option<tokio::fs::File>>>,
|
||||||
|
path: &str,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
let attempts = std::env::var("LESAVKA_HID_WRITE_RETRIES")
|
let attempts = std::env::var("LESAVKA_HID_WRITE_RETRIES")
|
||||||
@ -521,7 +562,22 @@ pub async fn write_hid_report(
|
|||||||
let mut last_error: Option<std::io::Error> = None;
|
let mut last_error: Option<std::io::Error> = None;
|
||||||
for attempt in 0..attempts {
|
for attempt in 0..attempts {
|
||||||
let mut file = dev.lock().await;
|
let mut file = dev.lock().await;
|
||||||
match file.write_all(data).await {
|
if file.is_none() {
|
||||||
|
match open_hid_file(path).await {
|
||||||
|
Ok(opened) => {
|
||||||
|
info!("✅ {path} opened lazily");
|
||||||
|
*file = Some(opened);
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(file_handle) = file.as_mut() else {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotConnected,
|
||||||
|
"HID endpoint is not open",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
match file_handle.write_all(data).await {
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(error)
|
Err(error)
|
||||||
if error.kind() == std::io::ErrorKind::WouldBlock
|
if error.kind() == std::io::ErrorKind::WouldBlock
|
||||||
@ -529,7 +585,12 @@ pub async fn write_hid_report(
|
|||||||
{
|
{
|
||||||
last_error = Some(error);
|
last_error = Some(error);
|
||||||
}
|
}
|
||||||
Err(error) => return Err(error),
|
Err(error) => {
|
||||||
|
if should_recover_hid_error(error.raw_os_error()) {
|
||||||
|
*file = None;
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
drop(file);
|
drop(file);
|
||||||
tokio::time::sleep(Duration::from_millis((attempt as u64 + 1) * base_delay_ms)).await;
|
tokio::time::sleep(Duration::from_millis((attempt as u64 + 1) * base_delay_ms)).await;
|
||||||
@ -661,9 +722,9 @@ mod tests {
|
|||||||
.open(tmp.path())
|
.open(tmp.path())
|
||||||
.await
|
.await
|
||||||
.expect("open temp file");
|
.expect("open temp file");
|
||||||
let shared = Arc::new(Mutex::new(file));
|
let shared = Arc::new(Mutex::new(Some(file)));
|
||||||
|
|
||||||
write_hid_report(&shared, &[1, 2, 3, 4])
|
write_hid_report(&shared, tmp.path().to_str().unwrap(), &[1, 2, 3, 4])
|
||||||
.await
|
.await
|
||||||
.expect("write succeeds");
|
.expect("write succeeds");
|
||||||
|
|
||||||
|
|||||||
@ -44,8 +44,8 @@ mod server_main_binary {
|
|||||||
(
|
(
|
||||||
dir,
|
dir,
|
||||||
Handler {
|
Handler {
|
||||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
||||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
||||||
gadget: UsbGadget::new("lesavka"),
|
gadget: UsbGadget::new("lesavka"),
|
||||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||||
|
|||||||
@ -86,8 +86,8 @@ mod server_main_binary_extra {
|
|||||||
(
|
(
|
||||||
dir,
|
dir,
|
||||||
Handler {
|
Handler {
|
||||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
||||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
||||||
gadget: UsbGadget::new("lesavka"),
|
gadget: UsbGadget::new("lesavka"),
|
||||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||||
@ -455,8 +455,8 @@ mod server_main_binary_extra {
|
|||||||
.expect("open hidg1"),
|
.expect("open hidg1"),
|
||||||
);
|
);
|
||||||
let handler = Handler {
|
let handler = Handler {
|
||||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
||||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
||||||
gadget: UsbGadget::new("lesavka"),
|
gadget: UsbGadget::new("lesavka"),
|
||||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||||
|
|||||||
@ -45,8 +45,8 @@ mod server_main_rpc {
|
|||||||
(
|
(
|
||||||
dir,
|
dir,
|
||||||
Handler {
|
Handler {
|
||||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
||||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
||||||
gadget: UsbGadget::new("lesavka"),
|
gadget: UsbGadget::new("lesavka"),
|
||||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||||
@ -399,8 +399,8 @@ mod server_main_rpc {
|
|||||||
Some(dir.path().join("cfg").to_string_lossy().to_string()),
|
Some(dir.path().join("cfg").to_string_lossy().to_string()),
|
||||||
|| {
|
|| {
|
||||||
let handler = Handler {
|
let handler = Handler {
|
||||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
||||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
||||||
gadget: UsbGadget::new("lesavka"),
|
gadget: UsbGadget::new("lesavka"),
|
||||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
|
||||||
false,
|
false,
|
||||||
|
|||||||
@ -187,8 +187,8 @@ fn runtime_recover_hid_ignores_non_transport_errors() {
|
|||||||
.await
|
.await
|
||||||
.expect("open temp ms");
|
.expect("open temp ms");
|
||||||
|
|
||||||
let kb = Arc::new(Mutex::new(kb));
|
let kb = Arc::new(Mutex::new(Some(kb)));
|
||||||
let ms = Arc::new(Mutex::new(ms));
|
let ms = Arc::new(Mutex::new(Some(ms)));
|
||||||
let did_cycle = Arc::new(AtomicBool::new(false));
|
let did_cycle = Arc::new(AtomicBool::new(false));
|
||||||
let err = std::io::Error::from_raw_os_error(libc::EAGAIN);
|
let err = std::io::Error::from_raw_os_error(libc::EAGAIN);
|
||||||
|
|
||||||
@ -197,6 +197,8 @@ fn runtime_recover_hid_ignores_non_transport_errors() {
|
|||||||
UsbGadget::new("lesavka"),
|
UsbGadget::new("lesavka"),
|
||||||
kb,
|
kb,
|
||||||
ms,
|
ms,
|
||||||
|
kb_tmp.path().to_string_lossy().to_string(),
|
||||||
|
ms_tmp.path().to_string_lossy().to_string(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -227,8 +229,8 @@ fn runtime_recover_hid_short_circuits_when_cycle_already_in_progress() {
|
|||||||
.await
|
.await
|
||||||
.expect("open temp ms");
|
.expect("open temp ms");
|
||||||
|
|
||||||
let kb = Arc::new(Mutex::new(kb));
|
let kb = Arc::new(Mutex::new(Some(kb)));
|
||||||
let ms = Arc::new(Mutex::new(ms));
|
let ms = Arc::new(Mutex::new(Some(ms)));
|
||||||
let did_cycle = Arc::new(AtomicBool::new(true));
|
let did_cycle = Arc::new(AtomicBool::new(true));
|
||||||
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
||||||
|
|
||||||
@ -237,6 +239,8 @@ fn runtime_recover_hid_short_circuits_when_cycle_already_in_progress() {
|
|||||||
UsbGadget::new("lesavka"),
|
UsbGadget::new("lesavka"),
|
||||||
kb,
|
kb,
|
||||||
ms,
|
ms,
|
||||||
|
kb_tmp.path().to_string_lossy().to_string(),
|
||||||
|
ms_tmp.path().to_string_lossy().to_string(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -268,8 +272,8 @@ fn runtime_recover_hid_resets_cycle_flag_after_async_recovery_path() {
|
|||||||
.await
|
.await
|
||||||
.expect("open temp ms");
|
.expect("open temp ms");
|
||||||
|
|
||||||
let kb = Arc::new(Mutex::new(kb));
|
let kb = Arc::new(Mutex::new(Some(kb)));
|
||||||
let ms = Arc::new(Mutex::new(ms));
|
let ms = Arc::new(Mutex::new(Some(ms)));
|
||||||
let did_cycle = Arc::new(AtomicBool::new(false));
|
let did_cycle = Arc::new(AtomicBool::new(false));
|
||||||
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
||||||
|
|
||||||
@ -278,6 +282,8 @@ fn runtime_recover_hid_resets_cycle_flag_after_async_recovery_path() {
|
|||||||
UsbGadget::new("lesavka"),
|
UsbGadget::new("lesavka"),
|
||||||
kb.clone(),
|
kb.clone(),
|
||||||
ms.clone(),
|
ms.clone(),
|
||||||
|
kb_tmp.path().to_string_lossy().to_string(),
|
||||||
|
ms_tmp.path().to_string_lossy().to_string(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -316,8 +322,8 @@ fn runtime_recover_hid_attempts_cycle_when_enabled() {
|
|||||||
.await
|
.await
|
||||||
.expect("open temp ms");
|
.expect("open temp ms");
|
||||||
|
|
||||||
let kb = Arc::new(Mutex::new(kb));
|
let kb = Arc::new(Mutex::new(Some(kb)));
|
||||||
let ms = Arc::new(Mutex::new(ms));
|
let ms = Arc::new(Mutex::new(Some(ms)));
|
||||||
let did_cycle = Arc::new(AtomicBool::new(false));
|
let did_cycle = Arc::new(AtomicBool::new(false));
|
||||||
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
let err = std::io::Error::from_raw_os_error(libc::EPIPE);
|
||||||
|
|
||||||
@ -326,6 +332,8 @@ fn runtime_recover_hid_attempts_cycle_when_enabled() {
|
|||||||
UsbGadget::new("lesavka"),
|
UsbGadget::new("lesavka"),
|
||||||
kb.clone(),
|
kb.clone(),
|
||||||
ms.clone(),
|
ms.clone(),
|
||||||
|
kb_tmp.path().to_string_lossy().to_string(),
|
||||||
|
ms_tmp.path().to_string_lossy().to_string(),
|
||||||
did_cycle.clone(),
|
did_cycle.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user