fix: update macos (#12578)

* fix: update macos

1. Use `ditto` instead of `cp -r`.
2. Add prompt for extracting dmg.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: error to err

Signed-off-by: fufesou <linlong1266@gmail.com>

* Refact: Remove "Extracting"

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2025-08-07 23:31:31 +08:00
committed by GitHub
parent e85989e9d9
commit 39b91911cb
4 changed files with 96 additions and 30 deletions

View File

@@ -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<UpdateProgress> {
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<UpdateProgress> {
_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<UpdateProgress> {
cancelQueryTimer();
platformFFI.unregisterEventHandler(
_eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
if (isMacOS) {
platformFFI.unregisterEventHandler(
_eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg);
}
super.dispose();
}
@@ -113,10 +127,13 @@ class UpdateProgressState extends State<UpdateProgress> {
}
}
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<UpdateProgress> {
final List<Widget> buttons = [
dialogButton('Download', onPressed: jumplink),
dialogButton('Retry', onPressed: retry),
if (!isExtractDmg) dialogButton('Retry', onPressed: retry),
dialogButton('Close', onPressed: close),
];
dialogManager.dismissAll();
@@ -194,6 +211,21 @@ class UpdateProgressState extends State<UpdateProgress> {
_onError('The download file size is 0.');
} else {
setState(() {});
if (isMacOS) {
bind.mainSetCommon(
key: 'extract-update-dmg', value: widget.downloadUrl);
_isExtracting.value = true;
} else {
updateMsgBox();
}
}
} else {
setState(() {});
}
}
}
void updateMsgBox() {
msgBox(
gFFI.sessionId,
'custom-nocancel',
@@ -208,23 +240,23 @@ class UpdateProgressState extends State<UpdateProgress> {
submitTimeout: 5,
);
}
Future<void> handleExtractUpdateDmg(Map<String, dynamic> evt) async {
_isExtracting.value = false;
if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) {
_onError(evt['err'] as String, isExtractDmg: true);
} else {
setState(() {});
}
updateMsgBox();
}
}
@override
Widget build(BuildContext context) {
return onDownloading(context);
}
Widget onDownloading(BuildContext context) {
final value = _totalSize == null
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],

View File

@@ -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);
}
}
}
}

View File

@@ -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() {

View File

@@ -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;"