mirror of https://github.com/lights0123/n-link
479 lines
11 KiB
Rust
479 lines
11 KiB
Rust
#![cfg_attr(
|
|
all(not(debug_assertions), target_os = "windows"),
|
|
windows_subsystem = "windows"
|
|
)]
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex, RwLock};
|
|
use std::time::Duration;
|
|
|
|
use hashbrown::HashMap;
|
|
use libnspire::{PID_CX2, VID};
|
|
use rusb::{GlobalContext, Hotplug, UsbContext};
|
|
use serde::Serialize;
|
|
use tauri::{Runtime, Window};
|
|
|
|
use crate::cmd::{add_device, AddDevice, DevId, ProgressUpdate};
|
|
|
|
mod cli;
|
|
mod cmd;
|
|
|
|
pub enum DeviceState {
|
|
Open(
|
|
Arc<Mutex<libnspire::Handle<GlobalContext>>>,
|
|
libnspire::info::Info,
|
|
),
|
|
Closed,
|
|
}
|
|
|
|
pub struct Device {
|
|
name: String,
|
|
device: Arc<rusb::Device<GlobalContext>>,
|
|
state: DeviceState,
|
|
needs_drivers: bool,
|
|
}
|
|
lazy_static::lazy_static! {
|
|
static ref DEVICES: RwLock<HashMap<(u8, u8), Device>> = RwLock::new(HashMap::new());
|
|
}
|
|
struct DeviceMon<R: Runtime> {
|
|
window: Window<R>,
|
|
}
|
|
|
|
impl<R: Runtime> Hotplug<GlobalContext> for DeviceMon<R> {
|
|
fn device_arrived(&mut self, device: rusb::Device<GlobalContext>) {
|
|
let handle = self.window.clone();
|
|
let is_cx_ii = device
|
|
.device_descriptor()
|
|
.map(|d| d.product_id() == PID_CX2)
|
|
.unwrap_or(false);
|
|
let device = Arc::new(device);
|
|
std::thread::spawn(move || loop {
|
|
match add_device(device.clone()) {
|
|
Ok(dev) => {
|
|
let name = (dev.1).name.clone();
|
|
let needs_drivers = (dev.1).needs_drivers;
|
|
DEVICES.write().unwrap().insert(dev.0, dev.1);
|
|
if let Err(msg) = handle.emit(
|
|
"addDevice",
|
|
AddDevice {
|
|
dev: DevId {
|
|
bus_number: (dev.0).0,
|
|
address: (dev.0).1,
|
|
},
|
|
name,
|
|
is_cx_ii,
|
|
needs_drivers,
|
|
},
|
|
) {
|
|
eprintln!("{}", msg);
|
|
};
|
|
return;
|
|
}
|
|
Err(rusb::Error::Busy) => {
|
|
println!("busy");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("{}", e);
|
|
return;
|
|
}
|
|
}
|
|
std::thread::sleep(Duration::from_millis(250));
|
|
});
|
|
}
|
|
|
|
fn device_left(&mut self, device: rusb::Device<GlobalContext>) {
|
|
if let Some((dev, _)) = DEVICES
|
|
.write()
|
|
.unwrap()
|
|
.remove_entry(&(device.bus_number(), device.address()))
|
|
{
|
|
if let Err(msg) = self.window.emit(
|
|
"removeDevice",
|
|
DevId {
|
|
bus_number: dev.0,
|
|
address: dev.1,
|
|
},
|
|
) {
|
|
eprintln!("{}", msg);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
fn err_wrap<T, R: Runtime>(
|
|
res: Result<T, libnspire::Error>,
|
|
dev: DevId,
|
|
window: &Window<R>,
|
|
) -> Result<T, libnspire::Error> {
|
|
if let Err(libnspire::Error::NoDevice) = res {
|
|
DEVICES
|
|
.write()
|
|
.unwrap()
|
|
.remove(&(dev.bus_number, dev.address));
|
|
if let Err(msg) = window.emit("removeDevice", dev) {
|
|
eprintln!("{}", msg);
|
|
};
|
|
}
|
|
res
|
|
}
|
|
|
|
fn progress_sender<R: Runtime>(
|
|
window: &Window<R>,
|
|
dev: DevId,
|
|
total: usize,
|
|
) -> impl FnMut(usize) + '_ {
|
|
let mut i = 0;
|
|
move |remaining| {
|
|
if i > 5 {
|
|
i = 0;
|
|
}
|
|
if i == 0 || remaining == 0 {
|
|
if let Err(msg) = window.emit(
|
|
"progress",
|
|
ProgressUpdate {
|
|
dev,
|
|
remaining,
|
|
total,
|
|
},
|
|
) {
|
|
eprintln!("{}", msg);
|
|
};
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
fn get_open_dev(
|
|
dev: &DevId,
|
|
) -> Result<Arc<Mutex<libnspire::Handle<GlobalContext>>>, anyhow::Error> {
|
|
if let Some(dev) = DEVICES.read().unwrap().get(&(dev.bus_number, dev.address)) {
|
|
match &dev.state {
|
|
DeviceState::Open(handle, _) => Ok(handle.clone()),
|
|
DeviceState::Closed => anyhow::bail!("Device closed"),
|
|
}
|
|
} else {
|
|
anyhow::bail!("Failed to find device");
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct SerializedError(String);
|
|
|
|
impl<T: std::fmt::Display> From<T> for SerializedError {
|
|
fn from(f: T) -> Self {
|
|
SerializedError(f.to_string())
|
|
}
|
|
}
|
|
|
|
mod invoked {
|
|
use std::fs::File;
|
|
use std::io::{Read, Write};
|
|
use std::path::PathBuf;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use libnspire::dir::EntryType;
|
|
use serde::Serialize;
|
|
use tauri::{Runtime, Window};
|
|
|
|
use crate::cmd::{DevId, FileInfo};
|
|
use crate::{err_wrap, get_open_dev, progress_sender, DeviceState, SerializedError};
|
|
|
|
use super::DEVICES;
|
|
|
|
#[tauri::command]
|
|
pub fn open_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
|
|
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());
|
|
};
|
|
let handle = libnspire::Handle::new(device.open()?)?;
|
|
let info = handle.info()?;
|
|
{
|
|
let mut guard = DEVICES.write().unwrap();
|
|
let device = guard
|
|
.get_mut(&(bus_number, address))
|
|
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
|
device.state = DeviceState::Open(Arc::new(Mutex::new(handle)), info.clone());
|
|
}
|
|
Ok(info)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn close_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
|
|
let mut guard = DEVICES.write().unwrap();
|
|
let device = guard
|
|
.get_mut(&(bus_number, address))
|
|
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
|
device.state = DeviceState::Closed;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn update_device<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
let info = err_wrap(handle.info(), dev, &window)?;
|
|
Ok(info)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn list_dir<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
let dir = err_wrap(handle.list_dir(&path), dev, &window)?;
|
|
|
|
Ok(
|
|
dir
|
|
.iter()
|
|
.map(|file| FileInfo {
|
|
path: file.name().to_string_lossy().to_string(),
|
|
is_dir: file.entry_type() == EntryType::Directory,
|
|
date: file.date(),
|
|
size: file.size(),
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn download_file<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: (String, u64),
|
|
dest: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let (file, size) = path;
|
|
let dest = PathBuf::from(dest);
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
let mut buf = vec![0; size as usize];
|
|
err_wrap(
|
|
handle.read_file(
|
|
&file,
|
|
&mut buf,
|
|
&mut progress_sender(&window, dev, size as usize),
|
|
),
|
|
dev,
|
|
&window,
|
|
)?;
|
|
if let Some(name) = file.split('/').last() {
|
|
File::create(dest.join(name))?.write_all(&buf)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn upload_file<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: String,
|
|
src: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let file = PathBuf::from(src);
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
let mut buf = vec![];
|
|
File::open(&file)?.read_to_end(&mut buf)?;
|
|
let name = file
|
|
.file_name()
|
|
.ok_or_else(|| anyhow::anyhow!("Failed to get file name"))?
|
|
.to_string_lossy()
|
|
.to_string();
|
|
err_wrap(
|
|
handle.write_file(
|
|
&format!("{}/{}", path, name),
|
|
&buf,
|
|
&mut progress_sender(&window, dev, buf.len()),
|
|
),
|
|
dev,
|
|
&window,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn upload_os<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
src: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
let mut buf = vec![];
|
|
File::open(&src)?.read_to_end(&mut buf)?;
|
|
err_wrap(
|
|
handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())),
|
|
dev,
|
|
&window,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn delete_file<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
err_wrap(handle.delete_file(&path), dev, &window)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn delete_dir<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
err_wrap(handle.delete_dir(&path), dev, &window)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn create_nspire_dir<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
path: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
err_wrap(handle.create_dir(&path), dev, &window)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn move_file<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
src: String,
|
|
dest: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
err_wrap(handle.move_file(&src, &dest), dev, &window)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn copy<R: Runtime>(
|
|
bus_number: u8,
|
|
address: u8,
|
|
src: String,
|
|
dest: String,
|
|
window: Window<R>,
|
|
) -> Result<impl Serialize, SerializedError> {
|
|
let dev = DevId {
|
|
bus_number,
|
|
address,
|
|
};
|
|
let handle = get_open_dev(&dev)?;
|
|
let handle = handle.lock().unwrap();
|
|
err_wrap(handle.copy_file(&src, &dest), dev, &window)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
if cli::run() {
|
|
return;
|
|
}
|
|
let has_registered_callback = AtomicBool::new(false);
|
|
tauri::Builder::default()
|
|
.on_page_load(move |window, _p| {
|
|
if !has_registered_callback.swap(true, Ordering::SeqCst) {
|
|
if rusb::has_hotplug() {
|
|
if let Err(msg) = GlobalContext::default().register_callback(
|
|
Some(VID),
|
|
None,
|
|
None,
|
|
Box::new(DeviceMon { window }),
|
|
) {
|
|
eprintln!("{}", msg);
|
|
};
|
|
std::thread::spawn(|| loop {
|
|
GlobalContext::default().handle_events(None).unwrap();
|
|
});
|
|
} else {
|
|
println!("no hotplug");
|
|
}
|
|
}
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
cmd::enumerate,
|
|
invoked::open_device,
|
|
invoked::close_device,
|
|
invoked::update_device,
|
|
invoked::list_dir,
|
|
invoked::download_file,
|
|
invoked::upload_file,
|
|
invoked::upload_os,
|
|
invoked::delete_file,
|
|
invoked::delete_dir,
|
|
invoked::create_nspire_dir,
|
|
invoked::move_file,
|
|
invoked::copy,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|