feat, update, win, macos (#11618)
Some checks failed
CI / ${{ matrix.job.target }} (${{ matrix.job.os }}) (map[os:ubuntu-22.04 target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
Full Flutter CI / run-ci (push) Has been cancelled
Flutter Nightly Build / run-flutter-nightly-build (push) Has been cancelled

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2025-05-04 07:32:47 +08:00 committed by GitHub
parent 62276b4f4f
commit ca00706a38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 2128 additions and 69 deletions

View File

@ -1152,15 +1152,23 @@ Widget createDialogContent(String text) {
void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
{bool? hasCancel,
ReconnectHandle? reconnect,
int? reconnectTimeout,
VoidCallback? onSubmit,
int? submitTimeout}) {
dialogManager.dismissAll();
List<Widget> buttons = [];
bool hasOk = false;
submit() {
dialogManager.dismissAll();
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection();
if (onSubmit != null) {
onSubmit.call();
} else {
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection();
}
}
}
@ -1176,7 +1184,18 @@ void msgBox(SessionID sessionId, String type, String title, String text,
if (type != "connecting" && type != "success" && !type.contains("nook")) {
hasOk = true;
buttons.insert(0, dialogButton('OK', onPressed: submit));
late final Widget btn;
if (submitTimeout != null) {
btn = _CountDownButton(
text: 'OK',
second: submitTimeout,
onPressed: submit,
submitOnTimeout: true,
);
} else {
btn = dialogButton('OK', onPressed: submit);
}
buttons.insert(0, btn);
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
@ -1197,7 +1216,8 @@ void msgBox(SessionID sessionId, String type, String title, String text,
reconnectTimeout != null) {
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(() => _ReconnectCountDownButton(
final button = Obx(() => _CountDownButton(
text: 'Reconnect',
second: reconnectTimeout,
onPressed: enabled.isTrue
? () {
@ -3183,21 +3203,24 @@ parseParamScreenRect(Map<String, dynamic> params) {
get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
class _ReconnectCountDownButton extends StatefulWidget {
_ReconnectCountDownButton({
class _CountDownButton extends StatefulWidget {
_CountDownButton({
Key? key,
required this.text,
required this.second,
required this.onPressed,
this.submitOnTimeout = false,
}) : super(key: key);
final String text;
final VoidCallback? onPressed;
final int second;
final bool submitOnTimeout;
@override
State<_ReconnectCountDownButton> createState() =>
_ReconnectCountDownButtonState();
State<_CountDownButton> createState() => _CountDownButtonState();
}
class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
class _CountDownButtonState extends State<_CountDownButton> {
late int _countdownSeconds = widget.second;
Timer? _timer;
@ -3218,6 +3241,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdownSeconds <= 0) {
timer.cancel();
if (widget.submitOnTimeout) {
widget.onPressed?.call();
}
} else {
setState(() {
_countdownSeconds--;
@ -3229,7 +3255,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
@override
Widget build(BuildContext context) {
return dialogButton(
'${translate('Reconnect')} (${_countdownSeconds}s)',
'${translate(widget.text)} (${_countdownSeconds}s)',
onPressed: widget.onPressed,
isOutline: true,
);

View File

@ -139,6 +139,7 @@ const String kOptionCurrentAbName = "current-ab-name";
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
const String kOptionAllowAutoUpdate = "allow-auto-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";

View File

@ -12,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@ -22,7 +23,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart';
class DesktopHomePage extends StatefulWidget {
@ -433,13 +433,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
updateUrl.isNotEmpty &&
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
String btnText = isToUpdate ? 'Click to update' : 'Click to download';
GestureTapCallback onPressed = () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
};
if (isToUpdate) {
onPressed = () {
handleUpdate(updateUrl);
};
}
return buildInstallCard(
"Status",
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
}, closeButton: true);
btnText,
onPressed,
closeButton: true);
}
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});

View File

@ -470,6 +470,8 @@ class _GeneralState extends State<_General> {
}
Widget other() {
final showAutoUpdate =
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
final children = <Widget>[
if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
@ -523,12 +525,19 @@ class _GeneralState extends State<_General> {
kOptionEnableCheckUpdate,
isServer: false,
),
if (showAutoUpdate)
_OptionCheckBox(
context,
'Auto update',
kOptionAllowAutoUpdate,
isServer: true,
),
if (isWindows && !bind.isOutgoingOnly())
_OptionCheckBox(
context,
'Capture screen using DirectX',
kOptionDirectxCapture,
)
),
],
];
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {

View File

@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
void handleUpdate(String releasePageUrl) {
String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
final String downloadFile =
bind.mainGetCommonSync(key: 'download-file-$version');
if (downloadFile.startsWith('error:')) {
final error = downloadFile.replaceFirst('error:', '');
msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error,
releasePageUrl, gFFI.dialogManager);
return;
}
downloadUrl = '$downloadUrl/$downloadFile';
SimpleWrapper downloadId = SimpleWrapper('');
SimpleWrapper<VoidCallback> onCanceled = SimpleWrapper(() {});
gFFI.dialogManager.dismissAll();
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate('Downloading {$appName}')),
content:
UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
.marginSymmetric(horizontal: 8)
.paddingOnly(top: 12),
actions: [
dialogButton(translate('Cancel'), onPressed: () async {
onCanceled.value();
await bind.mainSetCommon(
key: 'cancel-downloader', value: downloadId.value);
// Wait for the downloader to be removed.
for (int i = 0; i < 10; i++) {
await Future.delayed(const Duration(milliseconds: 300));
final isCanceled = 'error:Downloader not found' ==
await bind.mainGetCommon(
key: 'download-data-${downloadId.value}');
if (isCanceled) {
break;
}
}
close();
}, isOutline: true),
]);
});
}
class UpdateProgress extends StatefulWidget {
final String releasePageUrl;
final String downloadUrl;
final SimpleWrapper downloadId;
final SimpleWrapper onCanceled;
UpdateProgress(
this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled,
{Key? key})
: super(key: key);
@override
State<UpdateProgress> createState() => UpdateProgressState();
}
class UpdateProgressState extends State<UpdateProgress> {
Timer? _timer;
int? _totalSize;
int _downloadedSize = 0;
int _getDataFailedCount = 0;
final String _eventKeyDownloadNewVersion = 'download-new-version';
@override
void initState() {
super.initState();
widget.onCanceled.value = () {
cancelQueryTimer();
};
platformFFI.registerEventHandler(_eventKeyDownloadNewVersion,
_eventKeyDownloadNewVersion, handleDownloadNewVersion,
replace: true);
bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
}
@override
void dispose() {
cancelQueryTimer();
platformFFI.unregisterEventHandler(
_eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
super.dispose();
}
void cancelQueryTimer() {
_timer?.cancel();
_timer = null;
}
Future<void> handleDownloadNewVersion(Map<String, dynamic> evt) async {
if (evt.containsKey('id')) {
widget.downloadId.value = evt['id'] as String;
_timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
_updateDownloadData();
});
} else {
if (evt.containsKey('error')) {
_onError(evt['error'] as String);
} else {
// unreachable
_onError('$evt');
}
}
}
void _onError(String error) {
cancelQueryTimer();
debugPrint('Download new version error: $error');
final msgBoxType = 'custom-nocancel-nook-hasclose';
final msgBoxTitle = 'Error';
final msgBoxText = 'download-new-version-failed-tip';
final dialogManager = gFFI.dialogManager;
close() {
dialogManager.dismissAll();
}
jumplink() {
launchUrl(Uri.parse(widget.releasePageUrl));
dialogManager.dismissAll();
}
retry() {
dialogManager.dismissAll();
handleUpdate(widget.releasePageUrl);
}
final List<Widget> buttons = [
dialogButton('Download', onPressed: jumplink),
dialogButton('Retry', onPressed: retry),
dialogButton('Close', onPressed: close),
];
dialogManager.dismissAll();
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
actions: buttons,
),
tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
);
}
void _updateDownloadData() {
String err = '';
String downloadData =
bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}');
if (downloadData.startsWith('error:')) {
err = downloadData.substring('error:'.length);
} else {
try {
jsonDecode(downloadData).forEach((key, value) {
if (key == 'total_size') {
if (value != null && value is int) {
_totalSize = value;
}
} else if (key == 'downloaded_size') {
_downloadedSize = value as int;
} else if (key == 'error') {
if (value != null) {
err = value.toString();
}
}
});
} catch (e) {
_getDataFailedCount += 1;
debugPrint(
'Failed to get download data ${widget.downloadUrl}, error $e');
if (_getDataFailedCount > 3) {
err = e.toString();
}
}
}
if (err != '') {
_onError(err);
} else {
if (_totalSize != null && _downloadedSize >= _totalSize!) {
cancelQueryTimer();
bind.mainSetCommon(
key: 'remove-downloader', value: widget.downloadId.value);
if (_totalSize == 0) {
_onError('The download file size is 0.');
} else {
setState(() {});
msgBox(
gFFI.sessionId,
'custom-nocancel',
'{$appName} Update',
'{$appName}-to-update-tip',
'',
gFFI.dialogManager,
onSubmit: () {
debugPrint('Downloaded, update to new version now');
bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
},
submitTimeout: 5,
);
}
} else {
setState(() {});
}
}
}
@override
Widget build(BuildContext context) {
return onDownloading(context);
}
Widget onDownloading(BuildContext context) {
final value = _totalSize == null
? 0.0
: (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
return LinearProgressIndicator(
value: value,
minHeight: 20,
borderRadius: BorderRadius.circular(5),
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
);
}
}

View File

@ -56,7 +56,7 @@
<!-- Launch ClientLauncher if installing or already installed and not uninstalling -->
<!-- https://learn.microsoft.com/en-us/windows/win32/msi/uilevel -->
<Custom Action="LaunchApp" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) "/>
<Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(LAUNCH_TRAY_APP=&quot;Y&quot; OR LAUNCH_TRAY_APP=&quot;1&quot;) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<!-- https://learn.microsoft.com/en-us/windows/win32/msi/operating-system-property-values -->
<!-- We have to use `VersionNT` to instead of `IsWindows10OrGreater()` in the custom action.

View File

@ -9,6 +9,8 @@
<!--STOP_SERVICE is set to 'Y'. Because the cofig value may be empty or 'Y'-->
<Property Id="STOP_SERVICE" Value="&apos;Y&apos;" />
<Property Id="LAUNCH_TRAY_APP" Value="Y" />
<!--
Support entries shown when clicking "Click here for support information"
in Control Panel's Add/Remove Programs https://learn.microsoft.com/en-us/windows/win32/msi/property-reference

View File

@ -847,12 +847,12 @@ pub fn check_software_update() {
}
let opt = config::LocalConfig::get_option(config::keys::OPTION_ENABLE_CHECK_UPDATE);
if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) {
std::thread::spawn(move || allow_err!(check_software_update_()));
std::thread::spawn(move || allow_err!(do_check_software_update()));
}
}
#[tokio::main(flavor = "current_thread")]
async fn check_software_update_() -> hbb_common::ResultType<()> {
pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
let (request, url) =
hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string());
let latest_release_response = create_http_client_async()
@ -876,6 +876,8 @@ async fn check_software_update_() -> hbb_common::ResultType<()> {
}
}
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
} else {
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
}
Ok(())
}

