mirror of
https://github.com/rustdesk/rustdesk-server.git
synced 2026-03-10 05:53:13 +08:00
UI
This commit is contained in:
5
ui/src/adapter/mod.rs
Normal file
5
ui/src/adapter/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod view;
|
||||
pub mod service;
|
||||
|
||||
pub use view::*;
|
||||
pub use service::*;
|
||||
3
ui/src/adapter/service/mod.rs
Normal file
3
ui/src/adapter/service/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod windows;
|
||||
|
||||
pub use windows::*;
|
||||
130
ui/src/adapter/service/windows.rs
Normal file
130
ui/src/adapter/service/windows.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::{ffi::OsStr, process::Command};
|
||||
|
||||
use crate::{path, usecase::service::*};
|
||||
use derive_new::new;
|
||||
use windows_service::{
|
||||
service::ServiceAccess,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
#[derive(Debug, new)]
|
||||
pub struct WindowsDesktopService {
|
||||
#[new(value = "DesktopServiceState::Stopped")]
|
||||
pub state: DesktopServiceState,
|
||||
}
|
||||
|
||||
impl IDesktopService for WindowsDesktopService {
|
||||
fn start(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"%nssm% remove hbbr confirm",
|
||||
"%nssm% stop hbbs",
|
||||
"%nssm% remove hbbs confirm",
|
||||
"mkdir logs",
|
||||
"echo.",
|
||||
"service\\run.cmd hbbs",
|
||||
"echo.",
|
||||
"service\\run.cmd hbbr",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn stop(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"%nssm% remove hbbr confirm",
|
||||
"echo.",
|
||||
"%nssm% stop hbbs",
|
||||
"%nssm% remove hbbs confirm",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn restart(&mut self) {
|
||||
nssm(["restart", "hbbs"].map(|x| x.to_owned()));
|
||||
nssm(["restart", "hbbr"].map(|x| x.to_owned()));
|
||||
self.check();
|
||||
}
|
||||
fn pause(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"echo.",
|
||||
"%nssm% stop hbbs",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn check(&mut self) -> DesktopServiceState {
|
||||
self.state = match service_status("hbbs").as_str() {
|
||||
"Running" => DesktopServiceState::Started,
|
||||
// "Stopped" => DeskServerServiceState::Paused,
|
||||
_ => DesktopServiceState::Stopped,
|
||||
};
|
||||
self.state.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn call(cmd: String) {
|
||||
Command::new("cmd")
|
||||
.current_dir(&path())
|
||||
.env("nssm", "service\\nssm.exe")
|
||||
.arg("/c")
|
||||
.arg("start")
|
||||
.arg("cmd")
|
||||
.arg("/c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.expect("cmd exec error!");
|
||||
}
|
||||
|
||||
fn exec<I, S>(program: S, args: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
match Command::new(program).args(args).output() {
|
||||
Ok(out) => String::from_utf8(out.stdout).unwrap_or("".to_owned()),
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn nssm<I>(args: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
exec(
|
||||
format!("{}\\service\\nssm.exe", path().to_str().unwrap_or_default()),
|
||||
args,
|
||||
)
|
||||
.replace("\0", "")
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn service_status(name: &str) -> String {
|
||||
match ServiceManager::local_computer(None::<&OsStr>, ServiceManagerAccess::CONNECT) {
|
||||
Ok(manager) => match manager.open_service(name, ServiceAccess::QUERY_STATUS) {
|
||||
Ok(service) => match service.query_status() {
|
||||
Ok(status) => format!("{:?}", status.current_state),
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
}
|
||||
220
ui/src/adapter/view/desktop.rs
Normal file
220
ui/src/adapter/view/desktop.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use std::{
|
||||
process::exit,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
path,
|
||||
usecase::{view::Event, DesktopServiceState},
|
||||
BUFFER,
|
||||
};
|
||||
use async_std::task::sleep;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use tauri::{
|
||||
CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
||||
SystemTrayMenuItem, WindowEvent,
|
||||
};
|
||||
|
||||
pub async fn run(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
let setup_sender = sender.clone();
|
||||
let menu_sender = sender.clone();
|
||||
let tray_sender = sender.clone();
|
||||
let menu = Menu::new()
|
||||
.add_submenu(Submenu::new(
|
||||
"Service",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("restart", "Restart"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("start", "Start"))
|
||||
.add_item(CustomMenuItem::new("stop", "Stop")),
|
||||
))
|
||||
.add_submenu(Submenu::new(
|
||||
"Logs",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("hbbs.out", "hbbs.out"))
|
||||
.add_item(CustomMenuItem::new("hbbs.err", "hbbs.err"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("hbbr.out", "hbbr.out"))
|
||||
.add_item(CustomMenuItem::new("hbbr.err", "hbbr.err")),
|
||||
))
|
||||
.add_submenu(Submenu::new(
|
||||
"Configuration",
|
||||
Menu::new().add_item(CustomMenuItem::new(".env", ".env")),
|
||||
));
|
||||
let tray = SystemTray::new().with_menu(
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart", "Restart"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("start", "Start"))
|
||||
.add_item(CustomMenuItem::new("stop", "Stop"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("exit", "Exit GUI")),
|
||||
);
|
||||
let mut app = tauri::Builder::default()
|
||||
.on_window_event(|event| match event.event() {
|
||||
// WindowEvent::Resized(size) => {
|
||||
// if size.width == 0 && size.height == 0 {
|
||||
// event.window().hide().unwrap();
|
||||
// }
|
||||
// }
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
event.window().hide().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.menu(menu)
|
||||
.on_menu_event(move |event| {
|
||||
// println!(
|
||||
// "send {}: {}",
|
||||
// std::time::SystemTime::now()
|
||||
// .duration_since(std::time::UNIX_EPOCH)
|
||||
// .unwrap_or_default()
|
||||
// .as_millis(),
|
||||
// event.menu_item_id()
|
||||
// );
|
||||
menu_sender
|
||||
.send(Event::ViewAction(event.menu_item_id().to_owned()))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.system_tray(tray)
|
||||
.on_system_tray_event(move |app, event| match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let main = app.get_window("main").unwrap();
|
||||
if main.is_visible().unwrap() {
|
||||
main.hide().unwrap();
|
||||
} else {
|
||||
main.show().unwrap();
|
||||
main.unminimize().unwrap();
|
||||
main.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||
tray_sender.send(Event::ViewAction(id)).unwrap_or_default();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.setup(move |app| {
|
||||
setup_sender.send(Event::ViewInit).unwrap_or_default();
|
||||
app.listen_global("__action__", move |msg| {
|
||||
match msg.payload().unwrap_or_default() {
|
||||
r#""__init__""# => setup_sender.send(Event::BroswerInit).unwrap_or_default(),
|
||||
r#""restart""# => setup_sender
|
||||
.send(Event::BrowserAction("restart".to_owned()))
|
||||
.unwrap_or_default(),
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![root])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
let mut now = Instant::now();
|
||||
let mut blink = false;
|
||||
let mut span = 0;
|
||||
let mut title = "".to_owned();
|
||||
let product = "RustDesk Server";
|
||||
let buffer = BUFFER.get().unwrap().to_owned();
|
||||
loop {
|
||||
for _ in 1..buffer {
|
||||
match receiver.recv_timeout(Duration::from_nanos(1)) {
|
||||
Ok(event) => {
|
||||
let main = app.get_window("main").unwrap();
|
||||
let menu = main.menu_handle();
|
||||
let tray = app.tray_handle();
|
||||
match event {
|
||||
Event::BrowserUpdate((action, data)) => match action.as_str() {
|
||||
"file" => {
|
||||
let list = ["hbbs.out", "hbbs.err", "hbbr.out", "hbbr.err", ".env"];
|
||||
let id = data.as_str();
|
||||
if list.contains(&id) {
|
||||
for file in list {
|
||||
menu.get_item(file)
|
||||
.set_selected(file == id)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
// println!(
|
||||
// "emit {}: {}",
|
||||
// std::time::SystemTime::now()
|
||||
// .duration_since(std::time::UNIX_EPOCH)
|
||||
// .unwrap_or_default()
|
||||
// .as_millis(),
|
||||
// data
|
||||
// );
|
||||
app.emit_all("__update__", (action, data))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::ViewRenderAppExit => exit(0),
|
||||
Event::ViewRenderServiceState(state) => {
|
||||
let enabled = |id, enabled| {
|
||||
menu.get_item(id).set_enabled(enabled).unwrap_or_default();
|
||||
tray.get_item(id).set_enabled(enabled).unwrap_or_default();
|
||||
};
|
||||
title = format!("{} {:?}", product, state);
|
||||
main.set_title(title.as_str()).unwrap_or_default();
|
||||
match state {
|
||||
DesktopServiceState::Started => {
|
||||
enabled("start", false);
|
||||
enabled("stop", true);
|
||||
enabled("restart", true);
|
||||
blink = false;
|
||||
}
|
||||
DesktopServiceState::Stopped => {
|
||||
enabled("start", true);
|
||||
enabled("stop", false);
|
||||
enabled("restart", false);
|
||||
blink = true;
|
||||
}
|
||||
_ => {
|
||||
enabled("start", false);
|
||||
enabled("stop", false);
|
||||
enabled("restart", false);
|
||||
blink = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
let elapsed = now.elapsed().as_micros();
|
||||
if elapsed > 16666 {
|
||||
now = Instant::now();
|
||||
// println!("{}ms", elapsed as f64 * 0.001);
|
||||
let iteration = app.run_iteration();
|
||||
if iteration.window_count == 0 {
|
||||
break;
|
||||
}
|
||||
if blink {
|
||||
if span > 1000000 {
|
||||
span = 0;
|
||||
app.get_window("main")
|
||||
.unwrap()
|
||||
.set_title(title.as_str())
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
span += elapsed;
|
||||
if span > 500000 {
|
||||
app.get_window("main")
|
||||
.unwrap()
|
||||
.set_title(product)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sleep(Duration::from_micros(999)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn root() -> String {
|
||||
path().to_str().unwrap_or_default().to_owned()
|
||||
}
|
||||
3
ui/src/adapter/view/mod.rs
Normal file
3
ui/src/adapter/view/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod desktop;
|
||||
|
||||
pub use desktop::*;
|
||||
17
ui/src/lib.rs
Normal file
17
ui/src/lib.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::{env::current_exe, path::PathBuf};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
pub mod adapter;
|
||||
pub mod usecase;
|
||||
|
||||
pub static BUFFER: OnceCell<usize> = OnceCell::new();
|
||||
|
||||
pub fn path() -> PathBuf {
|
||||
current_exe()
|
||||
.unwrap_or_default()
|
||||
.as_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
25
ui/src/main.rs
Normal file
25
ui/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use async_std::{
|
||||
prelude::FutureExt,
|
||||
task::{spawn, spawn_local},
|
||||
};
|
||||
use crossbeam_channel::bounded;
|
||||
use rustdesk_server::{
|
||||
usecase::{presenter, view, watcher},
|
||||
BUFFER,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
let buffer = BUFFER.get_or_init(|| 10).to_owned();
|
||||
let (view_sender, presenter_receiver) = bounded(buffer);
|
||||
let (presenter_sender, view_receiver) = bounded(buffer);
|
||||
spawn_local(view::create(presenter_sender.clone(), presenter_receiver))
|
||||
.join(spawn(presenter::create(view_sender, view_receiver)))
|
||||
.join(spawn(watcher::create(presenter_sender)))
|
||||
.await;
|
||||
}
|
||||
9
ui/src/usecase/mod.rs
Normal file
9
ui/src/usecase/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod presenter;
|
||||
pub mod service;
|
||||
pub mod view;
|
||||
pub mod watcher;
|
||||
|
||||
pub use presenter::*;
|
||||
pub use service::*;
|
||||
pub use view::*;
|
||||
pub use watcher::*;
|
||||
59
ui/src/usecase/presenter.rs
Normal file
59
ui/src/usecase/presenter.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{service, DesktopServiceState, Event};
|
||||
use crate::BUFFER;
|
||||
use async_std::task::sleep;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
let mut now = Instant::now();
|
||||
let buffer = BUFFER.get().unwrap().to_owned();
|
||||
let send = |event| sender.send(event).unwrap_or_default();
|
||||
if let Some(mut service) = service::create() {
|
||||
let mut service_state = DesktopServiceState::Unknown;
|
||||
let mut file = "hbbs.out".to_owned();
|
||||
send(Event::ViewRenderServiceState(service_state.to_owned()));
|
||||
loop {
|
||||
for _ in 1..buffer {
|
||||
match receiver.recv_timeout(Duration::from_nanos(1)) {
|
||||
Ok(event) => match event {
|
||||
Event::BroswerInit => {
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
Event::BrowserAction(action) => match action.as_str() {
|
||||
"restart" => service.restart(),
|
||||
_ => (),
|
||||
},
|
||||
Event::FileChange(path) => {
|
||||
if path == file {
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
}
|
||||
Event::ViewAction(action) => match action.as_str() {
|
||||
"start" => service.start(),
|
||||
"stop" => service.stop(),
|
||||
"restart" => service.restart(),
|
||||
"pause" => service.pause(),
|
||||
"exit" => send(Event::ViewRenderAppExit),
|
||||
_ => {
|
||||
file = action;
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
},
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_micros(999)).await;
|
||||
if now.elapsed().as_millis() > 999 {
|
||||
let state = service.check();
|
||||
if state != service_state {
|
||||
service_state = state.to_owned();
|
||||
send(Event::ViewRenderServiceState(state));
|
||||
}
|
||||
now = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
ui/src/usecase/service.rs
Normal file
24
ui/src/usecase/service.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::adapter;
|
||||
|
||||
pub fn create() -> Option<Box<dyn IDesktopService + Send>> {
|
||||
if cfg!(target_os = "windows") {
|
||||
return Some(Box::new(adapter::WindowsDesktopService::new()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DesktopServiceState {
|
||||
Paused,
|
||||
Started,
|
||||
Stopped,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub trait IDesktopService {
|
||||
fn start(&mut self);
|
||||
fn stop(&mut self);
|
||||
fn restart(&mut self);
|
||||
fn pause(&mut self);
|
||||
fn check(&mut self) -> DesktopServiceState;
|
||||
}
|
||||
22
ui/src/usecase/view.rs
Normal file
22
ui/src/usecase/view.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use super::DesktopServiceState;
|
||||
use crate::adapter::desktop;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
desktop::run(sender, receiver).await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
BrowserAction(String),
|
||||
BroswerInit,
|
||||
BrowserUpdate((String, String)),
|
||||
BrowserRender(String),
|
||||
FileChange(String),
|
||||
ViewAction(String),
|
||||
ViewInit,
|
||||
ViewUpdate(String),
|
||||
ViewRender(String),
|
||||
ViewRenderAppExit,
|
||||
ViewRenderServiceState(DesktopServiceState),
|
||||
}
|
||||
46
ui/src/usecase/watcher.rs
Normal file
46
ui/src/usecase/watcher.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use super::Event;
|
||||
use crate::path;
|
||||
use async_std::task::{sleep, spawn_blocking};
|
||||
use crossbeam_channel::{bounded, Sender};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Result, Watcher};
|
||||
|
||||
pub async fn create(sender: Sender<Event>) {
|
||||
loop {
|
||||
let watch_sender = sender.clone();
|
||||
match spawn_blocking(|| {
|
||||
watch(
|
||||
format!("{}/logs/", path().to_str().unwrap_or_default()),
|
||||
watch_sender,
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(e) => println!("error: {e}"),
|
||||
}
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn watch<P: AsRef<Path>>(path: P, sender: Sender<Event>) -> Result<()> {
|
||||
let (tx, rx) = bounded(10);
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
|
||||
for res in rx {
|
||||
let event = res?;
|
||||
for p in event.paths {
|
||||
let path = p
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
if path.len() > 0 {
|
||||
sender.send(Event::FileChange(path)).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user