mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-06-06 17:32:51 +08:00
feat, update, win, macos (#11618)
Some checks failed
Some checks failed
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
parent
62276b4f4f
commit
ca00706a38
@ -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,
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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, "", () {});
|
||||
|
@ -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)) {
|
||||
|
234
flutter/lib/desktop/widgets/update_progress.dart
Normal file
234
flutter/lib/desktop/widgets/update_progress.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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="'Y'") AND (NOT CC_CONNECTION_TYPE="outgoing")"/>
|
||||
<Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(LAUNCH_TRAY_APP="Y" OR LAUNCH_TRAY_APP="1") AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE="'Y'") AND (NOT CC_CONNECTION_TYPE="outgoing")"/>
|
||||
|
||||
<!-- 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.
|
||||
|
@ -9,6 +9,8 @@
|
||||
<!--STOP_SERVICE is set to 'Y'. Because the cofig value may be empty or 'Y'-->
|
||||
<Property Id="STOP_SERVICE" Value="'Y'" />
|
||||
|
||||
<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
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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(
|
||||
|
@ -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
274
src/hbbs_http/downloader.rs
Normal 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);
|
||||
}
|
23
src/ipc.rs
23
src/ipc.rs
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 _
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
@ -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
|
18
src/platform/privileges_scripts/update.scpt
Normal file
18
src/platform/privileges_scripts/update.scpt
Normal 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
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
249
src/updater.rs
Normal 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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user