View File

@ -1,4 +1,4 @@
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "macos"))]
use crate::client::translate;
#[cfg(not(debug_assertions))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -189,6 +189,26 @@ pub fn core_main() -> Option<Vec<String>> {
log::error!("Failed to uninstall: {}", err);
}
return None;
} else if args[0] == "--update" {
if config::is_disable_installation() {
return None;
}
let res = platform::update_me(false);
let text = match res {
Ok(_) => translate("Update successfully!".to_string()),
Err(err) => {
log::error!("Failed with error: {err}");
translate("Update failed!".to_string())
}
};
Toast::new(Toast::POWERSHELL_APP_ID)
.title(&config::APP_NAME.read().unwrap())
.text1(&text)
.sound(Some(Sound::Default))
.duration(Duration::Short)
.show()
.ok();
return None;
} else if args[0] == "--after-install" {
if let Err(err) = platform::run_after_install() {
log::error!("Failed to after-install: {}", err);
@ -250,6 +270,21 @@ pub fn core_main() -> Option<Vec<String>> {
return None;
}
}
#[cfg(target_os = "macos")]
{
use crate::platform;
if args[0] == "--update" {
let _text = match platform::update_me() {
Ok(_) => {
log::info!("{}", translate("Update successfully!".to_string()));
}
Err(err) => {
log::error!("Update failed with error: {err}");
}
};
return None;
}
}
if args[0] == "--remove" {
if args.len() == 2 {
// sleep a while so that process of removed exe exit
@ -592,7 +627,8 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<Strin
let mut param_array = vec![];
while let Some(arg) = args.next() {
match arg.as_str() {
"--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" | "--rdp" => {
"--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward"
| "--rdp" => {
authority = Some((&arg.to_string()[2..]).to_owned());
id = args.next();
}

View File

@ -2034,7 +2034,10 @@ pub mod sessions {
None => {}
}
}
SESSIONS.write().unwrap().remove(&remove_peer_key?)
let s = SESSIONS.write().unwrap().remove(&remove_peer_key?);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_session_count_to_server();
s
}
fn check_remove_unused_displays(
@ -2136,6 +2139,14 @@ pub mod sessions {
.write()
.unwrap()
.insert(session_id, Default::default());
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_session_count_to_server();
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn update_session_count_to_server() {
crate::ipc::update_controlling_session_count(SESSIONS.read().unwrap().len()).ok();
}
#[inline]

View File

@ -21,11 +21,12 @@ use hbb_common::{
};
use std::{
collections::HashMap,
path::PathBuf,
sync::{
atomic::{AtomicI32, Ordering},
Arc,
},
time::SystemTime,
time::{Duration, SystemTime},
};
pub type SessionID = uuid::Uuid;
@ -2428,7 +2429,42 @@ pub fn main_get_common(key: String) -> String {
} else if key == "transfer-job-id" {
return hbb_common::fs::get_next_job_id().to_string();
} else {
"".to_owned()
if key.starts_with("download-data-") {
let id = key.replace("download-data-", "");
match crate::hbbs_http::downloader::get_download_data(&id) {
Ok(data) => serde_json::to_string(&data).unwrap_or_default(),
Err(e) => {
format!("error:{}", e)
}
}
} else if key.starts_with("download-file-") {
let _version = key.replace("download-file-", "");
#[cfg(target_os = "windows")]
return match crate::platform::windows::is_msi_installed() {
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"),
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"),
Err(e) => {
log::error!("Failed to check if is msi: {}", e);
format!("error:update-failed-check-msi-tip")
}
};
#[cfg(target_os = "macos")]
{
return if cfg!(target_arch = "x86_64") {
format!("rustdesk-{_version}-x86_64.dmg")
} else if cfg!(target_arch = "aarch64") {
format!("rustdesk-{_version}-aarch64.dmg")
} else {
"error:unsupported".to_owned()
};
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
"error:unsupported".to_owned()
}
} else {
"".to_owned()
}
}
}
@ -2469,6 +2505,72 @@ pub fn main_set_common(_key: String, _value: String) {
);
});
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
use crate::updater::get_download_file_from_url;
if _key == "download-new-version" {
let download_url = _value.clone();
let event_key = "download-new-version".to_owned();
let data = if let Some(download_file) = get_download_file_from_url(&download_url) {
std::fs::remove_file(&download_file).ok();
match crate::hbbs_http::downloader::download_file(
download_url,
Some(PathBuf::from(download_file)),
Some(Duration::from_secs(3)),
) {
Ok(id) => HashMap::from([("name", event_key), ("id", id)]),
Err(e) => HashMap::from([("name", event_key), ("error", e.to_string())]),
}
} else {
HashMap::from([
("name", event_key),
("error", "Invalid download url".to_string()),
])
};
let _res = flutter::push_global_event(
flutter::APP_TYPE_MAIN,
serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
);
} else if _key == "update-me" {
if let Some(new_version_file) = get_download_file_from_url(&_value) {
log::debug!("New version file is downloaed, update begin, {:?}", new_version_file.to_str());
if let Some(f) = new_version_file.to_str() {
// 1.4.0 does not support "--update"
// But we can assume that the new version supports it.
#[cfg(target_os = "windows")]
if f.ends_with(".exe") {
if let Err(e) =
crate::platform::run_exe_in_cur_session(f, vec!["--update"], false)
{
log::error!("Failed to run the update exe: {}", e);
}
} else if f.ends_with(".msi") {
if let Err(e) = crate::platform::update_me_msi(f, false) {
log::error!("Failed to run the update msi: {}", e);
}
} else {
// unreachable!()
}
#[cfg(target_os = "macos")]
match crate::platform::update_to(f) {
Ok(_) => {
log::info!("Update successfully!");
}
Err(e) => {
log::error!("Failed to update to new version, {}", e);
}
}
fs::remove_file(f).ok();
}
}
}
}
if _key == "remove-downloader" {
crate::hbbs_http::downloader::remove(&_value);
} else if _key == "cancel-downloader" {
crate::hbbs_http::downloader::cancel(&_value);
}
}
pub fn session_get_common_sync(

View File

@ -7,6 +7,7 @@ pub mod account;
mod http_client;
pub mod record_upload;
pub mod sync;
pub mod downloader;
pub use http_client::create_http_client;
pub use http_client::create_http_client_async;

274
src/hbbs_http/downloader.rs Normal file
View File

@ -0,0 +1,274 @@
use super::create_http_client_async;
use hbb_common::{
bail,
lazy_static::lazy_static,
log,
tokio::{
self,
fs::File,
io::AsyncWriteExt,
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
},
ResultType,
};
use serde_derive::Serialize;
use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
lazy_static! {
static ref DOWNLOADERS: Mutex<HashMap<String, Downloader>> = Default::default();
}
/// This struct is used to return the download data to the caller.
/// The caller should check if the file is downloaded successfully and remove the job from the map.
/// If the file is not downloaded successfully, the `data` field will be empty.
/// If the file is downloaded successfully, the `data` field will contain the downloaded data if `path` is None.
#[derive(Serialize, Debug)]
pub struct DownloadData {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub data: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_size: Option<u64>,
pub downloaded_size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
struct Downloader {
data: Vec<u8>,
path: Option<PathBuf>,
// Some file may be empty, so we use Option<u64> to indicate if the size is known
total_size: Option<u64>,
downloaded_size: u64,
error: Option<String>,
finished: bool,
tx_cancel: UnboundedSender<()>,
}
// The caller should check if the file is downloaded successfully and remove the job from the map.
pub fn download_file(
url: String,
path: Option<PathBuf>,
auto_del_dur: Option<Duration>,
) -> ResultType<String> {
let id = url.clone();
if DOWNLOADERS.lock().unwrap().contains_key(&id) {
return Ok(id);
}
if let Some(path) = path.as_ref() {
if path.exists() {
bail!("File {} already exists", path.display());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
}
let (tx, rx) = unbounded_channel();
let downloader = Downloader {
data: Vec::new(),
path: path.clone(),
total_size: None,
downloaded_size: 0,
error: None,
tx_cancel: tx,
finished: false,
};
let mut downloaders = DOWNLOADERS.lock().unwrap();
downloaders.insert(id.clone(), downloader);
let id2 = id.clone();
std::thread::spawn(
move || match do_download(&id2, url, path, auto_del_dur, rx) {
Ok(is_all_downloaded) => {
let mut downloaded_size = 0;
let mut total_size = 0;
DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| {
downloaded_size = downloader.downloaded_size;
total_size = downloader.total_size.unwrap_or(0);
});
log::info!(
"Download {} end, {}/{}, {:.2} %",
&id2,
downloaded_size,
total_size,
if total_size == 0 {
0.0
} else {
downloaded_size as f64 / total_size as f64 * 100.0
}
);
let is_canceled = !is_all_downloaded;
if is_canceled {
if let Some(downloader) = DOWNLOADERS.lock().unwrap().remove(&id2) {
if let Some(p) = downloader.path {
if p.exists() {
std::fs::remove_file(p).ok();
}
}
}
}
}
Err(e) => {
let err = e.to_string();
log::error!("Download {}, failed: {}", &id2, &err);
DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| {
downloader.error = Some(err);
});
}
},
);
Ok(id)
}
#[tokio::main(flavor = "current_thread")]
async fn do_download(
id: &str,
url: String,
path: Option<PathBuf>,
auto_del_dur: Option<Duration>,
mut rx_cancel: UnboundedReceiver<()>,
) -> ResultType<bool> {
let client = create_http_client_async();
let mut is_all_downloaded = false;
tokio::select! {
_ = rx_cancel.recv() => {
return Ok(is_all_downloaded);
}
head_resp = client.head(&url).send() => {
match head_resp {
Ok(resp) => {
if resp.status().is_success() {
let total_size = resp
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse::<u64>().ok());
let Some(total_size) = total_size else {
bail!("Failed to get content length");
};
DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| {
downloader.total_size = Some(total_size);
});
} else {
bail!("Failed to get content length: {}", resp.status());
}
}
Err(e) => {
return Err(e.into());
}
}
}
}
let mut response;
tokio::select! {
_ = rx_cancel.recv() => {
return Ok(is_all_downloaded);
}
resp = client.get(url).send() => {
response = resp?;
}
}
let mut dest: Option<File> = None;
if let Some(p) = path {
dest = Some(File::create(p).await?);
}
loop {
tokio::select! {
_ = rx_cancel.recv() => {
break;
}
chunk = response.chunk() => {
match chunk {
Ok(Some(chunk)) => {
match dest {
Some(ref mut f) => {
f.write_all(&chunk).await?;
f.flush().await?;
DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| {
downloader.downloaded_size += chunk.len() as u64;
});
}
None => {
DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| {
downloader.data.extend_from_slice(&chunk);
downloader.downloaded_size += chunk.len() as u64;
});
}
}
}
Ok(None) => {
is_all_downloaded = true;
break;
},
Err(e) => {
log::error!("Download {} failed: {}", id, e);
return Err(e.into());
}
}
}
}
}
if let Some(mut f) = dest.take() {
f.flush().await?;
}
if let Some(ref mut downloader) = DOWNLOADERS.lock().unwrap().get_mut(id) {
downloader.finished = true;
}
if is_all_downloaded {
let id_del = id.to_string();
if let Some(dur) = auto_del_dur {
tokio::spawn(async move {
tokio::time::sleep(dur).await;
DOWNLOADERS.lock().unwrap().remove(&id_del);
});
}
}
Ok(is_all_downloaded)
}
pub fn get_download_data(id: &str) -> ResultType<DownloadData> {
let downloaders = DOWNLOADERS.lock().unwrap();
if let Some(downloader) = downloaders.get(id) {
let downloaded_size = downloader.downloaded_size;
let total_size = downloader.total_size.clone();
let error = downloader.error.clone();
let data = if total_size.unwrap_or(0) == downloaded_size && downloader.path.is_none() {
downloader.data.clone()
} else {
Vec::new()
};
let path = downloader.path.clone();
let download_data = DownloadData {
data,
path,
total_size,
downloaded_size,
error,
};
Ok(download_data)
} else {
bail!("Downloader not found")
}
}
pub fn cancel(id: &str) {
if let Some(downloader) = DOWNLOADERS.lock().unwrap().get(id) {
// downloader.is_canceled.store(true, Ordering::SeqCst);
// The receiver may not be able to receive the cancel signal, so we also set the atomic bool to true
let _ = downloader.tx_cancel.send(());
}
}
pub fn remove(id: &str) {
let _ = DOWNLOADERS.lock().unwrap().remove(id);
}

