fix(desktop): run blocking USB commands on the blocking pool

Plan A verification found close_device timing out despite doing no I/O:
all commands were synchronous, Tauri runs sync commands on the main
thread, so a hung list_dir blocked every subsequent command.

The 11 libnspire-touching commands now spawn_blocking. enumerate and
close_device stay sync — neither performs a transfer. Return types become
concrete because impl Serialize cannot cross the async boundary. No
change to command names or payloads.
This commit is contained in:
Your Name 2026-07-21 20:37:09 -04:00
parent 81a77f1bcd
commit 15f219bd43

View file

@ -122,25 +122,32 @@ pub fn enumerate<R: Runtime>(window: WebviewWindow<R>) -> Result<Vec<AddDevice>,
} }
#[tauri::command] #[tauri::command]
pub fn open_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> { pub async fn open_device(
let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) { bus_number: u8,
if !matches!(dev.state, DeviceState::Closed) { address: u8,
return Err("Already open".into()); ) -> Result<libnspire::info::Info, SerializedError> {
tauri::async_runtime::spawn_blocking(move || {
let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) {
if !matches!(dev.state, DeviceState::Closed) {
return Err("Already open".into());
};
dev.device.clone()
} else {
return Err("Failed to find device".into());
}; };
dev.device.clone() let handle = libnspire::Handle::new(device.open()?)?;
} else { let info = handle.info()?;
return Err("Failed to find device".into()); {
}; let mut guard = DEVICES.write().unwrap();
let handle = libnspire::Handle::new(device.open()?)?; let device = guard
let info = handle.info()?; .get_mut(&(bus_number, address))
{ .ok_or_else(|| anyhow::anyhow!("Device lost"))?;
let mut guard = DEVICES.write().unwrap(); device.state = DeviceState::Open(Arc::new(std::sync::Mutex::new(handle)), info.clone());
let device = guard }
.get_mut(&(bus_number, address)) Ok(info)
.ok_or_else(|| anyhow::anyhow!("Device lost"))?; })
device.state = DeviceState::Open(Arc::new(std::sync::Mutex::new(handle)), info.clone()); .await
} .map_err(|e| SerializedError::from(e))?
Ok(info)
} }
#[tauri::command] #[tauri::command]
@ -154,221 +161,261 @@ pub fn close_device(bus_number: u8, address: u8) -> Result<impl Serialize, Seria
} }
#[tauri::command] #[tauri::command]
pub fn update_device<R: Runtime>( pub async fn update_device<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<libnspire::info::Info, SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
let info = err_wrap(handle.info(), dev, &window)?; let handle = handle.lock().unwrap();
Ok(info) let info = err_wrap(handle.info(), dev, &window)?;
Ok(info)
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn list_dir<R: Runtime>( pub async fn list_dir<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: String, path: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<Vec<FileInfo>, SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
let dir = err_wrap(handle.list_dir(&path), dev, &window)?; let handle = handle.lock().unwrap();
let dir = err_wrap(handle.list_dir(&path), dev, &window)?;
Ok( Ok(
dir dir
.iter() .iter()
.map(|file| FileInfo { .map(|file| FileInfo {
path: file.name().to_string_lossy().to_string(), path: file.name().to_string_lossy().to_string(),
is_dir: file.entry_type() == EntryType::Directory, is_dir: file.entry_type() == EntryType::Directory,
date: file.date(), date: file.date(),
size: file.size(), size: file.size(),
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn download_file<R: Runtime>( pub async fn download_file<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: (String, u64), path: (String, u64),
dest: String, dest: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let (file, size) = path; };
let dest = PathBuf::from(dest); let (file, size) = path;
let handle = get_open_dev(&dev)?; let dest = PathBuf::from(dest);
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
let mut buf = vec![0; size as usize]; let handle = handle.lock().unwrap();
err_wrap( let mut buf = vec![0; size as usize];
handle.read_file( err_wrap(
&file, handle.read_file(
&mut buf, &file,
&mut progress_sender(&window, dev, size as usize), &mut buf,
), &mut progress_sender(&window, dev, size as usize),
dev, ),
&window, dev,
)?; &window,
if let Some(name) = file.split('/').last() { )?;
File::create(dest.join(name))?.write_all(&buf)?; if let Some(name) = file.split('/').last() {
} File::create(dest.join(name))?.write_all(&buf)?;
Ok(()) }
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn upload_file<R: Runtime>( pub async fn upload_file<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: String, path: String,
src: String, src: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let file = PathBuf::from(src); };
let handle = get_open_dev(&dev)?; let file = PathBuf::from(src);
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
let mut buf = vec![]; let handle = handle.lock().unwrap();
File::open(&file)?.read_to_end(&mut buf)?; let mut buf = vec![];
let name = file File::open(&file)?.read_to_end(&mut buf)?;
.file_name() let name = file
.ok_or_else(|| anyhow::anyhow!("Failed to get file name"))? .file_name()
.to_string_lossy() .ok_or_else(|| anyhow::anyhow!("Failed to get file name"))?
.to_string(); .to_string_lossy()
err_wrap( .to_string();
handle.write_file( err_wrap(
&format!("{}/{}", path, name), handle.write_file(
&buf, &format!("{}/{}", path, name),
&mut progress_sender(&window, dev, buf.len()), &buf,
), &mut progress_sender(&window, dev, buf.len()),
dev, ),
&window, dev,
)?; &window,
Ok(()) )?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn upload_os<R: Runtime>( pub async fn upload_os<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
src: String, src: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
let mut buf = vec![]; let handle = handle.lock().unwrap();
File::open(&src)?.read_to_end(&mut buf)?; let mut buf = vec![];
err_wrap( File::open(&src)?.read_to_end(&mut buf)?;
handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())), err_wrap(
dev, handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())),
&window, dev,
)?; &window,
Ok(()) )?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn delete_file<R: Runtime>( pub async fn delete_file<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: String, path: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
err_wrap(handle.delete_file(&path), dev, &window)?; let handle = handle.lock().unwrap();
Ok(()) err_wrap(handle.delete_file(&path), dev, &window)?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn delete_dir<R: Runtime>( pub async fn delete_dir<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: String, path: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
err_wrap(handle.delete_dir(&path), dev, &window)?; let handle = handle.lock().unwrap();
Ok(()) err_wrap(handle.delete_dir(&path), dev, &window)?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn create_nspire_dir<R: Runtime>( pub async fn create_nspire_dir<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
path: String, path: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
err_wrap(handle.create_dir(&path), dev, &window)?; let handle = handle.lock().unwrap();
Ok(()) err_wrap(handle.create_dir(&path), dev, &window)?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn move_file<R: Runtime>( pub async fn move_file<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
src: String, src: String,
dest: String, dest: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
err_wrap(handle.move_file(&src, &dest), dev, &window)?; let handle = handle.lock().unwrap();
Ok(()) err_wrap(handle.move_file(&src, &dest), dev, &window)?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }
#[tauri::command] #[tauri::command]
pub fn copy<R: Runtime>( pub async fn copy<R: Runtime>(
bus_number: u8, bus_number: u8,
address: u8, address: u8,
src: String, src: String,
dest: String, dest: String,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<impl Serialize, SerializedError> { ) -> Result<(), SerializedError> {
let dev = DevId { tauri::async_runtime::spawn_blocking(move || {
bus_number, let dev = DevId {
address, bus_number,
}; address,
let handle = get_open_dev(&dev)?; };
let handle = handle.lock().unwrap(); let handle = get_open_dev(&dev)?;
err_wrap(handle.copy_file(&src, &dest), dev, &window)?; let handle = handle.lock().unwrap();
Ok(()) err_wrap(handle.copy_file(&src, &dest), dev, &window)?;
Ok(())
})
.await
.map_err(|e| SerializedError::from(e))?
} }