mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-27 23:21:06 +03:00
feat(fs): delegate win --server file reading to CM (#13736)
- Route Windows server-to-client file reads through CM instead of the connection layer - Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages (ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult) - Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up on completion, cancellation, and errors - Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path validation on the CM side - Improve Flutter file transfer UX and robustness: - Use explicit percent/percentText progress fields - Derive speed and cancel actions from the active job - Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure - Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -275,7 +275,7 @@ class TransferJobSerdeData {
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
id: int.tryParse(d['id'].toString()) ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
path: d['dataSource'] ?? '',
|
||||
isRemote: d['isRemote'] ?? false,
|
||||
totalSize: d['totalSize'] ?? 0,
|
||||
finishedSize: d['finishedSize'] ?? 0,
|
||||
|
||||
@@ -113,6 +113,34 @@ class FileModel {
|
||||
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
|
||||
}
|
||||
|
||||
// This method fixes a deadlock that occurred when the previous code directly
|
||||
// called jobController.jobError(evt) in the job_error event handler.
|
||||
//
|
||||
// The problem with directly calling jobController.jobError():
|
||||
// 1. fetchDirectoryRecursiveToRemove(jobID) registers readRecursiveTasks[jobID]
|
||||
// and waits for completion
|
||||
// 2. If the remote has no permission (or some other errors), it returns a FileTransferError
|
||||
// 3. The error triggers job_error event, which called jobController.jobError()
|
||||
// 4. jobController.jobError() calls getJob(jobID) to find the job in jobTable
|
||||
// 5. But addDeleteDirJob() is called AFTER fetchDirectoryRecursiveToRemove(),
|
||||
// so the job doesn't exist yet in jobTable
|
||||
// 6. Result: jobController.jobError() does nothing useful, and
|
||||
// readRecursiveTasks[jobID] never completes, causing a 2s timeout
|
||||
//
|
||||
// Solution: Before calling jobController.jobError(), we first check if there's
|
||||
// a pending readRecursiveTasks with this ID and complete it with the error.
|
||||
void handleJobError(Map<String, dynamic> evt) {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id != null) {
|
||||
final err = evt['err']?.toString() ?? 'Unknown error';
|
||||
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
||||
}
|
||||
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
||||
// even if the event does not have a valid job ID. This allows for generic error handling
|
||||
// or logging of unexpected errors.
|
||||
jobController.jobError(evt);
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
@@ -591,8 +619,21 @@ class FileController {
|
||||
} else if (item.isDirectory) {
|
||||
title = translate("Not an empty directory");
|
||||
dialogManager?.showLoading(translate("Waiting"));
|
||||
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
final FileDirectory fd;
|
||||
try {
|
||||
fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
} catch (e) {
|
||||
dialogManager?.dismissAll();
|
||||
final dm = dialogManager;
|
||||
if (dm != null) {
|
||||
msgBox(sessionId, 'custom-error-nook-nocancel-hasclose',
|
||||
translate("Error"), e.toString(), '', dm);
|
||||
} else {
|
||||
debugPrint("removeAction error msgbox failed: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fd.path.isEmpty) {
|
||||
fd.path = item.path;
|
||||
}
|
||||
@@ -606,7 +647,7 @@ class FileController {
|
||||
item.name,
|
||||
false);
|
||||
if (confirm == true) {
|
||||
sendRemoveEmptyDir(
|
||||
await sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
@@ -647,7 +688,7 @@ class FileController {
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
@@ -660,7 +701,7 @@ class FileController {
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -755,9 +796,9 @@ class FileController {
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
Future<void> sendRemoveEmptyDir(String path, int fileNum, int actId) async {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
await bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
@@ -1275,6 +1316,15 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Complete a pending recursive read task with an error.
|
||||
// See FileModel.handleJobError() for why this is necessary.
|
||||
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
||||
final completer = readRecursiveTasks.remove(id);
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.completeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FileDirectory>> readEmptyDirs(
|
||||
String path, bool isLocal, bool showHidden) async {
|
||||
try {
|
||||
@@ -1438,6 +1488,10 @@ class JobProgress {
|
||||
var err = "";
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
double get percent =>
|
||||
totalSize > 0 ? (finishedSize.toDouble() / totalSize) : 0.0;
|
||||
String get percentText => '${(percent * 100).toStringAsFixed(0)}%';
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
|
||||
@@ -363,7 +363,7 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
parent.target?.fileModel.handleJobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.postOverrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
|
||||
Reference in New Issue
Block a user