View File

@ -275,6 +275,11 @@ pub enum Data {
#[cfg(all(target_os = "windows", feature = "flutter"))]
PrinterData(Vec<u8>),
InstallOption(Option<(String, String)>),
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
ControllingSessionCount(usize),
}
#[tokio::main(flavor = "current_thread")]
@ -599,6 +604,13 @@ async fn handle(data: Data, stream: &mut Connection) {
.await
);
}
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
Data::ControllingSessionCount(count) => {
crate::updater::update_controlling_session_count(count);
}
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::CheckHwcodec => {
@ -1280,6 +1292,17 @@ pub async fn clear_wayland_screencast_restore_token(key: String) -> ResultType<b
return Ok(false);
}
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
#[tokio::main(flavor = "current_thread")]
pub async fn update_controlling_session_count(count: usize) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::ControllingSessionCount(count)).await?;
Ok(())
}
async fn handle_wayland_screencast_restore_token(
key: String,
value: String,

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "请选择如何继续截屏。"),
("Save as", "另存为"),
("Copy to clipboard", "复制到剪贴板"),
("Enable remote printer", "启用远程打印机"),
("Downloading {}", "正在下载 {}"),
("{} Update", "{} 更新"),
("{}-to-update-tip", "即将关闭 {} ,并安装新版本。"),
("download-new-version-failed-tip", "下载失败,您可以重试或者点击\"下载\"按钮,从发布网址下载,并手动升级。"),
("Auto update", ""),
("update-failed-check-msi-tip", "安装方式检测失败。请点击\"下载\"按钮,从发布网址下载,并手动升级。"),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "Bitte wählen Sie aus, wie Sie mit dem Screenshot fortfahren möchten."),
("Save as", "Speichern unter"),
("Copy to clipboard", "In Zwischenablage kopieren"),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -255,5 +255,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("dont-show-again-tip", "Don't show this again"),
("screenshot-merged-screen-not-supported-tip", "Merging screenshots of multiple displays is currently not supported. Please switch to a single display and try again."),
("screenshot-action-tip", "Please select how to continue with the screenshot."),
("{}-to-update-tip", "{} will close now and install the new version."),
("download-new-version-failed-tip", "Download failed. You can try again or click the \"Download\" button to download from the release page and upgrade manually."),
("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually.")
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -658,7 +658,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"),
("Accessible devices", "ხელმისაწვდომი მოწყობილობები"),
("View camera", "კამერის ნახვა"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"),
("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."),
("Enable camera", "კამერის ჩართვა"),
("No cameras", "კამერა არ არის"),
@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Connection Error", "Kapcsolódási hiba"),
("Error", "Hiba"),
("Reset by the peer", "A kapcsolatot a másik fél lezárta."),
("Connecting", "Kapcsolódás…"),
("Connecting...", "Kapcsolódás…"),
("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet…"),
("Please try 1 minute later", "Próbálja meg 1 perc múlva"),
("Login Error", "Bejelentkezési hiba"),
("Successful", "Sikeres"),
("Connected, waiting for image", "Kapcsolódva, várakozás a képadatokra…"),
("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra…"),
("Name", "Név"),
("Type", "Típus"),
("Modified", "Módosított"),
@ -152,7 +152,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Configure", "Beállítás"),
("config_acc", "A távoli vezérléshez a RustDesknek „Kisegítő lehetőségek” engedélyre van szüksége"),
("config_screen", "A távoli vezérléshez szükséges a „Képernyőfelvétel” engedély megadása"),
("Installing ", "Telepítés…"),
("Installing ...", "Telepítés…"),
("Install", "Telepítés"),
("Installation", "Telepítés"),
("Installation Path", "Telepítési útvonal"),
@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licensz szerződés."),
("Accept and Install", "Elfogadás és telepítés"),
("End-user license agreement", "Végfelhasználói licensz szerződés"),
("Generating ", "Létrehozás…"),
("Generating ...", "Létrehozás…"),
("Your installation is lower version.", "A telepített verzió alacsonyabb."),
("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"),
("Listening ", "Figyelés…"),
("Listening ...", "Figyelés…"),
("Remote Host", "Távoli kiszolgáló"),
("Remote Port", "Távoli port"),
("Action", "Indítás"),
@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"),
("Enter Remote ID", "Távoli számítógép azonosítója"),
("Enter your password", "Adja meg a jelszavát"),
("Logging in", "Belépés folyamatban…"),
("Logging in...", "Belépés folyamatban…"),
("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"),
("Auto Login", "Automatikus bejelentkezés"),
("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"),
@ -373,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Deny LAN discovery", "Felfedezés tiltása"),
("Write a message", "Üzenet írása"),
("Prompt", "Kérés"),
("Please wait for confirmation of UAC", "Várjon az UAC megerősítésére…"),
("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére…"),
("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."),
("Disconnected", "Kapcsolat bontva"),
("Other", "Egyéb"),
@ -394,7 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept sessions via password", "Munkamenetek elfogadása jelszóval"),
("Accept sessions via click", "Munkamenetek elfogadása kattintással"),
("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"),
("Please wait for the remote side to accept your session request", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"),
("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"),
("One-time Password", "Egyszer használatos jelszó"),
("Use one-time password", "Használjon ideiglenes jelszót"),
("One-time password length", "Egyszer használatos jelszó hossza"),
@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "Képernyőkép-művelet"),
("Save as", "Mentés másként"),
("Copy to clipboard", "Másolás a vágólapra"),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "Seleziona come continuare con la schermata."),
("Save as", "Salva come"),
("Copy to clipboard", "Copia negli appunti"),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "Выберите, что делать с полученным снимком экрана."),
("Save as", "Сохранить в файл"),
("Copy to clipboard", "Копировать в буфер обмена"),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", "請選擇要如何處理這張截圖。"),
("Save as", "另存為"),
("Copy to clipboard", "複製到剪貼簿"),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -687,5 +687,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
].iter().cloned().collect();
}

