diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart index ac425fa2b..fd948b790 100644 --- a/flutter/lib/desktop/widgets/update_progress.dart +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -7,7 +7,10 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher.dart'; +final _isExtracting = false.obs; + void handleUpdate(String releasePageUrl) { + _isExtracting.value = false; String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); final String downloadFile = @@ -25,13 +28,14 @@ void handleUpdate(String releasePageUrl) { gFFI.dialogManager.dismissAll(); gFFI.dialogManager.show((setState, close, context) { return CustomAlertDialog( - title: Text(translate('Downloading {$appName}')), + title: Obx(() => Text(translate( + _isExtracting.isTrue ? 'Installing ...' : 'Downloading {$appName}'))), content: UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) .marginSymmetric(horizontal: 8) .paddingOnly(top: 12), actions: [ - dialogButton(translate('Cancel'), onPressed: () async { + if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async { onCanceled.value(); await bind.mainSetCommon( key: 'cancel-downloader', value: downloadId.value); @@ -71,6 +75,7 @@ class UpdateProgressState extends State { int _downloadedSize = 0; int _getDataFailedCount = 0; final String _eventKeyDownloadNewVersion = 'download-new-version'; + final String _eventKeyExtractUpdateDmg = 'extract-update-dmg'; @override void initState() { @@ -82,6 +87,11 @@ class UpdateProgressState extends State { _eventKeyDownloadNewVersion, handleDownloadNewVersion, replace: true); bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl); + if (isMacOS) { + platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg, + _eventKeyExtractUpdateDmg, handleExtractUpdateDmg, + replace: true); + } } @override @@ -89,6 +99,10 @@ class UpdateProgressState extends State { cancelQueryTimer(); platformFFI.unregisterEventHandler( _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion); + if (isMacOS) { + platformFFI.unregisterEventHandler( + _eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg); + } super.dispose(); } @@ -113,10 +127,13 @@ class UpdateProgressState extends State { } } - void _onError(String error) { + // `isExtractDmg` is true when handling extract-update-dmg event. + // It's a rare case that the dmg file is corrupted and cannot be extracted. + void _onError(String error, {bool isExtractDmg = false}) { cancelQueryTimer(); - debugPrint('Download new version error: $error'); + debugPrint( + '${isExtractDmg ? "Extract" : "Download"} new version error: $error'); final msgBoxType = 'custom-nocancel-nook-hasclose'; final msgBoxTitle = 'Error'; final msgBoxText = 'download-new-version-failed-tip'; @@ -138,7 +155,7 @@ class UpdateProgressState extends State { final List buttons = [ dialogButton('Download', onPressed: jumplink), - dialogButton('Retry', onPressed: retry), + if (!isExtractDmg) dialogButton('Retry', onPressed: retry), dialogButton('Close', onPressed: close), ]; dialogManager.dismissAll(); @@ -194,19 +211,13 @@ class UpdateProgressState extends State { _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, - ); + if (isMacOS) { + bind.mainSetCommon( + key: 'extract-update-dmg', value: widget.downloadUrl); + _isExtracting.value = true; + } else { + updateMsgBox(); + } } } else { setState(() {}); @@ -214,17 +225,38 @@ class UpdateProgressState extends State { } } - @override - Widget build(BuildContext context) { - return onDownloading(context); + void updateMsgBox() { + 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, + ); } - Widget onDownloading(BuildContext context) { - final value = _totalSize == null + Future handleExtractUpdateDmg(Map evt) async { + _isExtracting.value = false; + if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) { + _onError(evt['err'] as String, isExtractDmg: true); + } else { + updateMsgBox(); + } + } + + @override + Widget build(BuildContext context) { + getValue() => _totalSize == null ? 0.0 : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!); return LinearProgressIndicator( - value: value, + value: _isExtracting.isTrue ? null : getValue(), minHeight: 20, borderRadius: BorderRadius.circular(5), backgroundColor: Colors.grey[300], diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 58afae528..3e947609f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -629,7 +629,10 @@ pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.open_terminal(terminal_id, rows, cols); } else { - log::error!("[flutter_ffi] Session not found for session_id: {}", session_id); + log::error!( + "[flutter_ffi] Session not found for session_id: {}", + session_id + ); } } @@ -2651,6 +2654,21 @@ pub fn main_set_common(_key: String, _value: String) { fs::remove_file(f).ok(); } } + } else if _key == "extract-update-dmg" { + #[cfg(target_os = "macos")] + { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + if let Some(f) = new_version_file.to_str() { + crate::platform::macos::extract_update_dmg(f); + } else { + // unreachable!() + log::error!("Failed to get the new version file path"); + } + } else { + // unreachable!() + log::error!("Failed to get the new version file from url: {}", _value); + } + } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index ac5e47f67..c525af749 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -28,6 +28,7 @@ use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; use std::{ + collections::HashMap, os::unix::process::CommandExt, path::{Path, PathBuf}, process::{Command, Stdio}, @@ -743,7 +744,7 @@ pub fn update_me() -> ResultType<()> { 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 +pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app " with prompt "RustDesk wants to update itself" with administrator privileges "#, std::process::id(), @@ -775,11 +776,26 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes } pub fn update_to(file: &str) -> ResultType<()> { - extract_dmg(file, UPDATE_TEMP_DIR)?; update_extracted(UPDATE_TEMP_DIR)?; Ok(()) } +pub fn extract_update_dmg(file: &str) { + let mut evt: HashMap<&str, String> = + HashMap::from([("name", "extract-update-dmg".to_string())]); + match extract_dmg(file, UPDATE_TEMP_DIR) { + Ok(_) => { + log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); + } + Err(e) => { + evt.insert("err", e.to_string()); + log::error!("Failed to extract dmg file {}: {}", file, e); + } + } + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); +} + fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { let mount_point = "/Volumes/RustDeskUpdate"; let target_path = Path::new(target_dir); @@ -807,8 +823,8 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { 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]) + let copy_status = Command::new("ditto") + .args(&[&src_path, &dest_path]) .status()?; if !copy_status.success() { diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt index f9faa4aae..dffb70bd7 100644 --- a/src/platform/privileges_scripts/update.scpt +++ b/src/platform/privileges_scripts/update.scpt @@ -4,7 +4,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} 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 copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /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;"