mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-10 06:41:10 +03:00
source code
This commit is contained in:
94
src/cli.rs
Normal file
94
src/cli.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use crate::client::*;
|
||||
use hbb_common::{
|
||||
config::PeerConfig,
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
tokio::{self, sync::mpsc},
|
||||
Stream,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
id: String,
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
||||
let mut password = "".to_owned();
|
||||
if PeerConfig::load(id).password.is_empty() {
|
||||
password = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap();
|
||||
}
|
||||
let session = Self {
|
||||
id: id.to_owned(),
|
||||
sender,
|
||||
password,
|
||||
lc: Default::default(),
|
||||
};
|
||||
session
|
||||
.lc
|
||||
.write()
|
||||
.unwrap()
|
||||
.initialize(id.to_owned(), false, true);
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interface for Session {
|
||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str) {
|
||||
if msgtype == "input-password" {
|
||||
self.sender
|
||||
.send(Data::Login((self.password.clone(), true)))
|
||||
.ok();
|
||||
} else if msgtype == "re-input-password" {
|
||||
log::error!("{}: {}", title, text);
|
||||
let pass = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap();
|
||||
self.sender.send(Data::Login((pass, true))).ok();
|
||||
} else if msgtype.contains("error") {
|
||||
log::error!("{}: {}: {}", msgtype, title, text);
|
||||
} else {
|
||||
log::info!("{}: {}: {}", msgtype, title, text);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_login_error(&mut self, err: &str) -> bool {
|
||||
self.lc.write().unwrap().handle_login_error(err, self)
|
||||
}
|
||||
|
||||
fn handle_peer_info(&mut self, pi: PeerInfo) {
|
||||
let username = self.lc.read().unwrap().get_username(&pi);
|
||||
self.lc.write().unwrap().handle_peer_info(username, pi);
|
||||
}
|
||||
|
||||
async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) {
|
||||
handle_hash(self.lc.clone(), hash, self, peer).await;
|
||||
}
|
||||
|
||||
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) {
|
||||
handle_login_from_ui(self.lc.clone(), password, remember, peer).await;
|
||||
}
|
||||
|
||||
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) {
|
||||
handle_test_delay(t, peer).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
pub async fn start_one_port_forward(id: String, port: i32, remote_host: String, remote_port: i32) {
|
||||
crate::common::test_rendezvous_server();
|
||||
crate::common::test_nat_type();
|
||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
||||
let handler = Session::new(&id, sender);
|
||||
handler.lc.write().unwrap().port_forward = (remote_host, remote_port);
|
||||
if let Err(err) =
|
||||
crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await
|
||||
{
|
||||
log::error!("Failed to listen on {}: {}", port, err);
|
||||
}
|
||||
log::info!("port forward (:{}) exit", port);
|
||||
}
|
||||
1113
src/client.rs
Normal file
1113
src/client.rs
Normal file
File diff suppressed because it is too large
Load Diff
365
src/common.rs
Normal file
365
src/common.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
pub use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
compress::{compress as compress_func, decompress},
|
||||
config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
protobuf::ProtobufEnum,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::FramedStream,
|
||||
tokio, ResultType,
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
||||
pub const CLIPBOARD_INTERVAL: u64 = 333;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CONTENT: Arc<Mutex<String>> = Default::default();
|
||||
pub static ref SOFTWARE_UPDATE_URL: Arc<Mutex<String>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref MOBILE_INFO1: Arc<Mutex<String>> = Default::default();
|
||||
pub static ref MOBILE_INFO2: Arc<Mutex<String>> = Default::default();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn valid_for_numlock(evt: &KeyEvent) -> bool {
|
||||
if let Some(key_event::Union::control_key(ck)) = evt.union {
|
||||
let v = ck.value();
|
||||
(v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value())
|
||||
|| v == ControlKey::Decimal.value()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn valid_for_capslock(evt: &KeyEvent) -> bool {
|
||||
if let Some(key_event::Union::chr(ch)) = evt.union {
|
||||
ch >= 'a' as u32 && ch <= 'z' as u32
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_clipboard(
|
||||
ctx: &mut ClipboardContext,
|
||||
old: Option<&Arc<Mutex<String>>>,
|
||||
) -> Option<Message> {
|
||||
let side = if old.is_none() { "host" } else { "client" };
|
||||
let old = if let Some(old) = old { old } else { &CONTENT };
|
||||
if let Ok(content) = ctx.get_contents() {
|
||||
if content.len() < 2_000_000 && !content.is_empty() {
|
||||
let changed = content != *old.lock().unwrap();
|
||||
if changed {
|
||||
log::info!("{} update found on {}", CLIPBOARD_NAME, side);
|
||||
let bytes = content.clone().into_bytes();
|
||||
*old.lock().unwrap() = content;
|
||||
let compressed = compress_func(&bytes, COMPRESS_LEVEL);
|
||||
let compress = compressed.len() < bytes.len();
|
||||
let content = if compress { compressed } else { bytes };
|
||||
let mut msg = Message::new();
|
||||
msg.set_clipboard(Clipboard {
|
||||
compress,
|
||||
content,
|
||||
..Default::default()
|
||||
});
|
||||
return Some(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>) {
|
||||
let content = if clipboard.compress {
|
||||
decompress(&clipboard.content)
|
||||
} else {
|
||||
clipboard.content
|
||||
};
|
||||
if let Ok(content) = String::from_utf8(content) {
|
||||
match ClipboardContext::new() {
|
||||
Ok(mut ctx) => {
|
||||
let side = if old.is_none() { "host" } else { "client" };
|
||||
let old = if let Some(old) = old { old } else { &CONTENT };
|
||||
*old.lock().unwrap() = content.clone();
|
||||
allow_err!(ctx.set_contents(content));
|
||||
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to create clipboard context: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resample_channels(
|
||||
data: &[f32],
|
||||
sample_rate0: u32,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
) -> Vec<f32> {
|
||||
use dasp::{interpolate::linear::Linear, signal, Signal};
|
||||
let n = data.len() / (channels as usize);
|
||||
let n = n * sample_rate as usize / sample_rate0 as usize;
|
||||
if channels == 2 {
|
||||
let mut source = signal::from_interleaved_samples_iter::<_, [_; 2]>(data.iter().cloned());
|
||||
let a = source.next();
|
||||
let b = source.next();
|
||||
let interp = Linear::new(a, b);
|
||||
let mut data = Vec::with_capacity(n << 1);
|
||||
for x in source
|
||||
.from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _)
|
||||
.take(n)
|
||||
{
|
||||
data.push(x[0]);
|
||||
data.push(x[1]);
|
||||
}
|
||||
data
|
||||
} else {
|
||||
let mut source = signal::from_iter(data.iter().cloned());
|
||||
let a = source.next();
|
||||
let b = source.next();
|
||||
let interp = Linear::new(a, b);
|
||||
source
|
||||
.from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _)
|
||||
.take(n)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_nat_type() {
|
||||
std::thread::spawn(move || loop {
|
||||
match test_nat_type_() {
|
||||
Ok(true) => break,
|
||||
Err(err) => {
|
||||
log::error!("test nat: {}", err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if Config::get_nat_type() != 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(12));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn test_nat_type_() -> ResultType<bool> {
|
||||
let start = std::time::Instant::now();
|
||||
let rendezvous_server = get_rendezvous_server(100).await;
|
||||
let server1 = rendezvous_server;
|
||||
let mut server2 = server1;
|
||||
server2.set_port(server1.port() - 1);
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
let serial = Config::get_serial();
|
||||
msg_out.set_test_nat_request(TestNatRequest {
|
||||
serial,
|
||||
..Default::default()
|
||||
});
|
||||
let mut port1 = 0;
|
||||
let mut port2 = 0;
|
||||
let mut addr = Config::get_any_listen_addr();
|
||||
for i in 0..2 {
|
||||
let mut socket = FramedStream::new(
|
||||
if i == 0 { &server1 } else { &server2 },
|
||||
addr,
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
addr = socket.get_ref().local_addr()?;
|
||||
socket.send(&msg_out).await?;
|
||||
if let Some(Ok(bytes)) = socket.next_timeout(3000).await {
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
if let Some(rendezvous_message::Union::test_nat_response(tnr)) = msg_in.union {
|
||||
if i == 0 {
|
||||
port1 = tnr.port;
|
||||
} else {
|
||||
port2 = tnr.port;
|
||||
}
|
||||
if let Some(cu) = tnr.cu.as_ref() {
|
||||
Config::set_option(
|
||||
"rendezvous-servers".to_owned(),
|
||||
cu.rendezvous_servers.join(","),
|
||||
);
|
||||
Config::set_serial(cu.serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let ok = port1 > 0 && port2 > 0;
|
||||
if ok {
|
||||
let t = if port1 == port2 {
|
||||
NatType::ASYMMETRIC
|
||||
} else {
|
||||
NatType::SYMMETRIC
|
||||
};
|
||||
Config::set_nat_type(t as _);
|
||||
log::info!("tested nat type: {:?} in {:?}", t, start.elapsed());
|
||||
}
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub async fn get_rendezvous_server(_ms_timeout: u64) -> std::net::SocketAddr {
|
||||
Config::get_rendezvous_server()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> std::net::SocketAddr {
|
||||
crate::ipc::get_rendezvous_server(ms_timeout).await
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub async fn get_nat_type(_ms_timeout: u64) -> i32 {
|
||||
Config::get_nat_type()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub async fn get_nat_type(ms_timeout: u64) -> i32 {
|
||||
crate::ipc::get_nat_type(ms_timeout).await
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn test_rendezvous_server_() {
|
||||
let servers = Config::get_rendezvous_servers();
|
||||
let mut futs = Vec::new();
|
||||
for host in servers {
|
||||
futs.push(tokio::spawn(async move {
|
||||
let tm = std::time::Instant::now();
|
||||
if FramedStream::new(
|
||||
&crate::check_port(&host, RENDEZVOUS_PORT),
|
||||
Config::get_any_listen_addr(),
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let elapsed = tm.elapsed().as_micros();
|
||||
Config::update_latency(&host, elapsed as _);
|
||||
} else {
|
||||
Config::update_latency(&host, -1);
|
||||
}
|
||||
}));
|
||||
}
|
||||
join_all(futs).await;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
pub fn test_rendezvous_server() {
|
||||
std::thread::spawn(test_rendezvous_server_);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_time() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0) as _
|
||||
}
|
||||
|
||||
pub fn run_me<T: AsRef<std::ffi::OsStr>>(args: Vec<T>) -> std::io::Result<std::process::Child> {
|
||||
let cmd = std::env::current_exe()?;
|
||||
return std::process::Command::new(cmd).args(&args).spawn();
|
||||
}
|
||||
|
||||
pub fn username() -> String {
|
||||
// fix bug of whoami
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return whoami::username().trim_end_matches('\0').to_owned();
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return MOBILE_INFO2.lock().unwrap().clone();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
|
||||
let host = host.to_string();
|
||||
if !host.contains(":") {
|
||||
return format!("{}:{}", host, port);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
pub const POSTFIX_SERVICE: &'static str = "_service";
|
||||
|
||||
#[inline]
|
||||
pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool {
|
||||
if let Some(key_event::Union::control_key(ck)) = evt.union {
|
||||
ck.value() == key.value()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_modifier(evt: &KeyEvent) -> bool {
|
||||
if let Some(key_event::Union::control_key(ck)) = evt.union {
|
||||
let v = ck.value();
|
||||
v == ControlKey::Alt.value()
|
||||
|| v == ControlKey::Shift.value()
|
||||
|| v == ControlKey::Control.value()
|
||||
|| v == ControlKey::Meta.value()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: String) -> String {
|
||||
let mut host = host;
|
||||
if !host.contains(":") {
|
||||
host = format!("{}:{}", host, 0);
|
||||
}
|
||||
match hbb_common::to_socket_addr(&host) {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut n = 0;
|
||||
for x in v.split(".") {
|
||||
n = n * 1000 + x.parse::<i64>().unwrap_or(0);
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
pub fn check_software_update() {
|
||||
std::thread::spawn(move || allow_err!(_check_software_update()));
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn _check_software_update() -> hbb_common::ResultType<()> {
|
||||
sleep(3.).await;
|
||||
let rendezvous_server = get_rendezvous_server(1_000).await;
|
||||
let mut socket = hbb_common::udp::FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_software_update(SoftwareUpdate {
|
||||
url: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, rendezvous_server).await?;
|
||||
use hbb_common::protobuf::Message;
|
||||
if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await {
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
if let Some(rendezvous_message::Union::software_update(su)) = msg_in.union {
|
||||
let version = hbb_common::get_version_from_url(&su.url);
|
||||
if get_version_number(&version) > get_version_number(crate::VERSION) {
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = su.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
464
src/ipc.rs
Normal file
464
src/ipc.rs
Normal file
@@ -0,0 +1,464 @@
|
||||
use hbb_common::{
|
||||
allow_err, bail, bytes,
|
||||
bytes_codec::BytesCodec,
|
||||
config::{self, Config},
|
||||
futures::StreamExt as _,
|
||||
futures_util::sink::SinkExt,
|
||||
log, timeout, tokio,
|
||||
tokio_util::codec::Framed,
|
||||
ResultType,
|
||||
};
|
||||
use parity_tokio_ipc::{Connection as Conn, Endpoint, Incoming, SecurityAttributes};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, net::SocketAddr};
|
||||
#[cfg(not(windows))]
|
||||
use std::{fs::File, io::prelude::*};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum FS {
|
||||
ReadDir {
|
||||
dir: String,
|
||||
include_hidden: bool,
|
||||
},
|
||||
RemoveDir {
|
||||
path: String,
|
||||
id: i32,
|
||||
recursive: bool,
|
||||
},
|
||||
RemoveFile {
|
||||
path: String,
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
},
|
||||
CreateDir {
|
||||
path: String,
|
||||
id: i32,
|
||||
},
|
||||
NewWrite {
|
||||
path: String,
|
||||
id: i32,
|
||||
files: Vec<(String, u64)>,
|
||||
},
|
||||
CancelWrite {
|
||||
id: i32,
|
||||
},
|
||||
WriteBlock {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
data: Vec<u8>,
|
||||
compressed: bool,
|
||||
},
|
||||
WriteDone {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum Data {
|
||||
Login {
|
||||
id: i32,
|
||||
is_file_transfer: bool,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
authorized: bool,
|
||||
port_forward: String,
|
||||
keyboard: bool,
|
||||
clipboard: bool,
|
||||
audio: bool,
|
||||
},
|
||||
ChatMessage {
|
||||
text: String,
|
||||
},
|
||||
SwitchPermission {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
},
|
||||
SystemInfo(Option<String>),
|
||||
ClickTime(i64),
|
||||
Authorize,
|
||||
Close,
|
||||
SAS,
|
||||
OnlineStatus(Option<(i64, bool)>),
|
||||
Config((String, Option<String>)),
|
||||
Options(Option<HashMap<String, String>>),
|
||||
NatType(Option<i32>),
|
||||
ConfirmedKey(Option<(Vec<u8>, Vec<u8>)>),
|
||||
RawMessage(Vec<u8>),
|
||||
FS(FS),
|
||||
Test,
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
pub async fn start(postfix: &str) -> ResultType<()> {
|
||||
if postfix.is_empty() {
|
||||
crate::common::test_nat_type();
|
||||
}
|
||||
let mut incoming = new_listener(postfix).await?;
|
||||
loop {
|
||||
if let Some(result) = incoming.next().await {
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let mut stream = Connection::new(stream);
|
||||
let postfix = postfix.to_owned();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Err(err) => {
|
||||
log::trace!("ipc{} connection closed: {}", postfix, err);
|
||||
break;
|
||||
}
|
||||
Ok(Some(data)) => {
|
||||
handle(data, &mut stream).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Couldn't get client: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
|
||||
let path = Config::ipc_path(postfix);
|
||||
#[cfg(not(windows))]
|
||||
check_pid(postfix).await;
|
||||
let mut endpoint = Endpoint::new(path.clone());
|
||||
match SecurityAttributes::allow_everyone_create() {
|
||||
Ok(attr) => endpoint.set_security_attributes(attr),
|
||||
Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err),
|
||||
};
|
||||
match endpoint.incoming() {
|
||||
Ok(incoming) => {
|
||||
log::info!("Started ipc{} server at path: {}", postfix, &path);
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
|
||||
write_pid(postfix);
|
||||
}
|
||||
Ok(incoming)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Faild to start ipc{} server at path {}: {}",
|
||||
postfix,
|
||||
path,
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(data: Data, stream: &mut Connection) {
|
||||
match data {
|
||||
Data::SystemInfo(_) => {
|
||||
let info = format!(
|
||||
"log_path: {}, config: {}, username: {}",
|
||||
Config::log_path().to_str().unwrap_or(""),
|
||||
Config::file().to_str().unwrap_or(""),
|
||||
crate::username(),
|
||||
);
|
||||
allow_err!(stream.send(&Data::SystemInfo(Some(info))).await);
|
||||
}
|
||||
Data::Close => {
|
||||
log::info!("Receive close message");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Data::OnlineStatus(_) => {
|
||||
let x = config::ONLINE
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.max()
|
||||
.unwrap_or(&0)
|
||||
.clone();
|
||||
let confirmed = Config::get_key_confirmed();
|
||||
allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await);
|
||||
}
|
||||
Data::ConfirmedKey(None) => {
|
||||
let out = if Config::get_key_confirmed() {
|
||||
Some(Config::get_key_pair())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
allow_err!(stream.send(&Data::ConfirmedKey(out)).await);
|
||||
}
|
||||
Data::Config((name, value)) => match value {
|
||||
None => {
|
||||
let value;
|
||||
if name == "id" {
|
||||
value = Some(Config::get_id());
|
||||
} else if name == "password" {
|
||||
value = Some(Config::get_password());
|
||||
} else if name == "salt" {
|
||||
value = Some(Config::get_salt());
|
||||
} else if name == "rendezvous_server" {
|
||||
value = Some(Config::get_rendezvous_server().to_string());
|
||||
} else {
|
||||
value = None;
|
||||
}
|
||||
allow_err!(stream.send(&Data::Config((name, value))).await);
|
||||
}
|
||||
Some(value) => {
|
||||
if name == "id" {
|
||||
Config::set_id(&value);
|
||||
} else if name == "password" {
|
||||
Config::set_password(&value);
|
||||
} else if name == "salt" {
|
||||
Config::set_salt(&value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
log::info!("{} updated", name);
|
||||
}
|
||||
},
|
||||
Data::Options(value) => match value {
|
||||
None => {
|
||||
let v = Config::get_options();
|
||||
allow_err!(stream.send(&Data::Options(Some(v))).await);
|
||||
}
|
||||
Some(value) => {
|
||||
Config::set_options(value);
|
||||
}
|
||||
},
|
||||
Data::NatType(_) => {
|
||||
let t = Config::get_nat_type();
|
||||
allow_err!(stream.send(&Data::NatType(Some(t))).await);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<Connection> {
|
||||
let path = Config::ipc_path(postfix);
|
||||
let client = timeout(ms_timeout, Endpoint::connect(&path)).await??;
|
||||
Ok(Connection::new(client))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(windows))]
|
||||
fn get_pid_file(postfix: &str) -> String {
|
||||
let path = Config::ipc_path(postfix);
|
||||
format!("{}.pid", path)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
async fn check_pid(postfix: &str) {
|
||||
let pid_file = get_pid_file(postfix);
|
||||
if let Ok(mut file) = File::open(&pid_file) {
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).ok();
|
||||
let pid = content.parse::<i32>().unwrap_or(0);
|
||||
if pid > 0 {
|
||||
if let Ok(p) = psutil::process::Process::new(pid as _) {
|
||||
if let Ok(current) = psutil::process::Process::current() {
|
||||
if current.name().unwrap_or("".to_owned()) == p.name().unwrap_or("".to_owned())
|
||||
{
|
||||
// double check with connect
|
||||
if connect(1000, postfix).await.is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(windows))]
|
||||
fn write_pid(postfix: &str) {
|
||||
let path = get_pid_file(postfix);
|
||||
if let Ok(mut file) = File::create(&path) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
|
||||
file.write_all(&std::process::id().to_string().into_bytes())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
inner: Framed<Conn, BytesCodec>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(conn: Conn) -> Self {
|
||||
Self {
|
||||
inner: Framed::new(conn, BytesCodec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, data: &Data) -> ResultType<()> {
|
||||
let v = serde_json::to_vec(data)?;
|
||||
self.inner.send(bytes::Bytes::from(v)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_config(&mut self, name: &str, value: String) -> ResultType<()> {
|
||||
self.send(&Data::Config((name.to_owned(), Some(value))))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn next_timeout(&mut self, ms_timeout: u64) -> ResultType<Option<Data>> {
|
||||
Ok(timeout(ms_timeout, self.next()).await??)
|
||||
}
|
||||
|
||||
pub async fn next_timeout2(&mut self, ms_timeout: u64) -> Option<ResultType<Option<Data>>> {
|
||||
if let Ok(x) = timeout(ms_timeout, self.next()).await {
|
||||
Some(x)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> ResultType<Option<Data>> {
|
||||
match self.inner.next().await {
|
||||
Some(res) => {
|
||||
let bytes = res?;
|
||||
if let Ok(s) = std::str::from_utf8(&bytes) {
|
||||
if let Ok(data) = serde_json::from_str::<Data>(s) {
|
||||
return Ok(Some(data));
|
||||
}
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {
|
||||
bail!("reset by the peer");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn get_config(name: &str) -> ResultType<Option<String>> {
|
||||
get_config_async(name, 1_000).await
|
||||
}
|
||||
|
||||
async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType<Option<String>> {
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send(&Data::Config((name.to_owned(), None))).await?;
|
||||
if let Some(Data::Config((name2, value))) = c.next_timeout(ms_timeout).await? {
|
||||
if name == name2 {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn set_config(name: &str, value: String) -> ResultType<()> {
|
||||
let mut c = connect(1000, "").await?;
|
||||
c.send_config(name, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_password(v: String) -> ResultType<()> {
|
||||
Config::set_password(&v);
|
||||
set_config("password", v)
|
||||
}
|
||||
|
||||
pub fn get_id() -> String {
|
||||
if let Ok(Some(v)) = get_config("id") {
|
||||
// update salt also, so that nexttime reinstallation not causing first-time auto-login failure
|
||||
if let Ok(Some(v2)) = get_config("salt") {
|
||||
Config::set_salt(&v2);
|
||||
}
|
||||
if v != Config::get_id() {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&v);
|
||||
}
|
||||
v
|
||||
} else {
|
||||
Config::get_id()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_password() -> String {
|
||||
if let Ok(Some(v)) = get_config("password") {
|
||||
Config::set_password(&v);
|
||||
v
|
||||
} else {
|
||||
Config::get_password()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> SocketAddr {
|
||||
if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await {
|
||||
if let Ok(v) = v.parse() {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return Config::get_rendezvous_server();
|
||||
}
|
||||
|
||||
async fn get_options_(ms_timeout: u64) -> ResultType<HashMap<String, String>> {
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send(&Data::Options(None)).await?;
|
||||
if let Some(Data::Options(Some(value))) = c.next_timeout(ms_timeout).await? {
|
||||
Config::set_options(value.clone());
|
||||
Ok(value)
|
||||
} else {
|
||||
Ok(Config::get_options())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
pub async fn get_options() -> HashMap<String, String> {
|
||||
get_options_(1000).await.unwrap_or(Config::get_options())
|
||||
}
|
||||
|
||||
pub fn get_option(key: &str) -> String {
|
||||
if let Some(v) = get_options().get(key) {
|
||||
v.clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_option(key: &str, value: &str) {
|
||||
let mut options = get_options();
|
||||
if value.is_empty() {
|
||||
options.remove(key);
|
||||
} else {
|
||||
options.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
set_options(options).ok();
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
pub async fn set_options(value: HashMap<String, String>) -> ResultType<()> {
|
||||
Config::set_options(value.clone());
|
||||
connect(1000, "")
|
||||
.await?
|
||||
.send(&Data::Options(Some(value)))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn get_nat_type_(ms_timeout: u64) -> ResultType<i32> {
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send(&Data::NatType(None)).await?;
|
||||
if let Some(Data::NatType(Some(value))) = c.next_timeout(ms_timeout).await? {
|
||||
Config::set_nat_type(value);
|
||||
Ok(value)
|
||||
} else {
|
||||
Ok(Config::get_nat_type())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_nat_type(ms_timeout: u64) -> i32 {
|
||||
get_nat_type_(ms_timeout)
|
||||
.await
|
||||
.unwrap_or(Config::get_nat_type())
|
||||
}
|
||||
29
src/lib.rs
Normal file
29
src/lib.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub mod platform;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod server;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use self::server::*;
|
||||
mod client;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod rendezvous_mediator;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use self::rendezvous_mediator::*;
|
||||
pub mod common;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub mod ipc;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
|
||||
pub mod ui;
|
||||
mod version;
|
||||
pub use version::*;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub mod mobile;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub mod mobile_ffi;
|
||||
use common::*;
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod cli;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod port_forward;
|
||||
148
src/main.rs
Normal file
148
src/main.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
// Specify the Windows subsystem to eliminate console window.
|
||||
// Requires Rust 1.18.
|
||||
//#![windows_subsystem = "windows"]
|
||||
|
||||
use hbb_common::log;
|
||||
use rustdesk::*;
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
fn main() {
|
||||
common::test_rendezvous_server();
|
||||
common::test_nat_type();
|
||||
#[cfg(target_os = "android")]
|
||||
crate::common::check_software_update();
|
||||
mobile::Session::start("");
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
|
||||
fn main() {
|
||||
let mut args = Vec::new();
|
||||
let mut i = 0;
|
||||
for arg in std::env::args() {
|
||||
if i > 0 {
|
||||
args.push(arg);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if args.len() > 0 && args[0] == "--version" {
|
||||
println!("{}", crate::VERSION);
|
||||
return;
|
||||
}
|
||||
#[cfg(not(feature = "inline"))]
|
||||
{
|
||||
use hbb_common::env_logger::*;
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
}
|
||||
#[cfg(feature = "inline")]
|
||||
{
|
||||
let mut path = hbb_common::config::Config::log_path();
|
||||
if args.len() > 0 && args[0].starts_with("--") {
|
||||
let name = args[0].replace("--", "");
|
||||
if !name.is_empty() {
|
||||
path.push(name);
|
||||
}
|
||||
}
|
||||
use flexi_logger::*;
|
||||
Logger::with_env_or_str("debug")
|
||||
.log_to_file()
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(6),
|
||||
)
|
||||
.directory(path)
|
||||
.start()
|
||||
.ok();
|
||||
}
|
||||
if args.is_empty() {
|
||||
std::thread::spawn(move || start_server(false, false));
|
||||
} else {
|
||||
if args[0] == "--uninstall" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Err(err) = platform::uninstall_me() {
|
||||
log::error!("Failed to uninstall: {}", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--update" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
hbb_common::allow_err!(platform::update_me());
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--reinstall" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
hbb_common::allow_err!(platform::uninstall_me());
|
||||
hbb_common::allow_err!(platform::install_me("desktopicon startmenu"));
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--remove" {
|
||||
if args.len() == 2 {
|
||||
// sleep a while so that process of removed exe exit
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
std::fs::remove_file(&args[1]).ok();
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--service" {
|
||||
log::info!("start --service");
|
||||
start_os_service();
|
||||
return;
|
||||
} else if args[0] == "--server" {
|
||||
log::info!("start --server");
|
||||
start_server(true, true);
|
||||
return;
|
||||
} else if args[0] == "--import-config" {
|
||||
if args.len() == 2 {
|
||||
hbb_common::config::Config::import(&args[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
ui::start(&mut args[..]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
fn main() {
|
||||
use clap::App;
|
||||
let args = format!(
|
||||
"-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]'
|
||||
-s, --server... 'Start server'",
|
||||
);
|
||||
let matches = App::new("rustdesk")
|
||||
.version(crate::VERSION)
|
||||
.author("CarrieZ Studio<info@rustdesk.com>")
|
||||
.about("RustDesk command line tool")
|
||||
.args_from_usage(&args)
|
||||
.get_matches();
|
||||
use hbb_common::env_logger::*;
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
if let Some(p) = matches.value_of("port-forward") {
|
||||
let options: Vec<String> = p.split(":").map(|x| x.to_owned()).collect();
|
||||
if options.len() < 3 {
|
||||
log::error!("Wrong port-forward options");
|
||||
return;
|
||||
}
|
||||
let mut port = 0;
|
||||
if let Ok(v) = options[1].parse::<i32>() {
|
||||
port = v;
|
||||
} else {
|
||||
log::error!("Wrong local-port");
|
||||
return;
|
||||
}
|
||||
let mut remote_port = 0;
|
||||
if let Ok(v) = options[2].parse::<i32>() {
|
||||
remote_port = v;
|
||||
} else {
|
||||
log::error!("Wrong remote-port");
|
||||
return;
|
||||
}
|
||||
let mut remote_host = "localhost".to_owned();
|
||||
if options.len() > 3 {
|
||||
remote_host = options[3].clone();
|
||||
}
|
||||
cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port);
|
||||
}
|
||||
}
|
||||
446
src/platform/linux.rs
Normal file
446
src/platform/linux.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use super::{CursorData, ResultType};
|
||||
use hbb_common::{allow_err, bail, log};
|
||||
use libc::{c_char, c_int, c_void};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
type Xdo = *const c_void;
|
||||
|
||||
pub const PA_SAMPLE_RATE: u32 = 24000;
|
||||
|
||||
thread_local! {
|
||||
static XDO: RefCell<Xdo> = RefCell::new(unsafe { xdo_new(std::ptr::null()) });
|
||||
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn xdo_get_mouse_location(
|
||||
xdo: Xdo,
|
||||
x: *mut c_int,
|
||||
y: *mut c_int,
|
||||
screen_num: *mut c_int,
|
||||
) -> c_int;
|
||||
fn xdo_new(display: *const c_char) -> Xdo;
|
||||
}
|
||||
|
||||
#[link(name = "X11")]
|
||||
extern "C" {
|
||||
fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
|
||||
// fn XCloseDisplay(d: *mut c_void) -> c_int;
|
||||
}
|
||||
|
||||
#[link(name = "Xfixes")]
|
||||
extern "C" {
|
||||
// fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int;
|
||||
fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image;
|
||||
fn XFree(data: *mut c_void);
|
||||
}
|
||||
|
||||
// /usr/include/X11/extensions/Xfixes.h
|
||||
#[repr(C)]
|
||||
pub struct xcb_xfixes_get_cursor_image {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub xhot: u16,
|
||||
pub yhot: u16,
|
||||
pub cursor_serial: libc::c_long,
|
||||
pub pixels: *const libc::c_long,
|
||||
}
|
||||
|
||||
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
let mut res = None;
|
||||
XDO.with(|xdo| {
|
||||
if let Ok(xdo) = xdo.try_borrow_mut() {
|
||||
if xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
let mut x: c_int = 0;
|
||||
let mut y: c_int = 0;
|
||||
unsafe {
|
||||
xdo_get_mouse_location(*xdo, &mut x as _, &mut y as _, std::ptr::null_mut());
|
||||
}
|
||||
res = Some((x, y));
|
||||
}
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
pub fn reset_input_cache() {}
|
||||
|
||||
pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
let mut res = None;
|
||||
DISPLAY.with(|conn| {
|
||||
if let Ok(d) = conn.try_borrow_mut() {
|
||||
if !d.is_null() {
|
||||
unsafe {
|
||||
let img = XFixesGetCursorImage(*d);
|
||||
if !img.is_null() {
|
||||
res = Some((*img).cursor_serial as u64);
|
||||
XFree(img as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
let mut res = None;
|
||||
DISPLAY.with(|conn| {
|
||||
if let Ok(ref mut d) = conn.try_borrow_mut() {
|
||||
if !d.is_null() {
|
||||
unsafe {
|
||||
let img = XFixesGetCursorImage(**d);
|
||||
if !img.is_null() && hcursor == (*img).cursor_serial as u64 {
|
||||
let mut cd: CursorData = Default::default();
|
||||
cd.hotx = (*img).xhot as _;
|
||||
cd.hoty = (*img).yhot as _;
|
||||
cd.width = (*img).width as _;
|
||||
cd.height = (*img).height as _;
|
||||
// to-do: how about if it is 0
|
||||
cd.id = (*img).cursor_serial as _;
|
||||
let pixels =
|
||||
std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _);
|
||||
cd.colors.resize(pixels.len() * 4, 0);
|
||||
for y in 0..cd.height {
|
||||
for x in 0..cd.width {
|
||||
let pos = (y * cd.width + x) as usize;
|
||||
let p = pixels[pos];
|
||||
let a = (p >> 24) & 0xff;
|
||||
let r = (p >> 16) & 0xff;
|
||||
let g = (p >> 8) & 0xff;
|
||||
let b = (p >> 0) & 0xff;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
let pos = pos * 4;
|
||||
cd.colors[pos] = r as _;
|
||||
cd.colors[pos + 1] = g as _;
|
||||
cd.colors[pos + 2] = b as _;
|
||||
cd.colors[pos + 3] = a as _;
|
||||
}
|
||||
}
|
||||
res = Some(cd);
|
||||
}
|
||||
if !img.is_null() {
|
||||
XFree(img as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
match res {
|
||||
Some(x) => Ok(x),
|
||||
_ => bail!("Failed to get cursor image of {}", hcursor),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_os_service() {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
let mut uid = "".to_owned();
|
||||
let mut server: Option<std::process::Child> = None;
|
||||
if let Err(err) = ctrlc::set_handler(move || {
|
||||
r.store(false, Ordering::SeqCst);
|
||||
}) {
|
||||
println!("Failed to set Ctrl-C handler: {}", err);
|
||||
}
|
||||
|
||||
let mut cm0 = false;
|
||||
let mut last_restart = std::time::Instant::now();
|
||||
while running.load(Ordering::SeqCst) {
|
||||
let cm = get_cm();
|
||||
let tmp = get_active_userid();
|
||||
let mut start_new = false;
|
||||
if tmp != uid && !tmp.is_empty() {
|
||||
uid = tmp;
|
||||
log::info!("uid of seat0: {}", uid);
|
||||
std::env::set_var("XAUTHORITY", format!("/run/user/{}/gdm/Xauthority", uid));
|
||||
std::env::set_var("DISPLAY", get_display());
|
||||
if let Some(ps) = server.as_mut() {
|
||||
allow_err!(ps.kill());
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
last_restart = std::time::Instant::now();
|
||||
}
|
||||
} else if !cm
|
||||
&& ((cm0 && last_restart.elapsed().as_secs() > 60)
|
||||
|| last_restart.elapsed().as_secs() > 3600)
|
||||
{
|
||||
// restart server if new connections all closed, or every one hour,
|
||||
// as a workaround to resolve "SpotUdp" (dns resolve)
|
||||
// and x server get displays failure issue
|
||||
if let Some(ps) = server.as_mut() {
|
||||
allow_err!(ps.kill());
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
last_restart = std::time::Instant::now();
|
||||
log::info!("restart server");
|
||||
}
|
||||
}
|
||||
if let Some(ps) = server.as_mut() {
|
||||
match ps.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
server = None;
|
||||
start_new = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
start_new = true;
|
||||
}
|
||||
if start_new {
|
||||
match crate::run_me(vec!["--server"]) {
|
||||
Ok(ps) => server = Some(ps),
|
||||
Err(err) => {
|
||||
log::error!("Failed to start server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
cm0 = cm;
|
||||
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
|
||||
}
|
||||
|
||||
if let Some(ps) = server.take().as_mut() {
|
||||
allow_err!(ps.kill());
|
||||
}
|
||||
println!("Exit");
|
||||
}
|
||||
|
||||
fn get_active_userid() -> String {
|
||||
get_value_of_seat0(1)
|
||||
}
|
||||
|
||||
fn is_active(sid: &str) -> bool {
|
||||
if let Ok(output) = std::process::Command::new("loginctl")
|
||||
.args(vec!["show-session", "-p", "State", sid])
|
||||
.output()
|
||||
{
|
||||
String::from_utf8_lossy(&output.stdout).contains("active")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cm() -> bool {
|
||||
if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if line.contains(&format!(
|
||||
"{} --cm",
|
||||
std::env::current_exe()
|
||||
.unwrap_or("".into())
|
||||
.to_string_lossy()
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_display() -> String {
|
||||
let user = get_active_username();
|
||||
if let Ok(output) = std::process::Command::new("w").arg(&user).output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
let mut iter = line.split_whitespace();
|
||||
let a = iter.nth(1);
|
||||
let b = iter.next();
|
||||
if a == b {
|
||||
if let Some(b) = b {
|
||||
if b.starts_with(":") {
|
||||
return b.to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// above not work for gdm user
|
||||
if let Ok(output) = std::process::Command::new("ls")
|
||||
.args(vec!["-l", "/tmp/.X11-unix/"])
|
||||
.output()
|
||||
{
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
let mut iter = line.split_whitespace();
|
||||
if iter.nth(2) == Some(&user) {
|
||||
if let Some(x) = iter.last() {
|
||||
if x.starts_with("X") {
|
||||
return x.replace("X", ":").to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
fn get_value_of_seat0(i: usize) -> String {
|
||||
if let Ok(output) = std::process::Command::new("loginctl").output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().nth(0) {
|
||||
if is_active(sid) {
|
||||
if let Some(uid) = line.split_whitespace().nth(i) {
|
||||
return uid.to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
let session = get_value_of_seat0(0);
|
||||
if let Ok(output) = std::process::Command::new("loginctl")
|
||||
.args(vec!["show-session", "-p", "Type", &session])
|
||||
.output()
|
||||
{
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.replace("Type=", "")
|
||||
.trim_end()
|
||||
.into()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_login_wayland() -> bool {
|
||||
if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") {
|
||||
contents.contains("#WaylandEnable=false")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fix_login_wayland() {
|
||||
match std::process::Command::new("pkexec")
|
||||
.args(vec![
|
||||
"sed",
|
||||
"-i",
|
||||
"s/#WaylandEnable=false/WaylandEnable=false/g",
|
||||
"/etc/gdm3/custom.conf",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(x) => {
|
||||
let x = String::from_utf8_lossy(&x.stderr);
|
||||
if !x.is_empty() {
|
||||
log::error!("fix_login_wayland failed: {}", x);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("fix_login_wayland failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// to-do: test the other display manager
|
||||
fn _get_display_manager() -> String {
|
||||
if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") {
|
||||
if let Some(x) = x.split("/").last() {
|
||||
return x.to_owned();
|
||||
}
|
||||
}
|
||||
"gdm3".to_owned()
|
||||
}
|
||||
|
||||
pub fn get_active_username() -> String {
|
||||
get_value_of_seat0(2)
|
||||
}
|
||||
|
||||
pub fn is_prelogin() -> bool {
|
||||
let n = get_active_userid().len();
|
||||
n < 4 && n > 1
|
||||
}
|
||||
|
||||
pub fn is_root() -> bool {
|
||||
crate::username() == "root"
|
||||
}
|
||||
|
||||
pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
|
||||
let uid = get_active_userid();
|
||||
let cmd = std::env::current_exe()?;
|
||||
let task = std::process::Command::new("sudo")
|
||||
.args(vec![
|
||||
&format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str,
|
||||
"-u",
|
||||
&get_active_username(),
|
||||
cmd.to_str().unwrap_or(""),
|
||||
arg,
|
||||
])
|
||||
.spawn()?;
|
||||
Ok(Some(task))
|
||||
}
|
||||
|
||||
pub fn get_pa_monitor() -> String {
|
||||
get_pa_sources()
|
||||
.drain(..)
|
||||
.map(|x| x.0)
|
||||
.filter(|x| x.contains("monitor"))
|
||||
.next()
|
||||
.unwrap_or("".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_pa_source_name(desc: &str) -> String {
|
||||
get_pa_sources()
|
||||
.drain(..)
|
||||
.filter(|x| x.1 == desc)
|
||||
.map(|x| x.0)
|
||||
.next()
|
||||
.unwrap_or("".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_pa_sources() -> Vec<(String, String)> {
|
||||
use pulsectl::controllers::*;
|
||||
let mut out = Vec::new();
|
||||
match SourceController::create() {
|
||||
Ok(mut handler) => {
|
||||
if let Ok(devices) = handler.list_devices() {
|
||||
for dev in devices.clone() {
|
||||
out.push((
|
||||
dev.name.unwrap_or("".to_owned()),
|
||||
dev.description.unwrap_or("".to_owned()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to get_pa_sources: {:?}", err);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn lock_screen() {
|
||||
std::thread::spawn(move || {
|
||||
use crate::server::input_service::handle_key;
|
||||
use hbb_common::message_proto::*;
|
||||
let mut evt = KeyEvent {
|
||||
down: true,
|
||||
modifiers: vec![ControlKey::Meta.into()],
|
||||
..Default::default()
|
||||
};
|
||||
evt.set_chr('l' as _);
|
||||
handle_key(&evt);
|
||||
evt.down = false;
|
||||
handle_key(&evt);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_privacy_mode(_v: bool) {
|
||||
// https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x
|
||||
}
|
||||
|
||||
pub fn block_input(_v: bool) {
|
||||
//
|
||||
}
|
||||
|
||||
pub fn is_installed() -> bool {
|
||||
true
|
||||
}
|
||||
335
src/platform/macos.rs
Normal file
335
src/platform/macos.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
// https://developer.apple.com/documentation/appkit/nscursor
|
||||
// https://github.com/servo/core-foundation-rs
|
||||
// https://github.com/rust-windowing/winit
|
||||
|
||||
use super::{CursorData, ResultType};
|
||||
use cocoa::{
|
||||
base::{id, nil, BOOL, NO, YES},
|
||||
foundation::{NSDictionary, NSPoint, NSSize, NSString},
|
||||
};
|
||||
use core_foundation::{
|
||||
array::{CFArrayGetCount, CFArrayGetValueAtIndex},
|
||||
dictionary::CFDictionaryRef,
|
||||
string::CFStringRef,
|
||||
};
|
||||
use core_graphics::{
|
||||
display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo},
|
||||
window::{kCGWindowName, kCGWindowOwnerPID},
|
||||
};
|
||||
use hbb_common::{allow_err, bail, log};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use scrap::{libc::c_void, quartz::ffi::*};
|
||||
|
||||
static mut LATEST_SEED: i32 = 0;
|
||||
|
||||
extern "C" {
|
||||
fn CGSCurrentCursorSeed() -> i32;
|
||||
fn CGEventCreate(r: *const c_void) -> *const c_void;
|
||||
fn CGEventGetLocation(e: *const c_void) -> CGPoint;
|
||||
static kAXTrustedCheckOptionPrompt: CFStringRef;
|
||||
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
|
||||
}
|
||||
|
||||
pub fn is_process_trusted(prompt: bool) -> bool {
|
||||
unsafe {
|
||||
let value = if prompt { YES } else { NO };
|
||||
let value: id = msg_send![class!(NSNumber), numberWithBool: value];
|
||||
let options = NSDictionary::dictionaryWithObject_forKey_(
|
||||
nil,
|
||||
value,
|
||||
kAXTrustedCheckOptionPrompt as _,
|
||||
);
|
||||
AXIsProcessTrustedWithOptions(options as _) == YES
|
||||
}
|
||||
}
|
||||
|
||||
// macOS >= 10.15
|
||||
// https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/
|
||||
// remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk
|
||||
pub fn is_can_screen_recording(prompt: bool) -> bool {
|
||||
let mut can_record_screen: bool = false;
|
||||
unsafe {
|
||||
let our_pid: i32 = std::process::id() as _;
|
||||
let our_pid: id = msg_send![class!(NSNumber), numberWithInteger: our_pid];
|
||||
let window_list =
|
||||
CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
|
||||
let n = CFArrayGetCount(window_list);
|
||||
let dock = NSString::alloc(nil).init_str("Dock");
|
||||
for i in 0..n {
|
||||
let w: id = CFArrayGetValueAtIndex(window_list, i) as _;
|
||||
let name: id = msg_send![w, valueForKey: kCGWindowName as id];
|
||||
if name.is_null() {
|
||||
continue;
|
||||
}
|
||||
let pid: id = msg_send![w, valueForKey: kCGWindowOwnerPID as id];
|
||||
let is_me: BOOL = msg_send![pid, isEqual: our_pid];
|
||||
if is_me == YES {
|
||||
continue;
|
||||
}
|
||||
let pid: i32 = msg_send![pid, intValue];
|
||||
let p: id = msg_send![
|
||||
class!(NSRunningApplication),
|
||||
runningApplicationWithProcessIdentifier: pid
|
||||
];
|
||||
if p.is_null() {
|
||||
// ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
|
||||
continue;
|
||||
}
|
||||
let url: id = msg_send![p, executableURL];
|
||||
let exe_name: id = msg_send![url, lastPathComponent];
|
||||
if exe_name.is_null() {
|
||||
continue;
|
||||
}
|
||||
let is_dock: BOOL = msg_send![exe_name, isEqual: dock];
|
||||
if is_dock == YES {
|
||||
// ignore the Dock, which provides the desktop picture
|
||||
continue;
|
||||
}
|
||||
can_record_screen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !can_record_screen && prompt {
|
||||
use scrap::{Capturer, Display};
|
||||
if let Ok(d) = Display::primary() {
|
||||
Capturer::new(d, true).ok();
|
||||
}
|
||||
}
|
||||
can_record_screen
|
||||
}
|
||||
|
||||
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
unsafe {
|
||||
let e = CGEventCreate(0 as _);
|
||||
let point = CGEventGetLocation(e);
|
||||
CFRelease(e);
|
||||
Some((point.x as _, point.y as _))
|
||||
}
|
||||
/*
|
||||
let mut pt: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
|
||||
let screen: id = unsafe { msg_send![class!(NSScreen), currentScreenForMouseLocation] };
|
||||
let frame: NSRect = unsafe { msg_send![screen, frame] };
|
||||
pt.x -= frame.origin.x;
|
||||
pt.y -= frame.origin.y;
|
||||
Some((pt.x as _, pt.y as _))
|
||||
*/
|
||||
}
|
||||
|
||||
pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
unsafe {
|
||||
let seed = CGSCurrentCursorSeed();
|
||||
if seed == LATEST_SEED {
|
||||
return Ok(None);
|
||||
}
|
||||
LATEST_SEED = seed;
|
||||
}
|
||||
let c = get_cursor_id()?;
|
||||
Ok(Some(c.1))
|
||||
}
|
||||
|
||||
pub fn reset_input_cache() {
|
||||
unsafe {
|
||||
LATEST_SEED = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cursor_id() -> ResultType<(id, u64)> {
|
||||
unsafe {
|
||||
let c: id = msg_send![class!(NSCursor), currentSystemCursor];
|
||||
if c == nil {
|
||||
bail!("Failed to call [NSCursor currentSystemCursor]");
|
||||
}
|
||||
let hotspot: NSPoint = msg_send![c, hotSpot];
|
||||
let img: id = msg_send![c, image];
|
||||
if img == nil {
|
||||
bail!("Failed to call [NSCursor image]");
|
||||
}
|
||||
let size: NSSize = msg_send![img, size];
|
||||
let tif: id = msg_send![img, TIFFRepresentation];
|
||||
if tif == nil {
|
||||
bail!("Failed to call [NSImage TIFFRepresentation]");
|
||||
}
|
||||
let rep: id = msg_send![class!(NSBitmapImageRep), imageRepWithData: tif];
|
||||
if rep == nil {
|
||||
bail!("Failed to call [NSBitmapImageRep imageRepWithData]");
|
||||
}
|
||||
let rep_size: NSSize = msg_send![rep, size];
|
||||
let mut hcursor =
|
||||
size.width + size.height + hotspot.x + hotspot.y + rep_size.width + rep_size.height;
|
||||
let x = (rep_size.width * hotspot.x / size.width) as usize;
|
||||
let y = (rep_size.height * hotspot.y / size.height) as usize;
|
||||
for i in 0..2 {
|
||||
let mut x2 = x + i;
|
||||
if x2 >= rep_size.width as usize {
|
||||
x2 = rep_size.width as usize - 1;
|
||||
}
|
||||
let mut y2 = y + i;
|
||||
if y2 >= rep_size.height as usize {
|
||||
y2 = rep_size.height as usize - 1;
|
||||
}
|
||||
let color: id = msg_send![rep, colorAtX:x2 y:y2];
|
||||
if color != nil {
|
||||
let r: f64 = msg_send![color, redComponent];
|
||||
let g: f64 = msg_send![color, greenComponent];
|
||||
let b: f64 = msg_send![color, blueComponent];
|
||||
let a: f64 = msg_send![color, alphaComponent];
|
||||
hcursor += (r + g + b + a) * (255 << i) as f64;
|
||||
}
|
||||
}
|
||||
Ok((c, hcursor as _))
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c
|
||||
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
unsafe {
|
||||
let (c, hcursor2) = get_cursor_id()?;
|
||||
if hcursor != hcursor2 {
|
||||
bail!("cursor changed");
|
||||
}
|
||||
let hotspot: NSPoint = msg_send![c, hotSpot];
|
||||
let img: id = msg_send![c, image];
|
||||
let size: NSSize = msg_send![img, size];
|
||||
let reps: id = msg_send![img, representations];
|
||||
if reps == nil {
|
||||
bail!("Failed to call [NSImage representations]");
|
||||
}
|
||||
let nreps: usize = msg_send![reps, count];
|
||||
if nreps == 0 {
|
||||
bail!("Get empty [NSImage representations]");
|
||||
}
|
||||
let rep: id = msg_send![reps, objectAtIndex: 0];
|
||||
/*
|
||||
let n: id = msg_send![class!(NSNumber), numberWithFloat:1.0];
|
||||
let props: id = msg_send![class!(NSDictionary), dictionaryWithObject:n forKey:NSString::alloc(nil).init_str("NSImageCompressionFactor")];
|
||||
let image_data: id = msg_send![rep, representationUsingType:2 properties:props];
|
||||
let () = msg_send![image_data, writeToFile:NSString::alloc(nil).init_str("cursor.jpg") atomically:0];
|
||||
*/
|
||||
let mut colors: Vec<u8> = Vec::new();
|
||||
colors.reserve((size.height * size.width) as usize * 4);
|
||||
// TIFF is rgb colrspace, no need to convert
|
||||
// let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace];
|
||||
for y in 0..(size.height as _) {
|
||||
for x in 0..(size.width as _) {
|
||||
let color: id = msg_send![rep, colorAtX:x y:y];
|
||||
// let color: id = msg_send![color, colorUsingColorSpace: cs];
|
||||
if color == nil {
|
||||
continue;
|
||||
}
|
||||
let r: f64 = msg_send![color, redComponent];
|
||||
let g: f64 = msg_send![color, greenComponent];
|
||||
let b: f64 = msg_send![color, blueComponent];
|
||||
let a: f64 = msg_send![color, alphaComponent];
|
||||
colors.push((r * 255.) as _);
|
||||
colors.push((g * 255.) as _);
|
||||
colors.push((b * 255.) as _);
|
||||
colors.push((a * 255.) as _);
|
||||
}
|
||||
}
|
||||
Ok(CursorData {
|
||||
id: hcursor,
|
||||
colors,
|
||||
hotx: hotspot.x as _,
|
||||
hoty: hotspot.y as _,
|
||||
width: size.width as _,
|
||||
height: size.height as _,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_active_user(t: &str) -> String {
|
||||
if let Ok(output) = std::process::Command::new("ls")
|
||||
.args(vec![t, "/dev/console"])
|
||||
.output()
|
||||
{
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if let Some(n) = line.split_whitespace().nth(2) {
|
||||
return n.to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn get_active_username() -> String {
|
||||
get_active_user("-l")
|
||||
}
|
||||
|
||||
pub fn get_active_userid() -> String {
|
||||
get_active_user("-n")
|
||||
}
|
||||
|
||||
pub fn is_prelogin() -> bool {
|
||||
get_active_userid() == "0"
|
||||
}
|
||||
|
||||
pub fn is_root() -> bool {
|
||||
crate::username() == "root"
|
||||
}
|
||||
|
||||
pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
|
||||
let uid = get_active_userid();
|
||||
let cmd = std::env::current_exe()?;
|
||||
let task = std::process::Command::new("launchctl")
|
||||
.args(vec!["asuser", &uid, cmd.to_str().unwrap_or(""), arg])
|
||||
.spawn()?;
|
||||
Ok(Some(task))
|
||||
}
|
||||
|
||||
pub fn lock_screen() {
|
||||
std::process::Command::new(
|
||||
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
|
||||
)
|
||||
.arg("-suspend")
|
||||
.output()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn start_os_service() {
|
||||
let mut server: Option<std::process::Child> = None;
|
||||
let mut uid = "".to_owned();
|
||||
loop {
|
||||
let tmp = get_active_userid();
|
||||
let mut start_new = false;
|
||||
if tmp != uid && !tmp.is_empty() {
|
||||
uid = tmp;
|
||||
log::info!("active uid: {}", uid);
|
||||
if let Some(ps) = server.as_mut() {
|
||||
allow_err!(ps.kill());
|
||||
}
|
||||
}
|
||||
if let Some(ps) = server.as_mut() {
|
||||
match ps.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
server = None;
|
||||
start_new = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
start_new = true;
|
||||
}
|
||||
if start_new {
|
||||
match crate::run_me(vec!["--server"]) {
|
||||
Ok(ps) => server = Some(ps),
|
||||
Err(err) => {
|
||||
log::error!("Failed to start server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_privacy_mode(_v: bool) {
|
||||
// https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily
|
||||
}
|
||||
|
||||
pub fn block_input(_v: bool) {
|
||||
//
|
||||
}
|
||||
|
||||
pub fn is_installed() -> bool {
|
||||
true
|
||||
}
|
||||
46
src/platform/mod.rs
Normal file
46
src/platform/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::*;
|
||||
#[cfg(windows)]
|
||||
pub use windows::*;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
use hbb_common::{message_proto::CursorData, ResultType};
|
||||
const SERVICE_INTERVAL: u64 = 300;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_cursor_data() {
|
||||
for _ in 0..30 {
|
||||
if let Some(hc) = get_cursor().unwrap() {
|
||||
let cd = get_cursor_data(hc).unwrap();
|
||||
repng::encode(
|
||||
std::fs::File::create("cursor.png").unwrap(),
|
||||
cd.width as _,
|
||||
cd.height as _,
|
||||
&cd.colors[..],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::is_process_trusted(false);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_get_cursor_pos() {
|
||||
for _ in 0..30 {
|
||||
assert!(!get_cursor_pos().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
981
src/platform/windows.rs
Normal file
981
src/platform/windows.rs
Normal file
@@ -0,0 +1,981 @@
|
||||
use super::{CursorData, ResultType};
|
||||
use crate::ipc;
|
||||
use hbb_common::{
|
||||
allow_err, bail,
|
||||
config::{Config, APP_NAME},
|
||||
futures_util::stream::StreamExt,
|
||||
log, sleep, timeout, tokio,
|
||||
};
|
||||
use std::io::prelude::*;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
io, mem,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use winapi::{
|
||||
shared::{minwindef::*, ntdef::NULL, windef::*},
|
||||
um::{
|
||||
errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE,
|
||||
processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*,
|
||||
},
|
||||
};
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
service::{
|
||||
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
|
||||
ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
};
|
||||
|
||||
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
unsafe {
|
||||
let mut out = mem::MaybeUninit::uninit().assume_init();
|
||||
if GetCursorPos(&mut out) == FALSE {
|
||||
return None;
|
||||
}
|
||||
return Some((out.x, out.y));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_input_cache() {}
|
||||
|
||||
pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
unsafe {
|
||||
let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init();
|
||||
ci.cbSize = std::mem::size_of::<CURSORINFO>() as _;
|
||||
if GetCursorInfo(&mut ci) == FALSE {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
if ci.flags & CURSOR_SHOWING == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(ci.hCursor as _))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IconInfo(ICONINFO);
|
||||
|
||||
impl IconInfo {
|
||||
fn new(icon: HICON) -> ResultType<Self> {
|
||||
unsafe {
|
||||
let mut ii = mem::MaybeUninit::uninit().assume_init();
|
||||
if GetIconInfo(icon, &mut ii) == FALSE {
|
||||
Err(io::Error::last_os_error().into())
|
||||
} else {
|
||||
let ii = Self(ii);
|
||||
if ii.0.hbmMask.is_null() {
|
||||
bail!("Cursor bitmap handle is NULL");
|
||||
}
|
||||
return Ok(ii);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_color(&self) -> bool {
|
||||
!self.0.hbmColor.is_null()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IconInfo {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.0.hbmColor.is_null() {
|
||||
DeleteObject(self.0.hbmColor as _);
|
||||
}
|
||||
if !self.0.hbmMask.is_null() {
|
||||
DeleteObject(self.0.hbmMask as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp
|
||||
// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx
|
||||
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
unsafe {
|
||||
let mut ii = IconInfo::new(hcursor as _)?;
|
||||
let bm_mask = get_bitmap(ii.0.hbmMask)?;
|
||||
let mut width = bm_mask.bmWidth;
|
||||
let mut height = if ii.is_color() {
|
||||
bm_mask.bmHeight
|
||||
} else {
|
||||
bm_mask.bmHeight / 2
|
||||
};
|
||||
let cbits_size = width * height * 4;
|
||||
let mut cbits: Vec<u8> = Vec::new();
|
||||
cbits.resize(cbits_size as _, 0);
|
||||
let mut mbits: Vec<u8> = Vec::new();
|
||||
mbits.resize((bm_mask.bmWidthBytes * bm_mask.bmHeight) as _, 0);
|
||||
let r = GetBitmapBits(ii.0.hbmMask, mbits.len() as _, mbits.as_mut_ptr() as _);
|
||||
if r == 0 {
|
||||
bail!("Failed to copy bitmap data");
|
||||
}
|
||||
if r != (mbits.len() as i32) {
|
||||
bail!(
|
||||
"Invalid mask cursor buffer size, got {} bytes, expected {}",
|
||||
r,
|
||||
mbits.len()
|
||||
);
|
||||
}
|
||||
let do_outline;
|
||||
if ii.is_color() {
|
||||
get_rich_cursor_data(ii.0.hbmColor, width, height, &mut cbits)?;
|
||||
do_outline = fix_cursor_mask(
|
||||
&mut mbits,
|
||||
&mut cbits,
|
||||
width as _,
|
||||
height as _,
|
||||
bm_mask.bmWidthBytes as _,
|
||||
);
|
||||
} else {
|
||||
do_outline = handleMask(
|
||||
cbits.as_mut_ptr(),
|
||||
mbits.as_ptr(),
|
||||
width,
|
||||
height,
|
||||
bm_mask.bmWidthBytes,
|
||||
) > 0;
|
||||
}
|
||||
if do_outline {
|
||||
let mut outline = Vec::new();
|
||||
outline.resize(((width + 2) * (height + 2) * 4) as _, 0);
|
||||
drawOutline(outline.as_mut_ptr(), cbits.as_ptr(), width, height);
|
||||
cbits = outline;
|
||||
width += 2;
|
||||
height += 2;
|
||||
ii.0.xHotspot += 1;
|
||||
ii.0.yHotspot += 1;
|
||||
}
|
||||
|
||||
Ok(CursorData {
|
||||
id: hcursor,
|
||||
colors: cbits,
|
||||
hotx: ii.0.xHotspot as _,
|
||||
hoty: ii.0.yHotspot as _,
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_bitmap(handle: HBITMAP) -> ResultType<BITMAP> {
|
||||
unsafe {
|
||||
let mut bm: BITMAP = mem::zeroed();
|
||||
if GetObjectA(
|
||||
handle as _,
|
||||
std::mem::size_of::<BITMAP>() as _,
|
||||
&mut bm as *mut BITMAP as *mut _,
|
||||
) == FALSE
|
||||
{
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
if bm.bmPlanes != 1 {
|
||||
bail!("Unsupported multi-plane cursor");
|
||||
}
|
||||
if bm.bmBitsPixel != 1 {
|
||||
bail!("Unsupported cursor mask format");
|
||||
}
|
||||
Ok(bm)
|
||||
}
|
||||
}
|
||||
|
||||
struct DC(HDC);
|
||||
|
||||
impl DC {
|
||||
fn new() -> ResultType<Self> {
|
||||
unsafe {
|
||||
let dc = GetDC(0 as _);
|
||||
if dc.is_null() {
|
||||
bail!("Failed to get a drawing context");
|
||||
}
|
||||
Ok(Self(dc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DC {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.0.is_null() {
|
||||
ReleaseDC(0 as _, self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CompatibleDC(HDC);
|
||||
|
||||
impl CompatibleDC {
|
||||
fn new(existing: HDC) -> ResultType<Self> {
|
||||
unsafe {
|
||||
let dc = CreateCompatibleDC(existing);
|
||||
if dc.is_null() {
|
||||
bail!("Failed to get a compatible drawing context");
|
||||
}
|
||||
Ok(Self(dc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CompatibleDC {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.0.is_null() {
|
||||
DeleteDC(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BitmapDC(CompatibleDC, HBITMAP);
|
||||
|
||||
impl BitmapDC {
|
||||
fn new(hdc: HDC, hbitmap: HBITMAP) -> ResultType<Self> {
|
||||
unsafe {
|
||||
let dc = CompatibleDC::new(hdc)?;
|
||||
let oldbitmap = SelectObject(dc.0, hbitmap as _) as HBITMAP;
|
||||
if oldbitmap.is_null() {
|
||||
bail!("Failed to select CompatibleDC");
|
||||
}
|
||||
Ok(Self(dc, oldbitmap))
|
||||
}
|
||||
}
|
||||
|
||||
fn dc(&self) -> HDC {
|
||||
(self.0).0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BitmapDC {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.1.is_null() {
|
||||
SelectObject((self.0).0, self.1 as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_rich_cursor_data(
|
||||
hbm_color: HBITMAP,
|
||||
width: i32,
|
||||
height: i32,
|
||||
out: &mut Vec<u8>,
|
||||
) -> ResultType<()> {
|
||||
unsafe {
|
||||
let dc = DC::new()?;
|
||||
let bitmap_dc = BitmapDC::new(dc.0, hbm_color)?;
|
||||
if get_di_bits(out.as_mut_ptr(), bitmap_dc.dc(), hbm_color, width, height) > 0 {
|
||||
bail!("Failed to get di bits: {}", get_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fix_cursor_mask(
|
||||
mbits: &mut Vec<u8>,
|
||||
cbits: &mut Vec<u8>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
width_bytes: usize,
|
||||
) -> bool {
|
||||
let mut pix_idx = 0;
|
||||
for _ in 0..height {
|
||||
for _ in 0..width {
|
||||
if cbits[pix_idx + 3] != 0 {
|
||||
return false;
|
||||
}
|
||||
pix_idx += 4;
|
||||
}
|
||||
}
|
||||
|
||||
let packed_width_bytes = (width + 7) >> 3;
|
||||
|
||||
// Pack and invert bitmap data (mbits)
|
||||
// borrow from tigervnc
|
||||
for y in 0..height {
|
||||
for x in 0..packed_width_bytes {
|
||||
mbits[y * packed_width_bytes + x] = !mbits[y * width_bytes + x];
|
||||
}
|
||||
}
|
||||
|
||||
// Replace "inverted background" bits with black color to ensure
|
||||
// cross-platform interoperability. Not beautiful but necessary code.
|
||||
// borrow from tigervnc
|
||||
let bytes_row = width << 2;
|
||||
for y in 0..height {
|
||||
let mut bitmask: u8 = 0x80;
|
||||
for x in 0..width {
|
||||
let mask_idx = y * packed_width_bytes + (x >> 3);
|
||||
let pix_idx = y * bytes_row + (x << 2);
|
||||
if (mbits[mask_idx] & bitmask) == 0 {
|
||||
for b1 in 0..4 {
|
||||
if cbits[pix_idx + b1] != 0 {
|
||||
mbits[mask_idx] ^= bitmask;
|
||||
for b2 in b1..4 {
|
||||
cbits[pix_idx + b2] = 0x00;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bitmask >>= 1;
|
||||
if bitmask == 0 {
|
||||
bitmask = 0x80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// borrow from noVNC
|
||||
let mut pix_idx = 0;
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let mask_idx = y * packed_width_bytes + (x >> 3);
|
||||
let alpha = if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 {
|
||||
0
|
||||
} else {
|
||||
255
|
||||
};
|
||||
let a = cbits[pix_idx + 2];
|
||||
let b = cbits[pix_idx + 1];
|
||||
let c = cbits[pix_idx];
|
||||
cbits[pix_idx] = a;
|
||||
cbits[pix_idx + 1] = b;
|
||||
cbits[pix_idx + 2] = c;
|
||||
cbits[pix_idx + 3] = alpha;
|
||||
pix_idx += 4;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
fn service_main(arguments: Vec<OsString>) {
|
||||
if let Err(e) = run_service(arguments) {
|
||||
log::error!("run_service failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_os_service() {
|
||||
if let Err(e) = windows_service::service_dispatcher::start(APP_NAME, ffi_service_main) {
|
||||
log::error!("start_service failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
|
||||
|
||||
extern "C" {
|
||||
fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE;
|
||||
fn selectInputDesktop() -> BOOL;
|
||||
fn inputDesktopSelected() -> BOOL;
|
||||
fn handleMask(out: *mut u8, mask: *const u8, width: i32, height: i32, bmWidthBytes: i32)
|
||||
-> i32;
|
||||
fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32);
|
||||
fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32;
|
||||
fn blank_screen(v: BOOL);
|
||||
fn BlockInput(v: BOOL) -> BOOL;
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
|
||||
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
||||
log::info!("Got service control event: {:?}", control_event);
|
||||
match control_event {
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
ServiceControl::Stop => {
|
||||
send_close(crate::POSTFIX_SERVICE).ok();
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
}
|
||||
};
|
||||
|
||||
// Register system service event handler
|
||||
let status_handle = service_control_handler::register(APP_NAME, event_handler)?;
|
||||
|
||||
let next_status = ServiceStatus {
|
||||
// Should match the one from system service registry
|
||||
service_type: SERVICE_TYPE,
|
||||
// The new state
|
||||
current_state: ServiceState::Running,
|
||||
// Accept stop events when running
|
||||
controls_accepted: ServiceControlAccept::STOP,
|
||||
// Used to report an error when starting or stopping only, otherwise must be zero
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
// Only used for pending states, otherwise must be zero
|
||||
checkpoint: 0,
|
||||
// Only used for pending states, otherwise must be zero
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
};
|
||||
|
||||
// Tell the system that the service is running now
|
||||
status_handle.set_service_status(next_status)?;
|
||||
|
||||
let mut session_id = unsafe { WTSGetActiveConsoleSessionId() };
|
||||
log::info!("session id {}", session_id);
|
||||
let mut h_process = launch_server(session_id, true).await.unwrap_or(NULL);
|
||||
let mut incoming = ipc::new_listener(crate::POSTFIX_SERVICE).await?;
|
||||
loop {
|
||||
let res = timeout(super::SERVICE_INTERVAL, incoming.next()).await;
|
||||
match res {
|
||||
Ok(res) => match res {
|
||||
Some(Ok(stream)) => {
|
||||
let mut stream = ipc::Connection::new(stream);
|
||||
if let Ok(Some(data)) = stream.next_timeout(1000).await {
|
||||
match data {
|
||||
ipc::Data::Close => {
|
||||
log::info!("close received");
|
||||
break;
|
||||
}
|
||||
ipc::Data::SAS => {
|
||||
send_sas();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(_) => {
|
||||
// timeout
|
||||
unsafe {
|
||||
let tmp = WTSGetActiveConsoleSessionId();
|
||||
if tmp == 0xFFFFFFFF {
|
||||
continue;
|
||||
}
|
||||
let mut close_sent = false;
|
||||
if tmp != session_id {
|
||||
log::info!("session changed from {} to {}", session_id, tmp);
|
||||
session_id = tmp;
|
||||
send_close_async("").await.ok();
|
||||
close_sent = true;
|
||||
}
|
||||
let mut exit_code: DWORD = 0;
|
||||
if h_process.is_null()
|
||||
|| (GetExitCodeProcess(h_process, &mut exit_code) == TRUE
|
||||
&& exit_code != STILL_ACTIVE
|
||||
&& CloseHandle(h_process) == TRUE)
|
||||
{
|
||||
match launch_server(session_id, !close_sent).await {
|
||||
Ok(ptr) => {
|
||||
h_process = ptr;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to launch server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !h_process.is_null() {
|
||||
send_close_async("").await.ok();
|
||||
unsafe { CloseHandle(h_process) };
|
||||
}
|
||||
|
||||
status_handle.set_service_status(ServiceStatus {
|
||||
service_type: SERVICE_TYPE,
|
||||
current_state: ServiceState::Stopped,
|
||||
controls_accepted: ServiceControlAccept::empty(),
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDLE> {
|
||||
if close_first {
|
||||
// in case started some elsewhere
|
||||
send_close_async("").await.ok();
|
||||
}
|
||||
let cmd = format!(
|
||||
"\"{}\" --server",
|
||||
std::env::current_exe()?.to_str().unwrap_or("")
|
||||
);
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
|
||||
.encode_wide()
|
||||
.chain(Some(0).into_iter())
|
||||
.collect();
|
||||
let wstr = wstr.as_ptr();
|
||||
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE) };
|
||||
if h.is_null() {
|
||||
log::error!("Failed to luanch server: {}", get_error());
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
|
||||
let cmd = format!(
|
||||
"\"{}\" {}",
|
||||
std::env::current_exe()?.to_str().unwrap_or(""),
|
||||
arg,
|
||||
);
|
||||
let session_id = unsafe { WTSGetActiveConsoleSessionId() };
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
|
||||
.encode_wide()
|
||||
.chain(Some(0).into_iter())
|
||||
.collect();
|
||||
let wstr = wstr.as_ptr();
|
||||
let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE) };
|
||||
if h.is_null() {
|
||||
bail!(
|
||||
"Failed to launch {} with session id {}: {}",
|
||||
arg,
|
||||
session_id,
|
||||
get_error()
|
||||
);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn send_close(postfix: &str) -> ResultType<()> {
|
||||
send_close_async(postfix).await
|
||||
}
|
||||
|
||||
async fn send_close_async(postfix: &str) -> ResultType<()> {
|
||||
ipc::connect(1000, postfix)
|
||||
.await?
|
||||
.send(&ipc::Data::Close)
|
||||
.await?;
|
||||
// sleep a while to wait for closing and exit
|
||||
sleep(0.1).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas
|
||||
// https://www.cnblogs.com/doutu/p/4892726.html
|
||||
fn send_sas() {
|
||||
#[link(name = "sas")]
|
||||
extern "C" {
|
||||
pub fn SendSAS(AsUser: BOOL);
|
||||
}
|
||||
unsafe {
|
||||
log::info!("SAS received");
|
||||
SendSAS(FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SUPRESS: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
}
|
||||
|
||||
pub fn desktop_changed() -> bool {
|
||||
unsafe { inputDesktopSelected() == FALSE }
|
||||
}
|
||||
|
||||
pub fn try_change_desktop() -> bool {
|
||||
unsafe {
|
||||
if inputDesktopSelected() == FALSE {
|
||||
let res = selectInputDesktop() == TRUE;
|
||||
if !res {
|
||||
let mut s = SUPRESS.lock().unwrap();
|
||||
if s.elapsed() > std::time::Duration::from_secs(3) {
|
||||
log::error!("Failed to switch desktop: {}", get_error());
|
||||
*s = Instant::now();
|
||||
}
|
||||
} else {
|
||||
log::info!("Desktop switched");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn get_error() -> String {
|
||||
unsafe {
|
||||
let buff_size = 256;
|
||||
let mut buff: Vec<u16> = Vec::with_capacity(buff_size);
|
||||
buff.resize(buff_size, 0);
|
||||
let errno = GetLastError();
|
||||
let chars_copied = FormatMessageW(
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS
|
||||
| FORMAT_MESSAGE_FROM_SYSTEM
|
||||
| FORMAT_MESSAGE_ARGUMENT_ARRAY,
|
||||
std::ptr::null(),
|
||||
errno,
|
||||
0,
|
||||
buff.as_mut_ptr(),
|
||||
(buff_size + 1) as u32,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if chars_copied == 0 {
|
||||
return "".to_owned();
|
||||
}
|
||||
let mut curr_char: usize = chars_copied as usize;
|
||||
while curr_char > 0 {
|
||||
let ch = buff[curr_char];
|
||||
|
||||
if ch >= ' ' as u16 {
|
||||
break;
|
||||
}
|
||||
curr_char -= 1;
|
||||
}
|
||||
let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char);
|
||||
let err_msg = String::from_utf16(sl);
|
||||
return err_msg.unwrap_or("".to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_username() -> String {
|
||||
let name = crate::username();
|
||||
if name != "SYSTEM" {
|
||||
return name;
|
||||
}
|
||||
if let Ok(output) = std::process::Command::new("query").arg("user").output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if line.contains("Active") {
|
||||
if let Some(name) = line.split_whitespace().next() {
|
||||
if name.starts_with(">") {
|
||||
return name.replace(">", "");
|
||||
} else {
|
||||
return name.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
pub fn is_prelogin() -> bool {
|
||||
let username = get_active_username();
|
||||
username.is_empty() || username == "SYSTEM"
|
||||
}
|
||||
|
||||
pub fn is_root() -> bool {
|
||||
crate::username() == "SYSTEM"
|
||||
}
|
||||
|
||||
pub fn lock_screen() {
|
||||
extern "C" {
|
||||
pub fn LockWorkStation() -> BOOL;
|
||||
}
|
||||
unsafe {
|
||||
LockWorkStation();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_install_info() -> (String, String, String, String) {
|
||||
let subkey = format!(
|
||||
"HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}",
|
||||
APP_NAME
|
||||
);
|
||||
let mut pf = "C:\\Program Files".to_owned();
|
||||
if let Ok(output) = std::process::Command::new("echo")
|
||||
.arg("%ProgramFiles%")
|
||||
.output()
|
||||
{
|
||||
let tmp = String::from_utf8_lossy(&output.stdout);
|
||||
if !tmp.starts_with("%") {
|
||||
pf = tmp.to_string();
|
||||
}
|
||||
}
|
||||
let path = format!("{}\\{}", pf, APP_NAME);
|
||||
let start_menu = format!(
|
||||
"%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\{}",
|
||||
APP_NAME
|
||||
);
|
||||
let exe = format!("{}\\{}.exe", path, APP_NAME);
|
||||
(subkey, path, start_menu, exe)
|
||||
}
|
||||
|
||||
pub fn update_me() -> ResultType<()> {
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
|
||||
let cmds = format!(
|
||||
"
|
||||
sc stop {app_name}
|
||||
taskkill /F /IM {app_name}.exe
|
||||
copy /Y \"{src_exe}\" \"{exe}\"
|
||||
sc start {app_name}
|
||||
",
|
||||
src_exe = src_exe,
|
||||
exe = exe,
|
||||
app_name = APP_NAME,
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
run_cmds(cmds, false)?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
std::process::Command::new(&exe).spawn()?;
|
||||
std::process::Command::new(&exe)
|
||||
.args(&["--remove", &src_exe])
|
||||
.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_me(options: &str) -> ResultType<()> {
|
||||
let (subkey, path, start_menu, exe) = get_install_info();
|
||||
let mut version_major = "0";
|
||||
let mut version_minor = "0";
|
||||
let mut version_build = "0";
|
||||
let versions: Vec<&str> = crate::VERSION.split(".").collect();
|
||||
if versions.len() > 0 {
|
||||
version_major = versions[0];
|
||||
}
|
||||
if versions.len() > 1 {
|
||||
version_minor = versions[1];
|
||||
}
|
||||
if versions.len() > 2 {
|
||||
version_build = versions[2];
|
||||
}
|
||||
|
||||
let tmp_path = "C:\\Windows\\temp";
|
||||
let mk_shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
|
||||
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Save
|
||||
",
|
||||
tmp_path = tmp_path,
|
||||
app_name = APP_NAME,
|
||||
exe = exe,
|
||||
),
|
||||
"vbs",
|
||||
)?
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
// https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd
|
||||
let uninstall_shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Arguments = \"--uninstall\"
|
||||
oLink.IconLocation = \"msiexec.exe\"
|
||||
oLink.Save
|
||||
",
|
||||
tmp_path = tmp_path,
|
||||
app_name = APP_NAME,
|
||||
exe = exe,
|
||||
),
|
||||
"vbs",
|
||||
)?
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let mut shortcuts = Default::default();
|
||||
if options.contains("desktopicon") {
|
||||
shortcuts = format!(
|
||||
"copy /Y \"{}\\{}.lnk\" \"%PUBLIC%\\Desktop\\\"",
|
||||
tmp_path, APP_NAME
|
||||
);
|
||||
}
|
||||
if options.contains("startmenu") {
|
||||
shortcuts = format!(
|
||||
"{}
|
||||
md \"{start_menu}\"
|
||||
copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\"
|
||||
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
|
||||
",
|
||||
shortcuts,
|
||||
start_menu = start_menu,
|
||||
tmp_path = tmp_path,
|
||||
app_name = APP_NAME
|
||||
);
|
||||
}
|
||||
|
||||
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
|
||||
let size = meta.len() / 1024;
|
||||
// save_tmp is for ensuring not copying file while writing
|
||||
let config_path = Config::save_tmp();
|
||||
let ext = APP_NAME.to_lowercase();
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa
|
||||
// https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10
|
||||
// https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html
|
||||
let cmds = format!(
|
||||
"
|
||||
md \"{path}\"
|
||||
copy /Y \"{src_exe}\" \"{exe}\"
|
||||
reg add {subkey} /f
|
||||
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
|
||||
reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\"
|
||||
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
|
||||
reg add {subkey} /f /v InstallLocation /t REG_SZ /d \"{path}\"
|
||||
reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\"
|
||||
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {major}
|
||||
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {minor}
|
||||
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {build}
|
||||
reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\"
|
||||
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||
reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0
|
||||
reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1
|
||||
\"{mk_shortcut}\"
|
||||
\"{uninstall_shortcut}\"
|
||||
{shortcuts}
|
||||
del /f \"{mk_shortcut}\"
|
||||
del /f \"{uninstall_shortcut}\"
|
||||
del /f \"{tmp_path}\\{app_name}.lnk\"
|
||||
del /f \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext} /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\"
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\"
|
||||
sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\"
|
||||
sc start {app_name}
|
||||
sc stop {app_name}
|
||||
sc delete {app_name}
|
||||
sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\"
|
||||
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes
|
||||
del /f \"{config_path}\"
|
||||
del /f \"{config2_path}\"
|
||||
sc start {app_name}
|
||||
",
|
||||
path=path,
|
||||
src_exe=std::env::current_exe()?.to_str().unwrap_or(""),
|
||||
exe=exe,
|
||||
subkey=subkey,
|
||||
app_name=APP_NAME,
|
||||
version=crate::VERSION,
|
||||
major=version_major,
|
||||
minor=version_minor,
|
||||
build=version_build,
|
||||
size=size,
|
||||
mk_shortcut=mk_shortcut,
|
||||
uninstall_shortcut=uninstall_shortcut,
|
||||
tmp_path=tmp_path,
|
||||
shortcuts=shortcuts,
|
||||
config_path=config_path,
|
||||
config2_path=config_path.replace(".toml", "2.toml"),
|
||||
ext=ext,
|
||||
);
|
||||
run_cmds(cmds, false)?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
std::process::Command::new(exe).spawn()?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall_me() -> ResultType<()> {
|
||||
let (subkey, path, start_menu, _) = get_install_info();
|
||||
let ext = APP_NAME.to_lowercase();
|
||||
let cmds = format!(
|
||||
"
|
||||
sc stop {app_name}
|
||||
sc delete {app_name}
|
||||
taskkill /F /IM {app_name}.exe
|
||||
reg delete {subkey} /f
|
||||
reg delete HKEY_CLASSES_ROOT\\.{ext} /f
|
||||
rd /s /q \"{path}\"
|
||||
rd /s /q \"{start_menu}\"
|
||||
del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\"
|
||||
netsh advfirewall firewall delete rule name=\"{app_name} Service\"
|
||||
",
|
||||
app_name = APP_NAME,
|
||||
path = path,
|
||||
subkey = subkey,
|
||||
start_menu = start_menu,
|
||||
ext = ext,
|
||||
);
|
||||
run_cmds(cmds, true)
|
||||
}
|
||||
|
||||
fn write_cmds(cmds: String, ext: &str) -> ResultType<std::path::PathBuf> {
|
||||
let mut tmp = std::env::temp_dir();
|
||||
tmp.push(format!("{}_{}.{}", APP_NAME, crate::get_time(), ext));
|
||||
let mut cmds = cmds;
|
||||
if ext == "cmd" {
|
||||
cmds = format!("{}\ndel /f \"{}\"", cmds, tmp.to_str().unwrap_or(""));
|
||||
}
|
||||
let mut file = std::fs::File::create(&tmp)?;
|
||||
file.write_all(cmds.as_bytes())?;
|
||||
file.sync_all()?;
|
||||
return Ok(tmp);
|
||||
}
|
||||
|
||||
fn run_cmds(cmds: String, show: bool) -> ResultType<()> {
|
||||
let tmp = write_cmds(cmds, "cmd")?;
|
||||
let res = runas::Command::new(tmp.to_str().unwrap_or(""))
|
||||
.show(show)
|
||||
.force_prompt(true)
|
||||
.status();
|
||||
// double confirm delete, because below delete not work if program
|
||||
// exit immediately such as --uninstall
|
||||
allow_err!(std::fs::remove_file(tmp));
|
||||
let _ = res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_privacy_mode(v: bool) {
|
||||
let v = if v { TRUE } else { FALSE };
|
||||
unsafe {
|
||||
blank_screen(v);
|
||||
BlockInput(v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_input(v: bool) {
|
||||
let v = if v { TRUE } else { FALSE };
|
||||
unsafe {
|
||||
BlockInput(v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_recent_document(path: &str) {
|
||||
extern "C" {
|
||||
fn AddRecentDocument(path: *const u16);
|
||||
}
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let wstr: Vec<u16> = std::ffi::OsStr::new(path)
|
||||
.encode_wide()
|
||||
.chain(Some(0).into_iter())
|
||||
.collect();
|
||||
let wstr = wstr.as_ptr();
|
||||
unsafe {
|
||||
AddRecentDocument(wstr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_installed() -> bool {
|
||||
use windows_service::{
|
||||
service::ServiceAccess,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
if !std::fs::metadata(exe).is_ok() {
|
||||
return false;
|
||||
}
|
||||
let manager_access = ServiceManagerAccess::CONNECT;
|
||||
if let Ok(service_manager) = ServiceManager::local_computer(None::<&str>, manager_access) {
|
||||
if let Ok(_) = service_manager.open_service(APP_NAME, ServiceAccess::QUERY_CONFIG) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_installed_version() -> String {
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
if let Ok(output) = std::process::Command::new(exe).arg("--version").output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
return line.to_owned();
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
163
src/port_forward.rs
Normal file
163
src/port_forward.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::client::*;
|
||||
use hbb_common::{
|
||||
allow_err, bail,
|
||||
config::CONNECT_TIMEOUT,
|
||||
futures::SinkExt,
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
tcp, timeout,
|
||||
tokio::{self, net::TcpStream, stream::StreamExt, sync::mpsc},
|
||||
tokio_util::codec::{BytesCodec, Framed},
|
||||
ResultType, Stream,
|
||||
};
|
||||
|
||||
fn run_rdp(port: u16) {
|
||||
std::process::Command::new("mstsc")
|
||||
.arg(format!("/v:localhost:{}", port))
|
||||
.spawn()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub async fn listen(
|
||||
id: String,
|
||||
port: i32,
|
||||
interface: impl Interface,
|
||||
ui_receiver: mpsc::UnboundedReceiver<Data>,
|
||||
) -> ResultType<()> {
|
||||
let mut listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
|
||||
let addr = listener.local_addr()?;
|
||||
log::info!("listening on port {:?}", addr);
|
||||
let is_rdp = port == 0;
|
||||
if is_rdp {
|
||||
run_rdp(addr.port());
|
||||
}
|
||||
let mut ui_receiver = ui_receiver;
|
||||
loop {
|
||||
tokio::select! {
|
||||
Ok((forward, addr)) = listener.accept() => {
|
||||
log::info!("new connection from {:?}", addr);
|
||||
let id = id.clone();
|
||||
let mut forward = Framed::new(forward, BytesCodec::new());
|
||||
match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward).await {
|
||||
Ok(Some(stream)) => {
|
||||
let interface = interface.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = run_forward(forward, stream).await {
|
||||
interface.msgbox("error", "Error", &err.to_string());
|
||||
}
|
||||
log::info!("connection from {:?} closed", addr);
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
interface.msgbox("error", "Error", &err.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
d = ui_receiver.recv() => {
|
||||
match d {
|
||||
Some(Data::Close) => {
|
||||
break;
|
||||
}
|
||||
Some(Data::NewRDP) => {
|
||||
run_rdp(addr.port());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_and_login(
|
||||
id: &str,
|
||||
ui_receiver: &mut mpsc::UnboundedReceiver<Data>,
|
||||
interface: impl Interface,
|
||||
forward: &mut Framed<TcpStream, BytesCodec>,
|
||||
) -> ResultType<Option<Stream>> {
|
||||
let (mut stream, _) = Client::start(&id).await?;
|
||||
let mut interface = interface;
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = timeout(CONNECT_TIMEOUT, stream.next()) => match res {
|
||||
Err(_) => {
|
||||
bail!("Timeout");
|
||||
}
|
||||
Ok(Some(Ok(bytes))) => {
|
||||
let msg_in = Message::parse_from_bytes(&bytes)?;
|
||||
match msg_in.union {
|
||||
Some(message::Union::hash(hash)) => {
|
||||
interface.handle_hash(hash, &mut stream).await;
|
||||
}
|
||||
Some(message::Union::login_response(lr)) => match lr.union {
|
||||
Some(login_response::Union::error(err)) => {
|
||||
interface.handle_login_error(&err);
|
||||
return Ok(None);
|
||||
}
|
||||
Some(login_response::Union::peer_info(pi)) => {
|
||||
interface.handle_peer_info(pi);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(message::Union::test_delay(t)) => {
|
||||
interface.handle_test_delay(t, &mut stream).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
bail!("Reset by the peer");
|
||||
}
|
||||
},
|
||||
d = ui_receiver.recv() => {
|
||||
match d {
|
||||
Some(Data::Login((password, remember))) => {
|
||||
interface.handle_login_from_ui(password, remember, &mut stream).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
res = forward.next() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
buffer.extend(bytes);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
stream.set_raw();
|
||||
if !buffer.is_empty() {
|
||||
allow_err!(stream.send_bytes(buffer.into()).await);
|
||||
}
|
||||
Ok(Some(stream))
|
||||
}
|
||||
|
||||
async fn run_forward(forward: Framed<TcpStream, BytesCodec>, stream: Stream) -> ResultType<()> {
|
||||
log::info!("new port forwarding connection started");
|
||||
let mut forward = forward;
|
||||
let mut stream = stream;
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = forward.next() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
allow_err!(stream.send_bytes(bytes.into()).await);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
res = stream.next() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
allow_err!(forward.send(bytes.into()).await);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
416
src/rendezvous_mediator.rs
Normal file
416
src/rendezvous_mediator.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
use crate::server::{check_zombie, new as new_server, ServerPtr};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
config::{Config, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
|
||||
futures::future::join_all,
|
||||
log,
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::FramedStream,
|
||||
tokio::{
|
||||
self, select,
|
||||
time::{interval, Duration},
|
||||
},
|
||||
udp::FramedSocket,
|
||||
AddrMangle, ResultType,
|
||||
};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
time::SystemTime,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
type Message = RendezvousMessage;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SOLVING_PK_MISMATCH: Arc<Mutex<String>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RendezvousMediator {
|
||||
addr: SocketAddr,
|
||||
host: String,
|
||||
host_prefix: String,
|
||||
rendezvous_servers: Vec<String>,
|
||||
last_id_pk_registery: String,
|
||||
}
|
||||
|
||||
impl RendezvousMediator {
|
||||
pub async fn start_all() {
|
||||
check_zombie();
|
||||
let server = new_server();
|
||||
loop {
|
||||
Config::reset_online();
|
||||
if Config::get_option("stop-service").is_empty() {
|
||||
let mut futs = Vec::new();
|
||||
let servers = Config::get_rendezvous_servers();
|
||||
for host in servers.clone() {
|
||||
let server = server.clone();
|
||||
let servers = servers.clone();
|
||||
futs.push(tokio::spawn(async move {
|
||||
allow_err!(Self::start(server, host, servers).await);
|
||||
}));
|
||||
}
|
||||
join_all(futs).await;
|
||||
}
|
||||
sleep(1.).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(
|
||||
server: ServerPtr,
|
||||
host: String,
|
||||
rendezvous_servers: Vec<String>,
|
||||
) -> ResultType<()> {
|
||||
log::info!("start rendezvous mediator of {}", host);
|
||||
let host_prefix: String = host
|
||||
.split(".")
|
||||
.next()
|
||||
.map(|x| {
|
||||
if x.parse::<i32>().is_ok() {
|
||||
host.clone()
|
||||
} else {
|
||||
x.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or(host.to_owned());
|
||||
let mut rz = Self {
|
||||
addr: Config::get_any_listen_addr(),
|
||||
host: host.clone(),
|
||||
host_prefix,
|
||||
rendezvous_servers,
|
||||
last_id_pk_registery: "".to_owned(),
|
||||
};
|
||||
allow_err!(rz.dns_check());
|
||||
let mut socket = FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
const TIMER_OUT: Duration = Duration::from_secs(1);
|
||||
let mut timer = interval(TIMER_OUT);
|
||||
let mut last_timer = SystemTime::UNIX_EPOCH;
|
||||
const REG_INTERVAL: i64 = 12_000;
|
||||
const REG_TIMEOUT: i64 = 3_000;
|
||||
const MAX_FAILS1: i64 = 3;
|
||||
const MAX_FAILS2: i64 = 6;
|
||||
const DNS_INTERVAL: i64 = 60_000;
|
||||
let mut fails = 0;
|
||||
let mut last_register_resp = SystemTime::UNIX_EPOCH;
|
||||
let mut last_register_sent = SystemTime::UNIX_EPOCH;
|
||||
let mut last_dns_check = SystemTime::UNIX_EPOCH;
|
||||
let mut old_latency = 0;
|
||||
let mut ema_latency = 0;
|
||||
loop {
|
||||
select! {
|
||||
Some(Ok((bytes, _))) = socket.next() => {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::register_peer_response(rpr)) => {
|
||||
if rpr.request_pk {
|
||||
log::info!("request_pk received from {}", host);
|
||||
allow_err!(rz.register_pk(&mut socket).await);
|
||||
continue;
|
||||
}
|
||||
last_register_resp = SystemTime::now();
|
||||
let mut latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0);
|
||||
if ema_latency == 0 {
|
||||
ema_latency = latency;
|
||||
} else {
|
||||
ema_latency = latency / 30 + (ema_latency * 29 / 30);
|
||||
latency = ema_latency;
|
||||
}
|
||||
let mut n = latency / 5;
|
||||
if n < 3000 {
|
||||
n = 3000;
|
||||
}
|
||||
if (latency - old_latency).abs() > n || old_latency <= 0 {
|
||||
Config::update_latency(&host, latency);
|
||||
log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.);
|
||||
old_latency = latency;
|
||||
}
|
||||
fails = 0;
|
||||
}
|
||||
Some(rendezvous_message::Union::register_pk_response(rpr)) => {
|
||||
match rpr.result.enum_value_or_default() {
|
||||
register_pk_response::Result::OK => {
|
||||
Config::set_key_confirmed(true);
|
||||
Config::set_host_key_confirmed(&rz.host_prefix, true);
|
||||
*SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned();
|
||||
last_register_resp = SystemTime::now();
|
||||
let latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0);
|
||||
Config::update_latency(&host, latency);
|
||||
log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.);
|
||||
fails = 0;
|
||||
}
|
||||
register_pk_response::Result::UUID_MISMATCH => {
|
||||
allow_err!(rz.handle_uuid_mismatch(&mut socket).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::punch_hole(ph)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_punch_hole(ph, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::request_relay(rr)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_request_relay(rr, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::fetch_local_addr(fla)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_intranet(fla, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::configure_update(cu)) => {
|
||||
Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(","));
|
||||
Config::set_serial(cu.serial);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
log::debug!("Non-protobuf message bytes received: {:?}", bytes);
|
||||
}
|
||||
},
|
||||
_ = timer.tick() => {
|
||||
if Config::get_rendezvous_servers() != rz.rendezvous_servers {
|
||||
break;
|
||||
}
|
||||
if !Config::get_option("stop-service").is_empty() {
|
||||
break;
|
||||
}
|
||||
if rz.addr.port() == 0 {
|
||||
allow_err!(rz.dns_check());
|
||||
if rz.addr.port() == 0 {
|
||||
continue;
|
||||
} else {
|
||||
// have to do this for osx, to avoid "Can't assign requested address"
|
||||
// when socket created before OS network ready
|
||||
socket = FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
}
|
||||
}
|
||||
let now = SystemTime::now();
|
||||
if now.duration_since(last_timer).map(|d| d < TIMER_OUT).unwrap_or(false) {
|
||||
// a workaround of tokio timer bug
|
||||
continue;
|
||||
}
|
||||
last_timer = now;
|
||||
let elapsed_resp = now.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(REG_INTERVAL);
|
||||
let timeout = last_register_sent.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(0) >= REG_TIMEOUT;
|
||||
if timeout || elapsed_resp >= REG_INTERVAL {
|
||||
allow_err!(rz.register_peer(&mut socket).await);
|
||||
last_register_sent = now;
|
||||
if timeout {
|
||||
fails += 1;
|
||||
if fails > MAX_FAILS2 {
|
||||
Config::update_latency(&host, -1);
|
||||
old_latency = 0;
|
||||
if now.duration_since(last_dns_check).map(|d| d.as_millis() as i64).unwrap_or(0) > DNS_INTERVAL {
|
||||
allow_err!(rz.dns_check());
|
||||
last_dns_check = now;
|
||||
}
|
||||
} else if fails > MAX_FAILS1 {
|
||||
Config::update_latency(&host, 0);
|
||||
old_latency = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dns_check(&mut self) -> ResultType<()> {
|
||||
self.addr = hbb_common::to_socket_addr(&crate::check_port(&self.host, RENDEZVOUS_PORT))?;
|
||||
log::debug!("Lookup dns of {}", self.host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> {
|
||||
self.create_relay(
|
||||
rr.socket_addr,
|
||||
rr.relay_server,
|
||||
rr.uuid,
|
||||
server,
|
||||
rr.secure,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_relay(
|
||||
&self,
|
||||
socket_addr: Vec<u8>,
|
||||
relay_server: String,
|
||||
uuid: String,
|
||||
server: ServerPtr,
|
||||
secure: bool,
|
||||
initiate: bool,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&socket_addr);
|
||||
log::info!(
|
||||
"create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}",
|
||||
peer_addr,
|
||||
relay_server,
|
||||
uuid,
|
||||
secure,
|
||||
);
|
||||
let mut socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?;
|
||||
let mut msg_out = Message::new();
|
||||
let mut rr = RelayResponse {
|
||||
socket_addr,
|
||||
..Default::default()
|
||||
};
|
||||
if initiate {
|
||||
rr.uuid = uuid.clone();
|
||||
rr.relay_server = relay_server.clone();
|
||||
rr.uuid = uuid.clone();
|
||||
rr.set_id(Config::get_id());
|
||||
}
|
||||
msg_out.set_relay_response(rr);
|
||||
socket.send(&msg_out).await?;
|
||||
crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
log::debug!("Handle intranet from {:?}", peer_addr);
|
||||
let (mut socket, port) = {
|
||||
let socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT)
|
||||
.await?;
|
||||
let port = socket.get_ref().local_addr()?.port();
|
||||
(socket, port)
|
||||
};
|
||||
let local_addr = socket.get_ref().local_addr()?;
|
||||
let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), port).parse()?;
|
||||
let mut msg_out = Message::new();
|
||||
let mut relay_server = Config::get_option("relay-server");
|
||||
if relay_server.is_empty() {
|
||||
relay_server = fla.relay_server;
|
||||
}
|
||||
msg_out.set_local_addr(LocalAddr {
|
||||
socket_addr: AddrMangle::encode(peer_addr),
|
||||
local_addr: AddrMangle::encode(local_addr),
|
||||
relay_server,
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
socket.send_raw(bytes).await?;
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, false).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
|
||||
let mut relay_server = Config::get_option("relay-server");
|
||||
if relay_server.is_empty() {
|
||||
relay_server = ph.relay_server;
|
||||
}
|
||||
if ph.nat_type.enum_value_or_default() == NatType::SYMMETRIC
|
||||
|| Config::get_nat_type() == NatType::SYMMETRIC as i32
|
||||
{
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
return self
|
||||
.create_relay(ph.socket_addr, relay_server, uuid, server, true, true)
|
||||
.await;
|
||||
}
|
||||
let peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
log::debug!("Punch hole to {:?}", peer_addr);
|
||||
let mut socket = {
|
||||
let socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT)
|
||||
.await?;
|
||||
allow_err!(FramedStream::new(peer_addr, socket.get_ref().local_addr()?, 300).await);
|
||||
socket
|
||||
};
|
||||
let mut msg_out = Message::new();
|
||||
use hbb_common::protobuf::ProtobufEnum;
|
||||
let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT);
|
||||
msg_out.set_punch_hole_sent(PunchHoleSent {
|
||||
socket_addr: ph.socket_addr,
|
||||
id: Config::get_id(),
|
||||
relay_server,
|
||||
nat_type: nat_type.into(),
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
socket.send_raw(bytes).await?;
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_pk(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
|
||||
let mut msg_out = Message::new();
|
||||
let pk = Config::get_key_pair().1;
|
||||
let uuid = if let Ok(id) = machine_uid::get() {
|
||||
log::info!("machine uid: {}", id);
|
||||
id.into()
|
||||
} else {
|
||||
pk.clone()
|
||||
};
|
||||
let id = Config::get_id();
|
||||
self.last_id_pk_registery = id.clone();
|
||||
msg_out.set_register_pk(RegisterPk {
|
||||
id,
|
||||
uuid,
|
||||
pk,
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, self.addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_uuid_mismatch(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
|
||||
if self.last_id_pk_registery != Config::get_id() {
|
||||
return Ok(());
|
||||
}
|
||||
{
|
||||
let mut solving = SOLVING_PK_MISMATCH.lock().unwrap();
|
||||
if solving.is_empty() || *solving == self.host {
|
||||
log::info!("UUID_MISMATCH received from {}", self.host);
|
||||
Config::set_key_confirmed(false);
|
||||
Config::update_id();
|
||||
*solving = self.host.clone();
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.register_pk(socket).await
|
||||
}
|
||||
|
||||
async fn register_peer(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
|
||||
if !SOLVING_PK_MISMATCH.lock().unwrap().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if !Config::get_key_confirmed() || !Config::get_host_key_confirmed(&self.host_prefix) {
|
||||
log::info!(
|
||||
"register_pk of {} due to key not confirmed",
|
||||
self.host_prefix
|
||||
);
|
||||
return self.register_pk(socket).await;
|
||||
}
|
||||
let id = Config::get_id();
|
||||
log::trace!(
|
||||
"Register my id {:?} to rendezvous server {:?}",
|
||||
id,
|
||||
self.addr,
|
||||
);
|
||||
let mut msg_out = Message::new();
|
||||
let serial = Config::get_serial();
|
||||
msg_out.set_register_peer(RegisterPeer {
|
||||
id,
|
||||
serial,
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, self.addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
335
src/server.rs
Normal file
335
src/server.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use crate::ipc::Data;
|
||||
use connection::{ConnInner, Connection};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::{anyhow, Context},
|
||||
bail,
|
||||
config::{Config, CONNECT_TIMEOUT, RELAY_PORT},
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::{Message as _, ProtobufEnum},
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
sodiumoxide::crypto::{box_, secretbox, sign},
|
||||
tcp::FramedStream,
|
||||
timeout, tokio, ResultType, Stream,
|
||||
};
|
||||
use service::{GenericService, Service, ServiceTmpl, Subscriber};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex, RwLock, Weak},
|
||||
};
|
||||
|
||||
mod audio_service;
|
||||
mod clipboard_service;
|
||||
mod connection;
|
||||
pub mod input_service;
|
||||
mod service;
|
||||
mod video_service;
|
||||
|
||||
use hbb_common::tcp::new_listener;
|
||||
|
||||
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
|
||||
type ConnMap = HashMap<i32, ConnInner>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CHILD_PROCESS: Childs = Default::default();
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
connections: ConnMap,
|
||||
services: HashMap<&'static str, Box<dyn Service>>,
|
||||
id_count: i32,
|
||||
}
|
||||
|
||||
pub type ServerPtr = Arc<RwLock<Server>>;
|
||||
pub type ServerPtrWeak = Weak<RwLock<Server>>;
|
||||
|
||||
pub fn new() -> ServerPtr {
|
||||
let mut server = Server {
|
||||
connections: HashMap::new(),
|
||||
services: HashMap::new(),
|
||||
id_count: 0,
|
||||
};
|
||||
server.add_service(Box::new(audio_service::new()));
|
||||
server.add_service(Box::new(video_service::new()));
|
||||
server.add_service(Box::new(clipboard_service::new()));
|
||||
server.add_service(Box::new(input_service::new_cursor()));
|
||||
server.add_service(Box::new(input_service::new_pos()));
|
||||
Arc::new(RwLock::new(server))
|
||||
}
|
||||
|
||||
async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> {
|
||||
let local_addr = socket.get_ref().local_addr()?;
|
||||
drop(socket);
|
||||
// even we drop socket, below still may fail if not use reuse_addr,
|
||||
// there is TIME_WAIT before socket really released, so sometimes we
|
||||
// see “Only one usage of each socket address is normally permitted” on windows sometimes,
|
||||
let mut listener = new_listener(local_addr, true).await?;
|
||||
log::info!("Server listening on: {}", &listener.local_addr()?);
|
||||
if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? {
|
||||
create_tcp_connection_(server, Stream::from(stream), addr, secure).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_tcp_connection_(
|
||||
server: ServerPtr,
|
||||
stream: Stream,
|
||||
addr: SocketAddr,
|
||||
secure: bool,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = stream;
|
||||
let id = {
|
||||
let mut w = server.write().unwrap();
|
||||
w.id_count += 1;
|
||||
w.id_count
|
||||
};
|
||||
let (sk, pk) = Config::get_key_pair();
|
||||
if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
|
||||
let mut sk_ = [0u8; sign::SECRETKEYBYTES];
|
||||
sk_[..].copy_from_slice(&sk);
|
||||
let sk = sign::SecretKey(sk_);
|
||||
let mut msg_out = Message::new();
|
||||
let signed_id = sign::sign(Config::get_id().as_bytes(), &sk);
|
||||
let (our_pk_b, our_sk_b) = box_::gen_keypair();
|
||||
msg_out.set_signed_id(SignedId {
|
||||
id: signed_id,
|
||||
pk: our_pk_b.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
|
||||
match timeout(CONNECT_TIMEOUT, stream.next()).await? {
|
||||
Some(res) => {
|
||||
let bytes = res?;
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
if let Some(message::Union::public_key(pk)) = msg_in.union {
|
||||
if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES {
|
||||
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(&pk.asymmetric_value);
|
||||
let their_pk_b = box_::PublicKey(pk_);
|
||||
let symmetric_key =
|
||||
box_::open(&pk.symmetric_value, &nonce, &their_pk_b, &our_sk_b)
|
||||
.map_err(|_| {
|
||||
anyhow!("Handshake failed: box decryption failure")
|
||||
})?;
|
||||
if symmetric_key.len() != secretbox::KEYBYTES {
|
||||
bail!("Handshake failed: invalid secret key length from peer");
|
||||
}
|
||||
let mut key = [0u8; secretbox::KEYBYTES];
|
||||
key[..].copy_from_slice(&symmetric_key);
|
||||
stream.set_key(secretbox::Key(key));
|
||||
} else if !pk.asymmetric_value.is_empty() {
|
||||
bail!("Handshake failed: invalid public sign key length from peer");
|
||||
}
|
||||
} else {
|
||||
log::error!("Handshake failed: invalid message type");
|
||||
}
|
||||
} else {
|
||||
bail!("Handshake failed: invalid message format");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
bail!("Failed to receive public key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connection::start(addr, stream, id, Arc::downgrade(&server)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn accept_connection(
|
||||
server: ServerPtr,
|
||||
socket: Stream,
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
) {
|
||||
if let Err(err) = accept_connection_(server, socket, secure).await {
|
||||
log::error!("Failed to accept connection from {}: {}", peer_addr, err);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_relay_connection(
|
||||
server: ServerPtr,
|
||||
relay_server: String,
|
||||
uuid: String,
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
) {
|
||||
if let Err(err) =
|
||||
create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await
|
||||
{
|
||||
log::error!(
|
||||
"Failed to create relay connection for {} with uuid {}: {}",
|
||||
peer_addr,
|
||||
uuid,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_relay_connection_(
|
||||
server: ServerPtr,
|
||||
relay_server: String,
|
||||
uuid: String,
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = FramedStream::new(
|
||||
&crate::check_port(relay_server, RELAY_PORT),
|
||||
Config::get_any_listen_addr(),
|
||||
CONNECT_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_request_relay(RequestRelay {
|
||||
uuid,
|
||||
..Default::default()
|
||||
});
|
||||
stream.send(&msg_out).await?;
|
||||
create_tcp_connection_(server, stream, peer_addr, secure).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) {
|
||||
for s in self.services.values() {
|
||||
if !noperms.contains(&s.name()) {
|
||||
s.on_subscribe(conn.clone());
|
||||
}
|
||||
}
|
||||
self.connections.insert(conn.id(), conn);
|
||||
}
|
||||
|
||||
pub fn remove_connection(&mut self, conn: &ConnInner) {
|
||||
for s in self.services.values() {
|
||||
s.on_unsubscribe(conn.id());
|
||||
}
|
||||
self.connections.remove(&conn.id());
|
||||
}
|
||||
|
||||
fn add_service(&mut self, service: Box<dyn Service>) {
|
||||
let name = service.name();
|
||||
self.services.insert(name, service);
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) {
|
||||
if let Some(s) = self.services.get(&name) {
|
||||
if s.is_subed(conn.id()) == sub {
|
||||
return;
|
||||
}
|
||||
if sub {
|
||||
s.on_subscribe(conn.clone());
|
||||
} else {
|
||||
s.on_unsubscribe(conn.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
for s in self.services.values() {
|
||||
s.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_zombie() {
|
||||
std::thread::spawn(|| loop {
|
||||
let mut lock = CHILD_PROCESS.lock().unwrap();
|
||||
let mut i = 0;
|
||||
while i != lock.len() {
|
||||
let c = &mut (*lock)[i];
|
||||
if let Ok(Some(_)) = c.try_wait() {
|
||||
lock.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
drop(lock);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn start_server(is_server: bool, _tray: bool) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
log::info!("DISPLAY={:?}", std::env::var("DISPLAY"));
|
||||
log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY"));
|
||||
}
|
||||
if is_server {
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = crate::ipc::start("") {
|
||||
log::error!("Failed to start ipc: {}", err);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
});
|
||||
/*
|
||||
tray is buggy, and not work on win10 2004, also cause crash, disable it
|
||||
#[cfg(windows)]
|
||||
if _tray {
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
if !crate::platform::is_prelogin() {
|
||||
let mut res = Ok(None);
|
||||
// while switching from prelogin to user screen, run_as_user may fails,
|
||||
// so we try more times
|
||||
for _ in 0..10 {
|
||||
res = crate::platform::run_as_user("--tray");
|
||||
if res.is_ok() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
allow_err!(res);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
crate::RendezvousMediator::start_all().await;
|
||||
} else {
|
||||
match crate::ipc::connect(1000, "").await {
|
||||
Ok(mut conn) => {
|
||||
allow_err!(conn.send(&Data::SystemInfo(None)).await);
|
||||
if let Ok(Some(data)) = conn.next_timeout(1000).await {
|
||||
log::info!("server info: {:?}", data);
|
||||
}
|
||||
// sync key pair
|
||||
let mut n = 0;
|
||||
loop {
|
||||
if Config::get_key_confirmed() {
|
||||
// check ipc::get_id(), key_confirmed may change, so give some chance to correct
|
||||
n += 1;
|
||||
if n > 3 {
|
||||
break;
|
||||
} else {
|
||||
sleep(1.).await;
|
||||
}
|
||||
} else {
|
||||
allow_err!(conn.send(&Data::ConfirmedKey(None)).await);
|
||||
if let Ok(Some(Data::ConfirmedKey(Some(pair)))) =
|
||||
conn.next_timeout(1000).await
|
||||
{
|
||||
Config::set_key_pair(pair);
|
||||
Config::set_key_confirmed(true);
|
||||
log::info!("key pair synced");
|
||||
break;
|
||||
} else {
|
||||
sleep(1.).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!("server not started (will try to start): {}", err);
|
||||
std::thread::spawn(|| start_server(true, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
350
src/server/audio_service.rs
Normal file
350
src/server/audio_service.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback.
|
||||
// libpulseaudio support loopback because pulseaudio is a standalone audio service with some
|
||||
// configuration, but need to install the library and start the service on OS, not a good choice.
|
||||
// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
|
||||
// mac: https://github.com/mattingalls/Soundflower
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient
|
||||
// https://github.com/ExistentialAudio/BlackHole
|
||||
|
||||
// if pactl not work, please run
|
||||
// sudo apt-get --purge --reinstall install pulseaudio
|
||||
// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card
|
||||
// https://wiki.debian.org/audio-loopback
|
||||
// https://github.com/krruzic/pulsectl
|
||||
|
||||
use super::*;
|
||||
use magnum_opus::{Application::*, Channels::*, Encoder};
|
||||
|
||||
pub const NAME: &'static str = "audio";
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.repeat::<cpal_impl::State, _>(33, cpal_impl::run);
|
||||
sp
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.run(pa_impl::run);
|
||||
sp
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod pa_impl {
|
||||
use super::*;
|
||||
#[tokio::main(basic_scheduler)]
|
||||
pub async fn run(sp: GenericService) -> ResultType<()> {
|
||||
if let Ok(mut stream) = crate::ipc::connect(1000, "_pa").await {
|
||||
let mut encoder =
|
||||
Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?;
|
||||
allow_err!(
|
||||
stream
|
||||
.send(&crate::ipc::Data::Config((
|
||||
"audio-input".to_owned(),
|
||||
Some(Config::get_option("audio-input"))
|
||||
)))
|
||||
.await
|
||||
);
|
||||
while sp.ok() {
|
||||
sp.snapshot(|sps| {
|
||||
sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2));
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(data) = stream.next_timeout2(1000).await {
|
||||
match data? {
|
||||
Some(crate::ipc::Data::RawMessage(bytes)) => {
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts::<f32>(
|
||||
bytes.as_ptr() as _,
|
||||
bytes.len() / 4,
|
||||
)
|
||||
};
|
||||
send_f32(data, &mut encoder, &sp);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
mod cpal_impl {
|
||||
use super::*;
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
Device, Host, SupportedStreamConfig,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HOST: Host = cpal::default_host();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
stream: Option<(Box<dyn StreamTrait>, Arc<Message>)>,
|
||||
}
|
||||
|
||||
impl super::service::Reset for State {
|
||||
fn reset(&mut self) {
|
||||
self.stream.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
|
||||
sp.snapshot(|sps| {
|
||||
match &state.stream {
|
||||
None => {
|
||||
state.stream = Some(play(&sp)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some((_, format)) = &state.stream {
|
||||
sps.send_shared(format.clone());
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send(
|
||||
data: &[f32],
|
||||
sample_rate0: u32,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
encoder: &mut Encoder,
|
||||
sp: &GenericService,
|
||||
) {
|
||||
if data.iter().filter(|x| **x != 0.).next().is_none() {
|
||||
return;
|
||||
}
|
||||
let buffer;
|
||||
let data = if sample_rate0 != sample_rate {
|
||||
buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels);
|
||||
&buffer
|
||||
} else {
|
||||
data
|
||||
};
|
||||
send_f32(data, encoder, sp);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
|
||||
let audio_input = Config::get_option("audio-input");
|
||||
if !audio_input.is_empty() {
|
||||
return get_audio_input(&audio_input);
|
||||
}
|
||||
let device = HOST
|
||||
.default_output_device()
|
||||
.with_context(|| "Failed to get default output device for loopback")?;
|
||||
log::info!(
|
||||
"Default output device: {}",
|
||||
device.name().unwrap_or("".to_owned())
|
||||
);
|
||||
let format = device
|
||||
.default_output_config()
|
||||
.map_err(|e| anyhow!(e))
|
||||
.with_context(|| "Failed to get default output format")?;
|
||||
log::info!("Default output format: {:?}", format);
|
||||
Ok((device, format))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
|
||||
let audio_input = Config::get_option("audio-input");
|
||||
get_audio_input(&audio_input)
|
||||
}
|
||||
|
||||
fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> {
|
||||
if audio_input == "Mute" {
|
||||
bail!("Mute");
|
||||
}
|
||||
let mut device = None;
|
||||
if !audio_input.is_empty() {
|
||||
for d in HOST
|
||||
.devices()
|
||||
.with_context(|| "Failed to get audio devices")?
|
||||
{
|
||||
if d.name().unwrap_or("".to_owned()) == audio_input {
|
||||
device = Some(d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if device.is_none() {
|
||||
device = Some(
|
||||
HOST.default_input_device()
|
||||
.with_context(|| "Failed to get default input device for loopback")?,
|
||||
);
|
||||
}
|
||||
let device = device.unwrap();
|
||||
log::info!("Input device: {}", device.name().unwrap_or("".to_owned()));
|
||||
let format = device
|
||||
.default_input_config()
|
||||
.map_err(|e| anyhow!(e))
|
||||
.with_context(|| "Failed to get default input format")?;
|
||||
log::info!("Default input format: {:?}", format);
|
||||
Ok((device, format))
|
||||
}
|
||||
|
||||
fn play(sp: &GenericService) -> ResultType<(Box<dyn StreamTrait>, Arc<Message>)> {
|
||||
let (device, config) = get_device()?;
|
||||
let sp = sp.clone();
|
||||
let err_fn = move |err| {
|
||||
log::error!("an error occurred on stream: {}", err);
|
||||
};
|
||||
// Sample rate must be one of 8000, 12000, 16000, 24000, or 48000.
|
||||
let sample_rate_0 = config.sample_rate().0;
|
||||
let sample_rate = if sample_rate_0 < 12000 {
|
||||
8000
|
||||
} else if sample_rate_0 < 16000 {
|
||||
12000
|
||||
} else if sample_rate_0 < 24000 {
|
||||
16000
|
||||
} else if sample_rate_0 < 48000 {
|
||||
24000
|
||||
} else {
|
||||
48000
|
||||
};
|
||||
let mut encoder = Encoder::new(
|
||||
sample_rate,
|
||||
if config.channels() > 1 { Stereo } else { Mono },
|
||||
LowDelay,
|
||||
)?;
|
||||
let channels = config.channels();
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| {
|
||||
send(
|
||||
data,
|
||||
sample_rate_0,
|
||||
sample_rate,
|
||||
channels,
|
||||
&mut encoder,
|
||||
&sp,
|
||||
);
|
||||
},
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data: &[i16], _: &_| {
|
||||
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
|
||||
send(
|
||||
&buffer,
|
||||
sample_rate_0,
|
||||
sample_rate,
|
||||
channels,
|
||||
&mut encoder,
|
||||
&sp,
|
||||
);
|
||||
},
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::U16 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data: &[u16], _: &_| {
|
||||
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
|
||||
send(
|
||||
&buffer,
|
||||
sample_rate_0,
|
||||
sample_rate,
|
||||
channels,
|
||||
&mut encoder,
|
||||
&sp,
|
||||
);
|
||||
},
|
||||
err_fn,
|
||||
)?,
|
||||
};
|
||||
stream.play()?;
|
||||
Ok((
|
||||
Box::new(stream),
|
||||
Arc::new(create_format_msg(sample_rate, channels)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
|
||||
let format = AudioFormat {
|
||||
sample_rate,
|
||||
channels: channels as _,
|
||||
..Default::default()
|
||||
};
|
||||
let mut misc = Misc::new();
|
||||
misc.set_audio_format(format);
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc);
|
||||
msg
|
||||
}
|
||||
|
||||
fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
|
||||
if data.iter().filter(|x| **x != 0.).next().is_some() {
|
||||
match encoder.encode_vec_float(data, data.len() * 6) {
|
||||
Ok(data) => {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_audio_frame(AudioFrame {
|
||||
data,
|
||||
..Default::default()
|
||||
});
|
||||
sp.send(msg_out);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_pulse() {
|
||||
use libpulse_binding as pulse;
|
||||
use libpulse_simple_binding as psimple;
|
||||
let spec = pulse::sample::Spec {
|
||||
format: pulse::sample::SAMPLE_FLOAT32NE,
|
||||
channels: 2,
|
||||
rate: 24000,
|
||||
};
|
||||
let hspec = hound::WavSpec {
|
||||
channels: spec.channels as _,
|
||||
sample_rate: spec.rate as _,
|
||||
bits_per_sample: (4 * 8) as _,
|
||||
sample_format: hound::SampleFormat::Float,
|
||||
};
|
||||
const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav");
|
||||
let mut writer =
|
||||
hound::WavWriter::create(PATH, hspec).expect("Could not create hsound writer");
|
||||
let device = crate::platform::linux::get_pa_monitor();
|
||||
let s = psimple::Simple::new(
|
||||
None, // Use the default server
|
||||
"Test", // Our application’s name
|
||||
pulse::stream::Direction::Record, // We want a record stream
|
||||
Some(&device), // Use the default device
|
||||
"Test", // Description of our stream
|
||||
&spec, // Our sample format
|
||||
None, // Use default channel map
|
||||
None, // Use default buffering attributes
|
||||
)
|
||||
.expect("Could not create simple pulse");
|
||||
let mut out: Vec<u8> = Vec::with_capacity(1024);
|
||||
unsafe {
|
||||
out.set_len(out.capacity());
|
||||
}
|
||||
for _ in 0..600 {
|
||||
s.read(&mut out).expect("Could not read pcm");
|
||||
let out2 =
|
||||
unsafe { std::slice::from_raw_parts::<f32>(out.as_ptr() as _, out.len() / 4) };
|
||||
for v in out2 {
|
||||
writer.write_sample(*v).ok();
|
||||
}
|
||||
}
|
||||
println!("{:?} {}", device, out.len());
|
||||
writer.finalize().expect("Could not finalize writer");
|
||||
}
|
||||
}
|
||||
53
src/server/clipboard_service.rs
Normal file
53
src/server/clipboard_service.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use super::*;
|
||||
pub use crate::common::{
|
||||
check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME,
|
||||
CONTENT,
|
||||
};
|
||||
|
||||
struct State {
|
||||
ctx: Option<ClipboardContext>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
let ctx = match ClipboardContext::new() {
|
||||
Ok(ctx) => Some(ctx),
|
||||
Err(err) => {
|
||||
log::error!("Failed to start {}: {}", NAME, err);
|
||||
None
|
||||
}
|
||||
};
|
||||
Self {
|
||||
ctx,
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::service::Reset for State {
|
||||
fn reset(&mut self) {
|
||||
*CONTENT.lock().unwrap() = Default::default();
|
||||
self.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, false);
|
||||
sp.repeat::<State, _>(INTERVAL, run);
|
||||
sp
|
||||
}
|
||||
|
||||
fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
|
||||
if let Some(ctx) = state.ctx.as_mut() {
|
||||
if let Some(msg) = check_clipboard(ctx, None) {
|
||||
if !state.initialized {
|
||||
state.initialized = true;
|
||||
// ignore clipboard update before service start
|
||||
return Ok(());
|
||||
}
|
||||
sp.send(msg);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
979
src/server/connection.rs
Normal file
979
src/server/connection.rs
Normal file
@@ -0,0 +1,979 @@
|
||||
use super::input_service::*;
|
||||
use super::*;
|
||||
use crate::common::update_clipboard;
|
||||
use crate::ipc;
|
||||
use hbb_common::{
|
||||
config::Config,
|
||||
fs,
|
||||
futures::SinkExt,
|
||||
sleep, timeout,
|
||||
tokio::{
|
||||
net::TcpStream,
|
||||
stream::StreamExt,
|
||||
sync::mpsc,
|
||||
time::{self, Duration, Instant, Interval},
|
||||
},
|
||||
tokio_util::codec::{BytesCodec, Framed},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLICK_TIME: Arc::<Mutex<i64>> = Default::default();
|
||||
static ref LOGIN_FAILURES: Arc::<Mutex<HashMap<String, (i32, i32, i32)>>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConnInner {
|
||||
id: i32,
|
||||
tx: Option<Sender>,
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
inner: ConnInner,
|
||||
stream: super::Stream,
|
||||
server: super::ServerPtrWeak,
|
||||
hash: Hash,
|
||||
read_jobs: Vec<fs::TransferJob>,
|
||||
timer: Interval,
|
||||
file_transfer: Option<(String, bool)>,
|
||||
port_forward_socket: Option<Framed<TcpStream, BytesCodec>>,
|
||||
port_forward_address: String,
|
||||
tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
authorized: bool,
|
||||
keyboard: bool,
|
||||
clipboard: bool,
|
||||
audio: bool,
|
||||
last_test_delay: i64,
|
||||
image_quality: i32,
|
||||
lock_after_session_end: bool,
|
||||
show_remote_cursor: bool, // by peer
|
||||
privacy_mode: bool,
|
||||
ip: String,
|
||||
disable_clipboard: bool, // by peer
|
||||
disable_audio: bool, // by peer
|
||||
}
|
||||
|
||||
impl Subscriber for ConnInner {
|
||||
#[inline]
|
||||
fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send(&mut self, msg: Arc<Message>) {
|
||||
self.tx.as_mut().map(|tx| {
|
||||
allow_err!(tx.send((Instant::now(), msg)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
const SEC30: Duration = Duration::from_secs(30);
|
||||
const H1: Duration = Duration::from_secs(3600);
|
||||
const MILLI1: Duration = Duration::from_millis(1);
|
||||
|
||||
impl Connection {
|
||||
pub async fn start(
|
||||
addr: SocketAddr,
|
||||
stream: super::Stream,
|
||||
id: i32,
|
||||
server: super::ServerPtrWeak,
|
||||
) {
|
||||
let hash = Hash {
|
||||
salt: Config::get_salt(),
|
||||
challenge: Config::get_auto_password(),
|
||||
..Default::default()
|
||||
};
|
||||
let (tx_from_cm, mut rx_from_cm) = mpsc::unbounded_channel::<ipc::Data>();
|
||||
let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::<ipc::Data>();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
|
||||
let mut conn = Self {
|
||||
inner: ConnInner { id, tx: Some(tx) },
|
||||
stream,
|
||||
server,
|
||||
hash,
|
||||
read_jobs: Vec::new(),
|
||||
timer: time::interval(SEC30),
|
||||
file_transfer: None,
|
||||
port_forward_socket: None,
|
||||
port_forward_address: "".to_owned(),
|
||||
tx_to_cm,
|
||||
authorized: false,
|
||||
keyboard: Config::get_option("enable-keyboard").is_empty(),
|
||||
clipboard: Config::get_option("enable-clipboard").is_empty(),
|
||||
audio: Config::get_option("audio-input") != "Mute",
|
||||
last_test_delay: 0,
|
||||
image_quality: ImageQuality::Balanced.value(),
|
||||
lock_after_session_end: false,
|
||||
show_remote_cursor: false,
|
||||
privacy_mode: false,
|
||||
ip: "".to_owned(),
|
||||
disable_audio: false,
|
||||
disable_clipboard: false,
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await {
|
||||
log::error!("ipc to connection manager exit: {}", err);
|
||||
}
|
||||
});
|
||||
if !conn.on_open(addr).await {
|
||||
return;
|
||||
}
|
||||
if !conn.keyboard {
|
||||
conn.send_permisssion(Permission::Keyboard, false).await;
|
||||
}
|
||||
if !conn.clipboard {
|
||||
conn.send_permisssion(Permission::Clipboard, false).await;
|
||||
}
|
||||
if !conn.audio {
|
||||
conn.send_permisssion(Permission::Audio, false).await;
|
||||
}
|
||||
let mut test_delay_timer =
|
||||
time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT);
|
||||
let mut last_recv_time = Instant::now();
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(data) = rx_from_cm.recv() => {
|
||||
match data {
|
||||
ipc::Data::Authorize => {
|
||||
conn.send_logon_response().await;
|
||||
if conn.port_forward_socket.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ipc::Data::Close => {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_close_reason("Closed manually by the peer".into());
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
conn.send(msg_out).await;
|
||||
conn.on_close("Close requested from connection manager", false);
|
||||
break;
|
||||
}
|
||||
ipc::Data::ChatMessage{text} => {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_chat_message(ChatMessage {
|
||||
text,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
conn.send(msg_out).await;
|
||||
}
|
||||
ipc::Data::SwitchPermission{name, enabled} => {
|
||||
log::info!("Change permission {} -> {}", name, enabled);
|
||||
if &name == "keyboard" {
|
||||
conn.keyboard = enabled;
|
||||
conn.send_permisssion(Permission::Keyboard, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
conn.inner.clone(), enabled || conn.show_remote_cursor);
|
||||
}
|
||||
} else if &name == "clipboard" {
|
||||
conn.clipboard = enabled;
|
||||
conn.send_permisssion(Permission::Clipboard, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::NAME,
|
||||
conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard);
|
||||
}
|
||||
} else if &name == "audio" {
|
||||
conn.audio = enabled;
|
||||
conn.send_permisssion(Permission::Audio, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::audio_service::NAME,
|
||||
conn.inner.clone(), conn.audio_enabled());
|
||||
}
|
||||
}
|
||||
}
|
||||
ipc::Data::RawMessage(bytes) => {
|
||||
allow_err!(conn.stream.send_raw(bytes).await);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some((instant, value)) = rx.recv() => {
|
||||
let latency = instant.elapsed().as_millis() as i64;
|
||||
super::video_service::update_internal_latency(id, latency);
|
||||
let msg: &Message = &value;
|
||||
if latency > 1000 {
|
||||
match &msg.union {
|
||||
Some(message::Union::video_frame(_)) => {
|
||||
continue;
|
||||
}
|
||||
Some(message::Union::audio_frame(_)) => {
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Err(err) = conn.stream.send(msg).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
res = conn.stream.next() => {
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), true);
|
||||
break;
|
||||
},
|
||||
Ok(bytes) => {
|
||||
last_recv_time = Instant::now();
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
if !conn.on_message(msg_in).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.on_close("Reset by the peer", true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = conn.timer.tick() => {
|
||||
if !conn.read_jobs.is_empty() {
|
||||
if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
conn.timer = time::interval_at(Instant::now() + SEC30, SEC30);
|
||||
}
|
||||
}
|
||||
_ = test_delay_timer.tick() => {
|
||||
if last_recv_time.elapsed() >= SEC30 {
|
||||
conn.on_close("Timeout", true);
|
||||
break;
|
||||
}
|
||||
let time = crate::get_time();
|
||||
if time > 0 && conn.last_test_delay == 0 {
|
||||
conn.last_test_delay = time;
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_test_delay(TestDelay{
|
||||
time,
|
||||
..Default::default()
|
||||
});
|
||||
conn.inner.send(msg_out.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super::video_service::update_internal_latency(id, 0);
|
||||
super::video_service::update_test_latency(id, 0);
|
||||
super::video_service::update_image_quality(id, None);
|
||||
if let Some(forward) = conn.port_forward_socket.as_mut() {
|
||||
log::info!("Running port forwarding loop");
|
||||
conn.stream.set_raw();
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(data) = rx_from_cm.recv() => {
|
||||
match data {
|
||||
ipc::Data::Close => {
|
||||
conn.on_close("Close requested from connection manager", false);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
res = forward.next() => {
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
},
|
||||
Ok(bytes) => {
|
||||
last_recv_time = Instant::now();
|
||||
if let Err(err) = conn.stream.send_bytes(bytes.into()).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.on_close("Forward reset by the peer", false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
res = conn.stream.next() => {
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
},
|
||||
Ok(bytes) => {
|
||||
last_recv_time = Instant::now();
|
||||
if let Err(err) = forward.send(bytes.into()).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.on_close("Stream reset by the peer", false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = conn.timer.tick() => {
|
||||
if last_recv_time.elapsed() >= H1 {
|
||||
conn.on_close("Timeout", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_permisssion(&mut self, permission: Permission, enabled: bool) {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_permission_info(PermissionInfo {
|
||||
permission: permission.into(),
|
||||
enabled,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
self.send(msg_out).await;
|
||||
}
|
||||
|
||||
async fn on_open(&mut self, addr: SocketAddr) -> bool {
|
||||
log::debug!("#{} Connection opened from {}.", self.inner.id, addr);
|
||||
let whitelist: Vec<String> = Config::get_option("whitelist")
|
||||
.split(",")
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(|x| x.to_owned())
|
||||
.collect();
|
||||
if !whitelist.is_empty()
|
||||
&& whitelist
|
||||
.iter()
|
||||
.filter(|x| x == &"0.0.0.0")
|
||||
.next()
|
||||
.is_none()
|
||||
&& whitelist
|
||||
.iter()
|
||||
.filter(|x| x.parse() == Ok(addr.ip()))
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
self.send_login_error("Your ip is blocked by the peer")
|
||||
.await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
self.ip = addr.ip().to_string();
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_hash(self.hash.clone());
|
||||
self.send(msg_out).await;
|
||||
true
|
||||
}
|
||||
|
||||
async fn send_logon_response(&mut self) {
|
||||
if self.authorized {
|
||||
return;
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
let mut username = crate::platform::get_active_username();
|
||||
let mut res = LoginResponse::new();
|
||||
if self.port_forward_socket.is_some() {
|
||||
let mut msg_out = Message::new();
|
||||
res.set_peer_info(PeerInfo {
|
||||
hostname: whoami::hostname(),
|
||||
username,
|
||||
platform: whoami::platform().to_string(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if !self.file_transfer.is_some() {
|
||||
let dtype = crate::platform::linux::get_display_server();
|
||||
if dtype != "x11" {
|
||||
res.set_error(format!(
|
||||
"Unsupported display server type {}, x11 expected",
|
||||
dtype
|
||||
));
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
let mut sas_enabled = false;
|
||||
#[cfg(windows)]
|
||||
if crate::platform::is_root() {
|
||||
sas_enabled = true;
|
||||
}
|
||||
if self.file_transfer.is_some() {
|
||||
if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() {
|
||||
username = "".to_owned();
|
||||
}
|
||||
}
|
||||
self.authorized = true;
|
||||
let mut pi = PeerInfo {
|
||||
hostname: whoami::hostname(),
|
||||
username,
|
||||
platform: whoami::platform().to_string(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
sas_enabled,
|
||||
..Default::default()
|
||||
};
|
||||
let mut sub_service = false;
|
||||
if self.file_transfer.is_some() {
|
||||
res.set_peer_info(pi);
|
||||
} else {
|
||||
try_activate_screen();
|
||||
match super::video_service::get_displays() {
|
||||
Err(err) => {
|
||||
res.set_error(err.to_string());
|
||||
}
|
||||
Ok((current, displays)) => {
|
||||
pi.displays = displays.into();
|
||||
pi.current_display = current as _;
|
||||
res.set_peer_info(pi);
|
||||
sub_service = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
if let Some((dir, show_hidden)) = self.file_transfer.clone() {
|
||||
let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() {
|
||||
&dir
|
||||
} else {
|
||||
""
|
||||
};
|
||||
self.read_dir(dir, show_hidden);
|
||||
} else if sub_service {
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
let mut noperms = Vec::new();
|
||||
if !self.keyboard && !self.show_remote_cursor {
|
||||
noperms.push(NAME_CURSOR);
|
||||
}
|
||||
if !self.show_remote_cursor {
|
||||
noperms.push(NAME_POS);
|
||||
}
|
||||
if !self.clipboard_enabled() || !self.keyboard {
|
||||
noperms.push(super::clipboard_service::NAME);
|
||||
}
|
||||
if !self.audio_enabled() {
|
||||
noperms.push(super::audio_service::NAME);
|
||||
}
|
||||
s.write()
|
||||
.unwrap()
|
||||
.add_connection(self.inner.clone(), &noperms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clipboard_enabled(&self) -> bool {
|
||||
self.clipboard && !self.disable_clipboard
|
||||
}
|
||||
|
||||
fn audio_enabled(&self) -> bool {
|
||||
self.audio && !self.disable_audio
|
||||
}
|
||||
|
||||
async fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) {
|
||||
self.send_to_cm(ipc::Data::Login {
|
||||
id: self.inner.id(),
|
||||
is_file_transfer: self.file_transfer.is_some(),
|
||||
port_forward: self.port_forward_address.clone(),
|
||||
peer_id,
|
||||
name,
|
||||
authorized,
|
||||
keyboard: self.keyboard,
|
||||
clipboard: self.clipboard,
|
||||
audio: self.audio,
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send_to_cm(&mut self, data: ipc::Data) {
|
||||
self.tx_to_cm.send(data).ok();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send_fs(&mut self, data: ipc::FS) {
|
||||
self.send_to_cm(ipc::Data::FS(data));
|
||||
}
|
||||
|
||||
async fn send_login_error<T: std::string::ToString>(&mut self, err: T) {
|
||||
let mut msg_out = Message::new();
|
||||
let mut res = LoginResponse::new();
|
||||
res.set_error(err.to_string());
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
}
|
||||
|
||||
async fn on_message(&mut self, msg: Message) -> bool {
|
||||
if let Some(message::Union::login_request(lr)) = msg.union {
|
||||
if let Some(o) = lr.option.as_ref() {
|
||||
self.update_option(o);
|
||||
}
|
||||
if self.authorized {
|
||||
return true;
|
||||
}
|
||||
match lr.union {
|
||||
Some(login_request::Union::file_transfer(ft)) => {
|
||||
if !Config::get_option("enable-file-transfer").is_empty() {
|
||||
self.send_login_error("No permission of file transfer")
|
||||
.await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
self.file_transfer = Some((ft.dir, ft.show_hidden));
|
||||
}
|
||||
Some(login_request::Union::port_forward(mut pf)) => {
|
||||
if !Config::get_option("enable-tunnel").is_empty() {
|
||||
self.send_login_error("No permission of IP tunneling").await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
let mut is_rdp = false;
|
||||
if pf.host == "RDP" && pf.port == 0 {
|
||||
pf.host = "localhost".to_owned();
|
||||
pf.port = 3389;
|
||||
is_rdp = true;
|
||||
}
|
||||
if pf.host.is_empty() {
|
||||
pf.host = "localhost".to_owned();
|
||||
}
|
||||
let mut addr = format!("{}:{}", pf.host, pf.port);
|
||||
self.port_forward_address = addr.clone();
|
||||
match timeout(3000, TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(sock)) => {
|
||||
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
|
||||
}
|
||||
_ => {
|
||||
if is_rdp {
|
||||
addr = "RDP".to_owned();
|
||||
}
|
||||
self.send_login_error(format!(
|
||||
"Failed to access remote {}, please make sure if it is open",
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if lr.username != Config::get_id() {
|
||||
self.send_login_error("Offline").await;
|
||||
} else if lr.password.is_empty() {
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false).await;
|
||||
} else {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&Config::get_password());
|
||||
hasher.update(&self.hash.salt);
|
||||
let mut hasher2 = Sha256::new();
|
||||
hasher2.update(&hasher.finalize()[..]);
|
||||
hasher2.update(&self.hash.challenge);
|
||||
let mut failure = LOGIN_FAILURES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&self.ip)
|
||||
.map(|x| x.clone())
|
||||
.unwrap_or((0, 0, 0));
|
||||
let time = (crate::get_time() / 60_000) as i32;
|
||||
if failure.2 > 30 {
|
||||
self.send_login_error("Too many wrong password attempts")
|
||||
.await;
|
||||
} else if time == failure.0 && failure.1 > 6 {
|
||||
self.send_login_error("Please try 1 minute later").await;
|
||||
} else if hasher2.finalize()[..] != lr.password[..] {
|
||||
if failure.0 == time {
|
||||
failure.1 += 1;
|
||||
failure.2 += 1;
|
||||
} else {
|
||||
failure.0 = time;
|
||||
failure.1 = 1;
|
||||
failure.2 += 1;
|
||||
}
|
||||
LOGIN_FAILURES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.ip.clone(), failure);
|
||||
self.send_login_error("Wrong Password").await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false).await;
|
||||
} else {
|
||||
if failure.0 != 0 {
|
||||
LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
|
||||
}
|
||||
self.send_logon_response().await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true).await;
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(message::Union::test_delay(t)) = msg.union {
|
||||
if t.from_client {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_test_delay(t);
|
||||
self.inner.send(msg_out.into());
|
||||
} else {
|
||||
self.last_test_delay = 0;
|
||||
let latency = crate::get_time() - t.time;
|
||||
if latency > 0 {
|
||||
super::video_service::update_test_latency(self.inner.id(), latency);
|
||||
}
|
||||
}
|
||||
} else if self.authorized {
|
||||
match msg.union {
|
||||
Some(message::Union::mouse_event(me)) => {
|
||||
if self.keyboard {
|
||||
handle_mouse(&me, self.inner.id());
|
||||
if is_left_up(&me) {
|
||||
*CLICK_TIME.lock().unwrap() = crate::get_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::key_event(mut me)) => {
|
||||
if self.keyboard {
|
||||
if me.press {
|
||||
if let Some(key_event::Union::unicode(_)) = me.union {
|
||||
handle_key(&me);
|
||||
} else if let Some(key_event::Union::seq(_)) = me.union {
|
||||
handle_key(&me);
|
||||
} else {
|
||||
me.down = true;
|
||||
handle_key(&me);
|
||||
me.down = false;
|
||||
handle_key(&me);
|
||||
}
|
||||
} else {
|
||||
handle_key(&me);
|
||||
}
|
||||
if is_enter(&me) {
|
||||
*CLICK_TIME.lock().unwrap() = crate::get_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::clipboard(cb)) => {
|
||||
if self.clipboard {
|
||||
update_clipboard(cb, None);
|
||||
}
|
||||
}
|
||||
Some(message::Union::file_action(fa)) => {
|
||||
if self.file_transfer.is_some() {
|
||||
match fa.union {
|
||||
Some(file_action::Union::read_dir(rd)) => {
|
||||
self.read_dir(&rd.path, rd.include_hidden);
|
||||
}
|
||||
Some(file_action::Union::all_files(f)) => {
|
||||
match fs::get_recursive_files(&f.path, f.include_hidden) {
|
||||
Err(err) => {
|
||||
self.send(fs::new_error(f.id, err, -1)).await;
|
||||
}
|
||||
Ok(files) => {
|
||||
self.send(fs::new_dir(f.id, files)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(file_action::Union::send(s)) => {
|
||||
let id = s.id;
|
||||
match fs::TransferJob::new_read(id, s.path, s.include_hidden) {
|
||||
Err(err) => {
|
||||
self.send(fs::new_error(id, err, 0)).await;
|
||||
}
|
||||
Ok(job) => {
|
||||
self.send(fs::new_dir(id, job.files().to_vec())).await;
|
||||
self.read_jobs.push(job);
|
||||
self.timer = time::interval(MILLI1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(file_action::Union::receive(r)) => {
|
||||
self.send_fs(ipc::FS::NewWrite {
|
||||
path: r.path,
|
||||
id: r.id,
|
||||
files: r
|
||||
.files
|
||||
.to_vec()
|
||||
.drain(..)
|
||||
.map(|f| (f.name, f.modified_time))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
Some(file_action::Union::remove_dir(d)) => {
|
||||
self.send_fs(ipc::FS::RemoveDir {
|
||||
path: d.path,
|
||||
id: d.id,
|
||||
recursive: d.recursive,
|
||||
});
|
||||
}
|
||||
Some(file_action::Union::remove_file(f)) => {
|
||||
self.send_fs(ipc::FS::RemoveFile {
|
||||
path: f.path,
|
||||
id: f.id,
|
||||
file_num: f.file_num,
|
||||
});
|
||||
}
|
||||
Some(file_action::Union::create(c)) => {
|
||||
self.send_fs(ipc::FS::CreateDir {
|
||||
path: c.path,
|
||||
id: c.id,
|
||||
});
|
||||
}
|
||||
Some(file_action::Union::cancel(c)) => {
|
||||
self.send_fs(ipc::FS::CancelWrite { id: c.id });
|
||||
fs::remove_job(c.id, &mut self.read_jobs);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::file_response(fr)) => match fr.union {
|
||||
Some(file_response::Union::block(block)) => {
|
||||
self.send_fs(ipc::FS::WriteBlock {
|
||||
id: block.id,
|
||||
file_num: block.file_num,
|
||||
data: block.data,
|
||||
compressed: block.compressed,
|
||||
});
|
||||
}
|
||||
Some(file_response::Union::done(d)) => {
|
||||
self.send_fs(ipc::FS::WriteDone {
|
||||
id: d.id,
|
||||
file_num: d.file_num,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(message::Union::misc(misc)) => match misc.union {
|
||||
Some(misc::Union::switch_display(s)) => {
|
||||
super::video_service::switch_display(s.display);
|
||||
}
|
||||
Some(misc::Union::chat_message(c)) => {
|
||||
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
|
||||
}
|
||||
Some(misc::Union::option(o)) => {
|
||||
self.update_option(&o);
|
||||
}
|
||||
Some(misc::Union::refresh_video(r)) => {
|
||||
if r {
|
||||
super::video_service::refresh();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn update_option(&mut self, o: &OptionMessage) {
|
||||
log::info!("Option update: {:?}", o);
|
||||
if let Ok(q) = o.image_quality.enum_value() {
|
||||
self.image_quality = q.value();
|
||||
super::video_service::update_image_quality(self.inner.id(), Some(q.value()));
|
||||
}
|
||||
let q = o.custom_image_quality;
|
||||
if q > 0 {
|
||||
self.image_quality = q;
|
||||
super::video_service::update_image_quality(self.inner.id(), Some(q));
|
||||
}
|
||||
if let Ok(q) = o.lock_after_session_end.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.lock_after_session_end = q == BoolOption::Yes;
|
||||
}
|
||||
}
|
||||
if let Ok(q) = o.show_remote_cursor.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.show_remote_cursor = q == BoolOption::Yes;
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
self.inner.clone(),
|
||||
self.keyboard || self.show_remote_cursor,
|
||||
);
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_POS,
|
||||
self.inner.clone(),
|
||||
self.show_remote_cursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(q) = o.disable_audio.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.disable_audio = q == BoolOption::Yes;
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
self.inner.clone(),
|
||||
self.audio_enabled(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(q) = o.disable_clipboard.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.disable_clipboard = q == BoolOption::Yes;
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
self.inner.clone(),
|
||||
self.clipboard_enabled() && self.keyboard,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(q) = o.privacy_mode.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.privacy_mode = q == BoolOption::Yes;
|
||||
if self.privacy_mode && self.keyboard {
|
||||
crate::platform::toggle_privacy_mode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.keyboard {
|
||||
if let Ok(q) = o.block_input.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
crate::platform::block_input(q == BoolOption::Yes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close(&mut self, reason: &str, lock: bool) {
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().remove_connection(&self.inner);
|
||||
}
|
||||
log::info!("#{} Connection closed: {}", self.inner.id(), reason);
|
||||
if lock && self.lock_after_session_end && self.keyboard {
|
||||
crate::platform::lock_screen();
|
||||
super::video_service::switch_to_primary();
|
||||
}
|
||||
if self.privacy_mode {
|
||||
crate::platform::toggle_privacy_mode(false);
|
||||
}
|
||||
self.port_forward_socket.take();
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, dir: &str, include_hidden: bool) {
|
||||
let dir = dir.to_string();
|
||||
self.send_fs(ipc::FS::ReadDir {
|
||||
dir,
|
||||
include_hidden,
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn send(&mut self, msg: Message) {
|
||||
allow_err!(self.stream.send(&msg).await);
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_ipc(
|
||||
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
|
||||
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
) -> ResultType<()> {
|
||||
loop {
|
||||
if !crate::platform::is_prelogin() {
|
||||
break;
|
||||
}
|
||||
sleep(1.).await;
|
||||
}
|
||||
let mut stream = None;
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
stream = Some(s);
|
||||
} else {
|
||||
let run_done;
|
||||
if crate::platform::is_root() {
|
||||
let mut res = Ok(None);
|
||||
for _ in 0..10 {
|
||||
res = crate::platform::run_as_user("--cm");
|
||||
if res.is_ok() {
|
||||
break;
|
||||
}
|
||||
sleep(1.).await;
|
||||
}
|
||||
if let Some(task) = res? {
|
||||
super::CHILD_PROCESS.lock().unwrap().push(task);
|
||||
}
|
||||
run_done = true;
|
||||
} else {
|
||||
run_done = false;
|
||||
}
|
||||
if !run_done {
|
||||
super::CHILD_PROCESS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(crate::run_me(vec!["--cm"])?);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
sleep(0.3).await;
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
stream = Some(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if stream.is_none() {
|
||||
bail!("Failed to connect to connection manager");
|
||||
}
|
||||
}
|
||||
let mut stream = stream.unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = stream.next() => {
|
||||
match res {
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(Some(data)) => {
|
||||
match data {
|
||||
ipc::Data::ClickTime(_)=> {
|
||||
unsafe {
|
||||
let ct = *CLICK_TIME.lock().unwrap();
|
||||
let data = ipc::Data::ClickTime(ct);
|
||||
stream.send(&data).await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tx_from_cm.send(data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
res = rx_to_cm.recv() => {
|
||||
match res {
|
||||
Some(data) => {
|
||||
stream.send(&data).await?;
|
||||
}
|
||||
None => {
|
||||
bail!("expected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// in case screen is sleep and blank, here to activate it
|
||||
fn try_activate_screen() {
|
||||
#[cfg(windows)]
|
||||
std::thread::spawn(|| {
|
||||
mouse_move_relative(-6, -6);
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
mouse_move_relative(6, 6);
|
||||
});
|
||||
}
|
||||
499
src/server/input_service.rs
Normal file
499
src/server/input_service.rs
Normal file
@@ -0,0 +1,499 @@
|
||||
use super::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
use dispatch::Queue;
|
||||
use enigo::{Enigo, KeyboardControllable, MouseButton, MouseControllable};
|
||||
use hbb_common::config::COMPRESS_LEVEL;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Default)]
|
||||
struct StateCursor {
|
||||
hcursor: u64,
|
||||
cursor_data: Arc<Message>,
|
||||
cached_cursor_data: HashMap<u64, Arc<Message>>,
|
||||
}
|
||||
|
||||
impl super::service::Reset for StateCursor {
|
||||
fn reset(&mut self) {
|
||||
*self = Default::default();
|
||||
crate::platform::reset_input_cache();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StatePos {
|
||||
cursor_pos: (i32, i32),
|
||||
}
|
||||
|
||||
impl super::service::Reset for StatePos {
|
||||
fn reset(&mut self) {
|
||||
self.cursor_pos = (0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Input {
|
||||
conn: i32,
|
||||
time: i64,
|
||||
}
|
||||
|
||||
static mut LATEST_INPUT: Input = Input { conn: 0, time: 0 };
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MouseCursorSub {
|
||||
inner: ConnInner,
|
||||
cached: HashMap<u64, Arc<Message>>,
|
||||
}
|
||||
|
||||
impl From<ConnInner> for MouseCursorSub {
|
||||
fn from(inner: ConnInner) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
cached: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber for MouseCursorSub {
|
||||
#[inline]
|
||||
fn id(&self) -> i32 {
|
||||
self.inner.id()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send(&mut self, msg: Arc<Message>) {
|
||||
if let Some(message::Union::cursor_data(cd)) = &msg.union {
|
||||
if let Some(msg) = self.cached.get(&cd.id) {
|
||||
self.inner.send(msg.clone());
|
||||
} else {
|
||||
self.inner.send(msg.clone());
|
||||
let mut tmp = Message::new();
|
||||
// only send id out, require client side cache also
|
||||
tmp.set_cursor_id(cd.id);
|
||||
self.cached.insert(cd.id, Arc::new(tmp));
|
||||
}
|
||||
} else {
|
||||
self.inner.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const NAME_CURSOR: &'static str = "mouse_cursor";
|
||||
pub const NAME_POS: &'static str = "mouse_pos";
|
||||
pub type MouseCursorService = ServiceTmpl<MouseCursorSub>;
|
||||
|
||||
pub fn new_cursor() -> MouseCursorService {
|
||||
let sp = MouseCursorService::new(NAME_CURSOR, true);
|
||||
sp.repeat::<StateCursor, _>(33, run_cursor);
|
||||
sp
|
||||
}
|
||||
|
||||
pub fn new_pos() -> GenericService {
|
||||
let sp = GenericService::new(NAME_POS, false);
|
||||
sp.repeat::<StatePos, _>(33, run_pos);
|
||||
sp
|
||||
}
|
||||
|
||||
fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> {
|
||||
if let Some((x, y)) = crate::get_cursor_pos() {
|
||||
if state.cursor_pos.0 != x || state.cursor_pos.1 != y {
|
||||
state.cursor_pos = (x, y);
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_cursor_position(CursorPosition {
|
||||
x,
|
||||
y,
|
||||
..Default::default()
|
||||
});
|
||||
let exclude = unsafe {
|
||||
if crate::get_time() - LATEST_INPUT.time < 300 {
|
||||
LATEST_INPUT.conn
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
sp.send_without(msg_out, exclude);
|
||||
}
|
||||
}
|
||||
|
||||
sp.snapshot(|sps| {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_cursor_position(CursorPosition {
|
||||
x: state.cursor_pos.0,
|
||||
y: state.cursor_pos.1,
|
||||
..Default::default()
|
||||
});
|
||||
sps.send(msg_out);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> {
|
||||
if let Some(hcursor) = crate::get_cursor()? {
|
||||
if hcursor != state.hcursor {
|
||||
let msg;
|
||||
if let Some(cached) = state.cached_cursor_data.get(&hcursor) {
|
||||
super::log::trace!("Cursor data cached, hcursor: {}", hcursor);
|
||||
msg = cached.clone();
|
||||
} else {
|
||||
let mut data = crate::get_cursor_data(hcursor)?;
|
||||
data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL);
|
||||
let mut tmp = Message::new();
|
||||
tmp.set_cursor_data(data);
|
||||
msg = Arc::new(tmp);
|
||||
state.cached_cursor_data.insert(hcursor, msg.clone());
|
||||
super::log::trace!("Cursor data updated, hcursor: {}", hcursor);
|
||||
}
|
||||
state.hcursor = hcursor;
|
||||
sp.send_shared(msg.clone());
|
||||
state.cursor_data = msg;
|
||||
}
|
||||
}
|
||||
sp.snapshot(|sps| {
|
||||
sps.send_shared(state.cursor_data.clone());
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENIGO: Arc<Mutex<Enigo>> = Arc::new(Mutex::new(Enigo::new()));
|
||||
}
|
||||
|
||||
// mac key input must be run in main thread, otherwise crash on >= osx 10.15
|
||||
#[cfg(target_os = "macos")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref QUEUE: Queue = Queue::main();
|
||||
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
|
||||
}
|
||||
|
||||
pub fn is_left_up(evt: &MouseEvent) -> bool {
|
||||
let buttons = evt.mask >> 3;
|
||||
let evt_type = evt.mask & 0x7;
|
||||
return buttons == 1 && evt_type == 2;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn mouse_move_relative(x: i32, y: i32) {
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
en.mouse_move_relative(x, y);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn modifier_sleep() {
|
||||
// sleep for a while, this is only for keying in rdp in peer so far
|
||||
#[cfg(windows)]
|
||||
std::thread::sleep(std::time::Duration::from_nanos(1));
|
||||
}
|
||||
|
||||
pub fn handle_mouse(evt: &MouseEvent, conn: i32) {
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
let buttons = evt.mask >> 3;
|
||||
let evt_type = evt.mask & 0x7;
|
||||
if evt_type == 0 {
|
||||
unsafe {
|
||||
let time = crate::get_time();
|
||||
LATEST_INPUT = Input { time, conn };
|
||||
}
|
||||
}
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut to_release = Vec::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
en.reset_flag();
|
||||
for ref ck in evt.modifiers.iter() {
|
||||
if let Some(key) = KEY_MAP.get(&ck.value()) {
|
||||
if evt_type == 1 || evt_type == 2 {
|
||||
#[cfg(target_os = "macos")]
|
||||
en.add_flag(key);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if key != &enigo::Key::CapsLock && key != &enigo::Key::NumLock {
|
||||
if !en.get_key_state(key.clone()) {
|
||||
en.key_down(key.clone()).ok();
|
||||
modifier_sleep();
|
||||
to_release.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match evt_type {
|
||||
0 => {
|
||||
en.mouse_move_to(evt.x, evt.y);
|
||||
}
|
||||
1 => match buttons {
|
||||
1 => {
|
||||
allow_err!(en.mouse_down(MouseButton::Left));
|
||||
}
|
||||
2 => {
|
||||
allow_err!(en.mouse_down(MouseButton::Right));
|
||||
}
|
||||
4 => {
|
||||
allow_err!(en.mouse_down(MouseButton::Middle));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
2 => match buttons {
|
||||
1 => {
|
||||
en.mouse_up(MouseButton::Left);
|
||||
}
|
||||
2 => {
|
||||
en.mouse_up(MouseButton::Right);
|
||||
}
|
||||
4 => {
|
||||
en.mouse_up(MouseButton::Middle);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
3 => {
|
||||
#[allow(unused_mut)]
|
||||
let mut x = evt.x;
|
||||
#[allow(unused_mut)]
|
||||
let mut y = evt.y;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
x = -x;
|
||||
y = -y;
|
||||
}
|
||||
if x != 0 {
|
||||
en.mouse_scroll_x(x);
|
||||
}
|
||||
if y != 0 {
|
||||
en.mouse_scroll_y(y);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
for key in to_release {
|
||||
en.key_up(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enter(evt: &KeyEvent) -> bool {
|
||||
if let Some(key_event::Union::control_key(ck)) = evt.union {
|
||||
if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref KEY_MAP: HashMap<i32, enigo::Key> =
|
||||
[
|
||||
(ControlKey::Alt, enigo::Key::Alt),
|
||||
(ControlKey::Backspace, enigo::Key::Backspace),
|
||||
(ControlKey::CapsLock, enigo::Key::CapsLock),
|
||||
(ControlKey::Control, enigo::Key::Control),
|
||||
(ControlKey::Delete, enigo::Key::Delete),
|
||||
(ControlKey::DownArrow, enigo::Key::DownArrow),
|
||||
(ControlKey::End, enigo::Key::End),
|
||||
(ControlKey::Escape, enigo::Key::Escape),
|
||||
(ControlKey::F1, enigo::Key::F1),
|
||||
(ControlKey::F10, enigo::Key::F10),
|
||||
(ControlKey::F11, enigo::Key::F11),
|
||||
(ControlKey::F12, enigo::Key::F12),
|
||||
(ControlKey::F2, enigo::Key::F2),
|
||||
(ControlKey::F3, enigo::Key::F3),
|
||||
(ControlKey::F4, enigo::Key::F4),
|
||||
(ControlKey::F5, enigo::Key::F5),
|
||||
(ControlKey::F6, enigo::Key::F6),
|
||||
(ControlKey::F7, enigo::Key::F7),
|
||||
(ControlKey::F8, enigo::Key::F8),
|
||||
(ControlKey::F9, enigo::Key::F9),
|
||||
(ControlKey::Home, enigo::Key::Home),
|
||||
(ControlKey::LeftArrow, enigo::Key::LeftArrow),
|
||||
(ControlKey::Meta, enigo::Key::Meta),
|
||||
(ControlKey::Option, enigo::Key::Option),
|
||||
(ControlKey::PageDown, enigo::Key::PageDown),
|
||||
(ControlKey::PageUp, enigo::Key::PageUp),
|
||||
(ControlKey::Return, enigo::Key::Return),
|
||||
(ControlKey::RightArrow, enigo::Key::RightArrow),
|
||||
(ControlKey::Shift, enigo::Key::Shift),
|
||||
(ControlKey::Space, enigo::Key::Space),
|
||||
(ControlKey::Tab, enigo::Key::Tab),
|
||||
(ControlKey::UpArrow, enigo::Key::UpArrow),
|
||||
(ControlKey::Numpad0, enigo::Key::Numpad0),
|
||||
(ControlKey::Numpad1, enigo::Key::Numpad1),
|
||||
(ControlKey::Numpad2, enigo::Key::Numpad2),
|
||||
(ControlKey::Numpad3, enigo::Key::Numpad3),
|
||||
(ControlKey::Numpad4, enigo::Key::Numpad4),
|
||||
(ControlKey::Numpad5, enigo::Key::Numpad5),
|
||||
(ControlKey::Numpad6, enigo::Key::Numpad6),
|
||||
(ControlKey::Numpad7, enigo::Key::Numpad7),
|
||||
(ControlKey::Numpad8, enigo::Key::Numpad8),
|
||||
(ControlKey::Numpad9, enigo::Key::Numpad9),
|
||||
(ControlKey::Cancel, enigo::Key::Cancel),
|
||||
(ControlKey::Clear, enigo::Key::Clear),
|
||||
(ControlKey::Menu, enigo::Key::Menu),
|
||||
(ControlKey::Pause, enigo::Key::Pause),
|
||||
(ControlKey::Kana, enigo::Key::Kana),
|
||||
(ControlKey::Hangul, enigo::Key::Hangul),
|
||||
(ControlKey::Junja, enigo::Key::Junja),
|
||||
(ControlKey::Final, enigo::Key::Final),
|
||||
(ControlKey::Hanja, enigo::Key::Hanja),
|
||||
(ControlKey::Kanji, enigo::Key::Kanji),
|
||||
(ControlKey::Convert, enigo::Key::Convert),
|
||||
(ControlKey::Select, enigo::Key::Select),
|
||||
(ControlKey::Print, enigo::Key::Print),
|
||||
(ControlKey::Execute, enigo::Key::Execute),
|
||||
(ControlKey::Snapshot, enigo::Key::Snapshot),
|
||||
(ControlKey::Insert, enigo::Key::Insert),
|
||||
(ControlKey::Help, enigo::Key::Help),
|
||||
(ControlKey::Sleep, enigo::Key::Sleep),
|
||||
(ControlKey::Separator, enigo::Key::Separator),
|
||||
(ControlKey::Scroll, enigo::Key::Scroll),
|
||||
(ControlKey::NumLock, enigo::Key::NumLock),
|
||||
(ControlKey::RWin, enigo::Key::RWin),
|
||||
(ControlKey::Apps, enigo::Key::Apps),
|
||||
(ControlKey::Multiply, enigo::Key::Multiply),
|
||||
(ControlKey::Add, enigo::Key::Add),
|
||||
(ControlKey::Subtract, enigo::Key::Subtract),
|
||||
(ControlKey::Decimal, enigo::Key::Decimal),
|
||||
(ControlKey::Divide, enigo::Key::Divide),
|
||||
(ControlKey::Equals, enigo::Key::Equals),
|
||||
(ControlKey::NumpadEnter, enigo::Key::NumpadEnter),
|
||||
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
|
||||
static ref NUMPAD_KEY_MAP: HashMap<i32, bool> =
|
||||
[
|
||||
(ControlKey::Home, true),
|
||||
(ControlKey::UpArrow, true),
|
||||
(ControlKey::PageUp, true),
|
||||
(ControlKey::LeftArrow, true),
|
||||
(ControlKey::RightArrow, true),
|
||||
(ControlKey::End, true),
|
||||
(ControlKey::DownArrow, true),
|
||||
(ControlKey::PageDown, true),
|
||||
(ControlKey::Insert, true),
|
||||
(ControlKey::Delete, true),
|
||||
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
|
||||
}
|
||||
|
||||
pub fn handle_key(evt: &KeyEvent) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if !*IS_SERVER {
|
||||
// having GUI, run main GUI thread, otherwise crash
|
||||
let evt = evt.clone();
|
||||
QUEUE.exec_async(move || handle_key_(&evt));
|
||||
return;
|
||||
}
|
||||
handle_key_(evt);
|
||||
}
|
||||
|
||||
fn handle_key_(evt: &KeyEvent) {
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
// disable numlock if press home etc when numlock is on,
|
||||
// because we will get numpad value (7,8,9 etc) if not
|
||||
#[cfg(windows)]
|
||||
let mut disable_numlock = false;
|
||||
#[cfg(target_os = "macos")]
|
||||
en.reset_flag();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut to_release = Vec::new();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut has_cap = false;
|
||||
#[cfg(windows)]
|
||||
let mut has_numlock = false;
|
||||
for ref ck in evt.modifiers.iter() {
|
||||
if let Some(key) = KEY_MAP.get(&ck.value()) {
|
||||
#[cfg(target_os = "macos")]
|
||||
en.add_flag(key);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
if key == &enigo::Key::CapsLock {
|
||||
has_cap = true;
|
||||
} else if key == &enigo::Key::NumLock {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
has_numlock = true;
|
||||
}
|
||||
} else {
|
||||
if !en.get_key_state(key.clone()) {
|
||||
en.key_down(key.clone()).ok();
|
||||
modifier_sleep();
|
||||
to_release.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if crate::common::valid_for_capslock(evt) {
|
||||
if has_cap != en.get_key_state(enigo::Key::CapsLock) {
|
||||
en.key_down(enigo::Key::CapsLock).ok();
|
||||
en.key_up(enigo::Key::CapsLock);
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if crate::common::valid_for_numlock(evt) {
|
||||
if has_numlock != en.get_key_state(enigo::Key::NumLock) {
|
||||
en.key_down(enigo::Key::NumLock).ok();
|
||||
en.key_up(enigo::Key::NumLock);
|
||||
}
|
||||
}
|
||||
match evt.union {
|
||||
Some(key_event::Union::control_key(ck)) => {
|
||||
if let Some(key) = KEY_MAP.get(&ck.value()) {
|
||||
#[cfg(windows)]
|
||||
if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) {
|
||||
disable_numlock = en.get_key_state(enigo::Key::NumLock);
|
||||
if disable_numlock {
|
||||
en.key_down(enigo::Key::NumLock).ok();
|
||||
en.key_up(enigo::Key::NumLock);
|
||||
}
|
||||
}
|
||||
if evt.down {
|
||||
allow_err!(en.key_down(key.clone()));
|
||||
} else {
|
||||
en.key_up(key.clone());
|
||||
}
|
||||
} else if ck.value() == ControlKey::CtrlAltDel.value() {
|
||||
// have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main.
|
||||
std::thread::spawn(|| {
|
||||
allow_err!(send_sas());
|
||||
});
|
||||
} else if ck.value() == ControlKey::LockScreen.value() {
|
||||
crate::platform::lock_screen();
|
||||
super::video_service::switch_to_primary();
|
||||
}
|
||||
}
|
||||
Some(key_event::Union::chr(chr)) => {
|
||||
if evt.down {
|
||||
allow_err!(en.key_down(enigo::Key::Layout(chr as u8 as _)));
|
||||
} else {
|
||||
en.key_up(enigo::Key::Layout(chr as u8 as _));
|
||||
}
|
||||
}
|
||||
Some(key_event::Union::unicode(chr)) => {
|
||||
if let Ok(chr) = char::try_from(chr) {
|
||||
en.key_sequence(&chr.to_string());
|
||||
}
|
||||
}
|
||||
Some(key_event::Union::seq(ref seq)) => {
|
||||
en.key_sequence(&seq);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
for key in to_release {
|
||||
en.key_up(key.clone());
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if disable_numlock {
|
||||
en.key_down(enigo::Key::NumLock).ok();
|
||||
en.key_up(enigo::Key::NumLock);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn send_sas() -> ResultType<()> {
|
||||
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
|
||||
timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??;
|
||||
Ok(())
|
||||
}
|
||||
249
src/server/service.rs
Normal file
249
src/server/service.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use super::*;
|
||||
use std::{
|
||||
thread::{self, JoinHandle},
|
||||
time,
|
||||
};
|
||||
|
||||
pub trait Service: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn on_subscribe(&self, sub: ConnInner);
|
||||
fn on_unsubscribe(&self, id: i32);
|
||||
fn is_subed(&self, id: i32) -> bool;
|
||||
fn join(&self);
|
||||
}
|
||||
|
||||
pub trait Subscriber: Default + Send + Sync + 'static {
|
||||
fn id(&self) -> i32;
|
||||
fn send(&mut self, msg: Arc<Message>);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
|
||||
name: &'static str,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
subscribes: HashMap<i32, T>,
|
||||
new_subscribes: HashMap<i32, T>,
|
||||
active: bool,
|
||||
need_snapshot: bool,
|
||||
}
|
||||
|
||||
pub trait Reset {
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
|
||||
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
|
||||
pub type GenericService = ServiceTmpl<ConnInner>;
|
||||
pub const HIBERATE_TIMEOUT: u64 = 30;
|
||||
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
|
||||
fn send_new_subscribes(&mut self, msg: Arc<Message>) {
|
||||
for s in self.new_subscribes.values_mut() {
|
||||
s.send(msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_new_subscribes(&mut self) {
|
||||
for (_, s) in self.new_subscribes.drain() {
|
||||
self.subscribes.insert(s.id(), s);
|
||||
}
|
||||
assert!(self.new_subscribes.is_empty());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_subscribes(&self) -> bool {
|
||||
self.subscribes.len() > 0 || self.new_subscribes.len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
|
||||
#[inline]
|
||||
fn name(&self) -> &'static str {
|
||||
self.0.read().unwrap().name
|
||||
}
|
||||
|
||||
fn is_subed(&self, id: i32) -> bool {
|
||||
self.0.read().unwrap().subscribes.get(&id).is_some()
|
||||
}
|
||||
|
||||
fn on_subscribe(&self, sub: ConnInner) {
|
||||
let mut lock = self.0.write().unwrap();
|
||||
if lock.subscribes.get(&sub.id()).is_some() {
|
||||
return;
|
||||
}
|
||||
if lock.need_snapshot {
|
||||
lock.new_subscribes.insert(sub.id(), sub.into());
|
||||
} else {
|
||||
lock.subscribes.insert(sub.id(), sub.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn on_unsubscribe(&self, id: i32) {
|
||||
let mut lock = self.0.write().unwrap();
|
||||
if let None = lock.subscribes.remove(&id) {
|
||||
lock.new_subscribes.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
fn join(&self) {
|
||||
self.0.write().unwrap().active = false;
|
||||
self.0.write().unwrap().handle.take().map(JoinHandle::join);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
pub fn new(name: &'static str, need_snapshot: bool) -> Self {
|
||||
Self(Arc::new(RwLock::new(ServiceInner::<T> {
|
||||
name,
|
||||
active: true,
|
||||
need_snapshot,
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_subscribes(&self) -> bool {
|
||||
self.0.read().unwrap().has_subscribes()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ok(&self) -> bool {
|
||||
let lock = self.0.read().unwrap();
|
||||
lock.active && lock.has_subscribes()
|
||||
}
|
||||
|
||||
pub fn snapshot<F>(&self, callback: F) -> ResultType<()>
|
||||
where
|
||||
F: FnMut(ServiceSwap<T>) -> ResultType<()>,
|
||||
{
|
||||
if self.0.read().unwrap().new_subscribes.len() > 0 {
|
||||
log::info!("Call snapshot of {} service", self.name());
|
||||
let mut callback = callback;
|
||||
callback(ServiceSwap::<T>(self.clone()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn send(&self, msg: Message) {
|
||||
self.send_shared(Arc::new(msg));
|
||||
}
|
||||
|
||||
pub fn send_shared(&self, msg: Arc<Message>) {
|
||||
let mut lock = self.0.write().unwrap();
|
||||
for s in lock.subscribes.values_mut() {
|
||||
s.send(msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_without(&self, msg: Message, sub: i32) {
|
||||
let mut lock = self.0.write().unwrap();
|
||||
let msg = Arc::new(msg);
|
||||
for s in lock.subscribes.values_mut() {
|
||||
if sub != s.id() {
|
||||
s.send(msg.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repeat<S, F>(&self, interval_ms: u64, callback: F)
|
||||
where
|
||||
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send,
|
||||
S: 'static + Default + Reset,
|
||||
{
|
||||
let interval = time::Duration::from_millis(interval_ms);
|
||||
let mut callback = callback;
|
||||
let sp = self.clone();
|
||||
let thread = thread::spawn(move || {
|
||||
let mut state = S::default();
|
||||
while sp.active() {
|
||||
let now = time::Instant::now();
|
||||
if sp.has_subscribes() {
|
||||
if let Err(err) = callback(sp.clone(), &mut state) {
|
||||
log::error!("Error of {} service: {}", sp.name(), err);
|
||||
thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT));
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
}
|
||||
} else {
|
||||
state.reset();
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
if elapsed < interval {
|
||||
thread::sleep(interval - elapsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.0.write().unwrap().handle = Some(thread);
|
||||
}
|
||||
|
||||
pub fn run<F>(&self, callback: F)
|
||||
where
|
||||
F: 'static + FnMut(Self) -> ResultType<()> + Send,
|
||||
{
|
||||
let sp = self.clone();
|
||||
let mut callback = callback;
|
||||
let thread = thread::spawn(move || {
|
||||
let mut error_timeout = HIBERATE_TIMEOUT;
|
||||
while sp.active() {
|
||||
if sp.has_subscribes() {
|
||||
log::debug!("Enter {} service inner loop", sp.name());
|
||||
let tm = time::Instant::now();
|
||||
if let Err(err) = callback(sp.clone()) {
|
||||
log::error!("Error of {} service: {}", sp.name(), err);
|
||||
if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) {
|
||||
error_timeout = HIBERATE_TIMEOUT;
|
||||
} else {
|
||||
error_timeout *= 2;
|
||||
}
|
||||
if error_timeout > MAX_ERROR_TIMEOUT {
|
||||
error_timeout = MAX_ERROR_TIMEOUT;
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(error_timeout));
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
} else {
|
||||
log::debug!("Exit {} service inner loop", sp.name());
|
||||
}
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(HIBERATE_TIMEOUT));
|
||||
}
|
||||
});
|
||||
self.0.write().unwrap().handle = Some(thread);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn active(&self) -> bool {
|
||||
self.0.read().unwrap().active
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceSwap<T> {
|
||||
#[inline]
|
||||
pub fn send(&self, msg: Message) {
|
||||
self.send_shared(Arc::new(msg));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn send_shared(&self, msg: Arc<Message>) {
|
||||
(self.0).0.write().unwrap().send_new_subscribes(msg);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_subscribes(&self) -> bool {
|
||||
(self.0).0.read().unwrap().subscribes.len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> Drop for ServiceSwap<T> {
|
||||
fn drop(&mut self) {
|
||||
(self.0).0.write().unwrap().swap_new_subscribes();
|
||||
}
|
||||
}
|
||||
384
src/server/video_service.rs
Normal file
384
src/server/video_service.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the
|
||||
// slowest playback rate that still looks smooth enough to feel real.
|
||||
// Our eyes can see a slight difference and even though 30FPS actually shows
|
||||
// more information and is more realistic.
|
||||
// 60FPS is commonly used in game, teamviewer 12 support this for video editing user.
|
||||
|
||||
// how to capture with mouse cursor:
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN
|
||||
|
||||
// 实现了硬件编解码和音频抓取,还绘制了鼠标
|
||||
// https://github.com/PHZ76/DesktopSharing
|
||||
|
||||
// dxgi memory leak issue
|
||||
// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device
|
||||
// but per my test, it is more related to AcquireNextFrame,
|
||||
// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582
|
||||
|
||||
// to-do:
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
|
||||
use super::*;
|
||||
use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN};
|
||||
use std::{
|
||||
io::ErrorKind::WouldBlock,
|
||||
time::{self, Instant},
|
||||
};
|
||||
|
||||
const WAIT_BASE: i32 = 17;
|
||||
pub const NAME: &'static str = "video";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
|
||||
static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
static ref SWITCH: Arc<Mutex<bool>> = Default::default();
|
||||
static ref INTERNAL_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
|
||||
static ref TEST_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
|
||||
static ref IMAGE_QUALITIES: Arc<Mutex<HashMap<i32, i32>>> = Default::default();
|
||||
}
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.run(run);
|
||||
sp
|
||||
}
|
||||
|
||||
fn run(sp: GenericService) -> ResultType<()> {
|
||||
let fps = 30;
|
||||
let spf = time::Duration::from_secs_f32(1. / (fps as f32));
|
||||
let (n, current, display) = get_current_display()?;
|
||||
let (origin, width, height) = (display.origin(), display.width(), display.height());
|
||||
log::debug!(
|
||||
"#displays={}, current={}, origin: {:?}, width={}, height={}",
|
||||
n,
|
||||
current,
|
||||
&origin,
|
||||
width,
|
||||
height
|
||||
);
|
||||
// Capturer object is expensive, avoiding to create it frequently.
|
||||
let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?;
|
||||
|
||||
let q = get_image_quality();
|
||||
let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q);
|
||||
log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer);
|
||||
let mut wait = WAIT_BASE;
|
||||
let cfg = Config {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000], // Output timestamp precision
|
||||
bitrate,
|
||||
codec: VideoCodecId::VP9,
|
||||
rc_min_quantizer,
|
||||
rc_max_quantizer,
|
||||
speed,
|
||||
};
|
||||
let mut vpx = Encoder::new(&cfg, 1).with_context(|| "Failed to create encoder")?;
|
||||
|
||||
if *SWITCH.lock().unwrap() {
|
||||
log::debug!("Broadcasting display switch");
|
||||
let mut misc = Misc::new();
|
||||
misc.set_switch_display(SwitchDisplay {
|
||||
display: current as _,
|
||||
x: origin.0 as _,
|
||||
y: origin.1 as _,
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
*SWITCH.lock().unwrap() = false;
|
||||
sp.send(msg_out);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if !c.is_gdi() {
|
||||
// dxgi duplicateoutput has no output if display no change, so we use gdi this as workaround
|
||||
if c.set_gdi() {
|
||||
// dgx capture release has memory leak somehow, so use gdi always before fixing it, just sacrificing some cpu
|
||||
/*
|
||||
if let Ok(frame) = c.frame(wait as _) {
|
||||
handle_one_frame(&sp, &frame, 0, &mut vpx)?;
|
||||
}
|
||||
c.cancel_gdi();
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
let start = time::Instant::now();
|
||||
let mut crc = (0, 0);
|
||||
let mut last_sent = time::Instant::now();
|
||||
while sp.ok() {
|
||||
if *SWITCH.lock().unwrap() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if current != *CURRENT_DISPLAY.lock().unwrap() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if get_image_quality() != q {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if crate::platform::windows::desktop_changed() {
|
||||
bail!("Desktop changed");
|
||||
}
|
||||
}
|
||||
let now = time::Instant::now();
|
||||
*LAST_ACTIVE.lock().unwrap() = now;
|
||||
if get_latency() < 1000 || last_sent.elapsed().as_millis() > 1000 {
|
||||
match c.frame(wait as _) {
|
||||
Ok(frame) => {
|
||||
let time = now - start;
|
||||
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
|
||||
handle_one_frame(&sp, &frame, ms, &mut crc, &mut vpx)?;
|
||||
last_sent = now;
|
||||
}
|
||||
Err(ref e) if e.kind() == WouldBlock => {
|
||||
// https://github.com/NVIDIA/video-sdk-samples/tree/master/nvEncDXGIOutputDuplicationSample
|
||||
wait = WAIT_BASE - now.elapsed().as_millis() as i32;
|
||||
if wait < 0 {
|
||||
wait = 0
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
// may need to enable frame(timeout)
|
||||
log::trace!("{:?} {:?}", time::Instant::now(), elapsed);
|
||||
if elapsed < spf {
|
||||
std::thread::sleep(spf - elapsed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_msg(vp9s: Vec<VP9>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(VP9s {
|
||||
frames: vp9s.into(),
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_frame(frame: &EncodeFrame) -> VP9 {
|
||||
VP9 {
|
||||
data: frame.data.to_vec(),
|
||||
key: frame.key,
|
||||
pts: frame.pts,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn handle_one_frame(
|
||||
sp: &GenericService,
|
||||
frame: &[u8],
|
||||
ms: i64,
|
||||
crc: &mut (u32, u32),
|
||||
vpx: &mut Encoder,
|
||||
) -> ResultType<()> {
|
||||
sp.snapshot(|sps| {
|
||||
// so that new sub and old sub share the same encoder after switch
|
||||
if sps.has_subscribes() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let mut hasher = crc32fast::Hasher::new();
|
||||
hasher.update(frame);
|
||||
let checksum = hasher.finalize();
|
||||
if checksum != crc.0 {
|
||||
crc.0 = checksum;
|
||||
crc.1 = 0;
|
||||
} else {
|
||||
crc.1 += 1;
|
||||
}
|
||||
if crc.1 <= 180 && crc.1 % 5 == 0 {
|
||||
let mut frames = Vec::new();
|
||||
for ref frame in vpx
|
||||
.encode(ms, frame, STRIDE_ALIGN)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
for ref frame in vpx.flush().with_context(|| "Failed to flush")? {
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
sp.send(create_msg(frames));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
|
||||
// switch to primary display if long time (30 seconds) no users
|
||||
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
|
||||
}
|
||||
let mut displays = Vec::new();
|
||||
let mut primary = 0;
|
||||
for (i, d) in Display::all()?.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
primary = i;
|
||||
}
|
||||
displays.push(DisplayInfo {
|
||||
x: d.origin().0 as _,
|
||||
y: d.origin().1 as _,
|
||||
width: d.width() as _,
|
||||
height: d.height() as _,
|
||||
name: d.name(),
|
||||
online: d.is_online(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let mut lock = CURRENT_DISPLAY.lock().unwrap();
|
||||
if *lock >= displays.len() {
|
||||
*lock = primary
|
||||
}
|
||||
Ok((*lock, displays))
|
||||
}
|
||||
|
||||
pub fn switch_display(i: i32) {
|
||||
let i = i as usize;
|
||||
if let Ok((_, displays)) = get_displays() {
|
||||
if i < displays.len() {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
fn get_primary() -> usize {
|
||||
if let Ok(all) = Display::all() {
|
||||
for (i, d) in all.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn switch_to_primary() {
|
||||
switch_display(get_primary() as _);
|
||||
}
|
||||
|
||||
fn get_current_display() -> ResultType<(usize, usize, Display)> {
|
||||
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
|
||||
let mut displays = Display::all()?;
|
||||
if displays.len() == 0 {
|
||||
bail!("No displays");
|
||||
}
|
||||
let n = displays.len();
|
||||
if current >= n {
|
||||
current = 0;
|
||||
for (i, d) in displays.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*CURRENT_DISPLAY.lock().unwrap() = current;
|
||||
}
|
||||
return Ok((n, current, displays.remove(current)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_latency(id: i32, latency: i64, latencies: &mut HashMap<i32, i64>) {
|
||||
if latency <= 0 {
|
||||
latencies.remove(&id);
|
||||
} else {
|
||||
latencies.insert(id, latency);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_test_latency(id: i32, latency: i64) {
|
||||
update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap());
|
||||
}
|
||||
|
||||
pub fn update_internal_latency(id: i32, latency: i64) {
|
||||
update_latency(id, latency, &mut *INTERNAL_LATENCIES.lock().unwrap());
|
||||
}
|
||||
|
||||
pub fn get_latency() -> i64 {
|
||||
INTERNAL_LATENCIES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.max()
|
||||
.unwrap_or(&0)
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn convert_quality(q: i32) -> i32 {
|
||||
let q = {
|
||||
if q == ImageQuality::Balanced.value() {
|
||||
(100 * 2 / 3, 12)
|
||||
} else if q == ImageQuality::Low.value() {
|
||||
(100 / 2, 18)
|
||||
} else if q == ImageQuality::Best.value() {
|
||||
(100, 12)
|
||||
} else {
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
(bitrate * 2, (100 - quantizer) * 36 / 100)
|
||||
}
|
||||
};
|
||||
if q.0 <= 0 {
|
||||
0
|
||||
} else {
|
||||
q.0 << 8 | q.1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_image_quality(id: i32, q: Option<i32>) {
|
||||
match q {
|
||||
Some(q) => {
|
||||
let q = convert_quality(q);
|
||||
if q > 0 {
|
||||
IMAGE_QUALITIES.lock().unwrap().insert(id, q);
|
||||
} else {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_quality() -> i32 {
|
||||
IMAGE_QUALITIES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.min()
|
||||
.unwrap_or(&convert_quality(ImageQuality::Balanced.value()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) {
|
||||
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
let b = ((w * h) / 1000) as u32;
|
||||
(bitrate as u32 * b / 100, quantizer as _, 56, 7)
|
||||
}
|
||||
BIN
src/tray-icon.ico
Normal file
BIN
src/tray-icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
647
src/ui.rs
Normal file
647
src/ui.rs
Normal file
@@ -0,0 +1,647 @@
|
||||
mod cm;
|
||||
#[cfg(feature = "inline")]
|
||||
mod inline;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod remote;
|
||||
use crate::common::SOFTWARE_UPDATE_URL;
|
||||
use crate::ipc;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
config::{Config, PeerConfig, APP_NAME, ICON},
|
||||
log, sleep,
|
||||
tokio::{self, time},
|
||||
};
|
||||
use sciter::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
iter::FromIterator,
|
||||
process::Child,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type Childs = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct UI(
|
||||
Childs,
|
||||
Arc<Mutex<(i32, bool)>>,
|
||||
Arc<Mutex<HashMap<String, String>>>,
|
||||
);
|
||||
|
||||
fn get_msgbox() -> String {
|
||||
#[cfg(feature = "inline")]
|
||||
return inline::get_msgbox();
|
||||
#[cfg(not(feature = "inline"))]
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
pub fn start(args: &mut [String]) {
|
||||
#[cfg(windows)]
|
||||
if args.len() > 0 && args[0] == "--tray" {
|
||||
let mut res;
|
||||
// while switching from prelogin to user screen, start_tray may fails,
|
||||
// so we try more times
|
||||
loop {
|
||||
res = start_tray();
|
||||
if res.is_ok() {
|
||||
log::info!("tray started with username {}", crate::username());
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
allow_err!(res);
|
||||
return;
|
||||
}
|
||||
use sciter::SCRIPT_RUNTIME_FEATURES::*;
|
||||
allow_err!(sciter::set_options(sciter::RuntimeOptions::ScriptFeatures(
|
||||
ALLOW_FILE_IO as u8 | ALLOW_SOCKET_IO as u8 | ALLOW_EVAL as u8 | ALLOW_SYSINFO as u8
|
||||
)));
|
||||
let mut frame = sciter::WindowBuilder::main_window().create();
|
||||
#[cfg(windows)]
|
||||
allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true)));
|
||||
frame.set_title(APP_NAME);
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::make_menubar();
|
||||
let page;
|
||||
if args.len() > 1 && args[0] == "--play" {
|
||||
args[0] = "--connect".to_owned();
|
||||
let path: std::path::PathBuf = (&args[1]).into();
|
||||
let id = path
|
||||
.file_stem()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
args[1] = id;
|
||||
}
|
||||
if args.is_empty() {
|
||||
let childs: Childs = Default::default();
|
||||
let cloned = childs.clone();
|
||||
std::thread::spawn(move || check_zombie(cloned));
|
||||
crate::common::check_software_update();
|
||||
frame.event_handler(UI::new(childs));
|
||||
page = "index.html";
|
||||
} else if args[0] == "--install" {
|
||||
let childs: Childs = Default::default();
|
||||
frame.event_handler(UI::new(childs));
|
||||
page = "install.html";
|
||||
} else if args[0] == "--cm" {
|
||||
frame.register_behavior("connection-manager", move || {
|
||||
Box::new(cm::ConnectionManager::new())
|
||||
});
|
||||
page = "cm.html";
|
||||
} else if (args[0] == "--connect"
|
||||
|| args[0] == "--file-transfer"
|
||||
|| args[0] == "--port-forward"
|
||||
|| args[0] == "--rdp")
|
||||
&& args.len() > 1
|
||||
{
|
||||
let mut iter = args.iter();
|
||||
let cmd = iter.next().unwrap().clone();
|
||||
let id = iter.next().unwrap().clone();
|
||||
let args: Vec<String> = iter.map(|x| x.clone()).collect();
|
||||
frame.set_title(&id);
|
||||
frame.register_behavior("native-remote", move || {
|
||||
Box::new(remote::Handler::new(cmd.clone(), id.clone(), args.clone()))
|
||||
});
|
||||
page = "remote.html";
|
||||
} else {
|
||||
log::error!("Wrong command: {:?}", args);
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "inline")]
|
||||
{
|
||||
let html = if page == "index.html" {
|
||||
inline::get_index()
|
||||
} else if page == "cm.html" {
|
||||
inline::get_cm()
|
||||
} else if page == "install.html" {
|
||||
inline::get_install()
|
||||
} else {
|
||||
inline::get_remote()
|
||||
};
|
||||
frame.load_html(html.as_bytes(), Some(page));
|
||||
}
|
||||
#[cfg(not(feature = "inline"))]
|
||||
frame.load_file(&format!(
|
||||
"file://{}/src/ui/{}",
|
||||
std::env::current_dir()
|
||||
.map(|c| c.display().to_string())
|
||||
.unwrap_or("".to_owned()),
|
||||
page
|
||||
));
|
||||
frame.run_app();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn start_tray() -> hbb_common::ResultType<()> {
|
||||
let mut app = systray::Application::new()?;
|
||||
let icon = include_bytes!("./tray-icon.ico");
|
||||
app.set_icon_from_buffer(icon, 32, 32).unwrap();
|
||||
app.add_menu_item("Open Window", |_| {
|
||||
crate::run_me(Vec::<&str>::new()).ok();
|
||||
Ok::<_, systray::Error>(())
|
||||
})?;
|
||||
let options = check_connect_status(false).1;
|
||||
let idx_stopped = Arc::new(Mutex::new((0, 0)));
|
||||
app.set_timer(std::time::Duration::from_millis(1000), move |app| {
|
||||
let stopped = if let Some(v) = options.lock().unwrap().get("stop-service") {
|
||||
!v.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let stopped = if stopped { 2 } else { 1 };
|
||||
let mut old = *idx_stopped.lock().unwrap();
|
||||
if stopped != old.1 {
|
||||
if old.0 > 0 {
|
||||
app.remove_menu_item(old.0)
|
||||
}
|
||||
if stopped == 1 {
|
||||
old.0 = app.add_menu_item("Stop Service", |_| {
|
||||
ipc::set_option("stop-service", "Y");
|
||||
Ok::<_, systray::Error>(())
|
||||
})?;
|
||||
} else {
|
||||
old.0 = app.add_menu_item("Start Service", |_| {
|
||||
ipc::set_option("stop-service", "");
|
||||
Ok::<_, systray::Error>(())
|
||||
})?;
|
||||
}
|
||||
old.1 = stopped;
|
||||
*idx_stopped.lock().unwrap() = old;
|
||||
}
|
||||
Ok::<_, systray::Error>(())
|
||||
})?;
|
||||
allow_err!(app.wait_for_message());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl UI {
|
||||
fn new(childs: Childs) -> Self {
|
||||
let res = check_connect_status(true);
|
||||
Self(childs, res.0, res.1)
|
||||
}
|
||||
|
||||
fn recent_sessions_updated(&mut self) -> bool {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
if lock.0 {
|
||||
lock.0 = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_id(&mut self) -> String {
|
||||
ipc::get_id()
|
||||
}
|
||||
|
||||
fn get_password(&mut self) -> String {
|
||||
ipc::get_password()
|
||||
}
|
||||
|
||||
fn update_password(&mut self, password: String) {
|
||||
if password.is_empty() {
|
||||
allow_err!(ipc::set_password(Config::get_auto_password()));
|
||||
} else {
|
||||
allow_err!(ipc::set_password(password));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_remote_id(&mut self) -> String {
|
||||
Config::get_remote_id()
|
||||
}
|
||||
|
||||
fn set_remote_id(&mut self, id: String) {
|
||||
Config::set_remote_id(&id);
|
||||
}
|
||||
|
||||
fn get_msgbox(&mut self) -> String {
|
||||
get_msgbox()
|
||||
}
|
||||
|
||||
fn goto_install(&mut self) {
|
||||
allow_err!(crate::run_me(vec!["--install"]));
|
||||
}
|
||||
|
||||
fn install_me(&mut self, _options: String) {
|
||||
#[cfg(windows)]
|
||||
std::thread::spawn(move || {
|
||||
allow_err!(crate::platform::windows::install_me(&_options));
|
||||
std::process::exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_me(&self, _path: String) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
std::process::Command::new("pkexec")
|
||||
.args(&["apt", "install", "-f", &_path])
|
||||
.spawn()
|
||||
.ok();
|
||||
std::fs::remove_file(&_path).ok();
|
||||
crate::run_me(Vec::<&str>::new()).ok();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut path = _path;
|
||||
if path.is_empty() {
|
||||
if let Ok(tmp) = std::env::current_exe() {
|
||||
path = tmp.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
std::process::Command::new(path)
|
||||
.arg("--update")
|
||||
.spawn()
|
||||
.ok();
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_option(&self, key: String) -> String {
|
||||
if let Some(v) = self.2.lock().unwrap().get(&key) {
|
||||
v.to_owned()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_options(&self) -> Value {
|
||||
let mut m = Value::map();
|
||||
for (k, v) in self.2.lock().unwrap().iter() {
|
||||
m.set_item(k, v);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn test_if_valid_server(&self, host: String) -> String {
|
||||
crate::common::test_if_valid_server(host)
|
||||
}
|
||||
|
||||
fn get_sound_inputs(&self) -> Value {
|
||||
let mut a = Value::array(0);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let inputs = Arc::new(Mutex::new(Vec::new()));
|
||||
let cloned = inputs.clone();
|
||||
// can not call below in UI thread, because conflict with sciter sound com initialization
|
||||
std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs())
|
||||
.join()
|
||||
.ok();
|
||||
for name in inputs.lock().unwrap().drain(..) {
|
||||
a.push(name);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
for name in get_sound_inputs() {
|
||||
a.push(name);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn set_options(&self, v: Value) {
|
||||
let mut m = HashMap::new();
|
||||
for (k, v) in v.items() {
|
||||
if let Some(k) = k.as_string() {
|
||||
if let Some(v) = v.as_string() {
|
||||
if !v.is_empty() {
|
||||
m.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ipc::set_options(m).ok();
|
||||
}
|
||||
|
||||
fn install_path(&mut self) -> String {
|
||||
#[cfg(windows)]
|
||||
return crate::platform::windows::get_install_info().1;
|
||||
#[cfg(not(windows))]
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
fn is_installed(&mut self) -> bool {
|
||||
crate::platform::is_installed()
|
||||
}
|
||||
|
||||
fn is_installed_lower_version(&self) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
return false;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let installed_version = crate::platform::windows::get_installed_version();
|
||||
let a = crate::common::get_version_number(crate::VERSION);
|
||||
let b = crate::common::get_version_number(&installed_version);
|
||||
return a > b;
|
||||
}
|
||||
}
|
||||
|
||||
fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) {
|
||||
Config::set_size(x, y, w, h);
|
||||
}
|
||||
|
||||
fn get_size(&mut self) -> Value {
|
||||
let s = Config::get_size();
|
||||
let mut v = Value::array(0);
|
||||
v.push(s.0);
|
||||
v.push(s.1);
|
||||
v.push(s.2);
|
||||
v.push(s.3);
|
||||
v
|
||||
}
|
||||
|
||||
fn get_connect_status(&mut self) -> Value {
|
||||
let mut v = Value::array(0);
|
||||
let x = *self.1.lock().unwrap();
|
||||
v.push(x.0);
|
||||
v.push(x.1);
|
||||
v
|
||||
}
|
||||
|
||||
fn get_recent_sessions(&mut self) -> Value {
|
||||
let peers: Vec<Value> = PeerConfig::peers()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let values = vec![
|
||||
p.0.clone(),
|
||||
p.2.username.clone(),
|
||||
p.2.hostname.clone(),
|
||||
p.2.platform.clone(),
|
||||
];
|
||||
Value::from_iter(values)
|
||||
})
|
||||
.collect();
|
||||
Value::from_iter(peers)
|
||||
}
|
||||
|
||||
fn get_icon(&mut self) -> String {
|
||||
ICON.to_owned()
|
||||
}
|
||||
|
||||
fn remove_peer(&mut self, id: String) {
|
||||
PeerConfig::remove(&id);
|
||||
}
|
||||
|
||||
fn new_remote(&mut self, id: String, remote_type: String) {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let args = vec![format!("--{}", remote_type), id.clone()];
|
||||
let key = (id.clone(), remote_type.clone());
|
||||
if let Some(c) = lock.1.get_mut(&key) {
|
||||
if let Ok(Some(_)) = c.try_wait() {
|
||||
lock.1.remove(&key);
|
||||
} else {
|
||||
if remote_type == "rdp" {
|
||||
allow_err!(c.kill());
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
c.try_wait().ok();
|
||||
lock.1.remove(&key);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
match crate::run_me(args) {
|
||||
Ok(child) => {
|
||||
lock.1.insert(key, child);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to spawn remote: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_process_trusted(&mut self, _prompt: bool) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
return crate::platform::macos::is_process_trusted(_prompt);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
return true;
|
||||
}
|
||||
|
||||
fn is_can_screen_recording(&mut self, _prompt: bool) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
return crate::platform::macos::is_can_screen_recording(_prompt);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
return true;
|
||||
}
|
||||
|
||||
fn get_error(&mut self) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let dtype = crate::platform::linux::get_display_server();
|
||||
if dtype != "x11" {
|
||||
return format!("Unsupported display server type {}, x11 expected!", dtype);
|
||||
}
|
||||
}
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
fn is_login_wayland(&mut self) -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
return crate::platform::linux::is_login_wayland();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return false;
|
||||
}
|
||||
|
||||
fn fix_login_wayland(&mut self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
return crate::platform::linux::fix_login_wayland();
|
||||
}
|
||||
|
||||
fn get_software_update_url(&self) -> String {
|
||||
SOFTWARE_UPDATE_URL.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn get_new_version(&self) -> String {
|
||||
hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap())
|
||||
}
|
||||
|
||||
fn get_version(&self) -> String {
|
||||
crate::VERSION.to_owned()
|
||||
}
|
||||
|
||||
fn get_app_name(&self) -> String {
|
||||
APP_NAME.to_owned()
|
||||
}
|
||||
|
||||
fn get_software_ext(&self) -> String {
|
||||
#[cfg(windows)]
|
||||
let p = "exe";
|
||||
#[cfg(target_os = "macos")]
|
||||
let p = "dmg";
|
||||
#[cfg(target_os = "linux")]
|
||||
let p = "deb";
|
||||
p.to_owned()
|
||||
}
|
||||
|
||||
fn get_software_store_path(&self) -> String {
|
||||
let mut p = std::env::temp_dir();
|
||||
let name = SOFTWARE_UPDATE_URL
|
||||
.lock()
|
||||
.unwrap()
|
||||
.split("/")
|
||||
.last()
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or(APP_NAME.to_owned());
|
||||
p.push(name);
|
||||
format!("{}.{}", p.to_string_lossy(), self.get_software_ext())
|
||||
}
|
||||
|
||||
fn open_url(&self, url: String) {
|
||||
#[cfg(windows)]
|
||||
let p = "explorer";
|
||||
#[cfg(target_os = "macos")]
|
||||
let p = "open";
|
||||
#[cfg(target_os = "linux")]
|
||||
let p = "xdg-open";
|
||||
allow_err!(std::process::Command::new(p).arg(url).spawn());
|
||||
}
|
||||
}
|
||||
|
||||
impl sciter::EventHandler for UI {
|
||||
sciter::dispatch_script_call! {
|
||||
fn get_id();
|
||||
fn get_password();
|
||||
fn update_password(String);
|
||||
fn get_remote_id();
|
||||
fn set_remote_id(String);
|
||||
fn save_size(i32, i32, i32, i32);
|
||||
fn get_size();
|
||||
fn new_remote(String, bool);
|
||||
fn remove_peer(String);
|
||||
fn get_connect_status();
|
||||
fn get_recent_sessions();
|
||||
fn recent_sessions_updated();
|
||||
fn get_icon();
|
||||
fn get_msgbox();
|
||||
fn install_me(String);
|
||||
fn is_installed();
|
||||
fn is_installed_lower_version();
|
||||
fn install_path();
|
||||
fn goto_install();
|
||||
fn is_process_trusted(bool);
|
||||
fn is_can_screen_recording(bool);
|
||||
fn get_error();
|
||||
fn is_login_wayland();
|
||||
fn fix_login_wayland();
|
||||
fn get_options();
|
||||
fn get_option(String);
|
||||
fn test_if_valid_server(String);
|
||||
fn get_sound_inputs();
|
||||
fn set_options(Value);
|
||||
fn get_software_update_url();
|
||||
fn get_new_version();
|
||||
fn get_version();
|
||||
fn update_me(String);
|
||||
fn get_app_name();
|
||||
fn get_software_store_path();
|
||||
fn get_software_ext();
|
||||
fn open_url(String);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_zombie(childs: Childs) {
|
||||
let mut deads = Vec::new();
|
||||
loop {
|
||||
let mut lock = childs.lock().unwrap();
|
||||
let mut n = 0;
|
||||
for (id, c) in lock.1.iter_mut() {
|
||||
if let Ok(Some(_)) = c.try_wait() {
|
||||
deads.push(id.clone());
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
for ref id in deads.drain(..) {
|
||||
lock.1.remove(id);
|
||||
}
|
||||
if n > 0 {
|
||||
lock.0 = true;
|
||||
}
|
||||
drop(lock);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
// notice: avoiding create ipc connecton repeatly,
|
||||
// because windows named pipe has serious memory leak issue.
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn check_connect_status_(
|
||||
reconnect: bool,
|
||||
status: Arc<Mutex<(i32, bool)>>,
|
||||
options: Arc<Mutex<HashMap<String, String>>>,
|
||||
) {
|
||||
let mut key_confirmed = false;
|
||||
loop {
|
||||
if let Ok(mut c) = ipc::connect(1000, "").await {
|
||||
let mut timer = time::interval(time::Duration::from_secs(1));
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = c.next() => {
|
||||
match res {
|
||||
Err(err) => {
|
||||
log::error!("ipc connection closed: {}", err);
|
||||
break;
|
||||
}
|
||||
Ok(Some(ipc::Data::Options(Some(v)))) => {
|
||||
*options.lock().unwrap() = v
|
||||
}
|
||||
Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => {
|
||||
if x > 0 {
|
||||
x = 1
|
||||
}
|
||||
key_confirmed = c;
|
||||
*status.lock().unwrap() = (x as _, key_confirmed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ = timer.tick() => {
|
||||
c.send(&ipc::Data::OnlineStatus(None)).await.ok();
|
||||
c.send(&ipc::Data::Options(None)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reconnect {
|
||||
std::process::exit(0);
|
||||
}
|
||||
*status.lock().unwrap() = (-1, key_confirmed);
|
||||
sleep(1.).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_sound_inputs() -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
let host = cpal::default_host();
|
||||
if let Ok(devices) = host.devices() {
|
||||
for device in devices {
|
||||
if device.default_input_config().is_err() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(name) = device.name() {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_sound_inputs() -> Vec<String> {
|
||||
crate::platform::linux::get_pa_sources()
|
||||
.drain(..)
|
||||
.map(|x| x.1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn check_connect_status(
|
||||
reconnect: bool,
|
||||
) -> (Arc<Mutex<(i32, bool)>>, Arc<Mutex<HashMap<String, String>>>) {
|
||||
let status = Arc::new(Mutex::new((0, false)));
|
||||
let options = Arc::new(Mutex::new(HashMap::new()));
|
||||
let cloned = status.clone();
|
||||
let cloned_options = options.clone();
|
||||
std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options));
|
||||
(status, options)
|
||||
}
|
||||
27
src/ui/chatbox.html
Normal file
27
src/ui/chatbox.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<html window-resizable>
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@media platform != "OSX" {
|
||||
body {
|
||||
border-top: color(border) solid 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
var p = view.parameters;
|
||||
view.refresh = function() {
|
||||
$(body).content(<ChatBox msgs={p.msgs} callback={p.callback} />);
|
||||
view.focus = $(input);
|
||||
}
|
||||
view.refresh();
|
||||
function self.closing() {
|
||||
view.windowState = View.WINDOW_HIDDEN;
|
||||
return false;
|
||||
}
|
||||
view.windowIcon = self.url(p.icon);
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
218
src/ui/cm.css
Normal file
218
src/ui/cm.css
Normal file
@@ -0,0 +1,218 @@
|
||||
body {
|
||||
behavior: connection-manager;
|
||||
}
|
||||
|
||||
div.content {
|
||||
flow: horizontal;
|
||||
size: *;
|
||||
}
|
||||
|
||||
div.left-panel {
|
||||
size: *;
|
||||
padding: 1em;
|
||||
border-spacing: 1em;
|
||||
overflow-x: scroll-indicator;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.chaticon svg {
|
||||
size: 24px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
div.chaticon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
size: 32px;
|
||||
}
|
||||
|
||||
div.chaticon.active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
div.chaticon:active {
|
||||
background: white;
|
||||
}
|
||||
|
||||
div.right-panel {
|
||||
background: white;
|
||||
border-left: color(border) 1px solid;
|
||||
size: *;
|
||||
}
|
||||
|
||||
div.icon-and-id {
|
||||
flow: horizontal;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
|
||||
div.icon {
|
||||
size: 96px;
|
||||
text-align: center;
|
||||
font-size: 96px;
|
||||
line-height: 96px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.id {
|
||||
color: color(green-blue);
|
||||
}
|
||||
|
||||
div.permissions {
|
||||
flow: horizontal;
|
||||
border-spacing: 0.5em;
|
||||
}
|
||||
|
||||
div.permissions > div {
|
||||
size: 48px;
|
||||
background: color(accent);
|
||||
}
|
||||
|
||||
div.permissions icon {
|
||||
margin: *;
|
||||
size: 32px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.permissions > div.disabled {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
div.permissions > div:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
icon.keyboard {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=');
|
||||
}
|
||||
|
||||
icon.clipboard {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII=');
|
||||
}
|
||||
|
||||
icon.audio {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg==');
|
||||
}
|
||||
|
||||
div.buttons {
|
||||
width: *;
|
||||
border-spacing: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.buttons button {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
button#disconnect {
|
||||
width: 160px;
|
||||
background: color(blood-red);
|
||||
border: none;
|
||||
}
|
||||
|
||||
button#disconnect:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
header .window-toolbar {
|
||||
left: 40px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media platform == "OSX" {
|
||||
header .tabs-wrapper {
|
||||
margin-left: 80px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
div.tabs-wrapper {
|
||||
size: *;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
size: *;
|
||||
flow: horizontal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 32px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
div.border-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: *;
|
||||
height: 1px;
|
||||
background: color(border) 1px solid;
|
||||
}
|
||||
|
||||
header div.window-icon {
|
||||
size: 32px;
|
||||
}
|
||||
|
||||
div.tabs > div {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
div.tab {
|
||||
width: 70px;
|
||||
@ELLIPSIS;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.active-tab {
|
||||
background: color(gray-bg);
|
||||
border: color(border) 1px solid;
|
||||
border-bottom: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.unreaded {
|
||||
position: absolute;
|
||||
font-size: 11px;
|
||||
size: 15px;
|
||||
border-radius: 15px;
|
||||
line-height: 15px;
|
||||
background: color(blood-red);
|
||||
display: inline-block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.left-panel {
|
||||
background: color(gray-bg);
|
||||
}
|
||||
|
||||
button.window#minimize {
|
||||
right: 0px!important;
|
||||
}
|
||||
|
||||
div.tab-arrows {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.tab-arrows span {
|
||||
display: inline-block;
|
||||
height: *;
|
||||
margin: 0;
|
||||
padding: 6px 2px;
|
||||
}
|
||||
21
src/ui/cm.html
Normal file
21
src/ui/cm.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@import url(cm.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "cm.tis";
|
||||
</script>
|
||||
</head>
|
||||
<header>
|
||||
<div.window-icon role="window-icon"><icon /></div>
|
||||
<caption role="window-caption" />
|
||||
<div.border-bottom />
|
||||
<div.window-toolbar />
|
||||
<div.window-buttons />
|
||||
</header>
|
||||
<body #handler>
|
||||
</body>
|
||||
</html>
|
||||
465
src/ui/cm.rs
Normal file
465
src/ui/cm.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
use crate::ipc::{self, new_listener, Connection, Data};
|
||||
#[cfg(windows)]
|
||||
use hbb_common::futures_util::stream::StreamExt;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
config::{Config, ICON},
|
||||
fs, log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
tokio::{self, sync::mpsc, task::spawn_blocking},
|
||||
};
|
||||
use sciter::{make_args, Element, Value, HELEMENT};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Deref,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
pub struct ConnectionManagerInner {
|
||||
root: Option<Element>,
|
||||
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionManager(Arc<RwLock<ConnectionManagerInner>>);
|
||||
|
||||
impl Deref for ConnectionManager {
|
||||
type Target = Arc<RwLock<ConnectionManagerInner>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
pub fn new() -> Self {
|
||||
#[cfg(target_os = "linux")]
|
||||
std::thread::spawn(start_pa);
|
||||
let inner = ConnectionManagerInner {
|
||||
root: None,
|
||||
senders: HashMap::new(),
|
||||
};
|
||||
let cm = Self(Arc::new(RwLock::new(inner)));
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let cloned = cm.clone();
|
||||
*super::macos::SHOULD_OPEN_UNTITLED_FILE_CALLBACK
|
||||
.lock()
|
||||
.unwrap() = Some(Box::new(move || {
|
||||
cloned.call("awake", &make_args!());
|
||||
}));
|
||||
}
|
||||
let cloned = cm.clone();
|
||||
std::thread::spawn(move || start_ipc(cloned));
|
||||
cm
|
||||
}
|
||||
|
||||
fn get_icon(&mut self) -> String {
|
||||
ICON.to_owned()
|
||||
}
|
||||
|
||||
fn check_click_time(&mut self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::ClickTime(crate::get_time())));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call(&self, func: &str, args: &[Value]) {
|
||||
let r = self.read().unwrap();
|
||||
if let Some(ref e) = r.root {
|
||||
allow_err!(e.call_method(func, args));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_connection(
|
||||
&self,
|
||||
id: i32,
|
||||
is_file_transfer: bool,
|
||||
port_forward: String,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
authorized: bool,
|
||||
keyboard: bool,
|
||||
clipboard: bool,
|
||||
audio: bool,
|
||||
tx: mpsc::UnboundedSender<Data>,
|
||||
) {
|
||||
self.call(
|
||||
"addConnection",
|
||||
&make_args!(
|
||||
id,
|
||||
is_file_transfer,
|
||||
port_forward,
|
||||
peer_id,
|
||||
name,
|
||||
authorized,
|
||||
keyboard,
|
||||
clipboard,
|
||||
audio
|
||||
),
|
||||
);
|
||||
self.write().unwrap().senders.insert(id, tx);
|
||||
}
|
||||
|
||||
fn remove_connection(&self, id: i32) {
|
||||
self.write().unwrap().senders.remove(&id);
|
||||
self.call("removeConnection", &make_args!(id));
|
||||
}
|
||||
|
||||
async fn handle_data(
|
||||
&self,
|
||||
id: i32,
|
||||
data: Data,
|
||||
write_jobs: &mut Vec<fs::TransferJob>,
|
||||
conn: &mut Connection,
|
||||
) {
|
||||
match data {
|
||||
Data::ChatMessage { text } => {
|
||||
self.call("newMessage", &make_args!(id, text));
|
||||
}
|
||||
Data::ClickTime(ms) => {
|
||||
self.call("resetClickCallback", &make_args!(ms as f64));
|
||||
}
|
||||
Data::FS(v) => match v {
|
||||
ipc::FS::ReadDir {
|
||||
dir,
|
||||
include_hidden,
|
||||
} => {
|
||||
Self::read_dir(&dir, include_hidden, conn).await;
|
||||
}
|
||||
ipc::FS::RemoveDir {
|
||||
path,
|
||||
id,
|
||||
recursive,
|
||||
} => {
|
||||
Self::remove_dir(path, id, recursive, conn).await;
|
||||
}
|
||||
ipc::FS::RemoveFile { path, id, file_num } => {
|
||||
Self::remove_file(path, id, file_num, conn).await;
|
||||
}
|
||||
ipc::FS::CreateDir { path, id } => {
|
||||
Self::create_dir(path, id, conn).await;
|
||||
}
|
||||
ipc::FS::NewWrite {
|
||||
path,
|
||||
id,
|
||||
mut files,
|
||||
} => {
|
||||
write_jobs.push(fs::TransferJob::new_write(
|
||||
id,
|
||||
path,
|
||||
files
|
||||
.drain(..)
|
||||
.map(|f| FileEntry {
|
||||
name: f.0,
|
||||
modified_time: f.1,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
ipc::FS::CancelWrite { id } => {
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
job.remove_download_file();
|
||||
fs::remove_job(id, write_jobs);
|
||||
}
|
||||
}
|
||||
ipc::FS::WriteDone { id, file_num } => {
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
job.modify_time();
|
||||
Self::send(fs::new_done(id, file_num), conn).await;
|
||||
fs::remove_job(id, write_jobs);
|
||||
}
|
||||
}
|
||||
ipc::FS::WriteBlock {
|
||||
id,
|
||||
file_num,
|
||||
data,
|
||||
compressed,
|
||||
} => {
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
if let Err(err) = job
|
||||
.write(FileTransferBlock {
|
||||
id,
|
||||
file_num,
|
||||
data,
|
||||
compressed,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) {
|
||||
let path = {
|
||||
if dir.is_empty() {
|
||||
Config::get_home()
|
||||
} else {
|
||||
fs::get_path(dir)
|
||||
}
|
||||
};
|
||||
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
|
||||
let mut msg_out = Message::new();
|
||||
let mut file_response = FileResponse::new();
|
||||
file_response.set_dir(fd);
|
||||
msg_out.set_file_response(file_response);
|
||||
Self::send(msg_out, conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
|
||||
res: std::result::Result<std::result::Result<(), F>, S>,
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
conn: &mut Connection,
|
||||
) {
|
||||
match res {
|
||||
Err(err) => {
|
||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
||||
}
|
||||
Ok(Ok(())) => {
|
||||
Self::send(fs::new_done(id, file_num), conn).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) {
|
||||
Self::handle_result(
|
||||
spawn_blocking(move || fs::remove_file(&path)).await,
|
||||
id,
|
||||
file_num,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn create_dir(path: String, id: i32, conn: &mut Connection) {
|
||||
Self::handle_result(
|
||||
spawn_blocking(move || fs::create_dir(&path)).await,
|
||||
id,
|
||||
0,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) {
|
||||
let path = fs::get_path(&path);
|
||||
Self::handle_result(
|
||||
spawn_blocking(move || {
|
||||
if recursive {
|
||||
fs::remove_all_empty_dir(&path)
|
||||
} else {
|
||||
std::fs::remove_dir(&path).map_err(|err| err.into())
|
||||
}
|
||||
})
|
||||
.await,
|
||||
id,
|
||||
0,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send(msg: Message, conn: &mut Connection) {
|
||||
match msg.write_to_bytes() {
|
||||
Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await),
|
||||
err => allow_err!(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_permission(&self, id: i32, name: String, enabled: bool) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::SwitchPermission { name, enabled }));
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::Close));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_msg(&self, id: i32, text: String) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::ChatMessage { text }));
|
||||
}
|
||||
}
|
||||
|
||||
fn authorize(&self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::Authorize));
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&self) {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl sciter::EventHandler for ConnectionManager {
|
||||
fn attached(&mut self, root: HELEMENT) {
|
||||
self.write().unwrap().root = Some(Element::from(root));
|
||||
}
|
||||
|
||||
sciter::dispatch_script_call! {
|
||||
fn check_click_time(i32);
|
||||
fn get_icon();
|
||||
fn close(i32);
|
||||
fn authorize(i32);
|
||||
fn switch_permission(i32, String, bool);
|
||||
fn send_msg(i32, String);
|
||||
fn exit();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn start_ipc(cm: ConnectionManager) {
|
||||
match new_listener("_cm").await {
|
||||
Ok(mut incoming) => {
|
||||
while let Some(result) = incoming.next().await {
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let mut stream = Connection::new(stream);
|
||||
let cm = cm.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut conn_id: i32 = 0;
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
|
||||
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = stream.next() => {
|
||||
match res {
|
||||
Err(err) => {
|
||||
log::info!("cm ipc connection closed: {}", err);
|
||||
break;
|
||||
}
|
||||
Ok(Some(data)) => {
|
||||
match data {
|
||||
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio} => {
|
||||
conn_id = id;
|
||||
cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, tx.clone());
|
||||
}
|
||||
_ => {
|
||||
cm.handle_data(conn_id, data, &mut write_jobs, &mut stream).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(data) = rx.recv() => {
|
||||
allow_err!(stream.send(&data).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
cm.remove_connection(conn_id);
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Couldn't get cm client: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to start cm ipc server: {}", err);
|
||||
}
|
||||
}
|
||||
std::process::exit(-1);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::main(basic_scheduler)]
|
||||
async fn start_pa() {
|
||||
use hbb_common::config::APP_NAME;
|
||||
use libpulse_binding as pulse;
|
||||
use libpulse_simple_binding as psimple;
|
||||
match new_listener("_pa").await {
|
||||
Ok(mut incoming) => {
|
||||
loop {
|
||||
if let Some(result) = incoming.next().await {
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let mut stream = Connection::new(stream);
|
||||
let mut device: String = "".to_owned();
|
||||
if let Some(Ok(Some(Data::Config((_, Some(x)))))) =
|
||||
stream.next_timeout2(1000).await
|
||||
{
|
||||
device = x;
|
||||
}
|
||||
if device == "Mute" {
|
||||
break;
|
||||
}
|
||||
if !device.is_empty() {
|
||||
device = crate::platform::linux::get_pa_source_name(&device);
|
||||
}
|
||||
if device.is_empty() {
|
||||
device = crate::platform::linux::get_pa_monitor();
|
||||
}
|
||||
if device.is_empty() {
|
||||
break;
|
||||
}
|
||||
let spec = pulse::sample::Spec {
|
||||
format: pulse::sample::Format::F32be,
|
||||
channels: 2,
|
||||
rate: crate::platform::linux::PA_SAMPLE_RATE,
|
||||
};
|
||||
log::info!("pa monitor: {:?}", device);
|
||||
if let Ok(s) = psimple::Simple::new(
|
||||
None, // Use the default server
|
||||
APP_NAME, // Our application’s name
|
||||
pulse::stream::Direction::Record, // We want a record stream
|
||||
Some(&device), // Use the default device
|
||||
APP_NAME, // Description of our stream
|
||||
&spec, // Our sample format
|
||||
None, // Use default channel map
|
||||
None, // Use default buffering attributes
|
||||
) {
|
||||
loop {
|
||||
if let Some(Err(_)) = stream.next_timeout2(1).await {
|
||||
break;
|
||||
}
|
||||
let mut out: Vec<u8> = Vec::with_capacity(480 * 4);
|
||||
unsafe {
|
||||
out.set_len(out.capacity());
|
||||
}
|
||||
if let Ok(_) = s.read(&mut out) {
|
||||
if out.iter().filter(|x| **x != 0).next().is_some() {
|
||||
allow_err!(stream.send(&Data::RawMessage(out)).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Could not create simple pulse");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Couldn't get pa client: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to start pa ipc server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
409
src/ui/cm.tis
Normal file
409
src/ui/cm.tis
Normal file
@@ -0,0 +1,409 @@
|
||||
view.windowFrame = is_osx ? #extended : #solid;
|
||||
|
||||
var body;
|
||||
var connections = [];
|
||||
var show_chat = false;
|
||||
var click_callback;
|
||||
var click_callback_time = 0;
|
||||
|
||||
class Body: Reactor.Component
|
||||
{
|
||||
this var cur = 0;
|
||||
|
||||
function this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (connections.length == 0) return <div />;
|
||||
var c = connections[this.cur];
|
||||
this.connection = c;
|
||||
this.cid = c.id;
|
||||
var auth = c.authorized;
|
||||
var me = this;
|
||||
var callback = function(msg) {
|
||||
me.sendMsg(msg);
|
||||
};
|
||||
self.timer(1ms, adaptSize);
|
||||
var right_style = show_chat ? "" : "display: none";
|
||||
return <div .content>
|
||||
<div .left-panel>
|
||||
<div .icon-and-id>
|
||||
<div .icon style={"background: " + string2RGB(c.name, 1)}>
|
||||
{c.name[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
|
||||
<div .id>({c.peer_id})</div>
|
||||
<div style="margin-top: 1.2em">Connected <span #time>{getElaspsed(c.time)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div>Permissions</div>}
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div .permissions>
|
||||
<div class={!c.keyboard ? "disabled" : ""} title="Allow to use keyboard and mouse"><icon .keyboard /></div>
|
||||
<div class={!c.clipboard ? "disabled" : ""} title="Allow to use clipboard"><icon .clipboard /></div>
|
||||
<div class={!c.audio ? "disabled" : ""} title="Allow to hear sound"><icon .audio /></div>
|
||||
</div>}
|
||||
{c.port_forward ? <div>Port Forwarding: {c.port_forward}</div> : ""}
|
||||
<div style="size:*"/>
|
||||
<div .buttons>
|
||||
{auth ? "" : <button .button tabindex="-1" #accept>Accept</button>}
|
||||
{auth ? "" : <button .button tabindex="-1" .outline #dismiss>Dismiss</button>}
|
||||
{auth ? <button .button tabindex="-1" #disconnect>Disconnect</button> : ""}
|
||||
</div>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div .chaticon>{svg_chat}</div>}
|
||||
</div>
|
||||
<div .right-panel style={right_style}>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <ChatBox msgs={c.msgs} callback={callback} />}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function sendMsg(text) {
|
||||
if (!text) return;
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.msgs.push({ name: "me", text: text, time: getNowStr()});
|
||||
handler.send_msg(cid, text);
|
||||
body.update();
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.keyboard) (e) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.keyboard = !connection.keyboard;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "keyboard", connection.keyboard);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.clipboard) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.clipboard = !connection.clipboard;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "clipboard", connection.clipboard);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.audio) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.audio = !connection.audio;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "audio", connection.audio);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#accept) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.authorized = true;
|
||||
body.update();
|
||||
handler.authorize(cid);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#dismiss) {
|
||||
var cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#disconnect) {
|
||||
var cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Body />);
|
||||
|
||||
var header;
|
||||
|
||||
class Header: Reactor.Component
|
||||
{
|
||||
function this() {
|
||||
header = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var conn = connections[body.cur];
|
||||
if (conn && conn.unreaded > 0) {;
|
||||
var el = me.select("#unreaded" + conn.id);
|
||||
if (el) el.style.set {
|
||||
display: "inline-block",
|
||||
};
|
||||
self.timer(300ms, function() {
|
||||
conn.unreaded = 0;
|
||||
var el = me.select("#unreaded" + conn.id);
|
||||
if (el) el.style.set {
|
||||
display: "none",
|
||||
};
|
||||
});
|
||||
}
|
||||
var tabs = connections.map(function(c, i) { return me.renderTab(c, i) });
|
||||
return <div .tabs-wrapper><div .tabs>
|
||||
{tabs}
|
||||
</div>
|
||||
<div .tab-arrows>
|
||||
<span .left-arrow><</span>
|
||||
<span .right-arrow>></span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderTab(c, i) {
|
||||
var cur = body.cur;
|
||||
return <div class={i == cur ? "active-tab tab" : "tab"}>
|
||||
{c.name}
|
||||
{c.unreaded > 0 ? <span .unreaded id={"unreaded" + c.id}>{c.unreaded}</span> : ""}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function update_cur(idx) {
|
||||
checkClickTime(function() {
|
||||
body.cur = idx;
|
||||
update();
|
||||
self.timer(1ms, adjustHeader);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(div.tab) (_, me) {
|
||||
var idx = me.index;
|
||||
if (idx == body.cur) return;
|
||||
this.update_cur(idx);
|
||||
}
|
||||
|
||||
event click $(span.left-arrow) {
|
||||
var cur = body.cur;
|
||||
if (cur == 0) return;
|
||||
this.update_cur(cur - 1);
|
||||
}
|
||||
|
||||
event click $(span.right-arrow) {
|
||||
var cur = body.cur;
|
||||
if (cur == connections.length - 1) return;
|
||||
this.update_cur(cur + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$(header).content(<Header />);
|
||||
$(header).attributes["role"] = "window-caption";
|
||||
} else {
|
||||
$(div.window-toolbar).content(<Header />);
|
||||
setWindowButontsAndIcon(true);
|
||||
}
|
||||
|
||||
event click $(div.chaticon) {
|
||||
checkClickTime(function() {
|
||||
show_chat = !show_chat;
|
||||
adaptSize();
|
||||
});
|
||||
}
|
||||
|
||||
handler.resetClickCallback = function(ms) {
|
||||
if (click_callback_time - ms < 120)
|
||||
click_callback = null;
|
||||
}
|
||||
|
||||
function checkClickTime(callback) {
|
||||
click_callback_time = getTime();
|
||||
click_callback = callback;
|
||||
handler.check_click_time(body.cid);
|
||||
self.timer(120ms, function() {
|
||||
if (click_callback) {
|
||||
click_callback();
|
||||
click_callback = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function adaptSize() {
|
||||
$(div.right-panel).style.set {
|
||||
display: show_chat ? "block" : "none",
|
||||
};
|
||||
var el = $(div.chaticon);
|
||||
if (el) el.attributes.toggleClass("active", show_chat);
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
if (show_chat && w < 600) {
|
||||
view.move(x - (600 - w), y, 600, h);
|
||||
} else if (!show_chat && w > 450) {
|
||||
view.move(x + (w - 300), y, 300, h);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
header.update();
|
||||
body.update();
|
||||
}
|
||||
|
||||
function bring_to_top(idx=-1) {
|
||||
if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
if (idx >= 0) body.cur = idx;
|
||||
} else {
|
||||
view.windowTopmost = true;
|
||||
view.windowTopmost = false;
|
||||
}
|
||||
}
|
||||
|
||||
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) {
|
||||
var conn;
|
||||
connections.map(function(c) {
|
||||
if (c.id == id) conn = c;
|
||||
});
|
||||
if (conn) {
|
||||
conn.authorized = authorized;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (!name) name = "NA";
|
||||
connections.push({
|
||||
id: id, is_file_transfer: is_file_transfer, peer_id: peer_id,
|
||||
port_forward: port_forward,
|
||||
name: name, authorized: authorized, time: new Date(),
|
||||
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
||||
audio: audio,
|
||||
});
|
||||
body.cur = connections.length - 1;
|
||||
bring_to_top();
|
||||
update();
|
||||
self.timer(1ms, adjustHeader);
|
||||
if (authorized) {
|
||||
self.timer(3s, function() {
|
||||
view.windowState = View.WINDOW_MINIMIZED;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handler.removeConnection = function(id) {
|
||||
var i = -1;
|
||||
connections.map(function(c, idx) {
|
||||
if (c.id == id) i = idx;
|
||||
});
|
||||
connections.splice(i, 1);
|
||||
if (connections.length == 0) {
|
||||
handler.exit();
|
||||
} else {
|
||||
if (body.cur >= i && body.cur > 0) body.cur -= 1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
handler.newMessage = function(id, text) {
|
||||
var idx = -1;
|
||||
connections.map(function(c, i) {
|
||||
if (c.id == id) idx = i;
|
||||
});
|
||||
var conn = connections[idx];
|
||||
if (!conn) return;
|
||||
conn.msgs.push({name: conn.name, text: text, time: getNowStr()});
|
||||
bring_to_top(idx);
|
||||
if (idx == body.cur) show_chat = true;
|
||||
conn.unreaded += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
handler.awake = function() {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
view.focus = self;
|
||||
}
|
||||
|
||||
view << event statechange {
|
||||
adjustBorder();
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
adjustBorder();
|
||||
var (sw, sh) = view.screenBox(#workarea, #dimension);
|
||||
var w = 300;
|
||||
var h = 400;
|
||||
view.move(sw - w, 0, w, h);
|
||||
}
|
||||
|
||||
function getElaspsed(time) {
|
||||
var now = new Date();
|
||||
var seconds = Date.diff(time, now, #seconds);
|
||||
var hours = seconds / 3600;
|
||||
var days = hours / 24;
|
||||
hours = hours % 24;
|
||||
var minutes = seconds % 3600 / 60;
|
||||
seconds = seconds % 60;
|
||||
var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
if (days > 0) {
|
||||
out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
self.timer(1s, function() {
|
||||
var el = $(#time);
|
||||
if (el) {
|
||||
var c = connections[body.cur];
|
||||
if (c) {
|
||||
el.text = getElaspsed(c.time);
|
||||
}
|
||||
}
|
||||
updateTime();
|
||||
});
|
||||
}
|
||||
|
||||
updateTime();
|
||||
|
||||
function self.closing() {
|
||||
view.windowState = View.WINDOW_HIDDEN;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function adjustHeader() {
|
||||
var hw = $(header).box(#width);
|
||||
var tabswrapper = $(div.tabs-wrapper);
|
||||
var tabs = $(div.tabs);
|
||||
var arrows = $(div.tab-arrows);
|
||||
if (!arrows) return;
|
||||
var n = connections.length;
|
||||
var wtab = 80;
|
||||
var max = hw - 98;
|
||||
var need_width = n * wtab + 2; // include border of active tab
|
||||
if (need_width < max) {
|
||||
arrows.style.set {
|
||||
display: "none",
|
||||
};
|
||||
tabs.style.set {
|
||||
width: need_width,
|
||||
margin-left: 0,
|
||||
};
|
||||
tabswrapper.style.set {
|
||||
width: need_width,
|
||||
};
|
||||
} else {
|
||||
var margin = (body.cur + 1) * wtab - max + 30;
|
||||
if (margin < 0) margin = 0;
|
||||
arrows.style.set {
|
||||
display: "block",
|
||||
};
|
||||
tabs.style.set {
|
||||
width: (max - 20 + margin) + 'px',
|
||||
margin-left: -margin + 'px'
|
||||
};
|
||||
tabswrapper.style.set {
|
||||
width: (max + 10) + 'px',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
view.on("size", adjustHeader);
|
||||
|
||||
// handler.addConnection(0, false, 0, "", "test1", true, false, false, false);
|
||||
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false);
|
||||
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false);
|
||||
// handler.newMessage(0, 'h');
|
||||
319
src/ui/common.css
Normal file
319
src/ui/common.css
Normal file
@@ -0,0 +1,319 @@
|
||||
html {
|
||||
var(accent): #0071ff;
|
||||
var(button): #2C8CFF;
|
||||
var(gray-bg): #eee;
|
||||
var(bg): white;
|
||||
var(border): #ccc;
|
||||
var(text): #222;
|
||||
var(placeholder): #aaa;
|
||||
var(lighter-text): #888;
|
||||
var(light-text): #666;
|
||||
var(dark-red): #A72145;
|
||||
var(dark-yellow): #FBC732;
|
||||
var(dark-blue): #2E2459;
|
||||
var(green-blue): #197260;
|
||||
var(gray-blue): #2B3439;
|
||||
var(blue-green): #4299bf;
|
||||
var(light-green): #D4EAB7;
|
||||
var(dark-green): #5CB85C;
|
||||
var(blood-red): #F82600;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: color(text);
|
||||
}
|
||||
|
||||
button.button {
|
||||
height: 2em;
|
||||
border-radius: 0.5em;
|
||||
background: color(button);
|
||||
color: color(bg);
|
||||
border-color: color(button);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
button[type=checkbox], button[type=checkbox]:active {
|
||||
background: none;
|
||||
border: none;
|
||||
color: unset;
|
||||
height: 1.4em;
|
||||
}
|
||||
|
||||
button.outline {
|
||||
border: color(border) solid 1px;
|
||||
background: transparent;
|
||||
color: color(text);
|
||||
}
|
||||
|
||||
button.button:active, button.active {
|
||||
background: color(accent);
|
||||
color: color(bg);
|
||||
border-color: color(accent);
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], input[type=number] {
|
||||
width: *;
|
||||
font-size: 1.5em;
|
||||
border-color: color(border);
|
||||
border-radius: 0;
|
||||
color: black;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
input:empty {
|
||||
color: color(placeholder);
|
||||
}
|
||||
|
||||
input.outline-focus:focus {
|
||||
outline: color(button) solid 3px;
|
||||
}
|
||||
|
||||
@set my-scrollbar
|
||||
{
|
||||
.prev { display:none; }
|
||||
.next { display:none; }
|
||||
.base, .next-page, .prev-page { background: white;}
|
||||
.slider { background: #bbb; border: white solid 4px; }
|
||||
.base:disabled { background: transparent; }
|
||||
.slider:hover { background: grey; }
|
||||
.slider:active { background: grey; }
|
||||
.base { size: 16px; }
|
||||
.corner { background: white; }
|
||||
}
|
||||
|
||||
@mixin ELLIPSIS {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
div.password svg {
|
||||
padding-left: 1em;
|
||||
size: 16px;
|
||||
color: #ddd;
|
||||
background: none;
|
||||
}
|
||||
|
||||
div.password input {
|
||||
font-family: Consolas, Menlo, Monaco, 'Courier New';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
svg {
|
||||
background: none;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: color(border) solid 1px;
|
||||
height: 22px;
|
||||
flow: horizontal;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media platform == "OSX" {
|
||||
header {
|
||||
background: linear-gradient(top,#E4E4E4,#D1D1D1);
|
||||
}
|
||||
}
|
||||
|
||||
header div.window-icon {
|
||||
size: 22px;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
header {
|
||||
background: white;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
header div.window-icon {
|
||||
size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
header div.window-icon icon {
|
||||
display: block;
|
||||
margin: *;
|
||||
size: 16px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
header caption {
|
||||
size: *;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
button.window {
|
||||
top: 0;
|
||||
padding: 0 10px;
|
||||
width: 22px;
|
||||
height: *;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
color: black;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
button.window div {
|
||||
size: 10px;
|
||||
margin: *;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
button.window:hover {
|
||||
background: color(gray-bg);
|
||||
}
|
||||
button.window#minimize {
|
||||
right: 84px;
|
||||
}
|
||||
button.window#maximize {
|
||||
right: 42px;
|
||||
}
|
||||
button.window#close {
|
||||
right: 0px;
|
||||
}
|
||||
button.window#minimize div {
|
||||
height: 3px;
|
||||
border-bottom: black solid 1px;
|
||||
width: 12px;
|
||||
}
|
||||
button.window#maximize div {
|
||||
border: black solid 1px;
|
||||
}
|
||||
button.window#close:hover {
|
||||
background: #F82600;
|
||||
}
|
||||
button.window#close:hover div {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC');
|
||||
}
|
||||
button.window#close div {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC');
|
||||
size: 12px;
|
||||
}
|
||||
button.window#maximize.restore div {
|
||||
border: none;
|
||||
size: 12px;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC');
|
||||
}
|
||||
}
|
||||
|
||||
div.msgbox {
|
||||
size: *;
|
||||
}
|
||||
|
||||
div.msgbox div.send svg {
|
||||
size: 16px;
|
||||
}
|
||||
|
||||
div.msgbox div.send span:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
div.msgbox div.send span {
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
div.msgbox .msgs {
|
||||
border: none;
|
||||
size: *;
|
||||
border-bottom: color(border) 1px solid;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll-indicator;
|
||||
border-spacing: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
div.msgbox div.send {
|
||||
flow: horizontal;
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.msgbox div.send input {
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
div.msgbox div.name {
|
||||
color: color(dark-green);
|
||||
}
|
||||
|
||||
div.msgbox div.right-side div {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.msgbox div.text {
|
||||
margin-top: 0.5em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
header .window-toolbar {
|
||||
width: max-content;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
header svg, menu svg {
|
||||
size: 14px;
|
||||
}
|
||||
|
||||
header span, menu span {
|
||||
padding: 4px 8px;
|
||||
margin: * 0.5em;
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
progress {
|
||||
display: inline-block;
|
||||
aspect: Progress;
|
||||
border: none;
|
||||
margin-right: 1em;
|
||||
height: 0.25em;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
menu div.separator {
|
||||
height: 1px;
|
||||
width: *;
|
||||
margin: 5px 0;
|
||||
background: color(gray-bg);
|
||||
border: none;
|
||||
}
|
||||
|
||||
menu li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
menu li span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
menu li.selected span {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
297
src/ui/common.tis
Normal file
297
src/ui/common.tis
Normal file
@@ -0,0 +1,297 @@
|
||||
include "sciter:reactor.tis";
|
||||
|
||||
var handler = $(#handler) || view;
|
||||
try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {}
|
||||
var OS = view.mediaVar("platform");
|
||||
var is_osx = OS == "OSX";
|
||||
var is_win = OS == "Windows";
|
||||
var is_linux = OS == "Linux";
|
||||
var is_file_transfer;
|
||||
|
||||
function hashCode(str) {
|
||||
var hash = 160 << 16 + 114 << 8 + 91;
|
||||
for (var i = 0; i < str.length; i += 1) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return hash % 16777216;
|
||||
}
|
||||
|
||||
function intToRGB(i, a = 1) {
|
||||
return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0xFF)
|
||||
+ ',' + (i & 0xFF) + ',' + a + ')';
|
||||
}
|
||||
|
||||
function string2RGB(s, a = 1) {
|
||||
return intToRGB(hashCode(s), a);
|
||||
}
|
||||
|
||||
function getTime() {
|
||||
var now = new Date();
|
||||
return now.valueOf();
|
||||
}
|
||||
|
||||
function platformSvg(platform, color) {
|
||||
platform = (platform || "").toLowerCase();
|
||||
if (platform == "linux") {
|
||||
return <svg viewBox="0 0 256 256">
|
||||
<g transform="translate(0 256) scale(.1 -.1)" fill={color}>
|
||||
<path d="m1215 2537c-140-37-242-135-286-278-23-75-23-131 1-383l18-200-54-60c-203-224-383-615-384-831v-51l-66-43c-113-75-194-199-194-300 0-110 99-234 244-305 103-50 185-69 296-69 100 0 156 14 211 54 26 18 35 19 78 10 86-18 233-24 335-12 85 10 222 38 269 56 9 4 19-7 29-35 20-50 52-64 136-57 98 8 180 52 282 156 124 125 180 244 180 380 0 80-28 142-79 179l-36 26 4 119c5 175-22 292-105 460-74 149-142 246-286 409-43 49-78 92-78 97 0 4-7 52-15 107-8 54-19 140-24 189-13 121-41 192-103 260-95 104-248 154-373 122zm172-112c62-19 134-80 163-140 15-31 28-92 41-193 27-214 38-276 57-304 9-14 59-74 111-134 92-106 191-246 236-334 69-137 115-339 101-451l-7-55-71 10c-100 13-234-5-265-36-54-55-85-207-82-412l1-141-51-17c-104-34-245-51-380-45-69 3-142 10-162 16-32 10-37 17-53 68-23 72-87 201-136 273-80 117-158 188-237 215-37 13-37 13-34 61 13 211 182 555 373 759 57 62 58 63 58 121 0 33-9 149-19 259-21 224-18 266 26 347 67 122 193 174 330 133zm687-1720c32-9 71-25 87-36 60-42 59-151-4-274-59-119-221-250-317-257-34-3-35-2-48 47-18 65-20 329-3 413 16 83 29 110 55 115 51 10 177 6 230-8zm-1418-80c79-46 187-195 247-340 41-99 43-121 12-141-39-25-148-30-238-10-142 32-264 112-307 202-20 41-21 50-10 87 24 83 102 166 192 207 54 25 53 25 104-5z"/>
|
||||
<path d="m1395 1945c-92-16-220-52-256-70-28-15-29-18-29-89 0-247 165-397 345-312 60 28 77 46 106 111 54 123 0 378-80 374-9 0-47-7-86-14zm74-156c15-69 14-112-5-159s-55-70-111-70c-48 0-78 20-102 68-15 29-41 131-41 159 0 9 230 63 242 57 3-2 11-27 17-55z"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
if (platform == "mac os") {
|
||||
return <svg viewBox="0 0 384 512">
|
||||
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" fill={color}/>
|
||||
</svg>;
|
||||
}
|
||||
return <svg viewBox="0 0 448 512">
|
||||
<path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" fill={color}/>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
function centerize(w, h) {
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
if (w > sw) w = sw;
|
||||
if (h > sh) h = sh;
|
||||
var x = (sx + sw - w) / 2;
|
||||
var y = (sy + sh - h) / 2;
|
||||
view.move(x, y, w, h);
|
||||
}
|
||||
|
||||
function setWindowButontsAndIcon(only_min=false) {
|
||||
if (only_min) {
|
||||
$(div.window-buttons).content(<div>
|
||||
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
|
||||
</div>);
|
||||
} else {
|
||||
$(div.window-buttons).content(<div>
|
||||
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
|
||||
<button.window tabindex="-1" role="window-maximize" #maximize><div /></button>
|
||||
<button.window tabindex="-1" role="window-close" #close><div /></button>
|
||||
</div>);
|
||||
}
|
||||
$(div.window-icon>icon).style.set {
|
||||
"background-image": "url('" + handler.get_icon() + "')",
|
||||
};
|
||||
}
|
||||
|
||||
function adjustBorder() {
|
||||
if (is_osx) {
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
$(header).style.set {
|
||||
display: "none",
|
||||
};
|
||||
} else {
|
||||
$(header).style.set {
|
||||
display: "block",
|
||||
padding: "0",
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (view.windowState == view.WINDOW_MAXIMIZED) {
|
||||
self.style.set {
|
||||
border: "window-frame-width solid transparent",
|
||||
};
|
||||
} else if (view.windowState == view.WINDOW_FULL_SCREEN) {
|
||||
self.style.set {
|
||||
border: "none",
|
||||
};
|
||||
} else {
|
||||
self.style.set {
|
||||
border: "black solid 1px",
|
||||
};
|
||||
}
|
||||
var el = $(button#maximize);
|
||||
if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED);
|
||||
el = $(span#fullscreen);
|
||||
if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN);
|
||||
}
|
||||
|
||||
var svg_checkmark = <svg viewBox="0 0 492 492"><path d="M484 105l-16-17a27 27 0 00-38 0L204 315 62 173c-5-5-12-7-19-7s-14 2-19 7L8 189a27 27 0 000 38l160 160v1l16 16c5 5 12 8 19 8 8 0 14-3 20-8l16-16v-1l245-244a27 27 0 000-38z"/></svg>;
|
||||
var svg_edit = <svg #edit viewBox="0 0 384 384">
|
||||
<path d="M0 304v80h80l236-236-80-80zM378 56L328 6c-8-8-22-8-30 0l-39 39 80 80 39-39c8-8 8-22 0-30z"/>
|
||||
</svg>;
|
||||
var svg_eye = <svg viewBox="0 0 469.33 469.33">
|
||||
<path d="m234.67 170.67c-35.307 0-64 28.693-64 64s28.693 64 64 64 64-28.693 64-64-28.694-64-64-64z"/>
|
||||
<path d="m234.67 74.667c-106.67 0-197.76 66.346-234.67 160 36.907 93.653 128 160 234.67 160 106.77 0 197.76-66.347 234.67-160-36.907-93.654-127.89-160-234.67-160zm0 266.67c-58.88 0-106.67-47.787-106.67-106.67s47.787-106.67 106.67-106.67 106.67 47.787 106.67 106.67-47.787 106.67-106.67 106.67z"/>
|
||||
</svg>;
|
||||
var svg_send = <svg viewBox="0 0 448 448">
|
||||
<polygon points="0.213 32 0 181.33 320 224 0 266.67 0.213 416 448 224"/>
|
||||
</svg>;
|
||||
var svg_chat = <svg viewBox="0 0 511.07 511.07">
|
||||
<path d="m74.39 480.54h-36.213l25.607-25.607c13.807-13.807 22.429-31.765 24.747-51.246-36.029-23.644-62.375-54.751-76.478-90.425-14.093-35.647-15.864-74.888-5.121-113.48 12.89-46.309 43.123-88.518 85.128-118.85 45.646-32.963 102.47-50.387 164.33-50.387 77.927 0 143.61 22.389 189.95 64.745 41.744 38.159 64.734 89.63 64.734 144.93 0 26.868-5.471 53.011-16.26 77.703-11.165 25.551-27.514 48.302-48.593 67.619-46.399 42.523-112.04 65-189.83 65-28.877 0-59.01-3.855-85.913-10.929-25.465 26.123-59.972 40.929-96.086 40.929zm182-420c-124.04 0-200.15 73.973-220.56 147.28-19.284 69.28 9.143 134.74 76.043 175.12l7.475 4.511-0.23 8.727c-0.456 17.274-4.574 33.912-11.945 48.952 17.949-6.073 34.236-17.083 46.99-32.151l6.342-7.493 9.405 2.813c26.393 7.894 57.104 12.241 86.477 12.241 154.37 0 224.68-93.473 224.68-180.32 0-46.776-19.524-90.384-54.976-122.79-40.713-37.216-99.397-56.888-169.71-56.888z"/>
|
||||
</svg>;
|
||||
|
||||
function scrollToBottom(el) {
|
||||
var y = el.box(#height, #content) - el.box(#height, #client);
|
||||
el.scrollTo(0, y);
|
||||
}
|
||||
|
||||
function getNowStr() {
|
||||
var now = new Date();
|
||||
return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second);
|
||||
}
|
||||
|
||||
/******************** start of chatbox ****************************************/
|
||||
class ChatBox: Reactor.Component {
|
||||
this var msgs = [];
|
||||
this var callback;
|
||||
|
||||
function this(params) {
|
||||
if (params) {
|
||||
this.msgs = params.msgs || [];
|
||||
this.callback = params.callback;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMsg(msg) {
|
||||
var cls = msg.name == "me" ? "right-side msg" : "left-side msg";
|
||||
return <div class={cls}>
|
||||
{msg.name == "me" ?
|
||||
<div .name>{msg.time + " "} me</div> :
|
||||
<div .name>{msg.name} {" " + msg.time}</div>
|
||||
}
|
||||
<div .text>{msg.text}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); });
|
||||
self.timer(1ms, function() {
|
||||
scrollToBottom(me.msgs);
|
||||
});
|
||||
return <div .msgbox>
|
||||
<htmlarea spellcheck="false" readonly .msgs @{this.msgs} >
|
||||
{msgs}
|
||||
</htmlarea>
|
||||
<div .send>
|
||||
<input|text .outline-focus />
|
||||
<span>{svg_send}</span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function send() {
|
||||
var el = this.$(input);
|
||||
var value = (el.value || "").trim();
|
||||
el.value = "";
|
||||
if (!value) return;
|
||||
if (this.callback) this.callback(value);
|
||||
}
|
||||
|
||||
event keydown $(input) (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
this.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event click $(div.send span) {
|
||||
this.send();
|
||||
view.focus = $(input);
|
||||
}
|
||||
}
|
||||
/******************** end of chatbox ****************************************/
|
||||
|
||||
/******************** start of msgbox ****************************************/
|
||||
var remember_password = false;
|
||||
var msgbox_params;
|
||||
function getMsgboxParams() {
|
||||
return msgbox_params;
|
||||
}
|
||||
|
||||
function msgbox(type, title, text, callback, height, width) {
|
||||
var has_msgbox = msgbox_params != null;
|
||||
if (!has_msgbox && !type) return;
|
||||
var remember = false;
|
||||
try {
|
||||
remember = handler.get_remember();
|
||||
} catch(e) {}
|
||||
msgbox_params = {
|
||||
remember: remember, type: type, text: text, title: title,
|
||||
getParams: getMsgboxParams,
|
||||
callback: callback
|
||||
};
|
||||
if (has_msgbox) return;
|
||||
var dialog = {
|
||||
client: true,
|
||||
parameters: msgbox_params,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
var html = handler.get_msgbox();
|
||||
if (html) dialog.html = html;
|
||||
else dialog.url = self.url("msgbox.html");
|
||||
var res = view.dialog(dialog);
|
||||
msgbox_params = null;
|
||||
stdout.printf("msgbox return, type: %s, res: %s\n", type, res);
|
||||
if (type.indexOf("custom") >= 0) {
|
||||
//
|
||||
} else if (!res) {
|
||||
if (!is_port_forward) view.close();
|
||||
} else if (res == "!alive") {
|
||||
// do nothing
|
||||
} else if (res.type == "input-password") {
|
||||
if (!is_port_forward) connecting();
|
||||
handler.login(res.password, res.remember);
|
||||
} else if (res.reconnect) {
|
||||
if (!is_port_forward) connecting();
|
||||
handler.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function connecting() {
|
||||
handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait.");
|
||||
}
|
||||
|
||||
handler.msgbox = function(type, title, text, callback=null, height=180, width=500) {
|
||||
// directly call view.Dialog from native may crash, add timer here, seem safe
|
||||
// too short time, msgbox won't get focus, per my test, 150 is almost minimun
|
||||
self.timer(150ms, function() { msgbox(type, title, text, callback, height, width); });
|
||||
}
|
||||
/******************** end of msgbox ****************************************/
|
||||
|
||||
function Progress()
|
||||
{
|
||||
var _val;
|
||||
var pos = -0.25;
|
||||
|
||||
function step() {
|
||||
if( _val !== undefined ) { this.refresh(); return false; }
|
||||
pos += 0.02;
|
||||
if( pos > 1.25)
|
||||
pos = -0.25;
|
||||
this.refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
function paintNoValue(gfx)
|
||||
{
|
||||
var (w,h) = this.box(#dimension,#inner);
|
||||
var x = pos * w;
|
||||
w = w * 0.25;
|
||||
gfx.fillColor( this.style#color )
|
||||
.pushLayer(#inner-box)
|
||||
.rectangle(x,0,w,h)
|
||||
.popLayer();
|
||||
return true;
|
||||
}
|
||||
|
||||
this[#value] = property(v) {
|
||||
get return _val;
|
||||
set {
|
||||
_val = undefined;
|
||||
pos = -0.25;
|
||||
this.paintContent = paintNoValue;
|
||||
this.animate(step);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
this.value = "";
|
||||
}
|
||||
255
src/ui/file_transfer.css
Normal file
255
src/ui/file_transfer.css
Normal file
@@ -0,0 +1,255 @@
|
||||
div#file-transfer-wrapper {
|
||||
size:*;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#file-transfer {
|
||||
size: *;
|
||||
margin: 0;
|
||||
flow: horizontal;
|
||||
background: color(gray-bg);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
font: system;
|
||||
border: 1px solid color(border);
|
||||
flow: table-fixed;
|
||||
prototype: Grid;
|
||||
size: *;
|
||||
padding:0;
|
||||
border-spacing: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
table > thead {
|
||||
behavior: column-resizer;
|
||||
border-bottom: color(border) solid 1px;
|
||||
}
|
||||
|
||||
table > tbody {
|
||||
overflow-y: scroll-indicator;
|
||||
size: *;
|
||||
background: white;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: color(gray-bg);
|
||||
}
|
||||
|
||||
table th
|
||||
{
|
||||
padding: 4px;
|
||||
foreground-repeat: no-repeat;
|
||||
foreground-position: 50% 3px auto auto;
|
||||
border-left: color(border) solid 1px;
|
||||
}
|
||||
|
||||
table th.sortable[sort=asc]
|
||||
{
|
||||
foreground-image: url(stock:arrow-down);
|
||||
}
|
||||
|
||||
table th.sortable[sort=desc]
|
||||
{
|
||||
foreground-image: url(stock:arrow-up);
|
||||
}
|
||||
|
||||
table th:nth-child(1) {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
table th:nth-child(2) {
|
||||
width: *;
|
||||
}
|
||||
|
||||
table th:nth-child(3) {
|
||||
width: *;
|
||||
}
|
||||
|
||||
table th:nth-child(4) {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
table.has_current thead th:current {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) { background-color: white; } /* each odd row */
|
||||
table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */
|
||||
|
||||
table.has_current tr:current /* current row */
|
||||
{
|
||||
background-color: color(accent);
|
||||
}
|
||||
|
||||
table td
|
||||
{
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
font-size: 1em;
|
||||
height: 1.4em;
|
||||
@ELLIPSIS;
|
||||
}
|
||||
|
||||
table.folder-view td:nth-child(1) {
|
||||
behavior:shell-icon;
|
||||
}
|
||||
|
||||
table td:nth-child(3), table td:nth-child(4) {
|
||||
color: color(lighter-text);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
table.has_current tr:current td {
|
||||
color: white;
|
||||
}
|
||||
|
||||
table td:nth-child(4) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
section {
|
||||
size: *;
|
||||
margin: 1em;
|
||||
border-spacing: 0.5em;
|
||||
}
|
||||
|
||||
table td:nth-child(1) {
|
||||
foreground-repeat: no-repeat;
|
||||
foreground-position: 50% 50%
|
||||
}
|
||||
|
||||
div.toolbar {
|
||||
flow: horizontal;
|
||||
}
|
||||
|
||||
div.toolbar svg {
|
||||
size: 16px;
|
||||
}
|
||||
|
||||
div.toolbar .spacer {
|
||||
width: *;
|
||||
}
|
||||
|
||||
div.toolbar > div.button {
|
||||
padding: 4px 8px;
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
div.toolbar > div.button:active {
|
||||
opacity: 1;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
div.toolbar > div.button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.toolbar > div.send {
|
||||
flow: horizontal;
|
||||
border-spacing: 0.5em;
|
||||
}
|
||||
|
||||
div.remote > div.send svg {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
div.navbar {
|
||||
border: color(border) solid 1px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
select.select-dir {
|
||||
width: *;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
div.title {
|
||||
flow: horizontal;
|
||||
border-spacing: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.title svg.computer {
|
||||
size: 48px;
|
||||
}
|
||||
|
||||
div.title div {
|
||||
margin: * 0;
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
div.title div.platform {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
div.title div.platform svg {
|
||||
size: 24px;
|
||||
}
|
||||
|
||||
table.job-table tr td {
|
||||
width: *;
|
||||
padding: 0.5em 1em;
|
||||
border-bottom: color(border) 1px solid;
|
||||
flow: horizontal;
|
||||
border-spacing: 1em;
|
||||
height: 3em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
table.job-table tr svg {
|
||||
size: 16px;
|
||||
}
|
||||
|
||||
table.job-table tr.is_remote svg {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
table.job-table tr td div.text {
|
||||
width: *;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
table.job-table tr td div.path {
|
||||
width: *;
|
||||
color: color(light-text);
|
||||
@ELLIPSIS;
|
||||
}
|
||||
|
||||
table.job-table tr:current td div.path {
|
||||
color: white;
|
||||
}
|
||||
|
||||
table#port-forward thead tr th {
|
||||
padding-left: 1em;
|
||||
size: *;
|
||||
}
|
||||
|
||||
table#port-forward tr td {
|
||||
height: 3em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table#port-forward input[type=text], table#port-forward input[type=number] {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
table#port-forward td.right-arrow svg {
|
||||
size: 1.2em;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
table#port-forward td.remove svg {
|
||||
size: 0.8em;
|
||||
}
|
||||
|
||||
table#port-forward tr.value td {
|
||||
padding-left: 1em;
|
||||
font-size: 1.5em;
|
||||
color: black;
|
||||
}
|
||||
617
src/ui/file_transfer.tis
Normal file
617
src/ui/file_transfer.tis
Normal file
@@ -0,0 +1,617 @@
|
||||
var remote_home_dir;
|
||||
|
||||
var svg_add_folder = <svg viewBox="0 0 443.29 443.29">
|
||||
<path d="m277.06 332.47h27.706v-55.412h55.412v-27.706h-55.412v-55.412h-27.706v55.412h-55.412v27.706h55.412z"/>
|
||||
<path d="m415.59 83.118h-202.06l-51.353-51.353c-2.597-2.597-6.115-4.058-9.794-4.058h-124.68c-15.274-1e-3 -27.706 12.431-27.706 27.705v332.47c0 15.273 12.432 27.706 27.706 27.706h387.88c15.273 0 27.706-12.432 27.706-27.706v-277.06c0-15.274-12.432-27.706-27.706-27.706zm0 304.76h-387.88v-332.47h118.94l51.354 51.353c2.597 2.597 6.115 4.058 9.794 4.058h207.79z"/>
|
||||
</svg>;
|
||||
var svg_trash = <svg viewBox="0 0 473.41 473.41">
|
||||
<path d="m443.82 88.765h-88.765v-73.971c0-8.177-6.617-14.794-14.794-14.794h-207.12c-8.177 0-14.794 6.617-14.794 14.794v73.971h-88.764v29.588h14.39l57.116 342.69c1.185 7.137 7.354 12.367 14.592 12.367h241.64c7.238 0 13.407-5.23 14.592-12.367l57.116-342.69h14.794c-1e-3 0-1e-3 -29.588-1e-3 -29.588zm-295.88-59.177h177.53v59.176h-177.53zm196.85 414.24h-216.58l-54.241-325.47h325.06z"/>
|
||||
<path transform="matrix(.064 -.998 .998 .064 -.546 19.418)" d="m-360.4 301.29h207.54v29.592h-207.54z"/>
|
||||
<path transform="matrix(.998 -.064 .064 .998 -.628 .399)" d="m141.64 202.35h29.592v207.54h-29.592z"/>
|
||||
</svg>;
|
||||
var svg_arrow = <svg viewBox="0 0 482.24 482.24">
|
||||
<path d="m206.81 447.79-206.81-206.67 206.74-206.67 24.353 24.284-165.17 165.17h416.31v34.445h-416.31l165.24 165.24z"/>
|
||||
</svg>;
|
||||
var svg_home = <svg viewBox="0 0 476.91 476.91">
|
||||
<path d="m461.78 209.41-212.21-204.89c-6.182-6.026-16.042-6.026-22.224 0l-212.2 204.88c-3.124 3.015-4.888 7.17-4.888 11.512 0 8.837 7.164 16 16 16h28.2v224c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-128h80v128c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-224h28.2c4.338 0 8.489-1.761 11.504-4.88 6.141-6.354 5.969-16.483-0.384-22.624zm-39.32 11.504c-8.837 0-16 7.163-16 16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837 0-16 7.163-16 16v128h-112v-224c0-8.837-7.163-16-16-16h-28.2l212.2-204.88 212.28 204.88h-28.28z"/>
|
||||
</svg>;
|
||||
var svg_refresh = <svg viewBox="0 0 551.13 551.13">
|
||||
<path d="m482.24 310.01c0 113.97-92.707 206.67-206.67 206.67s-206.67-92.708-206.67-206.67c0-102.21 74.639-187.09 172.23-203.56v65.78l86.114-86.114-86.114-86.115v71.641c-116.65 16.802-206.67 117.14-206.67 238.37 0 132.96 108.16 241.12 241.12 241.12s241.12-108.16 241.12-241.12z"/>
|
||||
</svg>;
|
||||
var svg_cancel = <svg .cancel viewBox="0 0 612 612"><polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"/></svg>;
|
||||
var svg_computer = <svg .computer viewBox="0 0 480 480">
|
||||
<g>
|
||||
<path fill="#2C8CFF" d="m276 395v11.148c0 2.327-1.978 4.15-4.299 3.985-21.145-1.506-42.392-1.509-63.401-0.011-2.322 0.166-4.3-1.657-4.3-3.985v-11.137c0-2.209 1.791-4 4-4h64c2.209 0 4 1.791 4 4zm204-340v288c0 17.65-14.35 32-32 32h-416c-17.65 0-32-14.35-32-32v-288c0-17.65 14.35-32 32-32h416c17.65 0 32 14.35 32 32zm-125.62 386.36c-70.231-21.843-158.71-21.784-228.76 0-4.22 1.31-6.57 5.8-5.26 10.02 1.278 4.085 5.639 6.591 10.02 5.26 66.093-20.58 151.37-21.125 219.24 0 4.22 1.31 8.71-1.04 10.02-5.26s-1.04-8.71-5.26-10.02z"/>
|
||||
</g>
|
||||
</svg>;
|
||||
|
||||
function getSize(type, size) {
|
||||
if (!size) {
|
||||
if (type <= 3) return "";
|
||||
return "0B";
|
||||
}
|
||||
size = size.toFloat();
|
||||
var toFixed = function(size) {
|
||||
size = (size * 100).toInteger();
|
||||
var a = (size / 100).toInteger();
|
||||
if (size % 100 == 0) return a;
|
||||
if (size % 10 == 0) return a + '.' + (size % 10);
|
||||
var b = size % 100;
|
||||
if (b < 10) b = '0' + b;
|
||||
return a + '.' + b;
|
||||
}
|
||||
if (size < 1024) return size.toInteger() + "B";
|
||||
if (size < 1024 * 1024) return toFixed(size / 1024) + "K";
|
||||
if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M";
|
||||
return toFixed(size / (1024 * 1024 * 1024)) + "G";
|
||||
}
|
||||
|
||||
function getParentPath(is_remote, path) {
|
||||
var sep = handler.get_path_sep(is_remote);
|
||||
var res = path.lastIndexOf(sep);
|
||||
if (res <= 0) return "/";
|
||||
return path.substr(0, res);
|
||||
}
|
||||
|
||||
function getFileName(is_remote, path) {
|
||||
var sep = handler.get_path_sep(is_remote);
|
||||
var res = path.lastIndexOf(sep);
|
||||
return path.substr(res + 1);
|
||||
}
|
||||
|
||||
function getExt(name) {
|
||||
if (name.indexOf(".") == 0) {
|
||||
return "";
|
||||
}
|
||||
var i = name.lastIndexOf(".");
|
||||
if (i > 0) return name.substr(i + 1);
|
||||
return "";
|
||||
}
|
||||
|
||||
var jobIdCounter = 1;
|
||||
|
||||
class JobTable: Reactor.Component {
|
||||
this var jobs = [];
|
||||
this var job_map = {};
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); });
|
||||
return <section><table .has_current .job-table>
|
||||
<tbody key={rows.length}>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table></section>;
|
||||
}
|
||||
|
||||
event click $(svg.cancel) (_, me) {
|
||||
var job = this.jobs[me.parent.parent.index];
|
||||
var id = job.id;
|
||||
handler.cancel_job(id);
|
||||
delete this.job_map[id];
|
||||
var i = -1;
|
||||
this.jobs.map(function(job, idx) {
|
||||
if (job.id == id) i = idx;
|
||||
});
|
||||
this.jobs.splice(i, 1);
|
||||
this.update();
|
||||
var is_remote = job.is_remote;
|
||||
if (job.type != "del-dir") is_remote = !is_remote;
|
||||
refreshDir(is_remote);
|
||||
}
|
||||
|
||||
function send(path, is_remote) {
|
||||
var to;
|
||||
var show_hidden;
|
||||
if (is_remote) {
|
||||
to = file_transfer.local_folder_view.fd.path;
|
||||
show_hidden = file_transfer.remote_folder_view.show_hidden;
|
||||
} else {
|
||||
to = file_transfer.remote_folder_view.fd.path;
|
||||
show_hidden = file_transfer.local_folder_view.show_hidden;
|
||||
}
|
||||
if (!to) return;
|
||||
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
|
||||
var id = jobIdCounter;
|
||||
jobIdCounter += 1;
|
||||
this.jobs.push({ type: "transfer",
|
||||
id: id, path: path, to: to,
|
||||
include_hidden: show_hidden,
|
||||
is_remote: is_remote });
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.send_files(id, path, to, show_hidden, is_remote);
|
||||
this.update();
|
||||
}
|
||||
|
||||
function addDelDir(path, is_remote) {
|
||||
var id = jobIdCounter;
|
||||
jobIdCounter += 1;
|
||||
this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote });
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.remove_dir_all(id, path, is_remote);
|
||||
this.update();
|
||||
}
|
||||
|
||||
function getSvg(job) {
|
||||
if (job.type == "transfer") {
|
||||
return svg_send;
|
||||
} else if (job.type == "del-dir") {
|
||||
return svg_trash;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatus(job) {
|
||||
if (!job.entries) return "Waiting";
|
||||
var i = job.file_num + 1;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
if (i > n) i = n;
|
||||
var res = i + ' / ' + n + " files";
|
||||
if (job.total_size > 0) res += ", " + getSize(0, job.finished_size) + ' / ' + getSize(0, job.total_size);
|
||||
// below has problem if some file skipped
|
||||
var percent = (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger();
|
||||
if (job.finished) percent = '100';
|
||||
res += ", " + percent + "%";
|
||||
if (job.finished) res = "Finished " + res;
|
||||
if (job.speed) res += ", " + getSize(0, job.speed) + "/s";
|
||||
return res;
|
||||
}
|
||||
|
||||
function updateJob(job) {
|
||||
var el = this.select("div[id=s" + job.id + "]");
|
||||
if (el) el.text = this.getStatus(job);
|
||||
}
|
||||
|
||||
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
|
||||
var job = this.job_map[id];
|
||||
if (!job) return;
|
||||
if (file_num < job.file_num) return;
|
||||
job.file_num = file_num;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
job.finished = job.file_num >= n - 1 || err == "cancel";
|
||||
job.finished_size = finished_size;
|
||||
job.speed = speed || 0;
|
||||
this.updateJob(job);
|
||||
if (job.type == "del-dir") {
|
||||
if (job.finished) {
|
||||
if (!err) {
|
||||
handler.remove_dir(job.id, job.path, job.is_remote);
|
||||
refreshDir(job.is_remote);
|
||||
}
|
||||
} else if (!job.no_confirm) {
|
||||
handler.confirm_delete_files(id, job.file_num + 1);
|
||||
}
|
||||
} else if (job.finished || file_num == -1) {
|
||||
refreshDir(!job.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRow(job, i) {
|
||||
var svg = this.getSvg(job);
|
||||
return <tr class={job.is_remote ? "is_remote" : ""}><td>
|
||||
{svg}
|
||||
<div .text>
|
||||
<div .path>{job.path}</div>
|
||||
<div id={"s" + job.id}>{this.getStatus(job)}</div>
|
||||
</div>
|
||||
{svg_cancel}
|
||||
</td></tr>;
|
||||
}
|
||||
}
|
||||
|
||||
class FolderView : Reactor.Component {
|
||||
this var fd = {};
|
||||
this var history = [];
|
||||
this var show_hidden = false;
|
||||
|
||||
function sep() {
|
||||
return handler.get_path_sep(this.is_remote);
|
||||
}
|
||||
|
||||
function this(params) {
|
||||
this.is_remote = params.is_remote;
|
||||
if (this.is_remote) {
|
||||
this.show_hidden = !!handler.get_option("remote_show_hidden");
|
||||
} else {
|
||||
this.show_hidden = !!handler.get_option("local_show_hidden");
|
||||
}
|
||||
if (!this.is_remote) {
|
||||
var dir = handler.get_option("local_dir");
|
||||
if (dir) {
|
||||
this.fd = handler.read_dir(dir, this.show_hidden);
|
||||
if (this.fd) return;
|
||||
}
|
||||
this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden);
|
||||
}
|
||||
}
|
||||
|
||||
// sort predicate
|
||||
function foldersFirst(a, b) {
|
||||
if (a.type <= 3 && b.type > 3) return -1;
|
||||
if (a.type > 3 && b.type <= 3) return +1;
|
||||
if (a.name == b.name) return 0;
|
||||
return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase());
|
||||
}
|
||||
|
||||
function render()
|
||||
{
|
||||
return <section>
|
||||
{this.renderTitle()}
|
||||
{this.renderNavBar()}
|
||||
{this.renderOpBar()}
|
||||
{this.renderTable()}
|
||||
</section>;
|
||||
}
|
||||
|
||||
function renderTitle() {
|
||||
return <div .title>
|
||||
{svg_computer}
|
||||
<div .platform>{platformSvg(handler.get_platform(this.is_remote), "white")}</div>
|
||||
<div><span>{this.is_remote ? "Remote Computer" : "Local Computer"}</span></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function renderNavBar() {
|
||||
return <div .toolbar .navbar>
|
||||
<div .home .button>{svg_home}</div>
|
||||
<div .goback .button>{svg_arrow}</div>
|
||||
<div .goup .button>{svg_arrow}</div>
|
||||
{this.renderSelect()}
|
||||
<div .refresh .button>{svg_refresh}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderSelect() {
|
||||
return <select editable .select-dir @{this.select_dir}>
|
||||
<option>/</option>
|
||||
</select>;
|
||||
}
|
||||
|
||||
function renderOpBar() {
|
||||
if (this.is_remote) {
|
||||
return <div .toolbar .remote>
|
||||
<div .send .button>{svg_send}<span>Receive</span></div>
|
||||
<div .spacer></div>
|
||||
<div .add-folder .button>{svg_add_folder}</div>
|
||||
<div .trash .button>{svg_trash}</div>
|
||||
</div>;
|
||||
}
|
||||
return <div .toolbar>
|
||||
<div .add-folder .button>{svg_add_folder}</div>
|
||||
<div .trash .button>{svg_trash}</div>
|
||||
<div .spacer></div>
|
||||
<div .send .button><span>Send</span>{svg_send}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function get_updated() {
|
||||
this.table.sortRows(false);
|
||||
if (this.fd && this.fd.path) this.select_dir.value = this.fd.path;
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
var fd = this.fd;
|
||||
var entries = fd.entries || [];
|
||||
var table = this.table;
|
||||
if (!table || !table.sortBy) {
|
||||
entries.sort(this.foldersFirst);
|
||||
}
|
||||
var me = this;
|
||||
var path = fd.path;
|
||||
if (path != "/" && path) {
|
||||
entries = [{ name: "..", type: 1 }].concat(entries);
|
||||
}
|
||||
var rows = entries.map(function(e) { return me.renderRow(e); });
|
||||
var id = (this.is_remote ? "remote" : "local") + "-folder-view";
|
||||
return <table @{this.table} .folder-view .has_current id={id}>
|
||||
<thead>
|
||||
<tr><th></th><th .sortable>Name</th><th .sortable>Modified</th><th .sortable>Size</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<popup>
|
||||
<menu.context id={id}>
|
||||
<li #switch-hidden class={this.show_hidden ? "selected" : ""}><span>{svg_checkmark}</span>Show Hidden Files</li>
|
||||
</menu>
|
||||
</popup>
|
||||
</table>;
|
||||
}
|
||||
|
||||
function joinPath(name) {
|
||||
var path = this.fd.path;
|
||||
if (path == "/") {
|
||||
if (this.sep() == "/") return this.sep() + name;
|
||||
else return name;
|
||||
}
|
||||
return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name;
|
||||
}
|
||||
|
||||
function attached() {
|
||||
var me = this;
|
||||
this.table.onRowDoubleClick = function (row) {
|
||||
var type = row[0].attributes["type"];
|
||||
if (type > 3) return;
|
||||
var name = row[1].text;
|
||||
var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name);
|
||||
me.goto(path, true);
|
||||
}
|
||||
this.get_updated();
|
||||
}
|
||||
|
||||
function goto(path, push) {
|
||||
if (!path) return;
|
||||
if (this.sep() == "\\" && path.length == 2) { // windows drive
|
||||
path += "\\";
|
||||
}
|
||||
if (push) this.pushHistory();
|
||||
if (this.is_remote) {
|
||||
handler.read_remote_dir(path, this.show_hidden);
|
||||
} else {
|
||||
var fd = handler.read_dir(path, this.show_hidden);
|
||||
this.refresh({ fd: fd });
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(data) {
|
||||
if (!data.fd || !data.fd.path) return;
|
||||
if (this.is_remote && !remote_home_dir) {
|
||||
remote_home_dir = data.fd.path;
|
||||
}
|
||||
this.update(data);
|
||||
var me = this;
|
||||
self.timer(1ms, function() { me.get_updated(); });
|
||||
}
|
||||
|
||||
function renderRow(entry) {
|
||||
var path;
|
||||
if (this.is_remote) {
|
||||
path = handler.get_icon_path(entry.type, getExt(entry.name));
|
||||
} else {
|
||||
path = this.joinPath(entry.name);
|
||||
}
|
||||
var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0;
|
||||
return <tr>
|
||||
<td type={entry.type} filename={path}></td>
|
||||
<td>{entry.name}</td>
|
||||
<td value={entry.time || 0}>{tm || ""}</td>
|
||||
<td value={entry.size || 0}>{getSize(entry.type, entry.size)}</td>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
event click $(#switch-hidden) {
|
||||
this.show_hidden = !this.show_hidden;
|
||||
this.refreshDir();
|
||||
}
|
||||
|
||||
event click $(.goup) () {
|
||||
var path = this.fd.path;
|
||||
if (!path || path == "/") return;
|
||||
path = getParentPath(this.is_remote, path);
|
||||
this.goto(path, true);
|
||||
}
|
||||
|
||||
event click $(.goback) () {
|
||||
var path = this.history.pop();
|
||||
if (!path) return;
|
||||
this.goto(path, false);
|
||||
}
|
||||
|
||||
event click $(.trash) () {
|
||||
var row = this.getCurrentRow();
|
||||
if (!row) return;
|
||||
var path = row[0];
|
||||
var type = row[1];
|
||||
var new_history = [];
|
||||
for (var i = 0; i < this.history.length; ++i) {
|
||||
var h = this.history[i];
|
||||
if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h);
|
||||
}
|
||||
this.history = new_history;
|
||||
if (type == 1) {
|
||||
file_transfer.job_table.addDelDir(path, this.is_remote);
|
||||
} else {
|
||||
confirmDelete(path, this.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(.add-folder) () {
|
||||
var me = this;
|
||||
handler.msgbox("custom", "Create Folder", "<div .form> \
|
||||
<div>Please enter the folder name:</div> \
|
||||
<div><input|text(name) .outline-focus /></div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) return;
|
||||
if (!res.name) return;
|
||||
var name = res.name.trim();
|
||||
if (!name) return;
|
||||
if (name.indexOf(me.sep()) >= 0) {
|
||||
handler.msgbox("custom-error", "Create Folder", "Invalid folder name");
|
||||
return;
|
||||
}
|
||||
var path = me.joinPath(name);
|
||||
handler.create_dir(jobIdCounter, path, me.is_remote);
|
||||
create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path };
|
||||
jobIdCounter += 1;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDir() {
|
||||
this.goto(this.fd.path, false);
|
||||
}
|
||||
|
||||
event click $(.refresh) () {
|
||||
this.refreshDir();
|
||||
}
|
||||
|
||||
event click $(.home) () {
|
||||
var path = this.is_remote ? remote_home_dir : handler.get_home_dir();
|
||||
if (!path) return;
|
||||
if (path == this.fd.path) return;
|
||||
this.goto(path, true);
|
||||
}
|
||||
|
||||
function getCurrentRow() {
|
||||
var row = this.table.getCurrentRow();
|
||||
if (!row) return;
|
||||
var name = row[1].text;
|
||||
if (!name || name == "..") return;
|
||||
var type = row[0].attributes["type"];
|
||||
return [this.joinPath(name), type];
|
||||
}
|
||||
|
||||
event click $(.send) () {
|
||||
var cur = this.getCurrentRow();
|
||||
if (!cur) return;
|
||||
file_transfer.job_table.send(cur[0], this.is_remote);
|
||||
}
|
||||
|
||||
event change $(.select-dir) (_, el) {
|
||||
var x = getTime() - last_key_time;
|
||||
if (x < 1000) return;
|
||||
if (this.fd.path != el.value) {
|
||||
this.goto(el.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
event keydown $(.select-dir) (evt, me) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
this.goto(me.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
function pushHistory() {
|
||||
var path = this.fd.path;
|
||||
if (!path) return;
|
||||
if (path != this.history[this.history.length - 1]) this.history.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
var file_transfer;
|
||||
|
||||
class FileTransfer: Reactor.Component {
|
||||
function this(params) {
|
||||
file_transfer = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
return <div #file-transfer>
|
||||
<FolderView is_remote={false} @{this.local_folder_view} />
|
||||
<FolderView is_remote={true} @{this.remote_folder_view}/>
|
||||
<JobTable @{this.job_table} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFileTransfer()
|
||||
{
|
||||
$(#file-transfer-wrapper).content(<FileTransfer />);
|
||||
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
|
||||
$(#file-transfer-wrapper).style.set { display: "block" };
|
||||
}
|
||||
|
||||
handler.updateFolderFiles = function(fd) {
|
||||
fd.entries = fd.entries || [];
|
||||
if (fd.id > 0) {
|
||||
var jt = file_transfer.job_table;
|
||||
var job = jt.job_map[fd.id];
|
||||
if (job) {
|
||||
job.file_num = -1;
|
||||
job.total_size = fd.total_size;
|
||||
job.entries = fd.entries;
|
||||
job.num_entries = fd.num_entries;
|
||||
file_transfer.job_table.updateJobStatus(job.id);
|
||||
}
|
||||
} else {
|
||||
file_transfer.remote_folder_view.refresh({ fd: fd });
|
||||
}
|
||||
}
|
||||
|
||||
handler.jobProgress = function(id, file_num, speed, finished_size) {
|
||||
file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size);
|
||||
}
|
||||
|
||||
handler.jobDone = function(id, file_num = -1) {
|
||||
var job = deleting_single_file_jobs[id] || create_dir_jobs[id];
|
||||
if (job) {
|
||||
refreshDir(job.is_remote);
|
||||
return;
|
||||
}
|
||||
file_transfer.job_table.updateJobStatus(id, file_num);
|
||||
}
|
||||
|
||||
handler.jobError = function(id, err, file_num = -1) {
|
||||
var job = deleting_single_file_jobs[id];
|
||||
if (job) {
|
||||
handler.msgbox("custom-error", "Delete File", err);
|
||||
return;
|
||||
}
|
||||
job = create_dir_jobs[id];
|
||||
if (job) {
|
||||
handler.msgbox("custom-error", "Create Folder", err);
|
||||
return;
|
||||
}
|
||||
if (file_num < 0) {
|
||||
handler.msgbox("custom-error", "Failed", err);
|
||||
}
|
||||
file_transfer.job_table.updateJobStatus(id, file_num, err);
|
||||
}
|
||||
|
||||
function refreshDir(is_remote) {
|
||||
if (is_remote) file_transfer.remote_folder_view.refreshDir();
|
||||
else file_transfer.local_folder_view.refreshDir();
|
||||
}
|
||||
|
||||
var deleting_single_file_jobs = {};
|
||||
var create_dir_jobs = {}
|
||||
|
||||
function confirmDelete(path, is_remote) {
|
||||
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>Are you sure you want to deelte this file?</div> \
|
||||
<div.ellipsis style=\"font-weight: bold;\">" + path + "</div> \
|
||||
</div>", function(res=null) {
|
||||
if (res) {
|
||||
handler.remove_file(jobIdCounter, path, 0, is_remote);
|
||||
deleting_single_file_jobs[jobIdCounter] = { is_remote: is_remote, path: path };
|
||||
jobIdCounter += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handler.confirmDeleteFiles = function(id, i, name) {
|
||||
var jt = file_transfer.job_table;
|
||||
var job = jt.job_map[id];
|
||||
if (!job) return;
|
||||
var n = job.num_entries;
|
||||
if (i >= n) return;
|
||||
var file_path = job.path;
|
||||
if (name) file_path += handler.get_path_sep(job.is_remote) + name;
|
||||
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>Deleting #" + (i + 1) + " of " + n + " files.</div> \
|
||||
<div>Are you sure you want to deelte this file?</div> \
|
||||
<div.ellipsis style=\"font-weight: bold;\" .text>" + name + "</div> \
|
||||
<div><button|checkbox(remember) {ts}>Do this for all conflicts</button></div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) {
|
||||
jt.updateJobStatus(id, i - 1, "cancel");
|
||||
} else if (res.skip) {
|
||||
if (res.remember) jt.updateJobStatus(id, i, "cancel");
|
||||
else handler.jobDone(id, i);
|
||||
} else {
|
||||
job.no_confirm = res.remember;
|
||||
if (job.no_confirm) handler.set_no_confirm(id);
|
||||
handler.remove_file(id, file_path, i, job.is_remote);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_file_transfer_close_state() {
|
||||
var local_dir = file_transfer.local_folder_view.fd.path || "";
|
||||
var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : "";
|
||||
var remote_dir = file_transfer.remote_folder_view.fd.path || "";
|
||||
var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : "";
|
||||
handler.save_close_state("local_dir", local_dir);
|
||||
handler.save_close_state("local_show_hidden", local_show_hidden);
|
||||
handler.save_close_state("remote_dir", remote_dir);
|
||||
handler.save_close_state("remote_show_hidden", remote_show_hidden);
|
||||
}
|
||||
234
src/ui/grid.tis
Normal file
234
src/ui/grid.tis
Normal file
@@ -0,0 +1,234 @@
|
||||
class Grid: Behavior {
|
||||
const TABLE_HEADER_CLICK = 0x81;
|
||||
const TABLE_ROW_CLICK = 0x82;
|
||||
const TABLE_ROW_DBL_CLICK = 0x83;
|
||||
function onHeaderClick(headerCell)
|
||||
{
|
||||
this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onRowClick(row , reason)
|
||||
{
|
||||
this.postEvent(TABLE_ROW_CLICK, row.index, row);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onRowDoubleClick(row)
|
||||
{
|
||||
this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCurrentRow()
|
||||
{
|
||||
return this.$(tbody>tr:current);
|
||||
}
|
||||
|
||||
function getCurrentColumn()
|
||||
{
|
||||
return this.$(thead>:current); // return current cell in header row
|
||||
}
|
||||
|
||||
function setCurrentRow(row, reason = #by_code, doubleClick = false)
|
||||
{
|
||||
if (!row) return;
|
||||
// get previously selected row:
|
||||
var prev = this.getCurrentRow();
|
||||
if (prev)
|
||||
{
|
||||
if (prev === row && !doubleClick) return; // already here, nothing to do.
|
||||
prev.state.current = false; // drop state flag
|
||||
}
|
||||
row.state.current = true;
|
||||
row.scrollToView();
|
||||
|
||||
if (doubleClick)
|
||||
this.onRowDoubleClick(row,reason);
|
||||
else
|
||||
this.onRowClick(row,reason);
|
||||
}
|
||||
|
||||
function setCurrentColumn(col)
|
||||
{
|
||||
// get previously selected column:
|
||||
var prev = this.getCurrentColumn();
|
||||
if (prev)
|
||||
{
|
||||
if (prev === col) return; // already here, nothing to do.
|
||||
prev.state.current = false; // drop state flag
|
||||
}
|
||||
col.state.current = true; // set state flag
|
||||
col.scrollToView();
|
||||
this.onHeaderClick(col);
|
||||
}
|
||||
|
||||
function sortRows(sortClicked)
|
||||
{
|
||||
var col = this.sortBy;
|
||||
if (!col) return;
|
||||
var byColumn = col.index;
|
||||
var nowDesc = (col.attributes["sort"] || "desc") == "desc";
|
||||
if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order.
|
||||
var getValue = function(x) {
|
||||
var value = x.attributes["value"];
|
||||
if (value == undefined) return x.text.toLowerCase();
|
||||
return value.toInteger();
|
||||
}
|
||||
var sort = function(r1, r2, asc) {
|
||||
if (r1[1].text == "..") {
|
||||
return -1;
|
||||
}
|
||||
if (r2[1].text == "..") {
|
||||
return 1;
|
||||
}
|
||||
if (!asc)
|
||||
return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1;
|
||||
else
|
||||
return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1;
|
||||
}
|
||||
if (nowDesc)
|
||||
{
|
||||
if (sortClicked) col.attributes["sort"] = "asc";
|
||||
this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false));
|
||||
} else {
|
||||
if (sortClicked) col.attributes["sort"] = "desc";
|
||||
this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true));
|
||||
}
|
||||
}
|
||||
|
||||
function attached()
|
||||
{
|
||||
assert this.tag == "table" : "wrong element type for grid, table expected";
|
||||
this.body = this.$(:root>tbody);
|
||||
assert this.body : "Grid require <tbody> element";
|
||||
}
|
||||
|
||||
function onMouse(evt)
|
||||
{
|
||||
if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK))
|
||||
return false;
|
||||
|
||||
if (!evt.mainButton)
|
||||
return false;
|
||||
|
||||
// auxiliary function, returns row this target element belongs to
|
||||
function targetRow(target) { return target.$p(tbody>tr); }
|
||||
|
||||
// auxiliary function, returns row this target element belongs to
|
||||
function targetHeaderCell(target) { return target.$p(thead>tr>th); }
|
||||
|
||||
if (var row = targetRow(evt.target)) // click on the row
|
||||
this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK);
|
||||
else if (var headerCell = targetHeaderCell(evt.target))
|
||||
{
|
||||
this.setCurrentColumn(headerCell); // click on the header cell
|
||||
if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) {
|
||||
this.sortBy = headerCell;
|
||||
this.sortRows(true);
|
||||
}
|
||||
}
|
||||
|
||||
//return true; // as it is always ours then stop event bubbling
|
||||
}
|
||||
|
||||
function onFocus(evt)
|
||||
{
|
||||
return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS);
|
||||
}
|
||||
|
||||
function onKey(evt)
|
||||
{
|
||||
|
||||
if (evt.type != Event.KEY_DOWN)
|
||||
return false;
|
||||
|
||||
switch(evt.keyCode)
|
||||
{
|
||||
case Event.VK_DOWN:
|
||||
{
|
||||
var crow = this.getCurrentRow();
|
||||
var idx = crow? crow.index + 1 : 0;
|
||||
if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key);
|
||||
}
|
||||
return true;
|
||||
|
||||
case Event.VK_UP:
|
||||
{
|
||||
var crow = this.getCurrentRow();
|
||||
var idx = crow? crow.index - 1 : this.length - 1;
|
||||
if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key);
|
||||
}
|
||||
return true;
|
||||
|
||||
case Event.VK_PRIOR:
|
||||
{
|
||||
var y = this.body.scroll(#top) - this.body.scroll(#height);
|
||||
var r;
|
||||
for(var i = this.body.length - 1; i >= 0; --i)
|
||||
{
|
||||
var pr = r; r = this.body[i];
|
||||
if (r.box(#top, #inner, #content) < y)
|
||||
{
|
||||
// this row is further than scroll pos - height of scroll area
|
||||
this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.setCurrentRow(r,#by_key); // just in case
|
||||
}
|
||||
return true;
|
||||
case Event.VK_NEXT:
|
||||
{
|
||||
var y = this.body.scroll(#top) + 2 * this.body.scroll(#height);
|
||||
var lastScrollable = this.body.length - 1;
|
||||
var r;
|
||||
for(var i = 0; i <= lastScrollable; ++i)
|
||||
{
|
||||
var pr = r; r = this.body[i];
|
||||
if (r.box(#bottom, #inner, #content) > y)
|
||||
{
|
||||
// this row is further than scroll pos - height of scroll area
|
||||
this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.setCurrentRow(r,#by_key); // just in case
|
||||
}
|
||||
return true;
|
||||
|
||||
case Event.VK_HOME:
|
||||
{
|
||||
if (this.body.length)
|
||||
this.setCurrentRow(this.body.first,#by_key);
|
||||
}
|
||||
return true;
|
||||
|
||||
case Event.VK_END:
|
||||
{
|
||||
if (this.body.length)
|
||||
this.setCurrentRow(this.body.last,#by_key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode);
|
||||
if (char) {
|
||||
var crow = this.getCurrentRow();
|
||||
var idx = crow? crow.index + 1 : 0;
|
||||
while (idx < this.body.length) {
|
||||
var el = this.body[idx];
|
||||
var text = el[1].text;
|
||||
if (text && text[0].toLowerCase() == char) {
|
||||
this.setCurrentRow(el, #by_key);
|
||||
return true;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
this.onRowDoubleClick(this.getCurrentRow());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
67
src/ui/header.css
Normal file
67
src/ui/header.css
Normal file
@@ -0,0 +1,67 @@
|
||||
header #screens {
|
||||
background: white;
|
||||
border: #A9A9A9 1px solid;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
flow: horizontal;
|
||||
border-spacing: 0.5em;
|
||||
padding-right: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header #screen {
|
||||
text-align: center;
|
||||
margin: 3px 0;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
border: color(border) solid 1px;
|
||||
font-size: 11px;
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
header #secure {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
header #secure svg {
|
||||
size: 18px;
|
||||
}
|
||||
|
||||
header .remote-id {
|
||||
width: *;
|
||||
padding-left: 30px;
|
||||
padding-right: 4em;
|
||||
margin: * 0;
|
||||
}
|
||||
|
||||
header span:active, header #screen:active {
|
||||
color: black;
|
||||
background: color(gray-bg);
|
||||
}
|
||||
|
||||
div#global-screens {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
div#global-screens > div {
|
||||
position: absolute;
|
||||
border: color(border) solid 1px;
|
||||
text-align: center;
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
header #screen.current, div#global-screens > div.current {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span#fullscreen.active {
|
||||
border: color(border) solid 1px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
377
src/ui/header.tis
Normal file
377
src/ui/header.tis
Normal file
@@ -0,0 +1,377 @@
|
||||
var pi = handler.get_default_pi(); // peer information
|
||||
var chat_msgs = [];
|
||||
|
||||
var svg_fullscreen = <svg viewBox="0 0 357 357">
|
||||
<path d="M51,229.5H0V357h127.5v-51H51V229.5z M0,127.5h51V51h76.5V0H0V127.5z M306,306h-76.5v51H357V229.5h-51V306z M229.5,0v51 H306v76.5h51V0H229.5z"/>
|
||||
</svg>;
|
||||
var svg_action = <svg viewBox="-91 0 512 512"><path d="M315 211H191L298 22a15 15 0 00-13-22H105c-6 0-12 4-14 10L1 281a15 15 0 0014 20h127L61 491a15 15 0 0025 16l240-271a15 15 0 00-11-25z"/></svg>;
|
||||
var svg_display = <svg viewBox="0 0 640 512">
|
||||
<path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"/>
|
||||
</svg>;
|
||||
var svg_secure = <svg viewBox="0 0 347.97 347.97">
|
||||
<path fill="#3F7D46" d="m317.31 54.367c-59.376 0-104.86-16.964-143.33-54.367-38.461 37.403-83.947 54.367-143.32 54.367 0 97.405-20.155 236.94 143.32 293.6 163.48-56.666 143.33-196.2 143.33-293.6zm-155.2 171.41-47.749-47.756 21.379-21.378 26.37 26.376 50.121-50.122 21.378 21.378-71.499 71.502z"/>
|
||||
</svg>;
|
||||
var svg_insecure = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M238.802 115.023l-111.573 114.68-8.6-8.367L230.2 106.656z"/><path d="M125.559 108.093l114.68 111.572-8.368 8.601-114.68-111.572z"/></g></svg>;
|
||||
var svg_insecure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z"/></g></svg>;
|
||||
var svg_secure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="#3f7d46" stroke="#3f7d46" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z" fill="#fff"/></g></svg>;
|
||||
|
||||
view << event statechange {
|
||||
adjustBorder();
|
||||
adaptDisplay();
|
||||
view.focus = handler;
|
||||
var fs = view.windowState == View.WINDOW_FULL_SCREEN;
|
||||
var el = $(#fullscreen);
|
||||
if (el) el.attributes.toggleClass("active", fs);
|
||||
el = $(#maximize);
|
||||
if (el) {
|
||||
el.state.disabled = fs;
|
||||
}
|
||||
}
|
||||
|
||||
var header;
|
||||
var old_window_state = View.WINDOW_SHOWN;
|
||||
var input_blocked;
|
||||
|
||||
class Header: Reactor.Component {
|
||||
function this(params) {
|
||||
header = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var icon_conn;
|
||||
var title_conn;
|
||||
if (this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_secure;
|
||||
title_conn = "Direct and secure connection";
|
||||
} else if (this.secure_connection && !this.direct_connection) {
|
||||
icon_conn = svg_secure_relay;
|
||||
title_conn = "Relayed and secure connection";
|
||||
} else if (!this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_insecure;
|
||||
title_conn = "Direct and insecure connection";
|
||||
} else {
|
||||
icon_conn = svg_insecure_relay;
|
||||
title_conn = "Relayed and insecure connection";
|
||||
}
|
||||
var title = handler.get_id();
|
||||
if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")";
|
||||
if ((pi.displays || []).length == 0) {
|
||||
return <div .ellipsis style={is_osx || is_win ? "size:*;text-align:center;margin:*;" : ""}>{title}</div>;
|
||||
}
|
||||
var screens = pi.displays.map(function(d, i) {
|
||||
return <div #screen class={pi.current_display == i ? "current" : ""}>
|
||||
{i+1}
|
||||
</div>;
|
||||
});
|
||||
updateWindowToolbarPosition();
|
||||
var style = "flow: horizontal;";
|
||||
if (is_osx) style += "margin: *";
|
||||
self.timer(1ms, toggleMenuState);
|
||||
return <div style={style}>
|
||||
{is_osx ? "" : <span #fullscreen>{svg_fullscreen}</span>}
|
||||
<div #screens>
|
||||
<span #secure title={title_conn}>{icon_conn}</span>
|
||||
<div .remote-id>{handler.get_id()}</div>
|
||||
<div style="flow:horizontal;border-spacing: 0.5em;">{screens}</div>
|
||||
{this.renderGlobalScreens()}
|
||||
</div>
|
||||
<span #chat>{svg_chat}</span>
|
||||
<span #action>{svg_action}</span>
|
||||
<span #display>{svg_display}</span>
|
||||
{this.renderDisplayPop()}
|
||||
{this.renderActionPop()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderDisplayPop() {
|
||||
return <popup>
|
||||
<menu.context #display-options>
|
||||
<li #adjust-window style="display:none">Adjust Window</li>
|
||||
<div #adjust-window .separator style="display:none"/>
|
||||
<li #original type="view-style"><span>{svg_checkmark}</span>Original</li>
|
||||
<li #shrink type="view-style"><span>{svg_checkmark}</span>Shrink</li>
|
||||
<li #stretch type="view-style"><span>{svg_checkmark}</span>Stretch</li>
|
||||
<div .separator />
|
||||
<li #best type="image-quality"><span>{svg_checkmark}</span>Good image quality</li>
|
||||
<li #balanced type="image-quality"><span>{svg_checkmark}</span>Balanced</li>
|
||||
<li #low type="image-quality"><span>{svg_checkmark}</span>Optimize reaction time</li>
|
||||
<li #custom type="image-quality"><span>{svg_checkmark}</span>Custom</li>
|
||||
<div .separator />
|
||||
<li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>Show remote cursor</li>
|
||||
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>Mute</li> : ""}
|
||||
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>Disable clipboard</li> : ""}
|
||||
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>Lock after session end</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li #privacy-mode .toggle-option><span>{svg_checkmark}</span>Privacy mode</li> : ""}
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
function renderActionPop() {
|
||||
return <popup>
|
||||
<menu.context #action-options>
|
||||
<li #transfer-file>Transfer File</li>
|
||||
<li #tunnel>IP Tunneling</li>
|
||||
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li #ctrl-alt-del>Insert Ctrl + Alt + Del</li> : ""}
|
||||
{keyboard_enabled ? <li #lock-screen>Insert Lock</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li #block-input>Block user input </li> : ""}
|
||||
{handler.support_refresh() ? <li #refresh>Refresh</li> : ""}
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
function renderGlobalScreens() {
|
||||
if (pi.displays.length < 2) return "";
|
||||
var x0 = 9999999;
|
||||
var y0 = 9999999;
|
||||
var x = -9999999;
|
||||
var y = -9999999;
|
||||
pi.displays.map(function(d, i) {
|
||||
if (d.x < x0) x0 = d.x;
|
||||
if (d.y < y0) y0 = d.y;
|
||||
var dx = d.x + d.width;
|
||||
if (dx > x) x = dx;
|
||||
var dy = d.y + d.height;
|
||||
if (dy > y) y = dy;
|
||||
});
|
||||
var w = x - x0;
|
||||
var h = y - y0;
|
||||
var scale = 16. / h;
|
||||
var screens = pi.displays.map(function(d, i) {
|
||||
var min_wh = d.width > d.height ? d.height : d.width;
|
||||
var style = "width:" + (d.width * scale) + "px;" +
|
||||
"height:" + (d.height * scale) + "px;" +
|
||||
"left:" + ((d.x - x0) * scale) + "px;" +
|
||||
"top:" + ((d.y - y0) * scale) + "px;" +
|
||||
"font-size:" + (min_wh * 0.9 * scale) + "px;";
|
||||
return <div style={style} class={pi.current_display == i ? "current" : ""}>{i+1}</div>;
|
||||
});
|
||||
|
||||
var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;";
|
||||
return <div #global-screens style={style}>
|
||||
{screens}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#fullscreen) (_, el) {
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
if (old_window_state == View.WINDOW_MAXIMIZED) {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
}
|
||||
view.windowState = old_window_state;
|
||||
} else {
|
||||
old_window_state = view.windowState;
|
||||
if (view.windowState == View.WINDOW_MAXIMIZED) {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
}
|
||||
view.windowState = View.WINDOW_FULL_SCREEN;
|
||||
}
|
||||
}
|
||||
|
||||
event click $(#chat) {
|
||||
startChat();
|
||||
}
|
||||
|
||||
event click $(#action) (_, me) {
|
||||
var menu = $(menu#action-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(#display) (_, me) {
|
||||
var menu = $(menu#display-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(#screen) (_, me) {
|
||||
if (pi.current_display == me.index) return;
|
||||
handler.switch_display(me.index);
|
||||
}
|
||||
|
||||
event click $(#transfer-file) {
|
||||
handler.transfer_file();
|
||||
}
|
||||
|
||||
event click $(#tunnel) {
|
||||
handler.tunnel();
|
||||
}
|
||||
|
||||
event click $(#ctrl-alt-del) {
|
||||
handler.ctrl_alt_del();
|
||||
}
|
||||
|
||||
event click $(#lock-screen) {
|
||||
handler.lock_screen();
|
||||
}
|
||||
|
||||
event click $(#refresh) {
|
||||
handler.refresh_video();
|
||||
}
|
||||
|
||||
event click $(#block-input) {
|
||||
if (!input_blocked) {
|
||||
handler.toggle_option("block-input");
|
||||
input_blocked = true;
|
||||
$(#block-input).text = "Unblock user input";
|
||||
} else {
|
||||
handler.toggle_option("unblock-input");
|
||||
input_blocked = false;
|
||||
$(#block-input).text = "Block user input";
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#display-options>li) (_, me) {
|
||||
if (me.id == "custom") {
|
||||
handle_custom_image_quality();
|
||||
} else if (me.attributes.hasClass("toggle-option")) {
|
||||
handler.toggle_option(me.id);
|
||||
toggleMenuState();
|
||||
} else if (!me.attributes.hasClass("selected")) {
|
||||
var type = me.attributes["type"];
|
||||
if (type == "image-quality") {
|
||||
handler.save_image_quality(me.id);
|
||||
} else if (type == "view-style") {
|
||||
handler.save_view_style(me.id);
|
||||
adaptDisplay();
|
||||
}
|
||||
toggleMenuState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handle_custom_image_quality() {
|
||||
var tmp = handler.get_custom_image_quality();
|
||||
var bitrate0 = tmp[0] || 50;
|
||||
var quantizer0 = tmp.length > 1 ? tmp[1] : 100;
|
||||
handler.msgbox("custom", "Custom Image Quality", "<div .form> \
|
||||
<div><input type=\"hslider\" style=\"width: 66%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate0 + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% bitrate</div> \
|
||||
<div><input type=\"hslider\" style=\"width: 66%\" name=\"quantizer\" max=\"100\" min=\"0\" value=\"" + quantizer0 + "\"/ buddy=\"quantizer-buddy\"><b #quantizer-buddy>x</b>% quantizer</div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) return;
|
||||
if (!res.bitrate) return;
|
||||
handler.save_custom_image_quality(res.bitrate, res.quantizer);
|
||||
toggleMenuState();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
var values = [];
|
||||
var q = handler.get_image_quality();
|
||||
if (!q) q = "balanced";
|
||||
values.push(q);
|
||||
var s = handler.get_view_style();
|
||||
if (!s) s = "original";
|
||||
values.push(s);
|
||||
for (var el in $$(menu#display-options>li)) {
|
||||
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
|
||||
}
|
||||
for (var id in ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) {
|
||||
var el = self.select('#' + id);
|
||||
if (el) {
|
||||
el.attributes.toggleClass("selected", handler.get_toggle_option(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$(header).content(<Header />);
|
||||
$(header).attributes["role"] = "window-caption";
|
||||
} else {
|
||||
if (is_file_transfer || is_port_forward) {
|
||||
$(caption).content(<Header />);
|
||||
} else {
|
||||
$(div.window-toolbar).content(<Header />);
|
||||
}
|
||||
setWindowButontsAndIcon();
|
||||
}
|
||||
|
||||
if (!(is_file_transfer || is_port_forward)) {
|
||||
$(header).style.set {
|
||||
height: "32px",
|
||||
};
|
||||
if (!is_osx) {
|
||||
$(div.window-icon).style.set {
|
||||
size: "32px",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
handler.updatePi = function(v) {
|
||||
pi = v;
|
||||
header.update();
|
||||
if (is_port_forward) {
|
||||
view.windowState = View.WINDOW_MINIMIZED;
|
||||
}
|
||||
}
|
||||
|
||||
handler.switchDisplay = function(i) {
|
||||
pi.current_display = i;
|
||||
header.update();
|
||||
}
|
||||
|
||||
function updateWindowToolbarPosition() {
|
||||
if (is_osx) return;
|
||||
self.timer(1ms, function() {
|
||||
var el = $(div.window-toolbar);
|
||||
var w1 = el.box(#width, #border);
|
||||
var w2 = $(header).box(#width, #border);
|
||||
var x = (w2 - w1) / 2;
|
||||
el.style.set {
|
||||
left: x + "px",
|
||||
display: "block",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
view.on("size", function() {
|
||||
// ensure size is done, so add timer
|
||||
self.timer(1ms, function() {
|
||||
updateWindowToolbarPosition();
|
||||
adaptDisplay();
|
||||
});
|
||||
});
|
||||
|
||||
handler.newMessage = function(text) {
|
||||
chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()});
|
||||
startChat();
|
||||
}
|
||||
|
||||
function sendMsg(text) {
|
||||
chat_msgs.push({text: text, name: "me", time: getNowStr()});
|
||||
handler.send_chat(text);
|
||||
if (chatbox) chatbox.refresh();
|
||||
}
|
||||
|
||||
var chatbox;
|
||||
function startChat() {
|
||||
if (chatbox) {
|
||||
chatbox.windowState = View.WINDOW_SHOWN;
|
||||
chatbox.refresh();
|
||||
return;
|
||||
}
|
||||
var icon = handler.get_icon();
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
var w = 300;
|
||||
var h = 400;
|
||||
var x = (sx + sw - w) / 2;
|
||||
var y = sy + 80;
|
||||
var params = {
|
||||
type: View.FRAME_WINDOW,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
client: true,
|
||||
parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon },
|
||||
caption: handler.get_id(),
|
||||
};
|
||||
var html = handler.get_chatbox();
|
||||
if (html) params.html = html;
|
||||
else params.url = self.url("chatbox.html");
|
||||
chatbox = view.window(params);
|
||||
}
|
||||
|
||||
handler.setConnectionType = function(secured, direct) {
|
||||
header.update({
|
||||
secure_connection: secured,
|
||||
direct_connection: direct,
|
||||
});
|
||||
}
|
||||
261
src/ui/index.css
Normal file
261
src/ui/index.css
Normal file
@@ -0,0 +1,261 @@
|
||||
html {
|
||||
background-color: transparent;
|
||||
var(gray-bg-osx): rgba(238, 238, 238, 0.75);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
body {
|
||||
border-top: color(border) solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.app {
|
||||
flow: horizontal;
|
||||
size: *;
|
||||
}
|
||||
|
||||
.lighter-text {
|
||||
color: color(lighter-text);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.left-pane {
|
||||
width: 200px;
|
||||
height: *;
|
||||
background: color(bg);
|
||||
border-right: color(border) 1px solid;
|
||||
}
|
||||
|
||||
.left-pane > div:nth-child(1) {
|
||||
border-spacing: 1em;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.your-desktop {
|
||||
border-spacing: 0.5em;
|
||||
border-left: color(accent) solid 2px;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.your-desktop input[type=text] {
|
||||
padding: 0;
|
||||
border: none;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.your-desktop > div {
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
.right-pane {
|
||||
size: *;
|
||||
background: color(gray-bg);
|
||||
}
|
||||
|
||||
.right-content {
|
||||
overflow: scroll-indicator;
|
||||
padding: 1.6em;
|
||||
border-spacing: 1.6em;
|
||||
size: *;
|
||||
flow: vertical;
|
||||
}
|
||||
|
||||
@media platform == "OSX" {
|
||||
.right-pane {
|
||||
background: color(gray-bg-osx);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin CARD {
|
||||
padding: 1.6em;
|
||||
border-spacing: 1em;
|
||||
background: color(bg);
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.card-connect {
|
||||
@CARD;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.right-buttons {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.right-buttons>button {
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
|
||||
div.connect-status {
|
||||
left: 240px;
|
||||
border-top: color(border) solid 1px;
|
||||
width: 100%;
|
||||
background: color(gray-bg);
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
div.connect-status > span.connect-status-icon {
|
||||
border-radius: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
div.connect-status > span.link {
|
||||
margin-left: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span.connect-status-1 {
|
||||
background: #e04f5f;
|
||||
}
|
||||
|
||||
span.connect-status1 {
|
||||
background: #32bea6;
|
||||
}
|
||||
|
||||
span.connect-status0 {
|
||||
background: #F5853B;
|
||||
}
|
||||
|
||||
div.recent-sessions-content {
|
||||
border-spacing: 1em;
|
||||
flow: horizontal-flow;
|
||||
}
|
||||
|
||||
div.recent-sessions-title {
|
||||
color: color(light-text);
|
||||
padding-top: 0.5em;
|
||||
border-top: color(border) solid 1px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
div.remote-session {
|
||||
border-radius: 1em;
|
||||
height: 140px;
|
||||
width: 220px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.remote-session:hover {
|
||||
outline: color(button) solid 2px -2px;
|
||||
}
|
||||
|
||||
div.remote-session .platform {
|
||||
width: *;
|
||||
height: 120px;
|
||||
padding: *;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.remote-session .platform .username{
|
||||
left: 0;
|
||||
color: #eee;
|
||||
position: absolute;
|
||||
bottom: 38px;
|
||||
font-size: 0.8em;
|
||||
width: 220px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.remote-session .platform svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
div.remote-session .text {
|
||||
background: white;
|
||||
position: absolute;
|
||||
height: 3em;
|
||||
width: 100%;
|
||||
border-radius: 0 0 1em 1em;
|
||||
bottom: 0;
|
||||
flow: horizontal;
|
||||
}
|
||||
|
||||
div.remote-session .text > div {
|
||||
padding-top: 1em;
|
||||
padding-left: 1em;
|
||||
width: *;
|
||||
}
|
||||
|
||||
svg#menu {
|
||||
size: 1em;
|
||||
background: none;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em;
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
svg#menu:active {
|
||||
color: black;
|
||||
border-radius: 1em;
|
||||
background: color(gray-bg);
|
||||
}
|
||||
|
||||
svg#edit:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
svg#edit {
|
||||
display: inline-block;
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.install-me, div.trust-me {
|
||||
margin-top: 0.5em;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
background: linear-gradient(left,#e242bc,#f4727c);
|
||||
}
|
||||
|
||||
div.install-me > div:nth-child(1) {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.install-me > div:nth-child(2) {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
div.trust-me > div:nth-child(1) {
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.trust-me > div:nth-child(2) {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
div.trust-me > div:nth-child(3) {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div#myid {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div#myid svg#menu {
|
||||
position: absolute;
|
||||
right: -1em;
|
||||
}
|
||||
30
src/ui/index.html
Normal file
30
src/ui/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@import url(index.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "index.tis";
|
||||
</script>
|
||||
<popup>
|
||||
<menu.context #remote-context>
|
||||
<li #connect>Connect</li>
|
||||
<li #transfer>Transfer File</li>
|
||||
<li #tunnel>TCP Tunneling</li>
|
||||
<li #rdp>RDP</li>
|
||||
<li #remove>Remove</li>
|
||||
</menu>
|
||||
</popup>
|
||||
<popup><menu.context #edit-password-context>
|
||||
<li #refresh-password>Refresh random password</li>
|
||||
<li #set-password>Set your own password</li>
|
||||
</menu></popup>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
713
src/ui/index.tis
Normal file
713
src/ui/index.tis
Normal file
@@ -0,0 +1,713 @@
|
||||
if (is_osx) view.windowBlurbehind = #light;
|
||||
stdout.println("current platform:", OS);
|
||||
|
||||
// html min-width, min-height not working on mac, below works for all
|
||||
view.windowMinSize = (500, 300);
|
||||
|
||||
var app;
|
||||
var tmp = handler.get_connect_status();
|
||||
var connect_status = tmp[0];
|
||||
var service_stopped = false;
|
||||
var software_update_url = "";
|
||||
var key_confirmed = tmp[1];
|
||||
var system_error = "";
|
||||
|
||||
var svg_menu = <svg #menu viewBox="0 0 512 512">
|
||||
<circle cx="256" cy="256" r="64"/>
|
||||
<circle cx="256" cy="448" r="64"/>
|
||||
<circle cx="256" cy="64" r="64"/>
|
||||
</svg>;
|
||||
|
||||
class ConnectStatus: Reactor.Component {
|
||||
function render() {
|
||||
return
|
||||
<div .connect-status>
|
||||
<span class={"connect-status-icon connect-status" + (service_stopped ? 0 : connect_status)} />
|
||||
{this.getConnectStatusStr()}
|
||||
{service_stopped ? <span class="link">Start Service</span> : ""}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getConnectStatusStr() {
|
||||
if (service_stopped) {
|
||||
return "Service is not running";
|
||||
} else if (connect_status == -1) {
|
||||
return "Not ready. Please check your connection";
|
||||
} else if (connect_status == 0) {
|
||||
return "Connecting to the RustDesk network...";
|
||||
}
|
||||
return "Ready";
|
||||
}
|
||||
|
||||
event click $(.connect-status .link) () {
|
||||
var options = handler.get_options();
|
||||
options["stop-service"] = "";
|
||||
handler.set_options(options);
|
||||
}
|
||||
}
|
||||
|
||||
class RecentSessions: Reactor.Component {
|
||||
function render() {
|
||||
var sessions = handler.get_recent_sessions();
|
||||
if (sessions.length == 0) return <span />;
|
||||
sessions = sessions.map(this.getSession);
|
||||
return <div style="width: *">
|
||||
<div .recent-sessions-title>RECENT SESSIONS</div>
|
||||
<div .recent-sessions-content key={sessions.length}>
|
||||
{sessions}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getSession(s) {
|
||||
var id = s[0];
|
||||
var username = s[1];
|
||||
var hostname = s[2];
|
||||
var platform = s[3];
|
||||
return <div .remote-session id={id} platform={platform} style={"background:"+string2RGB(id+platform, 0.5)}>
|
||||
<div .platform>
|
||||
{platformSvg(platform, "white")}
|
||||
<div .username>{username}@{hostname}</div>
|
||||
</div>
|
||||
<div .text>
|
||||
<div>{formatId(id)}</div>
|
||||
{svg_menu}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event dblclick $(div.remote-session) (evt, me) {
|
||||
createNewConnect(me.id, "connect");
|
||||
}
|
||||
|
||||
event click $(#menu) (_, me) {
|
||||
var id = me.parent.parent.id;
|
||||
var platform = me.parent.parent.attributes["platform"];
|
||||
$(#rdp).style.set{
|
||||
display: (platform == "Windows" && is_win) ? "block" : "none",
|
||||
};
|
||||
// https://sciter.com/forums/topic/replacecustomize-context-menu/
|
||||
var menu = $(menu#remote-context);
|
||||
menu.attributes["remote-id"] = id;
|
||||
me.popup(menu);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#remote-context li) (evt, me) {
|
||||
var action = me.id;
|
||||
var id = me.parent.attributes["remote-id"];
|
||||
if (action == "connect") {
|
||||
createNewConnect(id, "connect");
|
||||
} else if (action == "transfer") {
|
||||
createNewConnect(id, "file-transfer");
|
||||
} else if (action == "remove") {
|
||||
handler.remove_peer(id);
|
||||
app.recent_sessions.update();
|
||||
} else if (action == "rdp") {
|
||||
createNewConnect(id, "rdp");
|
||||
} else if (action == "tunnel") {
|
||||
createNewConnect(id, "port-forward");
|
||||
}
|
||||
}
|
||||
|
||||
function createNewConnect(id, type) {
|
||||
id = id.replace(/\s/g, "");
|
||||
app.remote_id.value = formatId(id);
|
||||
if (!id) return;
|
||||
if (id == handler.get_id()) {
|
||||
handler.msgbox("custom-error", "Error", "Sorry, it is yourself");
|
||||
return;
|
||||
}
|
||||
handler.set_remote_id(id);
|
||||
handler.new_remote(id, type);
|
||||
}
|
||||
|
||||
var myIdMenu;
|
||||
var audioInputMenu;
|
||||
var configOptions = {};
|
||||
class AudioInputs: Reactor.Component {
|
||||
function this() {
|
||||
audioInputMenu = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!this.show) return <li />;
|
||||
var inputs = handler.get_sound_inputs();
|
||||
if (is_win) inputs = ["System Sound"].concat(inputs);
|
||||
if (!inputs.length) return <li style="display:hidden" />;
|
||||
inputs = ["Mute"].concat(inputs);
|
||||
var me = this;
|
||||
self.timer(1ms, function() { me.toggleMenuState() });
|
||||
return <li>Audio Input
|
||||
<menu #audio-input key={inputs.length}>
|
||||
{inputs.map(function(name) {
|
||||
return <li id={name}><span>{svg_checkmark}</span>{name}</li>;
|
||||
})}
|
||||
</menu>
|
||||
</li>;
|
||||
}
|
||||
|
||||
function get_default() {
|
||||
if (is_win) return "System Sound";
|
||||
return "";
|
||||
}
|
||||
|
||||
function get_value() {
|
||||
return configOptions["audio-input"] || this.get_default();
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
var v = this.get_value();
|
||||
for (var el in $$(menu#audio-input>li)) {
|
||||
var selected = el.id == v;
|
||||
el.attributes.toggleClass("selected", selected);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#audio-input>li) (_, me) {
|
||||
var v = me.id;
|
||||
if (v == this.get_value()) return;
|
||||
if (v == this.get_default()) v = "";
|
||||
configOptions["audio-input"] = v;
|
||||
handler.set_options(configOptions);
|
||||
this.toggleMenuState();
|
||||
}
|
||||
}
|
||||
|
||||
class MyIdMenu: Reactor.Component {
|
||||
function this() {
|
||||
myIdMenu = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
return <div #myid>
|
||||
{this.renderPop()}
|
||||
ID{svg_menu}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderPop() {
|
||||
return <popup>
|
||||
<menu.context #config-options>
|
||||
<li #enable-keyboard><span>{svg_checkmark}</span>Enable Keyboard/Mouse</li>
|
||||
<li #enable-clipboard><span>{svg_checkmark}</span>Enable Clipboard</li>
|
||||
<li #enable-file-transfer><span>{svg_checkmark}</span>Enable File Transfer</li>
|
||||
<li #enable-tunnel><span>{svg_checkmark}</span>Enable TCP Tunneling</li>
|
||||
<AudioInputs />
|
||||
<div .separator />
|
||||
<li #whitelist title="Only whitelisted IP can access me">IP Whitelisting</li>
|
||||
<li #custom-server>ID/Relay Server</li>
|
||||
<div .separator />
|
||||
<li #stop-service>{service_stopped ? "Start service" : "Stop service"}</li>
|
||||
<div .separator />
|
||||
<li #forum>Forum</li>
|
||||
<li #about>About {handler.get_app_name()}</li>
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
event click $(svg#menu) (_, me) {
|
||||
audioInputMenu.update({ show: true });
|
||||
configOptions = handler.get_options();
|
||||
this.toggleMenuState();
|
||||
var menu = $(menu#config-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
for (var el in $$(menu#config-options>li)) {
|
||||
if (el.id && el.id.indexOf("enable-") == 0) {
|
||||
var enabled = configOptions[el.id] != "N";
|
||||
el.attributes.toggleClass("selected", enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#config-options>li) (_, me) {
|
||||
if (me.id && me.id.indexOf("enable-") == 0) {
|
||||
configOptions[me.id] = configOptions[me.id] == "N" ? "" : "N";
|
||||
handler.set_options(configOptions);
|
||||
this.toggleMenuState();
|
||||
}
|
||||
if (me.id == "whitelist") {
|
||||
var old_value = (configOptions["whitelist"] || "").split(",").join("\n");
|
||||
handler.msgbox("custom-whitelist", "IP Whitelisting", "<div .form> \
|
||||
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; height: 160px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var value = (res.text || "").trim();
|
||||
if (value) {
|
||||
var values = value.split(/[\s,;]+/g);
|
||||
for (var ip in values) {
|
||||
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return "Invalid ip: " + ip;
|
||||
}
|
||||
}
|
||||
value = values.join("\n");
|
||||
}
|
||||
if (value == old_value) return;
|
||||
configOptions["whitelist"] = value.replace("\n", ",");
|
||||
stdin.println("whitelist updated");
|
||||
handler.set_options(configOptions);
|
||||
}, 300);
|
||||
} else if (me.id == "custom-server") {
|
||||
var old_relay = configOptions["relay-server"] || "";
|
||||
var old_id = configOptions["custom-rendezvous-server"] || "";
|
||||
handler.msgbox("custom-server", "ID/Relay Server", "<div .form> \
|
||||
<div><span style='width: 100px; display:inline-block'>ID Server: </span><input style='width: 250px' name='id' value='" + old_id + "' /></div> \
|
||||
<div><span style='width: 100px; display:inline-block'>Relay Server: </span><input style='width: 250px' name='relay' value='" + old_relay + "' /></div> \
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var id = (res.id || "").trim();
|
||||
var relay = (res.relay || "").trim();
|
||||
if (id == old_id && relay == old_relay) return;
|
||||
if (id) {
|
||||
var err = handler.test_if_valid_server(id);
|
||||
if (err) return "ID Server: " + err;
|
||||
}
|
||||
if (relay) {
|
||||
var err = handler.test_if_valid_server(relay);
|
||||
if (err) return "Relay Server: " + err;
|
||||
}
|
||||
configOptions["custom-rendezvous-server"] = id;
|
||||
configOptions["relay-server"] = relay;
|
||||
handler.set_options(configOptions);
|
||||
});
|
||||
} else if (me.id == "forum") {
|
||||
handler.open_url("https:://forum.rustdesk.com");
|
||||
} else if (me.id == "stop-service") {
|
||||
configOptions["stop-service"] = service_stopped ? "" : "Y";
|
||||
handler.set_options(configOptions);
|
||||
} else if (me.id == "about") {
|
||||
var name = handler.get_app_name();
|
||||
handler.msgbox("custom-nocancel-nook-hasclose", "About " + name, "<div style='line-height: 2em'> \
|
||||
<div>Version: " + handler.get_version() + " \
|
||||
<div .link .custom-event url='http://rustdesk.com/privacy'>Privacy Statement</div> \
|
||||
<div .link .custom-event url='http://forum.rustdesk.com'>Forum</div> \
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2020 CarrieZ Studio \
|
||||
<br /> Author: Carrie \
|
||||
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
|
||||
</div>\
|
||||
</div>", function(el) {
|
||||
if (el && el.attributes) {
|
||||
handler.open_url(el.attributes['url']);
|
||||
};
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class App: Reactor.Component
|
||||
{
|
||||
function this() {
|
||||
app = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var is_can_screen_recording = handler.is_can_screen_recording(false);
|
||||
return
|
||||
<div .app>
|
||||
<div .left-pane>
|
||||
<div>
|
||||
<div .title>Your Desktop</div>
|
||||
<div .lighter-text>Your desktop can be accessed with this ID and password.</div>
|
||||
<div .your-desktop>
|
||||
<MyIdMenu />
|
||||
{key_confirmed ? <input type="text" readonly value={formatId(handler.get_id())}/> : "Generating ..."}
|
||||
</div>
|
||||
<div .your-desktop>
|
||||
<div>Password</div>
|
||||
<Password />
|
||||
</div>
|
||||
</div>
|
||||
{handler.is_installed() ? "": <InstalllMe />}
|
||||
{handler.is_installed() && software_update_url ? <UpdateMe /> : ""}
|
||||
{handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? <UpgradeMe /> : ""}
|
||||
{is_can_screen_recording ? "": <CanScreenRecording />}
|
||||
{is_can_screen_recording && !handler.is_process_trusted(false) ? <TrustMe /> : ""}
|
||||
{system_error ? <SystemError /> : ""}
|
||||
{!system_error && handler.is_login_wayland() ? <FixWayland /> : ""}
|
||||
</div>
|
||||
<div .right-pane>
|
||||
<div .right-content>
|
||||
<div .card-connect>
|
||||
<div .title>Control Remote Desktop</div>
|
||||
<ID @{this.remote_id} />
|
||||
<div .right-buttons>
|
||||
<button .button .outline #file-transfer>Transfer File</button>
|
||||
<button .button #connect>Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<RecentSessions @{this.recent_sessions} />
|
||||
</div>
|
||||
<ConnectStatus @{this.connect_status} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(button#connect) {
|
||||
this.newRemote("connect");
|
||||
}
|
||||
|
||||
event click $(button#file-transfer) {
|
||||
this.newRemote("file-transfer");
|
||||
}
|
||||
|
||||
function newRemote(type) {
|
||||
createNewConnect(this.remote_id.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
class InstalllMe: Reactor.Component {
|
||||
function render() {
|
||||
return <div .install-me>
|
||||
<div>Install RustDesk</div>
|
||||
<div #install-me .link>Install RustDesk on this computer ...</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
handler.goto_install();
|
||||
}
|
||||
}
|
||||
|
||||
const http = function() {
|
||||
|
||||
function makeRequest(httpverb) {
|
||||
return function( params ) {
|
||||
params.type = httpverb;
|
||||
view.request(params);
|
||||
};
|
||||
}
|
||||
|
||||
function download(from, to, args..)
|
||||
{
|
||||
var rqp = { type:#get, url: from, toFile: to };
|
||||
var fn = 0;
|
||||
var on = 0;
|
||||
for( var p in args )
|
||||
if( p instanceof Function )
|
||||
{
|
||||
switch(++fn) {
|
||||
case 1: rqp.success = p; break;
|
||||
case 2: rqp.error = p; break;
|
||||
case 3: rqp.progress = p; break;
|
||||
}
|
||||
} else if( p instanceof Object )
|
||||
{
|
||||
switch(++on) {
|
||||
case 1: rqp.params = p; break;
|
||||
case 2: rqp.headers = p; break;
|
||||
}
|
||||
}
|
||||
view.request(rqp);
|
||||
}
|
||||
|
||||
return {
|
||||
get: makeRequest(#get),
|
||||
post: makeRequest(#post),
|
||||
put: makeRequest(#put),
|
||||
del: makeRequest(#delete),
|
||||
download: download
|
||||
};
|
||||
|
||||
}();
|
||||
|
||||
class UpgradeMe: Reactor.Component {
|
||||
function render() {
|
||||
var update_or_download = is_osx ? "download" : "update";
|
||||
return <div .install-me>
|
||||
<div>{handler.get_app_name()} Status</div>
|
||||
<div>Your installation is lower version.</div>
|
||||
<div #install-me .link style="padding-top: 1em">Click to upgrade</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
handler.update_me("");
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateMe: Reactor.Component {
|
||||
function render() {
|
||||
var update_or_download = is_osx ? "download" : "update";
|
||||
return <div .install-me>
|
||||
<div>{handler.get_app_name()} Status</div>
|
||||
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</div>
|
||||
<div #install-me .link style="padding-top: 1em">Click to {update_or_download}</div>
|
||||
<div #download-percent style="display:hidden; padding-top: 1em;" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
if (is_osx) {
|
||||
handler.open_url("http://rustdesk.com");
|
||||
return;
|
||||
}
|
||||
var url = software_update_url + '.' + handler.get_software_ext();
|
||||
var path = handler.get_software_store_path();
|
||||
var onsuccess = function(md5) {
|
||||
$(#download-percent).content("Installing ...");
|
||||
handler.update_me(path);
|
||||
};
|
||||
var onerror = function(err) {
|
||||
handler.msgbox("custom-error", "Download Error", "Failed to download");
|
||||
};
|
||||
var onprogress = function(loaded, total) {
|
||||
if (!total) total = 5 * 1024 * 1024;
|
||||
var el = $(#download-percent);
|
||||
el.style.set{display: "block"};
|
||||
el.content("Downloading %" + (loaded * 100 / total));
|
||||
};
|
||||
stdout.println("Downloading " + url + " to " + path);
|
||||
http.download(
|
||||
url,
|
||||
self.url(path),
|
||||
onsuccess, onerror, onprogress);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemError: Reactor.Component {
|
||||
function render() {
|
||||
return <div .install-me>
|
||||
<div>{system_error}</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class TrustMe: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Configuration Permissions</div>
|
||||
<div>In order to control your Desktop remotely, you need to grant RustDesk "Accessibility" permissions</div>
|
||||
<div #trust-me .link>Configure</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#trust-me) {
|
||||
handler.is_process_trusted(true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class CanScreenRecording: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Configuration Permissions</div>
|
||||
<div>In order to access your Desktop remotely, you need to grant RustDesk "Screen Recording" permissions</div>
|
||||
<div #screen-recording .link>Configure</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#screen-recording) {
|
||||
handler.is_can_screen_recording(true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class FixWayland: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Warning</div>
|
||||
<div>Login screen using Wayland is not supported</div>
|
||||
<div #fix-wayland .link>Fix it</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#fix-wayland) {
|
||||
handler.fix_login_wayland();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
|
||||
function watch_trust() {
|
||||
// not use TrustMe::update, because it is buggy
|
||||
var trusted = handler.is_process_trusted(false);
|
||||
var el = $(div.trust-me);
|
||||
if (el) {
|
||||
el.style.set {
|
||||
display: trusted ? "none" : "block",
|
||||
};
|
||||
}
|
||||
// if (trusted) return;
|
||||
self.timer(1s, watch_trust);
|
||||
}
|
||||
|
||||
class PasswordEyeArea : Reactor.Component {
|
||||
render() {
|
||||
return
|
||||
<div .eye-area style="width: *">
|
||||
<input|text @{this.input} readonly value="******" />
|
||||
{svg_eye}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event mouseenter {
|
||||
var me = this;
|
||||
me.leaved = false;
|
||||
me.timer(300ms, function() {
|
||||
if (me.leaved) return;
|
||||
me.input.value = handler.get_password();
|
||||
});
|
||||
}
|
||||
|
||||
event mouseleave {
|
||||
this.leaved = true;
|
||||
this.input.value = "******";
|
||||
}
|
||||
}
|
||||
|
||||
class Password: Reactor.Component {
|
||||
function render() {
|
||||
return <div .password style="flow:horizontal">
|
||||
<PasswordEyeArea />
|
||||
{svg_edit}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(svg#edit) (_, me) {
|
||||
var menu = $(menu#edit-password-context);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(li#refresh-password) {
|
||||
handler.update_password("");
|
||||
this.update();
|
||||
}
|
||||
|
||||
event click $(li#set-password) {
|
||||
var me = this;
|
||||
handler.msgbox("custom-password", "Set Password", "<div .form .set-password> \
|
||||
<div><span>Password:</span><input|password(password) /></div> \
|
||||
<div><span>Confirmation:</span><input|password(confirmation) /></div> \
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var p0 = (res.password || "").trim();
|
||||
var p1 = (res.confirmation || "").trim();
|
||||
if (p0.length < 6) {
|
||||
return "Too short, at least 6 characters.";
|
||||
}
|
||||
if (p0 != p1) {
|
||||
return "The confirmation is not identical.";
|
||||
}
|
||||
handler.update_password(p0);
|
||||
me.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ID: Reactor.Component {
|
||||
function render() {
|
||||
return <input type="text" #remote_id .outline-focus novalue="Enter Remote ID" maxlength="13"
|
||||
value={formatId(handler.get_remote_id())} />;
|
||||
}
|
||||
|
||||
// https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm
|
||||
event change {
|
||||
var fid = formatId(this.value);
|
||||
var d = this.value.length - (this.old_value || "").length;
|
||||
this.old_value = this.value;
|
||||
var start = this.xcall(#selectionStart) || 0;
|
||||
var end = this.xcall(#selectionEnd);
|
||||
if (fid == this.value || d <= 0 || start != end) {
|
||||
return;
|
||||
}
|
||||
// fix Caret position
|
||||
this.value = fid;
|
||||
var text_after_caret = this.old_value.substr(start);
|
||||
var n = fid.length - formatId(text_after_caret).length;
|
||||
this.xcall(#setSelection, n, n);
|
||||
}
|
||||
}
|
||||
|
||||
var reg = /^\d+$/;
|
||||
function formatId(id) {
|
||||
id = id.replace(/\s/g, "");
|
||||
if (reg.test(id) && id.length > 3) {
|
||||
var n = id.length;
|
||||
var a = n % 3 || 3;
|
||||
var new_id = id.substr(0, a);
|
||||
for (var i = a; i < n; i += 3) {
|
||||
new_id += " " + id.substr(i, 3);
|
||||
}
|
||||
return new_id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
event keydown (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
var el = $(button#connect);
|
||||
view.focus = el;
|
||||
el.sendEvent("click");
|
||||
// simulate button click effect, windows does not have this issue
|
||||
el.attributes.toggleClass("active", true);
|
||||
self.timer(0.3s, function() {
|
||||
el.attributes.toggleClass("active", false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<App />);
|
||||
|
||||
function self.closing() {
|
||||
// return false; // can prevent window close
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
handler.save_size(x, y, w, h);
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
var r = handler.get_size();
|
||||
if (r[2] == 0) {
|
||||
centerize(800, 600);
|
||||
} else {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
}
|
||||
if (!handler.get_remote_id()) {
|
||||
view.focus = $(#remote_id);
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectStatus() {
|
||||
self.timer(1s, function() {
|
||||
var tmp = !!handler.get_option("stop-service");
|
||||
if (tmp != service_stopped) {
|
||||
service_stopped = tmp;
|
||||
app.connect_status.update();
|
||||
myIdMenu.update();
|
||||
}
|
||||
tmp = handler.get_connect_status();
|
||||
if (tmp[0] != connect_status) {
|
||||
connect_status = tmp[0];
|
||||
app.connect_status.update();
|
||||
}
|
||||
if (tmp[1] != key_confirmed) {
|
||||
key_confirmed = tmp[1];
|
||||
app.update();
|
||||
}
|
||||
tmp = handler.get_error();
|
||||
if (system_error != tmp) {
|
||||
system_error = tmp;
|
||||
app.update();
|
||||
}
|
||||
tmp = handler.get_software_update_url();
|
||||
if (tmp != software_update_url) {
|
||||
software_update_url = tmp;
|
||||
app.update();
|
||||
}
|
||||
if (handler.recent_sessions_updated()) {
|
||||
stdout.println("recent sessions updated");
|
||||
app.recent_sessions.update();
|
||||
}
|
||||
checkConnectStatus();
|
||||
});
|
||||
}
|
||||
|
||||
checkConnectStatus();
|
||||
22
src/ui/install.html
Normal file
22
src/ui/install.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
div.content {
|
||||
size: *;
|
||||
background: white;
|
||||
padding:2em 10em;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
input {
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "install.tis";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
45
src/ui/install.tis
Normal file
45
src/ui/install.tis
Normal file
@@ -0,0 +1,45 @@
|
||||
function self.ready() {
|
||||
centerize(800, 600);
|
||||
}
|
||||
|
||||
class Install: Reactor.Component {
|
||||
function render() {
|
||||
return <div .content>
|
||||
<div style="font-size: 2em;">Installation</div>
|
||||
<div style="margin: 2em 0;">Installation Path: <input|text disabled value={view.install_path()} /></div>
|
||||
<div><button|checkbox #startmenu checked>Create start menu shortcuts</button></div>
|
||||
<div><button|checkbox #desktopicon checked>Create desktop icon</button></div>
|
||||
<div #aggrement .link style="margin-top: 2em;">End-user license agreement</div>
|
||||
<div>By starting the installation, you accept the license agreement.</div>
|
||||
<div style="height: 1px; background: gray; margin-top: 1em" />
|
||||
<div style="text-align: right;">
|
||||
<progress style={"color:" + color} style="display: none" />
|
||||
<button .button id="cancel" .outline style="margin-right: 2em;">Cancel</button>
|
||||
<button .button id="submit">Accept and Install</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#cancel) {
|
||||
view.close();
|
||||
}
|
||||
|
||||
event click $(#aggrement) {
|
||||
view.open_url("http://rustdesk.com/privacy");
|
||||
}
|
||||
|
||||
event click $(#submit) {
|
||||
for (var el in $$(button)) el.state.disabled = true;
|
||||
$(progress).style.set{ display: "inline-block" };
|
||||
var args = "";
|
||||
if ($(#startmenu).value) {
|
||||
args += "startmenu ";
|
||||
}
|
||||
if ($(#desktopicon).value) {
|
||||
args += "desktopicon ";
|
||||
}
|
||||
view.install_me(args);
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Install />);
|
||||
145
src/ui/macos.rs
Normal file
145
src/ui/macos.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use cocoa::{
|
||||
appkit::{NSApp, NSApplication, NSMenu, NSMenuItem},
|
||||
base::{id, nil, YES},
|
||||
foundation::{NSAutoreleasePool, NSString},
|
||||
};
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{Object, Sel, BOOL},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
static APP_HANDLER_IVAR: &str = "GoDeskAppHandler";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SHOULD_OPEN_UNTITLED_FILE_CALLBACK: Arc<Mutex<Option<Box<dyn Fn() + Send>>>> = Default::default();
|
||||
}
|
||||
|
||||
trait AppHandler {
|
||||
fn command(&mut self, cmd: u32);
|
||||
}
|
||||
|
||||
struct DelegateState {
|
||||
handler: Option<Box<dyn AppHandler>>,
|
||||
}
|
||||
|
||||
impl DelegateState {
|
||||
fn command(&mut self, command: u32) {
|
||||
if command == 0 {
|
||||
unsafe {
|
||||
let () = msg_send!(NSApp(), terminate: nil);
|
||||
}
|
||||
} else if let Some(inner) = self.handler.as_mut() {
|
||||
inner.command(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs
|
||||
unsafe fn set_delegate(handler: Option<Box<dyn AppHandler>>) {
|
||||
let mut decl =
|
||||
ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed");
|
||||
decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR);
|
||||
|
||||
decl.add_method(
|
||||
sel!(applicationDidFinishLaunching:),
|
||||
application_did_finish_launching as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(applicationShouldOpenUntitledFile:),
|
||||
application_should_handle_open_untitled_file as extern "C" fn(&mut Object, Sel, id) -> BOOL,
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(handleMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
let decl = decl.register();
|
||||
let delegate: id = msg_send![decl, alloc];
|
||||
let () = msg_send![delegate, init];
|
||||
let state = DelegateState { handler };
|
||||
let handler_ptr = Box::into_raw(Box::new(state));
|
||||
(*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void);
|
||||
let () = msg_send![NSApp(), setDelegate: delegate];
|
||||
}
|
||||
|
||||
extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) {
|
||||
unsafe {
|
||||
let () = msg_send![NSApp(), activateIgnoringOtherApps: YES];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn application_should_handle_open_untitled_file(
|
||||
_this: &mut Object,
|
||||
_: Sel,
|
||||
_sender: id,
|
||||
) -> BOOL {
|
||||
if let Some(callback) = SHOULD_OPEN_UNTITLED_FILE_CALLBACK.lock().unwrap().as_ref() {
|
||||
callback();
|
||||
}
|
||||
YES
|
||||
}
|
||||
|
||||
/// This handles menu items in the case that all windows are closed.
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let tag: isize = msg_send![item, tag];
|
||||
if tag == 0 {
|
||||
let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR);
|
||||
let inner = &mut *(inner as *mut DelegateState);
|
||||
(*inner).command(tag as u32);
|
||||
} else if tag == 1 {
|
||||
crate::run_me(Vec::<String>::new()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_menubar() {
|
||||
unsafe {
|
||||
let _pool = NSAutoreleasePool::new(nil);
|
||||
set_delegate(None);
|
||||
let menubar = NSMenu::new(nil).autorelease();
|
||||
let app_menu_item = NSMenuItem::new(nil).autorelease();
|
||||
menubar.addItem_(app_menu_item);
|
||||
let app_menu = NSMenu::new(nil).autorelease();
|
||||
let quit_title =
|
||||
NSString::alloc(nil).init_str(&format!("Quit {}", hbb_common::config::APP_NAME));
|
||||
let quit_action = sel!(handleMenuItem:);
|
||||
let quit_key = NSString::alloc(nil).init_str("q");
|
||||
let quit_item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(quit_title, quit_action, quit_key)
|
||||
.autorelease();
|
||||
let () = msg_send![quit_item, setTag: 0];
|
||||
/*
|
||||
if !enabled {
|
||||
let () = msg_send![quit_item, setEnabled: NO];
|
||||
}
|
||||
|
||||
if selected {
|
||||
let () = msg_send![quit_item, setState: 1_isize];
|
||||
}
|
||||
let () = msg_send![item, setTag: id as isize];
|
||||
*/
|
||||
app_menu.addItem_(quit_item);
|
||||
if std::env::args().len() > 1 {
|
||||
let new_title = NSString::alloc(nil).init_str("New Window");
|
||||
let new_action = sel!(handleMenuItem:);
|
||||
let new_key = NSString::alloc(nil).init_str("n");
|
||||
let new_item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(new_title, new_action, new_key)
|
||||
.autorelease();
|
||||
let () = msg_send![new_item, setTag: 1];
|
||||
app_menu.addItem_(new_item);
|
||||
}
|
||||
app_menu_item.setSubmenu_(app_menu);
|
||||
NSApp().setMainMenu_(menubar);
|
||||
}
|
||||
}
|
||||
69
src/ui/msgbox.html
Normal file
69
src/ui/msgbox.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<html window-frame="extended">
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
body {
|
||||
border: none;
|
||||
color: black;
|
||||
}
|
||||
svg {
|
||||
size: 80px;
|
||||
background: white;
|
||||
}
|
||||
.form {
|
||||
border-spacing: 0.5em;
|
||||
}
|
||||
caption {
|
||||
@ELLIPSIS;
|
||||
size: *;
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding-top: 0.33em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form .text {
|
||||
@ELLIPSIS;
|
||||
}
|
||||
button.button {
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
div.password {
|
||||
position: relative;
|
||||
}
|
||||
div.password svg {
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
top: 0.25em;
|
||||
padding: 0.5em;
|
||||
color: color(text);
|
||||
}
|
||||
div.set-password > div {
|
||||
flow: horizontal;
|
||||
}
|
||||
div.set-password > div > span {
|
||||
width: 30%;
|
||||
line-height: 2em;
|
||||
}
|
||||
div.set-password div.password {
|
||||
width: *;
|
||||
}
|
||||
div.set-password input {
|
||||
font-size: 1em;
|
||||
}
|
||||
#error {
|
||||
color: red;
|
||||
}
|
||||
body div.ellipsis {
|
||||
@ELLIPSIS;
|
||||
}
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "msgbox.tis";
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
271
src/ui/msgbox.tis
Normal file
271
src/ui/msgbox.tis
Normal file
@@ -0,0 +1,271 @@
|
||||
var type, title, text, getParams, remember, hasRetry, callback;
|
||||
|
||||
function updateParams(params) {
|
||||
type = params.type;
|
||||
title = params.title;
|
||||
text = params.text;
|
||||
getParams = params.getParams;
|
||||
remember = params.remember;
|
||||
callback = params.callback;
|
||||
hasRetry = type == "error" &&
|
||||
title == "Connection Error" &&
|
||||
text.toLowerCase().indexOf("offline") < 0 &&
|
||||
text.toLowerCase().indexOf("exist") < 0 &&
|
||||
text.toLowerCase().indexOf("handshake") < 0 &&
|
||||
text.toLowerCase().indexOf("failed") < 0 &&
|
||||
text.toLowerCase().indexOf("resolve") < 0 &&
|
||||
text.toLowerCase().indexOf("manually") < 0;
|
||||
if (hasRetry) {
|
||||
self.timer(1s, function() {
|
||||
view.close({ reconnect: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var params = view.parameters;
|
||||
updateParams(params);
|
||||
|
||||
var svg_eye_cross = <svg viewBox="0 -21 511.96 511">
|
||||
<path d="m506.68 261.88c7.043-16.984 7.043-36.461 0-53.461-41.621-100.4-140.03-165.27-250.71-165.27-46.484 0-90.797 11.453-129.64 32.191l-68.605-68.609c-8.3438-8.3398-21.824-8.3398-30.168 0-8.3398 8.3398-8.3398 21.824 0 30.164l271.49 271.49 86.484 86.488 68.676 68.672c4.1797 4.1797 9.6406 6.2695 15.102 6.2695 5.4609 0 10.922-2.0898 15.082-6.25 8.3438-8.3398 8.3438-21.824 0-30.164l-62.145-62.145c36.633-27.883 66.094-65.109 84.438-109.38zm-293.91-100.1c12.648-7.5742 27.391-11.969 43.199-11.969 47.062 0 85.332 38.273 85.332 85.336 0 15.805-4.3945 30.547-11.969 43.199z"/>
|
||||
<path d="m255.97 320.48c-47.062 0-85.336-38.273-85.336-85.332 0-3.0938 0.59766-6.0195 0.91797-9.0039l-106.15-106.16c-25.344 24.707-46.059 54.465-60.117 88.43-7.043 16.98-7.043 36.457 0 53.461 41.598 100.39 140.01 165.27 250.69 165.27 34.496 0 67.797-6.3164 98.559-18.027l-89.559-89.559c-2.9844 0.32031-5.9062 0.91797-9 0.91797z"/>
|
||||
</svg>;
|
||||
|
||||
class Password: Reactor.Component {
|
||||
this var visible = false;
|
||||
|
||||
function render() {
|
||||
return <div .password>
|
||||
<input name="password" type={this.visible ? "text" : "password"} .outline-focus />
|
||||
{this.visible ? svg_eye_cross : svg_eye}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(svg) {
|
||||
var el = this.$(input);
|
||||
var value = el.value;
|
||||
var start = el.xcall(#selectionStart) || 0;
|
||||
var end = el.xcall(#selectionEnd);
|
||||
this.update({ visible: !this.visible });
|
||||
self.timer(30ms, function() {
|
||||
var el = this.$(input);
|
||||
view.focus = el;
|
||||
el.value = value;
|
||||
el.xcall(#setSelection, start, end);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var body;
|
||||
|
||||
class Body: Reactor.Component {
|
||||
function this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
function getIcon(color) {
|
||||
if (type == "input-password") {
|
||||
return <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
|
||||
}
|
||||
if (type == "connecting") {
|
||||
return <svg viewBox="0 0 300 300"><g fill={color}><path d="m221.76 89.414h-143.51c-1.432 0-2.594 1.162-2.594 2.594v95.963c0 1.432 1.162 2.594 2.594 2.594h143.51c1.432 0 2.594-1.162 2.594-2.594v-95.964c0-1.431-1.162-2.593-2.594-2.593z"/><path d="m150 0c-82.839 0-150 67.161-150 150s67.156 150 150 150 150-67.163 150-150-67.164-150-150-150zm92.508 187.97c0 11.458-9.29 20.749-20.749 20.749h-47.144v11.588h23.801c4.298 0 7.781 3.483 7.781 7.781s-3.483 7.781-7.781 7.781h-96.826c-4.298 0-7.781-3.483-7.781-7.781s3.483-7.781 7.781-7.781h23.801v-11.588h-47.145c-11.458 0-20.749-9.29-20.749-20.749v-95.963c0-11.458 9.29-20.749 20.749-20.749h143.51c11.458 0 20.749 9.29 20.749 20.749v95.963z"/></g><path d="m169.62 154.35c-5.0276-5.0336-11.97-8.1508-19.624-8.1508-7.6551 0-14.597 3.1172-19.624 8.1508l-11.077-11.091c7.8656-7.8752 18.725-12.754 30.701-12.754s22.835 4.8788 30.701 12.754l-11.077 11.091zm-32.184 7.0728 12.56 12.576 12.56-12.576c-3.2147-3.2172-7.6555-5.208-12.56-5.208-4.9054 0-9.3457 1.9908-12.56 5.208zm12.56-39.731c14.403 0 27.464 5.8656 36.923 15.338l11.078-11.091c-12.298-12.314-29.276-19.94-48-19.94-18.724 0-35.703 7.626-48 19.94l11.077 11.091c9.4592-9.4728 22.52-15.338 36.923-15.338z" fill="#fff"/></svg>;
|
||||
}
|
||||
if (type == "success") {
|
||||
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
|
||||
}
|
||||
return <span />;
|
||||
}
|
||||
|
||||
function getInputPasswordContent() {
|
||||
var ts = remember ? { checked: true } : {};
|
||||
return <div .form>
|
||||
<div>Please enter your password</div>
|
||||
<Password />
|
||||
<div><button|checkbox(remember) {ts}>Remember password</button></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
if (type == "input-password") {
|
||||
return this.getInputPasswordContent();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function getColor() {
|
||||
if (type == "input-password") {
|
||||
return "#AD448E";
|
||||
}
|
||||
if (type == "success") {
|
||||
return "#32bea6";
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return "#e04f5f";
|
||||
}
|
||||
return "#2C8CFF";
|
||||
}
|
||||
|
||||
function hasSkip() {
|
||||
return type.indexOf("skip") >= 0;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var color = this.getColor();
|
||||
var icon = this.getIcon(color);
|
||||
var content = this.getContent();
|
||||
var hasCancel = type.indexOf("error") < 0 && type != "success" && type.indexOf("nocancel") < 0;
|
||||
var hasOk = type != "connecting" && type.indexOf("nook") < 0;
|
||||
var hasClose = type.indexOf("hasclose") >= 0;
|
||||
var show_progress = type == "connecting";
|
||||
self.style.set { border: color + " solid 1px" };
|
||||
var me = this;
|
||||
self.timer(1ms, function() {
|
||||
if (typeof content == "string")
|
||||
me.$(#content).html = content;
|
||||
else
|
||||
me.$(#content).content(content);
|
||||
});
|
||||
return (
|
||||
<div style="size: *">
|
||||
<header style={"height: 2em; background: " + color}>
|
||||
<caption role="window-caption">{title}</caption>
|
||||
</header>
|
||||
<div style="padding: 1em 2em; size: *;">
|
||||
<div style="height: *; flow: horizontal">
|
||||
{icon && <div style="height: *; margin: * 0; padding-right: 2em;">{icon}</div>}
|
||||
<div style="size: *; margin: * 0;" #content />
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span #error />
|
||||
{show_progress ? <progress style={"color:" + color} /> : ""}
|
||||
{hasCancel || hasRetry ? <button .button #cancel .outline>{hasRetry ? "OK" : "Cancel"}</button> : ""}
|
||||
{this.hasSkip() ? <button .button #skip .outline>Skip</button> : ""}
|
||||
{hasOk || hasRetry ? <button .button #submit>{hasRetry ? "Retry" : "OK"}</button> : ""}
|
||||
{hasClose ? <button .button #cancel .outline>Close</button> : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
event click $(.custom-event) (_, me) {
|
||||
if (callback) callback(me);
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Body />);
|
||||
|
||||
function submit() {
|
||||
if ($(button#submit)) {
|
||||
$(button#submit).sendEvent("click");
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if ($(button#cancel)) {
|
||||
$(button#cancel).sendEvent("click");
|
||||
}
|
||||
}
|
||||
|
||||
event click $(button#cancel) {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
|
||||
event click $(button#skip) {
|
||||
var values = getValues();
|
||||
values.skip = true;
|
||||
view.close(values);
|
||||
if (callback) callback(values);
|
||||
}
|
||||
|
||||
function getValues() {
|
||||
var values = { type: type };
|
||||
for (var el in $$(.form input)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
for (var el in $$(.form textarea)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
for (var el in $$(.form button)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
if (type == "input-password") {
|
||||
values.password = (values.password || "").trim();
|
||||
if (!values.password) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
event click $(button#submit) {
|
||||
if (type == "error") {
|
||||
if (hasRetry) {
|
||||
view.close({ reconnect: true });
|
||||
} else {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == "re-input-password") {
|
||||
type = "input-password";
|
||||
body.update();
|
||||
set_outline_focus();
|
||||
return;
|
||||
}
|
||||
var values = getValues();
|
||||
if (callback) {
|
||||
var err = callback(values);
|
||||
if (err) {
|
||||
$(#error).text = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
view.close(values);
|
||||
}
|
||||
|
||||
event keydown (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
submit();
|
||||
}
|
||||
if (evt.keyCode == Event.VK_ESCAPE) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function set_outline_focus() {
|
||||
self.timer(30ms, function() {
|
||||
var el = $(input.outline-focus);
|
||||
if (el) view.focus = el;
|
||||
else {
|
||||
el = $(#submit);
|
||||
if (el) view.focus = el;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_outline_focus();
|
||||
|
||||
function checkParams() {
|
||||
self.timer(30ms, function() {
|
||||
var tmp = getParams();
|
||||
if (!tmp || !tmp.type) {
|
||||
view.close("!alive");
|
||||
return;
|
||||
} else if (tmp != params) {
|
||||
params = tmp;
|
||||
updateParams(params);
|
||||
body.update();
|
||||
set_outline_focus();
|
||||
}
|
||||
checkParams();
|
||||
});
|
||||
}
|
||||
|
||||
checkParams();
|
||||
77
src/ui/port_forward.tis
Normal file
77
src/ui/port_forward.tis
Normal file
@@ -0,0 +1,77 @@
|
||||
class PortForward: Reactor.Component {
|
||||
function render() {
|
||||
var args = handler.get_args();
|
||||
var is_rdp = handler.is_rdp();
|
||||
if (is_rdp) {
|
||||
this.pfs = [["", "", "RDP"]];
|
||||
args = ["rdp"];
|
||||
} else if (args.length) {
|
||||
this.pfs = [args];
|
||||
} else {
|
||||
this.pfs = handler.get_port_forwards();
|
||||
}
|
||||
var pfs = this.pfs.map(function(pf, i) {
|
||||
return <tr key={i} .value>
|
||||
<td>{is_rdp ? <button .button #new-rdp>New RDP</button> : pf[0]}</td>
|
||||
<td .right-arrow style="text-align: center; padding-left: 0">{args.length ? svg_arrow : ""}</td>
|
||||
<td>{pf[1] || "localhost"}</td>
|
||||
<td>{pf[2]}</td>
|
||||
{args.length ? "" : <td .remove>{svg_cancel}</td>}
|
||||
</tr>;
|
||||
});
|
||||
return <div #file-transfer><section>
|
||||
{pfs.length ? <div style="background: green; color: white; text-align: center; padding: 0.5em;">
|
||||
<span style="font-size: 1.2em">Listenning ...</span><br/>
|
||||
<span style="font-size: 0.8em; color: #ddd">Don't close this window while your are using tunnel</span>
|
||||
</div> : ""}
|
||||
<table #port-forward>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Local Port</th>
|
||||
<th style="width: 1em" />
|
||||
<th>Remote Host</th>
|
||||
<th>Remote Port</th>
|
||||
{args.length ? "" : <th style="width: 6em">Action</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody key={pfs.length}>
|
||||
{args.length ? "" :
|
||||
<tr>
|
||||
<td><input|number #port /></td>
|
||||
<td .right-arrow style="text-align: center">{svg_arrow}</td>
|
||||
<td><input|text #remote-host novalue="localhost" /></td>
|
||||
<td><input|number #remote-port /></td>
|
||||
<td style="margin:0;"><button .button #add>Add</button></td>
|
||||
</tr>
|
||||
}
|
||||
{pfs}
|
||||
</tbody>
|
||||
</table></section></div>;
|
||||
}
|
||||
|
||||
event click $(#add) () {
|
||||
var port = ($(#port).value || "").toInteger() || 0;
|
||||
var remote_host = $(#remote-host).value || "";
|
||||
var remote_port = ($(#remote-port).value || "").toInteger() || 0;
|
||||
if (port <= 0 || remote_port <= 0) return;
|
||||
handler.add_port_forward(port, remote_host, remote_port);
|
||||
this.update();
|
||||
}
|
||||
|
||||
event click $(#new-rdp) {
|
||||
handler.new_rdp();
|
||||
}
|
||||
|
||||
event click $(.remove svg) (_, me) {
|
||||
var pf = this.pfs[me.parent.parent.index - 1];
|
||||
handler.remove_port_forward(pf[0]);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
function initializePortForward()
|
||||
{
|
||||
$(#file-transfer-wrapper).content(<PortForward />);
|
||||
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
|
||||
$(#file-transfer-wrapper).style.set { display: "block" };
|
||||
}
|
||||
37
src/ui/remote.css
Normal file
37
src/ui/remote.css
Normal file
@@ -0,0 +1,37 @@
|
||||
body {
|
||||
margin: 0;
|
||||
color: black;
|
||||
overflow: scroll-indicator;
|
||||
}
|
||||
|
||||
div#video-wrapper {
|
||||
size: *;
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
video#handler {
|
||||
behavior: native-remote video;
|
||||
size: *;
|
||||
margin: *;
|
||||
foreground-size: contain;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img#cursor {
|
||||
position: absolute;
|
||||
display: none;
|
||||
//opacity: 0.66,
|
||||
//transform: scale(0.8);
|
||||
}
|
||||
|
||||
.goup {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
table#remote-folder-view {
|
||||
context-menu: selector(menu#remote-folder-view);
|
||||
}
|
||||
|
||||
table#local-folder-view {
|
||||
context-menu: selector(menu#local-folder-view);
|
||||
}
|
||||
33
src/ui/remote.html
Normal file
33
src/ui/remote.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<html window-resizable window-frame="extended">
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@import url(remote.css);
|
||||
@import url(file_transfer.css);
|
||||
@import url(header.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "remote.tis";
|
||||
include "file_transfer.tis";
|
||||
include "port_forward.tis";
|
||||
include "grid.tis";
|
||||
include "header.tis";
|
||||
</script>
|
||||
</head>
|
||||
<header>
|
||||
<div.window-icon role="window-icon"><icon /></div>
|
||||
<caption role="window-caption" />
|
||||
<div.window-toolbar />
|
||||
<div.window-buttons />
|
||||
</header>
|
||||
<body>
|
||||
<div #video-wrapper>
|
||||
<video #handler>
|
||||
<img #cursor src="in-memory:cursor" />
|
||||
</video>
|
||||
</div>
|
||||
<div #file-transfer-wrapper>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1660
src/ui/remote.rs
Normal file
1660
src/ui/remote.rs
Normal file
File diff suppressed because it is too large
Load Diff
434
src/ui/remote.tis
Normal file
434
src/ui/remote.tis
Normal file
@@ -0,0 +1,434 @@
|
||||
var cursor_img = $(img#cursor);
|
||||
var last_key_time = 0;
|
||||
is_file_transfer = handler.is_file_transfer();
|
||||
var is_port_forward = handler.is_port_forward();
|
||||
var display_width = 0;
|
||||
var display_height = 0;
|
||||
var display_origin_x = 0;
|
||||
var display_origin_y = 0;
|
||||
var display_scale = 1;
|
||||
var keyboard_enabled = true; // server side
|
||||
var clipboard_enabled = true; // server side
|
||||
var audio_enabled = true; // server side
|
||||
|
||||
handler.setDisplay = function(x, y, w, h) {
|
||||
display_width = w;
|
||||
display_height = h;
|
||||
display_origin_x = x;
|
||||
display_origin_y = y;
|
||||
adaptDisplay();
|
||||
}
|
||||
|
||||
function adaptDisplay() {
|
||||
var w = display_width;
|
||||
var h = display_height;
|
||||
if (!w || !h) return;
|
||||
var style = handler.get_view_style();
|
||||
display_scale = 1.;
|
||||
var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw);
|
||||
if (sw >= w && sh > h) {
|
||||
var hh = $(header).box(#height, #border);
|
||||
var el = $(div#adjust-window);
|
||||
if (sh > h + hh && el) {
|
||||
el.style.set{ display: "block" };
|
||||
el = $(li#adjust-window);
|
||||
el.style.set{ display: "block" };
|
||||
el.onClick = function() {
|
||||
view.windowState == View.WINDOW_SHOWN;
|
||||
var (x, y) = view.box(#position, #border, #screen);
|
||||
// extra for border
|
||||
var extra = 2;
|
||||
view.move(x, y, w + extra, h + hh + extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style != "original") {
|
||||
var bw = $(body).box(#width, #border);
|
||||
var bh = $(body).box(#height, #border);
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
bw = sw;
|
||||
bh = sh;
|
||||
}
|
||||
if (bw > 0 && bh > 0) {
|
||||
var scale_x = bw.toFloat() / w;
|
||||
var scale_y = bh.toFloat() / h;
|
||||
var scale = scale_x < scale_y ? scale_x : scale_y;
|
||||
if ((scale > 1 && style == "stretch") ||
|
||||
(scale < 1 && style == "shrink")) {
|
||||
display_scale = scale;
|
||||
w = w * scale;
|
||||
h = h * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.style.set {
|
||||
width: w + "px",
|
||||
height: h + "px",
|
||||
};
|
||||
}
|
||||
|
||||
// https://sciter.com/event-handling/
|
||||
// https://sciter.com/docs/content/sciter/Event.htm
|
||||
|
||||
var entered = false;
|
||||
|
||||
var keymap = {};
|
||||
for (var (k, v) in Event) {
|
||||
k = k + ""
|
||||
if (k[0] == "V" && k[1] == "K") {
|
||||
keymap[v] = k;
|
||||
}
|
||||
}
|
||||
|
||||
// VK_ENTER = VK_RETURN
|
||||
// somehow, handler.onKey and view.onKey not working
|
||||
function self.onKey(evt) {
|
||||
last_key_time = getTime();
|
||||
if (is_file_transfer || is_port_forward) return false;
|
||||
if (!entered) return false;
|
||||
if (!keyboard_enabled) return false;
|
||||
switch (evt.type) {
|
||||
case Event.KEY_DOWN:
|
||||
handler.key_down_or_up(1, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
if (is_osx && evt.commandKey) {
|
||||
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
}
|
||||
break;
|
||||
case Event.KEY_UP:
|
||||
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
break;
|
||||
case Event.KEY_CHAR:
|
||||
// the keypress event is fired when the element receives character value. Event.keyCode is a UNICODE code point of the character
|
||||
handler.key_down_or_up(2, "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var wait_window_toolbar = false;
|
||||
var last_mouse_mask;
|
||||
var acc_wheel_delta_x = 0;
|
||||
var acc_wheel_delta_y = 0;
|
||||
var last_wheel_time = 0;
|
||||
var inertia_velocity_x = 0;
|
||||
var inertia_velocity_y = 0;
|
||||
var acc_wheel_delta_x0 = 0;
|
||||
var acc_wheel_delta_y0 = 0;
|
||||
var total_wheel_time = 0;
|
||||
var wheeling = false;
|
||||
var dragging = false;
|
||||
|
||||
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
||||
function resetWheel() {
|
||||
acc_wheel_delta_x = 0;
|
||||
acc_wheel_delta_y = 0;
|
||||
last_wheel_time = 0;
|
||||
inertia_velocity_x = 0;
|
||||
inertia_velocity_y = 0;
|
||||
acc_wheel_delta_x0 = 0;
|
||||
acc_wheel_delta_y0 = 0;
|
||||
total_wheel_time = 0;
|
||||
wheeling = false;
|
||||
}
|
||||
|
||||
var INERTIA_ACCELERATION = 30;
|
||||
|
||||
// not good, precision not enough to simulate accelation effect,
|
||||
// seems have to use pixel based rather line based delta
|
||||
function accWheel(v, is_x) {
|
||||
if (wheeling) return;
|
||||
var abs_v = Math.abs(v);
|
||||
var max_t = abs_v / INERTIA_ACCELERATION;
|
||||
for (var t = 0.1; t < max_t; t += 0.1) {
|
||||
var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger();
|
||||
if (d >= 1) {
|
||||
abs_v -= t * INERTIA_ACCELERATION;
|
||||
if (v < 0) {
|
||||
d = -d;
|
||||
v = -abs_v;
|
||||
} else {
|
||||
v = abs_v;
|
||||
}
|
||||
handler.send_mouse(3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false);
|
||||
accWheel(v, is_x);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handler.onMouse(evt)
|
||||
{
|
||||
if (is_file_transfer || is_port_forward) return false;
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||
if (evt.y < 10) {
|
||||
if (!wait_window_toolbar) {
|
||||
wait_window_toolbar = true;
|
||||
self.timer(300ms, function() {
|
||||
if (!wait_window_toolbar) return;
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
$(header).style.set {
|
||||
display: "block",
|
||||
padding: (2 * workarea_offset) + "px 0 0 0",
|
||||
};
|
||||
}
|
||||
wait_window_toolbar = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wait_window_toolbar = false;
|
||||
}
|
||||
}
|
||||
var mask = 0;
|
||||
var wheel_delta_x;
|
||||
var wheel_delta_y;
|
||||
switch(evt.type) {
|
||||
case Event.MOUSE_DOWN:
|
||||
mask = 1;
|
||||
dragging = true;
|
||||
break;
|
||||
case Event.MOUSE_UP:
|
||||
mask = 2;
|
||||
dragging = false;
|
||||
break;
|
||||
case Event.MOUSE_MOVE:
|
||||
if (cursor_img.style#display != "none" && keyboard_enabled) cursor_img.style#display = "none";
|
||||
break;
|
||||
case Event.MOUSE_WHEEL:
|
||||
// mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
|
||||
// seems buggy, it always -1 or 1, even I change system scrolling speed.
|
||||
// to-do: should we use client side prefrence or server side?
|
||||
mask = 3;
|
||||
{
|
||||
var (dx, dy) = evt.wheelDeltas;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
dy = 0;
|
||||
} else {
|
||||
dx = 0;
|
||||
}
|
||||
acc_wheel_delta_x += dx;
|
||||
acc_wheel_delta_y += dy;
|
||||
wheel_delta_x = acc_wheel_delta_x.toInteger();
|
||||
wheel_delta_y = acc_wheel_delta_y.toInteger();
|
||||
acc_wheel_delta_x -= wheel_delta_x;
|
||||
acc_wheel_delta_y -= wheel_delta_y;
|
||||
var now = getTime();
|
||||
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
|
||||
if (dt > 0) {
|
||||
var vx = dx / dt;
|
||||
var vy = dy / dt;
|
||||
if (vx != 0 || vy != 0) {
|
||||
inertia_velocity_x = vx;
|
||||
inertia_velocity_y = vy;
|
||||
}
|
||||
}
|
||||
acc_wheel_delta_x0 += dx;
|
||||
acc_wheel_delta_y0 += dy;
|
||||
total_wheel_time += dt;
|
||||
if (dx == 0 && dy == 0) {
|
||||
wheeling = false;
|
||||
if (dt < 0.1 && total_wheel_time > 0) {
|
||||
var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y;
|
||||
if (v2 > 0) {
|
||||
v2 = Math.sqrt(v2);
|
||||
inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2;
|
||||
accWheel(inertia_velocity_y, false);
|
||||
}
|
||||
v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x;
|
||||
if (v2 > 0) {
|
||||
v2 = Math.sqrt(v2);
|
||||
inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2;
|
||||
accWheel(inertia_velocity_x, true);
|
||||
}
|
||||
}
|
||||
resetWheel();
|
||||
} else {
|
||||
wheeling = true;
|
||||
}
|
||||
last_wheel_time = now;
|
||||
if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled;
|
||||
}
|
||||
break;
|
||||
case Event.MOUSE_DCLICK: // seq: down, up, dclick, up
|
||||
mask = 1;
|
||||
break;
|
||||
case Event.MOUSE_ENTER:
|
||||
entered = true;
|
||||
stdout.println("enter");
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||
wait_window_toolbar = false;
|
||||
$(header).style.set {
|
||||
display: "none",
|
||||
};
|
||||
}
|
||||
return keyboard_enabled;
|
||||
case Event.MOUSE_LEAVE:
|
||||
entered = false;
|
||||
stdout.println("leave");
|
||||
return keyboard_enabled;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
var x = evt.x;
|
||||
var y = evt.y;
|
||||
if (mask != 0) {
|
||||
// to gain control of the mouse, user must move mouse
|
||||
if (cur_x != x || cur_y != y) {
|
||||
return keyboard_enabled;
|
||||
}
|
||||
// save bandwidth
|
||||
x = 0;
|
||||
y = 0;
|
||||
} else {
|
||||
cur_x = x;
|
||||
cur_y = y;
|
||||
}
|
||||
if (mask != 3) {
|
||||
resetWheel();
|
||||
}
|
||||
if (!keyboard_enabled) return false;
|
||||
x = (x / display_scale).toInteger();
|
||||
y = (y / display_scale).toInteger();
|
||||
// insert down between two up, osx has this behavior for triple click
|
||||
if (last_mouse_mask == 2 && mask == 2) {
|
||||
handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
}
|
||||
last_mouse_mask = mask;
|
||||
// to-do: altKey, ctrlKey etc
|
||||
handler.send_mouse((evt.buttons << 3) | mask,
|
||||
mask == 3 ? wheel_delta_x : x + display_origin_x,
|
||||
mask == 3 ? wheel_delta_y : y + display_origin_y,
|
||||
evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
return true;
|
||||
};
|
||||
|
||||
var cur_hotx = 0;
|
||||
var cur_hoty = 0;
|
||||
var cur_img = null;
|
||||
var cur_x = 0;
|
||||
var cur_y = 0;
|
||||
var cursors = {};
|
||||
var image_binded;
|
||||
|
||||
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||
cur_hotx = hotx;
|
||||
cur_hoty = hoty;
|
||||
cursor_img.style.set {
|
||||
width: width + "px",
|
||||
height: height + "px",
|
||||
};
|
||||
var img = Image.fromBytes(colors);
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cursors[id] = [img, hotx, hoty, width, height];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
handler.setCursorId = function(id) {
|
||||
var img = cursors[id];
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cur_hotx = img[1];
|
||||
cur_hoty = img[2];
|
||||
cursor_img.style.set {
|
||||
width: img[3] + "px",
|
||||
height: img[4] + "px",
|
||||
};
|
||||
img = img[0];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
handler.setCursorPosition = function(x, y) {
|
||||
if (!image_binded) return;
|
||||
cur_x = x - display_origin_x;
|
||||
cur_y = y - display_origin_y;
|
||||
var x = cur_x - cur_hotx;
|
||||
var y = cur_y - cur_hoty;
|
||||
x *= display_scale;
|
||||
y *= display_scale;
|
||||
cursor_img.style.set {
|
||||
left: x + "px",
|
||||
top: y + "px",
|
||||
display: "block",
|
||||
};
|
||||
handler.style.cursor(null);
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
var w = 960;
|
||||
var h = 640;
|
||||
if (is_file_transfer || is_port_forward) {
|
||||
var r = handler.get_size();
|
||||
if (r[0] > 0) {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
if (!is_port_forward) connecting();
|
||||
if (is_file_transfer) initializeFileTransfer();
|
||||
if (is_port_forward) initializePortForward();
|
||||
}
|
||||
|
||||
var workarea_offset = 0;
|
||||
var size_adapted;
|
||||
handler.adaptSize = function() {
|
||||
if (size_adapted) return;
|
||||
size_adapted = true;
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
var (fx, fy, fw, fh) = view.screenBox(#frame, #rectw);
|
||||
workarea_offset = sy;
|
||||
var r = handler.get_size();
|
||||
if (r[2] > 0) {
|
||||
if (r[2] >= fw && r[3] >= fh) {
|
||||
view.windowState = View.WINDOW_FULL_SCREEN;
|
||||
} else if (r[2] >= sw && r[3] >= sh) {
|
||||
view.windowState = View.WINDOW_MAXIMIZED;
|
||||
} else {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
}
|
||||
} else {
|
||||
var w = handler.box(#width, #border)
|
||||
if (sw == w) {
|
||||
view.windowState = View.WINDOW_MAXIMIZED;
|
||||
return;
|
||||
}
|
||||
var h = $(header).box(#height, #border);
|
||||
// extra for border
|
||||
var extra = 2;
|
||||
centerize(w + extra, handler.box(#height, #border) + h + extra);
|
||||
}
|
||||
}
|
||||
|
||||
function self.closing() {
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
if (is_file_transfer) save_file_transfer_close_state();
|
||||
if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h);
|
||||
}
|
||||
|
||||
handler.setPermission = function(name, enabled) {
|
||||
if (name == "keyboard") keyboard_enabled = enabled;
|
||||
if (name == "audio") audio_enabled = enabled;
|
||||
if (name == "clipboard") clipboard_enabled = enabled;
|
||||
header.update();
|
||||
}
|
||||
|
||||
handler.closeSuccess = function() {
|
||||
// handler.msgbox("success", "Successful", "Ready to go.");
|
||||
handler.msgbox("", "", "");
|
||||
}
|
||||
366
src/windows.cc
Normal file
366
src/windows.cc
Normal file
@@ -0,0 +1,366 @@
|
||||
#include <windows.h>
|
||||
#include <wtsapi32.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <intrin.h>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <shlobj.h> // NOLINT(build/include_order)
|
||||
#include <userenv.h>
|
||||
|
||||
void flog(char const *fmt, ...)
|
||||
{
|
||||
FILE *h = fopen("C:\\Windows\\temp\\test_rustdesk.log", "at");
|
||||
if (!h)
|
||||
return;
|
||||
va_list arg;
|
||||
va_start(arg, fmt);
|
||||
vfprintf(h, fmt, arg);
|
||||
va_end(arg);
|
||||
fclose(h);
|
||||
}
|
||||
|
||||
// ultravnc has rdp support
|
||||
// https://github.com/veyon/ultravnc/blob/master/winvnc/winvnc/service.cpp
|
||||
// https://github.com/TigerVNC/tigervnc/blob/master/win/winvnc/VNCServerService.cxx
|
||||
// https://blog.csdn.net/MA540213/article/details/84638264
|
||||
|
||||
DWORD GetLogonPid(DWORD dwSessionId, BOOL as_user)
|
||||
{
|
||||
DWORD dwLogonPid = 0;
|
||||
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnap != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
PROCESSENTRY32W procEntry;
|
||||
procEntry.dwSize = sizeof procEntry;
|
||||
|
||||
if (Process32FirstW(hSnap, &procEntry))
|
||||
do
|
||||
{
|
||||
DWORD dwLogonSessionId = 0;
|
||||
if (_wcsicmp(procEntry.szExeFile, as_user ? L"explorer.exe" : L"winlogon.exe") == 0 &&
|
||||
ProcessIdToSessionId(procEntry.th32ProcessID, &dwLogonSessionId) &&
|
||||
dwLogonSessionId == dwSessionId)
|
||||
{
|
||||
dwLogonPid = procEntry.th32ProcessID;
|
||||
break;
|
||||
}
|
||||
} while (Process32NextW(hSnap, &procEntry));
|
||||
CloseHandle(hSnap);
|
||||
}
|
||||
return dwLogonPid;
|
||||
}
|
||||
|
||||
// if should try WTSQueryUserToken?
|
||||
// https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in
|
||||
BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user)
|
||||
{
|
||||
BOOL bResult = FALSE;
|
||||
DWORD Id = GetLogonPid(dwSessionId, as_user);
|
||||
if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id))
|
||||
{
|
||||
bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken);
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
return bResult;
|
||||
}
|
||||
|
||||
// START the app as system
|
||||
extern "C"
|
||||
{
|
||||
HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user)
|
||||
{
|
||||
HANDLE hProcess = NULL;
|
||||
HANDLE hToken = NULL;
|
||||
if (GetSessionUserTokenWin(&hToken, dwSessionId, as_user))
|
||||
{
|
||||
STARTUPINFOW si;
|
||||
ZeroMemory(&si, sizeof si);
|
||||
si.cb = sizeof si;
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
wchar_t buf[MAX_PATH];
|
||||
wcscpy_s(buf, sizeof(buf), cmd);
|
||||
PROCESS_INFORMATION pi;
|
||||
LPVOID lpEnvironment = NULL;
|
||||
DWORD dwCreationFlags = DETACHED_PROCESS;
|
||||
if (as_user)
|
||||
{
|
||||
|
||||
CreateEnvironmentBlock(&lpEnvironment, // Environment block
|
||||
hToken, // New token
|
||||
TRUE); // Inheritence
|
||||
}
|
||||
if (lpEnvironment)
|
||||
{
|
||||
dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
|
||||
}
|
||||
if (CreateProcessAsUserW(hToken, NULL, buf, NULL, NULL, FALSE, dwCreationFlags, lpEnvironment, NULL, &si, &pi))
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
hProcess = pi.hProcess;
|
||||
}
|
||||
CloseHandle(hToken);
|
||||
if (lpEnvironment)
|
||||
DestroyEnvironmentBlock(lpEnvironment);
|
||||
}
|
||||
return hProcess;
|
||||
}
|
||||
|
||||
// Switch the current thread to the specified desktop
|
||||
static bool
|
||||
switchToDesktop(HDESK desktop)
|
||||
{
|
||||
HDESK old_desktop = GetThreadDesktop(GetCurrentThreadId());
|
||||
if (!SetThreadDesktop(desktop))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!CloseDesktop(old_desktop))
|
||||
{
|
||||
//
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://github.com/TigerVNC/tigervnc/blob/8c6c584377feba0e3b99eecb3ef33b28cee318cb/win/rfb_win32/Service.cxx
|
||||
|
||||
// Determine whether the thread's current desktop is the input one
|
||||
BOOL
|
||||
inputDesktopSelected()
|
||||
{
|
||||
HDESK current = GetThreadDesktop(GetCurrentThreadId());
|
||||
HDESK input = OpenInputDesktop(0, FALSE,
|
||||
DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW |
|
||||
DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL |
|
||||
DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP | GENERIC_WRITE);
|
||||
if (!input)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
DWORD size;
|
||||
char currentname[256];
|
||||
char inputname[256];
|
||||
|
||||
if (!GetUserObjectInformation(current, UOI_NAME, currentname, sizeof(currentname), &size))
|
||||
{
|
||||
CloseDesktop(input);
|
||||
return FALSE;
|
||||
}
|
||||
if (!GetUserObjectInformation(input, UOI_NAME, inputname, sizeof(inputname), &size))
|
||||
{
|
||||
CloseDesktop(input);
|
||||
return FALSE;
|
||||
}
|
||||
CloseDesktop(input);
|
||||
// flog("%s %s\n", currentname, inputname);
|
||||
return strcmp(currentname, inputname) == 0 ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
// Switch the current thread into the input desktop
|
||||
bool
|
||||
selectInputDesktop()
|
||||
{
|
||||
// - Open the input desktop
|
||||
HDESK desktop = OpenInputDesktop(0, FALSE,
|
||||
DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW |
|
||||
DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL |
|
||||
DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP | GENERIC_WRITE);
|
||||
if (!desktop)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// - Switch into it
|
||||
if (!switchToDesktop(desktop))
|
||||
{
|
||||
CloseDesktop(desktop);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ***
|
||||
DWORD size = 256;
|
||||
char currentname[256];
|
||||
if (GetUserObjectInformation(desktop, UOI_NAME, currentname, 256, &size))
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int handleMask(uint8_t *rwbuffer, const uint8_t *mask, int width, int height, int bmWidthBytes)
|
||||
{
|
||||
auto andMask = mask;
|
||||
auto xorMask = mask + height * bmWidthBytes;
|
||||
int doOutline = 0;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int byte = y * bmWidthBytes + x / 8;
|
||||
int bit = 7 - x % 8;
|
||||
|
||||
if (!(andMask[byte] & (1 << bit)))
|
||||
{
|
||||
// Valid pixel, so make it opaque
|
||||
rwbuffer[3] = 0xff;
|
||||
|
||||
// Black or white?
|
||||
if (xorMask[byte] & (1 << bit))
|
||||
rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0xff;
|
||||
else
|
||||
rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0;
|
||||
}
|
||||
else if (xorMask[byte] & (1 << bit))
|
||||
{
|
||||
// Replace any XORed pixels with black, because RFB doesn't support
|
||||
// XORing of cursors. XORing is used for the I-beam cursor, which is most
|
||||
// often used over a white background, but also sometimes over a black
|
||||
// background. We set the XOR'd pixels to black, then draw a white outline
|
||||
// around the whole cursor.
|
||||
|
||||
rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0;
|
||||
rwbuffer[3] = 0xff;
|
||||
|
||||
doOutline = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transparent pixel
|
||||
rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = rwbuffer[3] = 0;
|
||||
}
|
||||
|
||||
rwbuffer += 4;
|
||||
}
|
||||
}
|
||||
return doOutline;
|
||||
}
|
||||
|
||||
void drawOutline(uint8_t *out0, const uint8_t *in0, int width, int height)
|
||||
{
|
||||
auto in = in0;
|
||||
auto out = out0 + width * 4 + 4;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
// Visible pixel?
|
||||
if (in[3] > 0)
|
||||
{
|
||||
// Outline above...
|
||||
memset(out - (width + 2) * 4 - 4, 0xff, 4 * 3);
|
||||
// ...besides...
|
||||
memset(out - 4, 0xff, 4 * 3);
|
||||
// ...and above
|
||||
memset(out + (width + 2) * 4 - 4, 0xff, 4 * 3);
|
||||
}
|
||||
in += 4;
|
||||
out += 4;
|
||||
}
|
||||
// outline is slightly larger
|
||||
out += 2 * 4;
|
||||
}
|
||||
|
||||
// Pass 2, overwrite with actual cursor
|
||||
in = in0;
|
||||
out = out0 + width * 4 + 4;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
if (in[3] > 0)
|
||||
memcpy(out, in, 4);
|
||||
in += 4;
|
||||
out += 4;
|
||||
}
|
||||
out += 2 * 4;
|
||||
}
|
||||
}
|
||||
|
||||
int ffi(unsigned v)
|
||||
{
|
||||
static const int MultiplyDeBruijnBitPosition[32] =
|
||||
{
|
||||
0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
|
||||
31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9};
|
||||
return MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];
|
||||
}
|
||||
|
||||
int get_di_bits(uint8_t *out, HDC dc, HBITMAP hbmColor, int width, int height)
|
||||
{
|
||||
BITMAPV5HEADER bi;
|
||||
memset(&bi, 0, sizeof(BITMAPV5HEADER));
|
||||
|
||||
bi.bV5Size = sizeof(BITMAPV5HEADER);
|
||||
bi.bV5Width = width;
|
||||
bi.bV5Height = -height; // Negative for top-down
|
||||
bi.bV5Planes = 1;
|
||||
bi.bV5BitCount = 32;
|
||||
bi.bV5Compression = BI_BITFIELDS;
|
||||
bi.bV5RedMask = 0x000000FF;
|
||||
bi.bV5GreenMask = 0x0000FF00;
|
||||
bi.bV5BlueMask = 0x00FF0000;
|
||||
bi.bV5AlphaMask = 0xFF000000;
|
||||
|
||||
if (!GetDIBits(dc, hbmColor, 0, height,
|
||||
out, (LPBITMAPINFO)&bi, DIB_RGB_COLORS))
|
||||
return 1;
|
||||
|
||||
// We may not get the RGBA order we want, so shuffle things around
|
||||
int ridx, gidx, bidx, aidx;
|
||||
|
||||
ridx = ffi(bi.bV5RedMask) / 8;
|
||||
gidx = ffi(bi.bV5GreenMask) / 8;
|
||||
bidx = ffi(bi.bV5BlueMask) / 8;
|
||||
// Usually not set properly
|
||||
aidx = 6 - ridx - gidx - bidx;
|
||||
|
||||
if ((bi.bV5RedMask != ((unsigned)0xff << ridx * 8)) ||
|
||||
(bi.bV5GreenMask != ((unsigned)0xff << gidx * 8)) ||
|
||||
(bi.bV5BlueMask != ((unsigned)0xff << bidx * 8)))
|
||||
return 1;
|
||||
|
||||
auto rwbuffer = out;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
uint8_t r, g, b, a;
|
||||
|
||||
r = rwbuffer[ridx];
|
||||
g = rwbuffer[gidx];
|
||||
b = rwbuffer[bidx];
|
||||
a = rwbuffer[aidx];
|
||||
|
||||
rwbuffer[0] = r;
|
||||
rwbuffer[1] = g;
|
||||
rwbuffer[2] = b;
|
||||
rwbuffer[3] = a;
|
||||
|
||||
rwbuffer += 4;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void blank_screen(BOOL set)
|
||||
{
|
||||
if (set)
|
||||
{
|
||||
SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2);
|
||||
}
|
||||
else
|
||||
{
|
||||
SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1);
|
||||
}
|
||||
}
|
||||
|
||||
void AddRecentDocument(PCWSTR path)
|
||||
{
|
||||
SHAddToRecentDocs(SHARD_PATHW, path);
|
||||
}
|
||||
} // end of extern "C"
|
||||
Reference in New Issue
Block a user