View File

@ -39,14 +39,14 @@ use common::*;
mod auth_2fa;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(not(target_os = "ios"))]
mod clipboard;
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
pub mod core_main;
mod custom_server;
mod lang;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod port_forward;
#[cfg(not(target_os = "ios"))]
mod clipboard;
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -55,6 +55,9 @@ pub mod plugin;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod tray;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod updater;
mod ui_cm_interface;
mod ui_interface;
mod ui_session_interface;

View File

@ -27,12 +27,18 @@ use include_dir::{include_dir, Dir};
use objc::rc::autoreleasepool;
use objc::{class, msg_send, sel, sel_impl};
use scrap::{libc::c_void, quartz::ffi::*};
use std::path::{Path, PathBuf};
use std::{
os::unix::process::CommandExt,
path::{Path, PathBuf},
process::{Command, Stdio},
};
static PRIVILEGES_SCRIPTS_DIR: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
static mut LATEST_SEED: i32 = 0;
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
extern "C" {
fn CGSCurrentCursorSeed() -> i32;
fn CGEventCreate(r: *const c_void) -> *const c_void;
@ -155,6 +161,9 @@ pub fn install_service() -> bool {
is_installed_daemon(false)
}
// Remember to check if `update_daemon_agent()` need to be changed if changing `is_installed_daemon()`.
// No need to merge the existing dup code, because the code in these two functions are too critical.
// New code should be written in a common function.
pub fn is_installed_daemon(prompt: bool) -> bool {
let daemon = format!("{}_service.plist", crate::get_full_name());
let agent = format!("{}_server.plist", crate::get_full_name());
@ -218,6 +227,70 @@ pub fn is_installed_daemon(prompt: bool) -> bool {
false
}
fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync: bool) {
let update_script_file = "update.scpt";
let Some(update_script) = PRIVILEGES_SCRIPTS_DIR.get_file(update_script_file) else {
return;
};
let Some(update_script_body) = update_script.contents_utf8().map(correct_app_name) else {
return;
};
let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else {
return;
};
let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else {
return;
};
let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else {
return;
};
let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else {
return;
};
let func = move || {
let mut binding = std::process::Command::new("osascript");
let mut cmd = binding
.arg("-e")
.arg(update_script_body)
.arg(daemon_plist_body)
.arg(agent_plist_body)
.arg(&get_active_username())
.arg(std::process::id().to_string())
.arg(update_source_dir);
match cmd.status() {
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {
let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", &agent_plist_file, installed);
if installed {
// Unload first, or load may not work if already loaded.
// We hope that the load operation can immediately trigger a start.
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
let status = std::process::Command::new("launchctl")
.args(&["load", "-w", &agent_plist_file])
.status();
log::info!("launch server, status: {:?}", &status);
}
}
}
};
if sync {
func();
} else {
std::thread::spawn(func);
}
}
fn correct_app_name(s: &str) -> String {
let s = s.replace("rustdesk", &crate::get_app_name().to_lowercase());
let s = s.replace("RustDesk", &crate::get_app_name());
@ -634,6 +707,140 @@ pub fn quit_gui() {
};
}
pub fn update_me() -> ResultType<()> {
let is_installed_daemon = is_installed_daemon(false);
let option_stop_service = "stop-service";
let is_service_stopped = hbb_common::config::option2bool(
option_stop_service,
&crate::ui_interface::get_option(option_stop_service),
);
let cmd = std::env::current_exe()?;
// RustDesk.app/Contents/MacOS/RustDesk
let app_dir = cmd
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.map(|d| d.to_string_lossy().to_string());
let Some(app_dir) = app_dir else {
bail!("Unknown app directory of current exe file: {:?}", cmd);
};
if is_installed_daemon && !is_service_stopped {
let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
update_daemon_agent(agent_plist_file, app_dir, true);
} else {
// `kill -9` may not work without "administrator privileges"
let update_body = format!(
r#"
do shell script "
pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && cp -R '{}' /Applications/ && chown -R {}:staff /Applications/RustDesk.app
" with prompt "RustDesk wants to update itself" with administrator privileges
"#,
std::process::id(),
app_dir,
get_active_username()
);
match Command::new("osascript")
.arg("-e")
.arg(update_body)
.status()
{
Ok(status) if !status.success() => {
log::error!("osascript execution failed with status: {}", status);
}
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {}
}
}
std::process::Command::new("open")
.arg("-n")
.arg(&format!("/Applications/{}.app", crate::get_app_name()))
.spawn()
.ok();
// leave open a little time
std::thread::sleep(std::time::Duration::from_millis(300));
Ok(())
}
pub fn update_to(file: &str) -> ResultType<()> {
extract_dmg(file, UPDATE_TEMP_DIR)?;
update_extracted(UPDATE_TEMP_DIR)?;
Ok(())
}
fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> {
let mount_point = "/Volumes/RustDeskUpdate";
let target_path = Path::new(target_dir);
if target_path.exists() {
std::fs::remove_dir_all(target_path)?;
}
std::fs::create_dir_all(target_path)?;
Command::new("hdiutil")
.args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path])
.status()?;
struct DmgGuard(&'static str);
impl Drop for DmgGuard {
fn drop(&mut self) {
let _ = Command::new("hdiutil")
.args(&["detach", self.0, "-force"])
.status();
}
}
let _guard = DmgGuard(mount_point);
let app_name = "RustDesk.app";
let src_path = format!("{}/{}", mount_point, app_name);
let dest_path = format!("{}/{}", target_dir, app_name);
let copy_status = Command::new("cp")
.args(&["-R", &src_path, &dest_path])
.status()?;
if !copy_status.success() {
bail!("Failed to copy application {:?}", copy_status);
}
if !Path::new(&dest_path).exists() {
bail!(
"Copy operation failed - destination not found at {}",
dest_path
);
}
Ok(())
}
fn update_extracted(target_dir: &str) -> ResultType<()> {
let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir);
let _child = unsafe {
Command::new(&exe_path)
.arg("--update")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.pre_exec(|| {
hbb_common::libc::setsid();
Ok(())
})
.spawn()?
};
Ok(())
}
pub fn get_double_click_time() -> u32 {
// to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823
500 as _

View File

@ -27,7 +27,11 @@ pub mod linux_desktop_manager;
pub mod gtk_sudo;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::{message_proto::CursorData, ResultType};
use hbb_common::{
message_proto::CursorData,
sysinfo::{Pid, System},
ResultType,
};
use std::sync::{Arc, Mutex};
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
pub const SERVICE_INTERVAL: u64 = 300;
@ -137,6 +141,71 @@ pub fn is_prelogin() -> bool {
false
}
// Note: This method is inefficient on Windows. It will get all the processes.
// It should only be called when performance is not critical.
// If we wanted to get the command line ourselves, there would be a lot of new code.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn get_pids_of_process_with_args<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
args: &[S2],
) -> Vec<Pid> {
// This function does not work when the process is 32-bit and the OS is 64-bit Windows,
// `process.cmd()` always returns [] in this case.
// So we use `windows::get_pids_with_args_by_wmic()` instead.
#[cfg(all(target_os = "windows", not(target_pointer_width = "64")))]
{
return windows::get_pids_with_args_by_wmic(name, args);
}
#[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))]
{
let name = name.as_ref().to_lowercase();
let system = System::new_all();
system
.processes()
.iter()
.filter(|(_, process)| {
process.name().to_lowercase() == name
&& process.cmd().len() == args.len() + 1
&& args.iter().enumerate().all(|(i, arg)| {
process.cmd()[i + 1].to_lowercase() == arg.as_ref().to_lowercase()
})
})
.map(|(&pid, _)| pid)
.collect()
}
}
// Note: This method is inefficient on Windows. It will get all the processes.
// It should only be called when performance is not critical.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_pids_of_process_with_first_arg<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
arg: S2,
) -> Vec<Pid> {
// This function does not work when the process is 32-bit and the OS is 64-bit Windows,
// `process.cmd()` always returns [] in this case.
// So we use `windows::get_pids_with_first_arg_by_wmic()` instead.
#[cfg(all(target_os = "windows", not(target_pointer_width = "64")))]
{
return windows::get_pids_with_first_arg_by_wmic(name, arg);
}
#[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))]
{
let name = name.as_ref().to_lowercase();
let system = System::new_all();
system
.processes()
.iter()
.filter(|(_, process)| {
process.name().to_lowercase() == name
&& process.cmd().len() >= 2
&& process.cmd()[1].to_lowercase() == arg.as_ref().to_lowercase()
})
.map(|(&pid, _)| pid)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -12,5 +12,5 @@ on run {daemon_file, agent_file, user}
set sh to sh1 & sh2 & sh3 & sh4 & sh5
do shell script sh with prompt "RustDesk want to install daemon and agent" with administrator privileges
do shell script sh with prompt "RustDesk wants to install daemon and agent" with administrator privileges
end run

View File

@ -3,4 +3,4 @@ set sh2 to "/bin/rm /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
set sh3 to "/bin/rm /Library/LaunchAgents/com.carriez.RustDesk_server.plist;"
set sh to sh1 & sh2 & sh3
do shell script sh with prompt "RustDesk want to unload daemon" with administrator privileges
do shell script sh with prompt "RustDesk wants to unload daemon" with administrator privileges

View File

@ -0,0 +1,18 @@
on run {daemon_file, agent_file, user, cur_pid, source_dir}
set unload_service to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist || true;"
set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;"
set copy_files to "rm -rf /Applications/RustDesk.app && cp -r " & source_dir & " /Applications && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app;"
set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;"
set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
set sh to unload_service & kill_others & copy_files & sh1 & sh2 & sh3
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
end run

View File

@ -230,7 +230,7 @@ extern "C"
return IsWindows10OrGreater();
}
HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid)
HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, BOOL show, DWORD *pDwTokenPid)
{
HANDLE hProcess = NULL;
HANDLE hToken = NULL;
@ -240,6 +240,11 @@ extern "C"
ZeroMemory(&si, sizeof si);
si.cb = sizeof si;
si.dwFlags = STARTF_USESHOWWINDOW;
if (show)
{
si.lpDesktop = (LPWSTR)L"winsta0\\default";
si.wShowWindow = SW_SHOW;
}
wchar_t buf[MAX_PATH];
wcscpy_s(buf, sizeof(buf), cmd);
PROCESS_INFORMATION pi;

View File

@ -13,7 +13,9 @@ use hbb_common::{
libc::{c_int, wchar_t},
log,
message_proto::{DisplayInfo, Resolution, WindowsSession},
sleep, timeout, tokio,
sleep,
sysinfo::{Pid, System},
timeout, tokio,
};
use std::{
collections::HashMap,
@ -484,6 +486,7 @@ extern "C" {
cmd: *const u16,
session_id: DWORD,
as_user: BOOL,
show: BOOL,
token_pid: &mut DWORD,
) -> HANDLE;
fn GetSessionUserTokenWin(
@ -669,6 +672,10 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
"\"{}\" --server",
std::env::current_exe()?.to_str().unwrap_or("")
);
launch_privileged_process(session_id, &cmd)
}
pub fn launch_privileged_process(session_id: DWORD, cmd: &str) -> ResultType<HANDLE> {
use std::os::windows::ffi::OsStrExt;
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
.encode_wide()
@ -676,9 +683,12 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
.collect();
let wstr = wstr.as_ptr();
let mut token_pid = 0;
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE, &mut token_pid) };
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE, FALSE, &mut token_pid) };
if h.is_null() {
log::error!("Failed to launch server: {}", io::Error::last_os_error());
log::error!(
"Failed to launch privileged process: {}",
io::Error::last_os_error()
);
if token_pid == 0 {
log::error!("No process winlogon.exe");
}
@ -687,22 +697,43 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
}
pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
let cmd = format!(
"\"{}\" {}",
std::env::current_exe()?.to_str().unwrap_or(""),
arg.join(" "),
);
run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false)
}
pub fn run_exe_in_cur_session(
exe: &str,
arg: Vec<&str>,
show: bool,
) -> ResultType<Option<std::process::Child>> {
let Some(session_id) = get_current_process_session_id() else {
bail!("Failed to get current process session id");
};
run_exe_in_session(exe, arg, session_id, show)
}
pub fn run_exe_in_session(
exe: &str,
arg: Vec<&str>,
session_id: DWORD,
show: bool,
) -> ResultType<Option<std::process::Child>> {
use std::os::windows::ffi::OsStrExt;
let cmd = format!("\"{}\" {}", exe, arg.join(" "),);
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
.encode_wide()
.chain(Some(0).into_iter())
.collect();
let wstr = wstr.as_ptr();
let mut token_pid = 0;
let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE, &mut token_pid) };
let h = unsafe {
LaunchProcessWin(
wstr,
session_id,
TRUE,
if show { TRUE } else { FALSE },
&mut token_pid,
)
};
if h.is_null() {
if token_pid == 0 {
bail!(
@ -800,8 +831,12 @@ pub fn set_share_rdp(enable: bool) {
}
pub fn get_current_process_session_id() -> Option<u32> {
get_session_id_of_process(unsafe { GetCurrentProcessId() })
}
pub fn get_session_id_of_process(pid: DWORD) -> Option<u32> {
let mut sid = 0;
if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut sid) == TRUE } {
if unsafe { ProcessIdToSessionId(pid, &mut sid) == TRUE } {
Some(sid)
} else {
None
@ -1348,6 +1383,9 @@ copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\
")
};
// Remember to check if `update_me` need to be changed if changing the `cmds`.
// No need to merge the existing dup code, because the code in these two functions are too critical.
// New code should be written in a common function.
let cmds = format!(
"
{uninstall_str}
@ -2366,6 +2404,171 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
std::process::exit(0);
}
pub fn update_me(debug: bool) -> ResultType<()> {
let app_name = crate::get_app_name();
let src_exe = std::env::current_exe()?.to_string_lossy().to_string();
let (subkey, path, _, exe) = get_install_info();
let is_installed = std::fs::metadata(&exe).is_ok();
if !is_installed {
bail!("{} is not installed.", &app_name);
}
let app_exe_name = &format!("{}.exe", &app_name);
let main_window_pids =
crate::platform::get_pids_of_process_with_args::<_, &str>(&app_exe_name, &[]);
let main_window_sessions = main_window_pids
.iter()
.map(|pid| get_session_id_of_process(pid.as_u32()))
.flatten()
.collect::<Vec<_>>();
kill_process_by_pids(&app_exe_name, main_window_pids)?;
let tray_pids = crate::platform::get_pids_of_process_with_args(&app_exe_name, &["--tray"]);
let tray_sessions = tray_pids
.iter()
.map(|pid| get_session_id_of_process(pid.as_u32()))
.flatten()
.collect::<Vec<_>>();
kill_process_by_pids(&app_exe_name, tray_pids)?;
let is_service_running = is_self_service_running();
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 meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
let size = meta.len() / 1024;
let reg_cmd = format!(
"
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\"
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
",
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
);
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
let restore_service_cmd = if is_service_running {
format!("sc start {}", &app_name)
} else {
"".to_owned()
};
// We do not try to remove all files in the old version.
// Because I don't know whether additional files will be installed here after installation, such as drivers.
// Just copy files to the installation directory works fine.
//if exist \"{path}\" rd /s /q \"{path}\"
// md \"{path}\"
//
// We need `taskkill` because:
// 1. There may be some other processes like `rustdesk --connect` are running.
// 2. Sometimes, the main window and the tray icon are showing
// while I cannot find them by `tasklist` or the methods above.
// There's should be 4 processes running: service, server, tray and main window.
// But only 2 processes are shown in the tasklist.
let cmds = format!(
"
chcp 65001
sc stop {app_name}
taskkill /F /IM {app_name}.exe{filter}
{reg_cmd}
{copy_exe}
{restore_service_cmd}
{sleep}
",
app_name = app_name,
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
sleep = if debug { "timeout 300" } else { "" },
);
run_cmds(cmds, debug, "update")?;
std::thread::sleep(std::time::Duration::from_millis(2000));
if tray_sessions.is_empty() {
log::info!("No tray process found.");
} else {
log::info!("Try to restore the tray process...");
log::info!(
"Try to restore the tray process..., sessions: {:?}",
&tray_sessions
);
for s in tray_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
}
}
}
if main_window_sessions.is_empty() {
log::info!("No main window process found.");
} else {
log::info!("Try to restore the main window process...");
std::thread::sleep(std::time::Duration::from_millis(2000));
for s in main_window_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec![], s, true));
}
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
log::info!("Update completed.");
Ok(())
}
// Double confirm the process name
fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
let name = name.to_lowercase();
let s = System::new_all();
// No need to check all names of `pids` first, and kill them then.
// It's rare case that they're not matched.
for pid in pids {
if let Some(process) = s.process(pid) {
if process.name().to_lowercase() != name {
bail!("Failed to kill the process, the names are mismatched.");
}
if !process.kill() {
bail!("Failed to kill the process");
}
} else {
bail!("Failed to kill the process, the pid is not found");
}
}
Ok(())
}
// Don't launch tray app when running with `\qn`.
// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission.
// Or launching the main window from the tray app will cause the main window to be launched with administrator permission.
// 2. We are not able to launch the tray app if the UI is in the login screen.
// `fn update_me()` can handle the above cases, but for msi update, we need to do more work to handle the above cases.
// 1. Record the tray app session ids.
// 2. Do the update.
// 3. Restore the tray app sessions.
// `1` and `3` must be done in custom actions.
// We need also to handle the command line parsing to find the tray processes.
pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
let cmds = format!(
"chcp 65001 && msiexec /i {msi} {}",
if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" }
);
run_cmds(cmds, false, "update-msi")?;
Ok(())
}
pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
Ok(write_cmds(
format!(
@ -2450,23 +2653,6 @@ pub fn try_kill_broker() {
.spawn());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());
}
#[test]
fn test_get_unicode_char_by_vk() {
let chr = get_char_from_vk(0x41); // VK_A
assert_eq!(chr, Some('a'));
let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC
assert_eq!(chr, None)
}
}
pub fn message_box(text: &str) {
let mut text = text.to_owned();
let nodialog = std::env::var("NO_DIALOG").unwrap_or_default() == "Y";
@ -2974,3 +3160,264 @@ fn get_pids<S: AsRef<str>>(name: S) -> ResultType<Vec<u32>> {
Ok(pids)
}
pub fn is_msi_installed() -> std::io::Result<bool> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let uninstall_key = hklm.open_subkey(format!(
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}",
crate::get_app_name()
))?;
Ok(1 == uninstall_key.get_value::<u32, _>("WindowsInstaller")?)
}
#[cfg(not(target_pointer_width = "64"))]
fn get_pids_with_args_from_wmic_output<S2: AsRef<str>>(
output: std::borrow::Cow<'_, str>,
name: &str,
args: &[S2],
) -> Vec<hbb_common::sysinfo::Pid> {
// CommandLine=
// ProcessId=33796
//
// CommandLine=
// ProcessId=34668
//
// CommandLine="C:\Program Files\RustDesk\RustDesk.exe" --tray
// ProcessId=13728
//
// CommandLine="C:\Program Files\RustDesk\RustDesk.exe"
// ProcessId=10136
let mut pids = Vec::new();
let mut proc_found = false;
for line in output.lines() {
if line.starts_with("ProcessId=") {
if proc_found {
if let Ok(pid) = line["ProcessId=".len()..].trim().parse::<u32>() {
pids.push(hbb_common::sysinfo::Pid::from_u32(pid));
}
proc_found = false;
}
} else if line.starts_with("CommandLine=") {
proc_found = false;
let cmd = line["CommandLine=".len()..].trim().to_lowercase();
if args.is_empty() {
if cmd.ends_with(&name) || cmd.ends_with(&format!("{}\"", &name)) {
proc_found = true;
}
} else {
proc_found = args.iter().all(|arg| cmd.contains(arg.as_ref()));
}
}
}
pids
}
// Note the args are not compared strictly, only check if the args are contained in the command line.
// If we want to check the args strictly, we need to parse the command line and compare each arg.
// Maybe we have to introduce some external crate like `shell_words` to do this.
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn get_pids_with_args_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
args: &[S2],
) -> Vec<hbb_common::sysinfo::Pid> {
let name = name.as_ref().to_lowercase();
std::process::Command::new("wmic.exe")
.args([
"process",
"where",
&format!("name='{}'", name),
"get",
"commandline,processid",
"/value",
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|output| {
get_pids_with_args_from_wmic_output::<S2>(
String::from_utf8_lossy(&output.stdout),
&name,
args,
)
})
.unwrap_or_default()
}
#[cfg(not(target_pointer_width = "64"))]
fn get_pids_with_first_arg_from_wmic_output(
output: std::borrow::Cow<'_, str>,
name: &str,
arg: &str,
) -> Vec<hbb_common::sysinfo::Pid> {
let mut pids = Vec::new();
let mut proc_found = false;
for line in output.lines() {
if line.starts_with("ProcessId=") {
if proc_found {
if let Ok(pid) = line["ProcessId=".len()..].trim().parse::<u32>() {
pids.push(hbb_common::sysinfo::Pid::from_u32(pid));
}
proc_found = false;
}
} else if line.starts_with("CommandLine=") {
proc_found = false;
let cmd = line["CommandLine=".len()..].trim().to_lowercase();
if cmd.is_empty() {
continue;
}
if !arg.is_empty() && cmd.starts_with(arg) {
proc_found = true;
} else {
for x in [&format!("{}\"", name), &format!("{}", name)] {
if cmd.contains(x) {
let cmd = cmd.split(x).collect::<Vec<_>>()[1..].join("");
if arg.is_empty() {
if cmd.trim().is_empty() {
proc_found = true;
}
} else if cmd.trim().starts_with(arg) {
proc_found = true;
}
break;
}
}
}
}
}
pids
}
// Note the args are not compared strictly, only check if the args are contained in the command line.
// If we want to check the args strictly, we need to parse the command line and compare each arg.
// Maybe we have to introduce some external crate like `shell_words` to do this.
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
arg: S2,
) -> Vec<hbb_common::sysinfo::Pid> {
let name = name.as_ref().to_lowercase();
let arg = arg.as_ref().to_lowercase();
std::process::Command::new("wmic.exe")
.args([
"process",
"where",
&format!("name='{}'", name),
"get",
"commandline,processid",
"/value",
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|output| {
get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(&output.stdout),
&name,
&arg,
)
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());
}
#[test]
fn test_get_unicode_char_by_vk() {
let chr = get_char_from_vk(0x41); // VK_A
assert_eq!(chr, Some('a'));
let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC
assert_eq!(chr, None)
}
#[cfg(not(target_pointer_width = "64"))]
#[test]
fn test_get_pids_with_args_from_wmic_output() {
let output = r#"
CommandLine=
ProcessId=33796
CommandLine=
ProcessId=34668
CommandLine="C:\Program Files\testapp\TestApp.exe" --tray
ProcessId=13728
CommandLine="C:\Program Files\testapp\TestApp.exe"
ProcessId=10136
"#;
let name = "testapp.exe";
let args = vec!["--tray"];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 13728);
let args: Vec<&str> = vec![];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 10136);
let args = vec!["--other"];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 0);
}
#[cfg(not(target_pointer_width = "64"))]
#[test]
fn test_get_pids_with_first_arg_from_wmic_output() {
let output = r#"
CommandLine=
ProcessId=33796
CommandLine=
ProcessId=34668
CommandLine="C:\Program Files\testapp\TestApp.exe" --tray
ProcessId=13728
CommandLine="C:\Program Files\testapp\TestApp.exe"
ProcessId=10136
"#;
let name = "testapp.exe";
let arg = "--tray";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 13728);
let arg = "";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 10136);
let arg = "--other";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 0);
}
}

View File

@ -61,6 +61,10 @@ impl RendezvousMediator {
}
}
crate::hbbs_http::sync::start();
#[cfg(target_os = "windows")]
if crate::platform::is_installed() && !crate::is_custom_client() {
crate::updater::start_auto_update();
}
let mut nat_tested = false;
check_zombie();
let server = new_server();

View File

@ -338,6 +338,7 @@ class MyIdMenu: Reactor.Component {
<div .separator />
<li #allow-darktheme><span>{svg_checkmark}</span>{translate('Dark Theme')}</li>
<Languages />
<li #allow-auto-update><span>{svg_checkmark}</span>{translate('Auto update')}</li>
<li #about>{translate('About')} {" "}{handler.get_app_name()}</li>
</menu>
</popup>;

249
src/updater.rs Normal file
View File

@ -0,0 +1,249 @@
use crate::{common::do_check_software_update, hbbs_http::create_http_client};
use hbb_common::{bail, config, log, ResultType};
use std::{
io::{self, Write},
path::PathBuf,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::{channel, Receiver, Sender},
Mutex,
},
time::{Duration, Instant},
};
enum UpdateMsg {
CheckUpdate,
Exit,
}
lazy_static::lazy_static! {
static ref TX_MSG : Mutex<Sender<UpdateMsg>> = Mutex::new(start_auto_update_check());
}
static CONTROLLING_SESSION_COUNT: AtomicUsize = AtomicUsize::new(0);
const DUR_ONE_DAY: Duration = Duration::from_secs(60 * 60 * 24);
pub fn update_controlling_session_count(count: usize) {
CONTROLLING_SESSION_COUNT.store(count, Ordering::SeqCst);
}
pub fn start_auto_update() {
let _sender = TX_MSG.lock().unwrap();
}
#[allow(dead_code)]
pub fn manually_check_update() -> ResultType<()> {
let sender = TX_MSG.lock().unwrap();
sender.send(UpdateMsg::CheckUpdate)?;
Ok(())
}
#[allow(dead_code)]
pub fn stop_auto_update() {
let sender = TX_MSG.lock().unwrap();
sender.send(UpdateMsg::Exit).unwrap_or_default();
}
#[inline]
fn has_no_active_conns() -> bool {
let conns = crate::Connection::alive_conns();
conns.is_empty() && has_no_controlling_conns()
}
#[cfg(any(not(target_os = "windows"), feature = "flutter"))]
fn has_no_controlling_conns() -> bool {
CONTROLLING_SESSION_COUNT.load(Ordering::SeqCst) == 0
}
#[cfg(not(any(not(target_os = "windows"), feature = "flutter")))]
fn has_no_controlling_conns() -> bool {
let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase());
for arg in [
"--connect",
"--play",
"--file-transfer",
"--view-camera",
"--port-forward",
"--rdp",
] {
if !crate::platform::get_pids_of_process_with_first_arg(&app_exe, arg).is_empty() {
return false;
}
}
true
}
fn start_auto_update_check() -> Sender<UpdateMsg> {
let (tx, rx) = channel();
std::thread::spawn(move || start_auto_update_check_(rx));
return tx;
}
fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
std::thread::sleep(Duration::from_secs(30));
if let Err(e) = check_update(false) {
log::error!("Error checking for updates: {}", e);
}
const MIN_INTERVAL: Duration = Duration::from_secs(60 * 10);
const RETRY_INTERVAL: Duration = Duration::from_secs(60 * 30);
let mut last_check_time = Instant::now();
let mut check_interval = DUR_ONE_DAY;
loop {
let recv_res = rx_msg.recv_timeout(check_interval);
match &recv_res {
Ok(UpdateMsg::CheckUpdate) | Err(_) => {
if last_check_time.elapsed() < MIN_INTERVAL {
// log::debug!("Update check skipped due to minimum interval.");
continue;
}
// Don't check update if there are alive connections.
if !has_no_active_conns() {
check_interval = RETRY_INTERVAL;
continue;
}
if let Err(e) = check_update(matches!(recv_res, Ok(UpdateMsg::CheckUpdate))) {
log::error!("Error checking for updates: {}", e);
check_interval = RETRY_INTERVAL;
} else {
last_check_time = Instant::now();
check_interval = DUR_ONE_DAY;
}
}
Ok(UpdateMsg::Exit) => break,
}
}
}
fn check_update(manually: bool) -> ResultType<()> {
#[cfg(target_os = "windows")]
let is_msi = crate::platform::is_msi_installed()?;
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
return Ok(());
}
if !do_check_software_update().is_ok() {
// ignore
return Ok(());
}
let update_url = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone();
if update_url.is_empty() {
log::debug!("No update available.");
} else {
let download_url = update_url.replace("tag", "download");
let version = download_url.split('/').last().unwrap_or_default();
#[cfg(target_os = "windows")]
let download_url = if cfg!(feature = "flutter") {
format!(
"{}/rustdesk-{}-x86_64.{}",
download_url,
version,
if is_msi { "msi" } else { "exe" }
)
} else {
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
};
log::debug!("New version available: {}", &version);
let client = create_http_client();
let Some(file_path) = get_download_file_from_url(&download_url) else {
bail!("Failed to get the file path from the URL: {}", download_url);
};
let mut is_file_exists = false;
if file_path.exists() {
// Check if the file size is the same as the server file size
// If the file size is the same, we don't need to download it again.
let file_size = std::fs::metadata(&file_path)?.len();
let response = client.head(&download_url).send()?;
if !response.status().is_success() {
bail!("Failed to get the file size: {}", response.status());
}
let total_size = response
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse::<u64>().ok());
let Some(total_size) = total_size else {
bail!("Failed to get content length");
};
if file_size == total_size {
is_file_exists = true;
} else {
std::fs::remove_file(&file_path)?;
}
}
if !is_file_exists {
let response = client.get(&download_url).send()?;
if !response.status().is_success() {
bail!(
"Failed to download the new version file: {}",
response.status()
);
}
let file_data = response.bytes()?;
let mut file = std::fs::File::create(&file_path)?;
file.write_all(&file_data)?;
}
// We have checked if the `conns`` is empty before, but we need to check again.
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
// before the download, but not empty after the download.
if has_no_active_conns() {
#[cfg(target_os = "windows")]
update_new_version(is_msi, &version, &file_path);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
log::debug!("New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}", file_path.to_str());
if let Some(p) = file_path.to_str() {
if let Some(session_id) = crate::platform::get_current_process_session_id() {
if is_msi {
match crate::platform::update_me_msi(p, true) {
Ok(_) => {
log::debug!("New version \"{}\" updated.", version);
}
Err(e) => {
log::error!(
"Failed to install the new msi version \"{}\": {}",
version,
e
);
}
}
} else {
match crate::platform::launch_privileged_process(
session_id,
&format!("{} --update", p),
) {
Ok(h) => {
if h.is_null() {
log::error!("Failed to update to the new version: {}", version);
}
}
Err(e) => {
log::error!("Failed to run the new version: {}", e);
}
}
}
} else {
log::error!(
"Failed to get the current process session id, Error {}",
io::Error::last_os_error()
);
}
} else {
// unreachable!()
log::error!(
"Failed to convert the file path to string: {}",
file_path.display()
);
}
}
pub fn get_download_file_from_url(url: &str) -> Option<PathBuf> {
let filename = url.split('/').last()?;
Some(std::env::temp_dir().join(filename))
}