mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-14 11:37:12 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d934318b |
@@ -1984,7 +1984,6 @@ jobs:
|
||||
sudo apt-get install -y libarchive-tools libfuse2
|
||||
# set-up appimage-builder
|
||||
# https://github.com/AppImage/AppImageKit/issues/1395
|
||||
sudo pip3 install "setuptools_scm<10"
|
||||
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
|
||||
# run appimage-builder
|
||||
pushd appimage
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
name: wf-cliprdr CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: wf_cliprdr invariant test
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Setup vcpkg with GitHub Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependency
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
|
||||
|
||||
- name: Build test
|
||||
shell: pwsh
|
||||
run: |
|
||||
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
|
||||
New-Item -ItemType Directory -Force $testRoot | Out-Null
|
||||
|
||||
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
|
||||
$cmakeLists = @(
|
||||
'cmake_minimum_required(VERSION 3.20)'
|
||||
'project(test_invariant_wf_cliprdr C)'
|
||||
''
|
||||
'set(CMAKE_C_STANDARD 11)'
|
||||
'set(CMAKE_C_STANDARD_REQUIRED ON)'
|
||||
'set(CMAKE_C_EXTENSIONS OFF)'
|
||||
''
|
||||
'find_package(check CONFIG REQUIRED)'
|
||||
''
|
||||
'add_executable(test_invariant_wf_cliprdr'
|
||||
' "TEST_SOURCE"'
|
||||
')'
|
||||
''
|
||||
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
|
||||
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
|
||||
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
|
||||
')'
|
||||
) -join [Environment]::NewLine
|
||||
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
|
||||
|
||||
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
|
||||
cmake --build (Join-Path $testRoot 'out') --config Release
|
||||
|
||||
- name: Run test
|
||||
shell: pwsh
|
||||
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe
|
||||
+1
-1
@@ -213,7 +213,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
|
||||
@@ -8,7 +8,6 @@ package com.carriez.flutter_hbb
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.content.Intent
|
||||
import android.graphics.Path
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -69,16 +68,6 @@ class InputService : AccessibilityService() {
|
||||
get() = ctx != null
|
||||
}
|
||||
|
||||
private fun notifyInputState() {
|
||||
val inputState = isOpen.toString()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to inputState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val logTag = "input service"
|
||||
private var leftIsDown = false
|
||||
private var touchPath = Path()
|
||||
@@ -727,7 +716,6 @@ class InputService : AccessibilityService() {
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
ctx = this
|
||||
notifyInputState()
|
||||
val info = AccessibilityServiceInfo()
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
@@ -746,16 +734,8 @@ class InputService : AccessibilityService() {
|
||||
|
||||
override fun onDestroy() {
|
||||
ctx = null
|
||||
// Keep this fallback even though onUnbind usually notifies first.
|
||||
notifyInputState()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
ctx = null
|
||||
notifyInputState()
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
override fun onInterrupt() {}
|
||||
}
|
||||
|
||||
@@ -200,13 +200,12 @@ class MainActivity : FlutterActivity() {
|
||||
"stop_input" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
InputService.ctx?.disableSelf()
|
||||
} else {
|
||||
InputService.ctx = null
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
}
|
||||
InputService.ctx = null
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
"cancel_notification" -> {
|
||||
|
||||
@@ -483,15 +483,13 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final incomingOnly = bind.isIncomingOnly();
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||
final children = <Widget>[
|
||||
if (!isWeb && !incomingOnly)
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
kOptionEnableConfirmClosingTabs,
|
||||
isServer: false),
|
||||
if (!incomingOnly)
|
||||
if (!bind.isIncomingOnly())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'allow-remote-toolbar-docking-any-edge',
|
||||
@@ -501,10 +499,9 @@ class _GeneralState extends State<_General> {
|
||||
reloadAllWindows();
|
||||
},
|
||||
),
|
||||
if (!isWeb && !outgoingOnly)
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
if (!isWeb) wallpaper(),
|
||||
if (!isWeb && !incomingOnly) ...[
|
||||
if (!isWeb && !bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -543,40 +540,40 @@ class _GeneralState extends State<_General> {
|
||||
isServer: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
kOptionEnableCheckUpdate,
|
||||
isServer: false,
|
||||
),
|
||||
if (showAutoUpdate)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Auto update',
|
||||
kOptionAllowAutoUpdate,
|
||||
isServer: true,
|
||||
),
|
||||
if (isWindows && !outgoingOnly)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Capture screen using DirectX',
|
||||
kOptionDirectxCapture,
|
||||
),
|
||||
if (!isWeb && !incomingOnly) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable UDP hole punching',
|
||||
kOptionEnableUdpPunch,
|
||||
isServer: false,
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable IPv6 P2P connection',
|
||||
kOptionEnableIpv6Punch,
|
||||
isServer: false,
|
||||
),
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
kOptionEnableCheckUpdate,
|
||||
isServer: false,
|
||||
),
|
||||
if (showAutoUpdate)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Auto update',
|
||||
kOptionAllowAutoUpdate,
|
||||
isServer: true,
|
||||
),
|
||||
if (isWindows && !bind.isOutgoingOnly())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Capture screen using DirectX',
|
||||
kOptionDirectxCapture,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable UDP hole punching',
|
||||
kOptionEnableUdpPunch,
|
||||
isServer: false,
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable IPv6 P2P connection',
|
||||
kOptionEnableIpv6Punch,
|
||||
isServer: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2474,7 +2471,7 @@ class _AboutState extends State<_About> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
|
||||
@@ -95,13 +95,6 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Auto-close tab when shell exits
|
||||
_terminalModel.onClosed = () {
|
||||
if (mounted) {
|
||||
widget.tabController.closeBy(widget.tabKey);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
@@ -1167,8 +1167,8 @@ class _MonitorMenu extends StatelessWidget {
|
||||
tooltip: isMulti
|
||||
? ''
|
||||
: isAllMonitors
|
||||
? 'All monitors'
|
||||
: '#{${i + 1}} monitor',
|
||||
? 'all monitors'
|
||||
: '#${i + 1} monitor',
|
||||
hMargin: isMulti ? null : 6,
|
||||
vMargin: isMulti ? null : 12,
|
||||
topLevel: false,
|
||||
@@ -2852,7 +2852,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
|
||||
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
|
||||
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
|
||||
button = Tooltip(
|
||||
message: translate(widget.tooltip),
|
||||
message: widget.tooltip,
|
||||
child: button,
|
||||
);
|
||||
if (widget.topLevel) {
|
||||
|
||||
@@ -83,13 +83,6 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Auto-close connection when shell exits
|
||||
_terminalModel.onClosed = () {
|
||||
if (mounted) {
|
||||
closeConnection(id: widget.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||
// terminal extra keys bar is unnecessary and disabled.
|
||||
_showTerminalExtraKeys = !isWebDesktop &&
|
||||
|
||||
@@ -142,22 +142,12 @@ class FileModel {
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id == null || !jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore stale override confirm event: $evt");
|
||||
return;
|
||||
}
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
}
|
||||
|
||||
Future<void> overrideFileConfirm(Map<String, dynamic> evt,
|
||||
{bool? overrideConfirm, bool skip = false}) async {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
|
||||
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore override confirm for inactive job: $evt");
|
||||
return;
|
||||
}
|
||||
// If `skip == true`, it means to skip this file without showing dialog.
|
||||
// Because `resp` may be null after the user operation or the last remembered operation,
|
||||
// and we should distinguish them.
|
||||
@@ -166,12 +156,15 @@ class FileModel {
|
||||
? await showFileConfirmDialog(translate("Overwrite"),
|
||||
"${evt['read_path']}", true, evt['is_identical'] == "true")
|
||||
: null);
|
||||
if (!jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore override confirm result for inactive job: $evt");
|
||||
return;
|
||||
}
|
||||
final id = int.tryParse(evt['id']) ?? 0;
|
||||
if (false == resp) {
|
||||
await jobController.cancelTransferConflictBatch(id);
|
||||
final jobIndex = jobController.getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
await jobController.cancelJob(id);
|
||||
final job = jobController.jobTable[jobIndex];
|
||||
job.state = JobState.done;
|
||||
jobController.jobTable.refresh();
|
||||
}
|
||||
} else {
|
||||
var need_override = false;
|
||||
if (resp == null) {
|
||||
@@ -183,7 +176,6 @@ class FileModel {
|
||||
}
|
||||
// Update the loop config.
|
||||
if (fileConfirmCheckboxRemember) {
|
||||
jobController.rememberTransferConflictBatch(id, resp);
|
||||
evtLoop.setSkip(!need_override);
|
||||
}
|
||||
await bind.sessionSetConfirmOverrideFile(
|
||||
@@ -293,8 +285,6 @@ class FileModel {
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final jobID = jobController.addTransferJob(entry, false);
|
||||
jobController.registerTransferConflictBatch([jobID],
|
||||
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
|
||||
webSendLocalFiles(
|
||||
handleIndex: handleIndex,
|
||||
actId: jobID,
|
||||
@@ -580,15 +570,8 @@ class FileController {
|
||||
final toPath = otherSideData.directory.path;
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final transferJobs = <(Entry, int)>[];
|
||||
final transferJobIds = <int>[];
|
||||
for (var from in items.items) {
|
||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||
transferJobs.add((from, jobID));
|
||||
transferJobIds.add(jobID);
|
||||
}
|
||||
jobController.registerTransferConflictBatch(transferJobIds);
|
||||
for (final (from, jobID) in transferJobs) {
|
||||
bind.sessionSendFiles(
|
||||
sessionId: sessionId,
|
||||
actId: jobID,
|
||||
@@ -934,10 +917,6 @@ class JobController {
|
||||
static final JobID jobID = JobID();
|
||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||
int _nextTransferConflictBatchId = 1;
|
||||
final Map<int, int> _transferConflictJobToBatch = {};
|
||||
int? _transferConflictRememberBatchId;
|
||||
bool? _transferConflictRememberOverrideConfirm;
|
||||
final GetSessionID getSessionID;
|
||||
final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
@@ -950,57 +929,6 @@ class JobController {
|
||||
return jobTable.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
|
||||
final ids = jobIds.toList(growable: false);
|
||||
if (ids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
batchId ??= _nextTransferConflictBatchId++;
|
||||
if (batchId >= _nextTransferConflictBatchId) {
|
||||
_nextTransferConflictBatchId = batchId + 1;
|
||||
}
|
||||
for (final jobId in ids) {
|
||||
_transferConflictJobToBatch[jobId] = batchId;
|
||||
}
|
||||
}
|
||||
|
||||
int? transferConflictBatchId(int jobId) {
|
||||
return _transferConflictJobToBatch[jobId];
|
||||
}
|
||||
|
||||
bool hasTransferConflictJob(int jobId) {
|
||||
return transferConflictBatchId(jobId) != null;
|
||||
}
|
||||
|
||||
bool isTransferConflictRememberBatch(int? batchId) {
|
||||
return batchId != null && batchId == _transferConflictRememberBatchId;
|
||||
}
|
||||
|
||||
bool? transferConflictRememberOverrideConfirm(int? batchId) {
|
||||
if (!isTransferConflictRememberBatch(batchId)) {
|
||||
return null;
|
||||
}
|
||||
return _transferConflictRememberOverrideConfirm;
|
||||
}
|
||||
|
||||
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
|
||||
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
|
||||
_transferConflictRememberOverrideConfirm = overrideConfirm;
|
||||
}
|
||||
|
||||
void unregisterTransferConflictJob(int jobId) {
|
||||
final batchId = _transferConflictJobToBatch.remove(jobId);
|
||||
if (batchId == null) {
|
||||
return;
|
||||
}
|
||||
if (!_transferConflictJobToBatch.containsValue(batchId)) {
|
||||
if (_transferConflictRememberBatchId == batchId) {
|
||||
_transferConflictRememberBatchId = null;
|
||||
_transferConflictRememberOverrideConfirm = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return jobID
|
||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||
final jobID = JobController.jobID.next();
|
||||
@@ -1072,10 +1000,7 @@ class JobController {
|
||||
id = int.parse(evt['id']);
|
||||
} catch (_) {}
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex == -1) {
|
||||
unregisterTransferConflictJob(id);
|
||||
return true;
|
||||
}
|
||||
if (jobIndex == -1) return true;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.deleteFile) {
|
||||
@@ -1101,9 +1026,6 @@ class JobController {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.state == JobState.done || job.state == JobState.error) {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
if (job.type == JobType.deleteDir) {
|
||||
return job.state == JobState.done;
|
||||
} else {
|
||||
@@ -1113,15 +1035,9 @@ class JobController {
|
||||
|
||||
void jobError(Map<String, dynamic> evt) {
|
||||
final err = evt['err'].toString();
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id == null) {
|
||||
debugPrint("Ignore job error with invalid id: $evt");
|
||||
return;
|
||||
}
|
||||
int jobIndex = getJob(id);
|
||||
int jobIndex = getJob(int.parse(evt['id']));
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
if (job.state == JobState.done && job.err == "cancel") return;
|
||||
job.state = JobState.error;
|
||||
job.err = err;
|
||||
job.recvJobRes = true;
|
||||
@@ -1144,11 +1060,6 @@ class JobController {
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.state == JobState.done || job.state == JobState.error) {
|
||||
unregisterTransferConflictJob(job.id);
|
||||
}
|
||||
} else {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
if (err == _kOneWayFileTransferError) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
||||
@@ -1185,42 +1096,9 @@ class JobController {
|
||||
}
|
||||
|
||||
Future<void> cancelJob(int id) async {
|
||||
unregisterTransferConflictJob(id);
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
|
||||
Future<void> cancelTransferConflictBatch(int jobId) async {
|
||||
final batchId = _transferConflictJobToBatch[jobId];
|
||||
final batchJobIds = batchId == null ? [jobId] : <int>[];
|
||||
if (batchId != null) {
|
||||
for (final entry in _transferConflictJobToBatch.entries) {
|
||||
if (entry.value == batchId) {
|
||||
batchJobIds.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (final id in batchJobIds) {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
}
|
||||
final jobIdsToCancel = batchJobIds.toSet();
|
||||
for (final job in jobTable) {
|
||||
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
|
||||
continue;
|
||||
}
|
||||
job.state = JobState.done;
|
||||
job.err = "cancel";
|
||||
job.recvJobRes = true;
|
||||
}
|
||||
jobTable.refresh();
|
||||
for (final id in batchJobIds) {
|
||||
try {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
||||
debugPrint("load last job: $evt");
|
||||
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||
@@ -1267,7 +1145,7 @@ class JobController {
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
}
|
||||
registerTransferConflictBatch([currJobId]);
|
||||
|
||||
await bind.sessionAddJob(
|
||||
sessionId: sessionId,
|
||||
isRemote: isRemote,
|
||||
@@ -1315,9 +1193,6 @@ class JobController {
|
||||
|
||||
void clear() {
|
||||
jobTable.clear();
|
||||
_transferConflictJobToBatch.clear();
|
||||
_transferConflictRememberBatchId = null;
|
||||
_transferConflictRememberOverrideConfirm = null;
|
||||
jobResultListener.clear();
|
||||
}
|
||||
}
|
||||
@@ -1660,9 +1535,6 @@ class JobProgress {
|
||||
|
||||
String display() {
|
||||
if (type == JobType.transfer) {
|
||||
if (state == JobState.done && err == "cancel") {
|
||||
return translate("Cancel");
|
||||
}
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
@@ -1972,44 +1844,21 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
|
||||
|
||||
class FileDialogEventLoop
|
||||
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
|
||||
int? _batchId;
|
||||
bool? _overrideConfirm;
|
||||
bool _skip = false;
|
||||
|
||||
@override
|
||||
Future<void> onPreConsume(
|
||||
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
|
||||
final event = evt as _FileDialogEvent;
|
||||
final model = event.fileModel.target;
|
||||
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
|
||||
final batchId = model == null || jobId == null
|
||||
? null
|
||||
: model.jobController.transferConflictBatchId(jobId);
|
||||
final keepRemembered = model != null &&
|
||||
model.jobController.isTransferConflictRememberBatch(batchId);
|
||||
// The loop only preloads the remembered batch choice. The model updates it
|
||||
// after the user answers the current overwrite dialog.
|
||||
if (_batchId != batchId && !keepRemembered) {
|
||||
_batchId = batchId;
|
||||
_overrideConfirm = null;
|
||||
_skip = false;
|
||||
} else {
|
||||
_batchId = batchId;
|
||||
}
|
||||
if (keepRemembered) {
|
||||
_overrideConfirm =
|
||||
model.jobController.transferConflictRememberOverrideConfirm(batchId);
|
||||
_skip = _overrideConfirm == null;
|
||||
}
|
||||
var event = evt as _FileDialogEvent;
|
||||
event.setOverrideConfirm(_overrideConfirm);
|
||||
event.setSkip(_skip);
|
||||
debugPrint(
|
||||
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
||||
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onEventsClear() {
|
||||
_batchId = null;
|
||||
_overrideConfirm = null;
|
||||
_skip = false;
|
||||
return super.onEventsClear();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@@ -37,10 +38,6 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||
|
||||
/// Called when the terminal session ends (shell exits).
|
||||
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
||||
VoidCallback? onClosed;
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
|
||||
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
|
||||
@@ -250,33 +247,6 @@ class TerminalModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
|
||||
if (evt.containsKey('exit_code')) {
|
||||
final v = evt['exit_code'];
|
||||
if (v is int) {
|
||||
// Desktop and mobile send exit_code as an int
|
||||
return v;
|
||||
} else if (v is String) {
|
||||
// Web sends exit_code as a string
|
||||
final parsed = int.tryParse(v);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Event does not contain exit_code');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||
final String? type = evt['type'];
|
||||
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
||||
@@ -499,12 +469,10 @@ class TerminalModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = getExitCodeFromEvt(evt);
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
// Auto-close the tab/page
|
||||
onClosed?.call();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
|
||||
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
|
||||
|
||||
@@ -89,11 +89,11 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
|
||||
VALUE "CompanyName", "Purslane Ltd" "\0"
|
||||
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "rustdesk" "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "rustdesk.exe" "\0"
|
||||
VALUE "ProductName", "RustDesk" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
|
||||
@@ -533,7 +533,7 @@ impl FuseServer {
|
||||
offset: i64,
|
||||
size: u32,
|
||||
) -> Result<Vec<u8>, std::io::Error> {
|
||||
let request_stream_id = rand::random();
|
||||
// todo: async and concurrent read, generate stream_id per request
|
||||
let cb_requested = unsafe {
|
||||
// convert `size` from u32 to i32
|
||||
// yet with same bit representation
|
||||
@@ -543,7 +543,7 @@ impl FuseServer {
|
||||
let (n_position_high, n_position_low) =
|
||||
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
|
||||
let request = ClipboardFile::FileContentsRequest {
|
||||
stream_id: request_stream_id,
|
||||
stream_id: node.stream_id,
|
||||
list_index: node.index as i32,
|
||||
dw_flags: 2,
|
||||
n_position_low,
|
||||
@@ -573,7 +573,7 @@ impl FuseServer {
|
||||
stream_id,
|
||||
requested_data,
|
||||
} => {
|
||||
if stream_id != request_stream_id {
|
||||
if stream_id != node.stream_id {
|
||||
log::debug!("stream id mismatch, ignore");
|
||||
continue;
|
||||
}
|
||||
@@ -611,6 +611,11 @@ struct FuseNode {
|
||||
/// connection id
|
||||
pub conn_id: i32,
|
||||
|
||||
// todo: use stream_id to identify a FileContents request-reply
|
||||
// instead of a whole file
|
||||
/// stream id
|
||||
pub stream_id: i32,
|
||||
|
||||
/// file index in peer's file list
|
||||
/// NOTE:
|
||||
/// it is NOT the same as inode, this is the index in the file list
|
||||
@@ -634,6 +639,7 @@ impl FuseNode {
|
||||
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
|
||||
Self {
|
||||
conn_id: desc.conn_id,
|
||||
stream_id: rand::random(),
|
||||
index: inode as usize - 2,
|
||||
name: desc
|
||||
.name
|
||||
@@ -650,6 +656,7 @@ impl FuseNode {
|
||||
pub fn new_root() -> Self {
|
||||
Self {
|
||||
conn_id: 0,
|
||||
stream_id: rand::random(),
|
||||
index: 0,
|
||||
name: String::from("/"),
|
||||
parent: None,
|
||||
|
||||
@@ -4,24 +4,23 @@ use super::filetype::FileDescription;
|
||||
use crate::{ClipboardFile, CliprdrError};
|
||||
use cs::FuseServer;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{config::Config, log};
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||
let mnt_path = fuse_mount_point("cliprdr-client");
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||
let mnt_path = fuse_mount_point("cliprdr-server");
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
@@ -32,21 +31,6 @@ lazy_static::lazy_static! {
|
||||
|
||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum MountPointState {
|
||||
HealthyMount,
|
||||
NotMounted,
|
||||
StaleMount,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn fuse_mount_point(name: &str) -> String {
|
||||
let mut path = PathBuf::from(Config::ipc_path(""));
|
||||
path.pop();
|
||||
path.push(name);
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||
if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
@@ -69,27 +53,8 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if let Some(ctx) = fuse_context_lock.as_ref() {
|
||||
match inspect_mount_point_state(&ctx.mount_point) {
|
||||
MountPointState::HealthyMount => return Ok(()),
|
||||
MountPointState::StaleMount | MountPointState::NotMounted => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is disconnected, remounting",
|
||||
ctx.mount_point.display()
|
||||
);
|
||||
let stale_context = fuse_context_lock.take();
|
||||
drop(fuse_context_lock);
|
||||
drop(stale_context);
|
||||
return init_fuse_context(is_client);
|
||||
}
|
||||
MountPointState::Unknown => {
|
||||
log::warn!(
|
||||
"failed to verify clipboard FUSE mount {}",
|
||||
ctx.mount_point.display()
|
||||
);
|
||||
return Err(CliprdrError::CliprdrInit);
|
||||
}
|
||||
}
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
@@ -98,32 +63,10 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
};
|
||||
|
||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||
match inspect_mount_point_state(&mount_point) {
|
||||
MountPointState::HealthyMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is already active in another context",
|
||||
mount_point.display()
|
||||
);
|
||||
return Err(CliprdrError::ClipboardOccupied);
|
||||
}
|
||||
MountPointState::StaleMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is stale, cleaning up before remount",
|
||||
mount_point.display()
|
||||
);
|
||||
unmount_fuse_mount_point(&mount_point);
|
||||
validate_mount_state_after_stale_cleanup(
|
||||
&mount_point,
|
||||
inspect_mount_point_state(&mount_point),
|
||||
)?;
|
||||
}
|
||||
MountPointState::Unknown => return Err(CliprdrError::CliprdrInit),
|
||||
MountPointState::NotMounted => {}
|
||||
}
|
||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
prepare_fuse_mount_point(&mount_point)?;
|
||||
prepare_fuse_mount_point(&mount_point);
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
@@ -216,246 +159,35 @@ struct FuseContext {
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> {
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
if let Some(parent) = mount_point.parent() {
|
||||
reject_symlink_path(parent)?;
|
||||
if let Err(e) = fs::create_dir_all(parent) {
|
||||
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
|
||||
return Err(CliprdrError::CliprdrInit);
|
||||
}
|
||||
}
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
reject_symlink_path(mount_point)?;
|
||||
|
||||
let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
|
||||
log::warn!(
|
||||
"failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
unmount_fuse_mount_point(mount_point);
|
||||
fs::create_dir_all(mount_point).map_err(|e| {
|
||||
log::error!(
|
||||
"failed to create clipboard FUSE mount point {} after cleanup: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
|
||||
log::warn!(
|
||||
"failed to set clipboard FUSE mount point permissions {}: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if !recovered_stale_mount {
|
||||
unmount_fuse_mount_point(mount_point);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inspect_mount_point_state(mount_point: &Path) -> MountPointState {
|
||||
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||
return MountPointState::Unknown;
|
||||
}
|
||||
inspect_mount_point_state_with(
|
||||
mount_point,
|
||||
std::fs::metadata(mount_point),
|
||||
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_mount_state_after_stale_cleanup(
|
||||
mount_point: &Path,
|
||||
mount_state: MountPointState,
|
||||
) -> Result<(), CliprdrError> {
|
||||
match mount_state {
|
||||
MountPointState::NotMounted => Ok(()),
|
||||
MountPointState::HealthyMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is still active after stale cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::ClipboardOccupied)
|
||||
}
|
||||
MountPointState::StaleMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is still stale after cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
MountPointState::Unknown => {
|
||||
log::warn!(
|
||||
"failed to verify clipboard FUSE mount {} after cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_mount_point_state_with<T>(
|
||||
mount_point: &Path,
|
||||
metadata_result: io::Result<T>,
|
||||
mountinfo_result: io::Result<String>,
|
||||
) -> MountPointState {
|
||||
match metadata_result {
|
||||
Ok(_) => match mountinfo_result {
|
||||
Ok(mountinfo) => {
|
||||
if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) {
|
||||
MountPointState::HealthyMount
|
||||
} else {
|
||||
MountPointState::NotMounted
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e);
|
||||
MountPointState::Unknown
|
||||
}
|
||||
},
|
||||
Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted,
|
||||
Err(e) => {
|
||||
log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e);
|
||||
MountPointState::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool {
|
||||
let mount_point = mount_point.to_string_lossy();
|
||||
mountinfo.lines().any(|line| {
|
||||
let mut fields = line.split_whitespace();
|
||||
let _mount_id = fields.next();
|
||||
let _parent_id = fields.next();
|
||||
let _major_minor = fields.next();
|
||||
let _root = fields.next();
|
||||
let mount_path = fields.next();
|
||||
mount_path == Some(mount_point.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_symlink_metadata_result(
|
||||
path: &Path,
|
||||
metadata_result: io::Result<std::fs::Metadata>,
|
||||
allow_disconnected_mount: bool,
|
||||
) -> Result<(), CliprdrError> {
|
||||
match metadata_result {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
log::warn!("refusing to use symlinked FUSE path {:?}", path);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => {
|
||||
log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> {
|
||||
reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false)
|
||||
}
|
||||
|
||||
fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> {
|
||||
if let Some(parent) = mount_point.parent() {
|
||||
reject_symlink_path(parent)?;
|
||||
}
|
||||
reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true)
|
||||
}
|
||||
|
||||
fn unmount_fuse_mount_point(mount_point: &Path) {
|
||||
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||
log::warn!(
|
||||
"refusing to unmount unsafe clipboard FUSE mount point {:?}",
|
||||
mount_point
|
||||
);
|
||||
return;
|
||||
}
|
||||
if inspect_mount_point_state_with(
|
||||
mount_point,
|
||||
std::fs::metadata(mount_point),
|
||||
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||
) == MountPointState::NotMounted
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (program, args) in unmount_command_candidates() {
|
||||
if run_unmount_command(program, args, mount_point) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::warn!(
|
||||
"failed to unmount clipboard FUSE mount point {:?}",
|
||||
mount_point
|
||||
);
|
||||
}
|
||||
|
||||
fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] {
|
||||
[
|
||||
("fusermount3", &["-uz"]),
|
||||
("fusermount", &["-uz"]),
|
||||
("umount", &["-l"]),
|
||||
]
|
||||
}
|
||||
|
||||
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
|
||||
match std::process::Command::new(program)
|
||||
.args(args)
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => {
|
||||
log::debug!(
|
||||
"{} {:?} exited with status {:?}",
|
||||
program,
|
||||
mount_point,
|
||||
status.code()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
|
||||
return false;
|
||||
}
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
let ctx = {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
fuse_context_lock.take()
|
||||
};
|
||||
drop(ctx);
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!(
|
||||
"unmounting clipboard FUSE from {}",
|
||||
self.mount_point.display()
|
||||
);
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,66 +223,3 @@ impl FuseContext {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{fs, io};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
#[test]
|
||||
fn classifies_mount_point_state_from_metadata_and_mountinfo() {
|
||||
let mount_point = std::env::temp_dir().join(format!(
|
||||
"rustdesk-fuse-mount-state-test-{}-{}",
|
||||
std::process::id(),
|
||||
line!()
|
||||
));
|
||||
let mountinfo = format!(
|
||||
"123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n",
|
||||
mount_point.display()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)),
|
||||
MountPointState::HealthyMount
|
||||
);
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())),
|
||||
MountPointState::NotMounted
|
||||
);
|
||||
|
||||
let disconnected_metadata: io::Result<()> =
|
||||
Err(io::Error::from_raw_os_error(libc::ENOTCONN));
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())),
|
||||
MountPointState::StaleMount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn rejects_symlink_mount_point() {
|
||||
let base = std::env::temp_dir().join(format!(
|
||||
"rustdesk-fuse-symlink-test-{}-{}",
|
||||
std::process::id(),
|
||||
line!()
|
||||
));
|
||||
let mount_parent = base.join("parent");
|
||||
let mount_point = mount_parent.join("cliprdr-client");
|
||||
let symlink_target = base.join("symlink-target");
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
fs::create_dir_all(&mount_parent).unwrap();
|
||||
fs::create_dir_all(&symlink_target).unwrap();
|
||||
symlink(&symlink_target, &mount_point).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
prepare_fuse_mount_point(&mount_point),
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
));
|
||||
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,16 +192,20 @@ impl LocalFile {
|
||||
});
|
||||
};
|
||||
|
||||
let read_result = if offset != self.offset.load(Ordering::Relaxed) {
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.and_then(|_| handle.read_exact(buf))
|
||||
} else {
|
||||
handle.read_exact(buf)
|
||||
};
|
||||
if let Err(e) = read_result {
|
||||
return Err(self.invalidate_handle(e));
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
self.offset.store(new_offset, Ordering::Relaxed);
|
||||
|
||||
@@ -213,15 +217,6 @@ impl LocalFile {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError {
|
||||
self.offset.store(0, Ordering::Relaxed);
|
||||
self.handle = None;
|
||||
CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
@@ -283,10 +278,7 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_list_test {
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use std::{path::PathBuf, sync::atomic::AtomicU64};
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
@@ -392,30 +384,4 @@ mod file_list_test {
|
||||
as_bin_parse_test("/test")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exact_at_reopens_after_read_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_path = std::env::temp_dir().join(format!(
|
||||
"rustdesk-clipboard-local-file-{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::write(&file_path, b"")?;
|
||||
|
||||
let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?;
|
||||
file.size = 1;
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
assert!(file.read_exact_at(&mut buf, 0).is_err());
|
||||
assert!(file.handle.is_none());
|
||||
assert_eq!(file.offset.load(Ordering::Relaxed), 0);
|
||||
|
||||
std::fs::write(&file_path, [42u8])?;
|
||||
|
||||
file.read_exact_at(&mut buf, 0)?;
|
||||
assert_eq!(buf, [42u8]);
|
||||
assert!(file.handle.is_none());
|
||||
|
||||
std::fs::remove_file(file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use hbb_common::{
|
||||
log,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
|
||||
use std::{path::PathBuf, sync::Arc, usize};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// local files are cached, this value should not be changed when copying files
|
||||
@@ -30,35 +30,9 @@ enum FileContentsRequest {
|
||||
},
|
||||
}
|
||||
|
||||
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
|
||||
// or a directory in the selection -- forces sync_files() to rebuild (see below).
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
struct FileSig {
|
||||
size: u64,
|
||||
mtime: Option<SystemTime>,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
// Stat the top-level selected paths only (no recursion), same order as `files`.
|
||||
fn fingerprint(files: &[String]) -> Vec<FileSig> {
|
||||
files
|
||||
.iter()
|
||||
.map(|s| match std::fs::metadata(s) {
|
||||
Ok(mt) => FileSig {
|
||||
size: mt.len(),
|
||||
mtime: mt.modified().ok(),
|
||||
is_dir: mt.is_dir(),
|
||||
},
|
||||
Err(_) => FileSig::default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipFiles {
|
||||
files: Vec<String>,
|
||||
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
|
||||
sigs: Vec<FileSig>,
|
||||
file_list: Vec<LocalFile>,
|
||||
first_file_index: usize,
|
||||
files_pdu: Vec<u8>,
|
||||
@@ -67,17 +41,12 @@ struct ClipFiles {
|
||||
impl ClipFiles {
|
||||
fn clear(&mut self) {
|
||||
self.files.clear();
|
||||
self.sigs.clear();
|
||||
self.file_list.clear();
|
||||
self.first_file_index = usize::MAX;
|
||||
self.files_pdu.clear();
|
||||
}
|
||||
|
||||
fn sync_files(
|
||||
&mut self,
|
||||
clipboard_files: &[String],
|
||||
sigs: Vec<FileSig>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
||||
let clipboard_paths = clipboard_files
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(s))
|
||||
@@ -89,7 +58,6 @@ impl ClipFiles {
|
||||
.position(|f| !f.path.is_dir())
|
||||
.unwrap_or(usize::MAX);
|
||||
self.files = clipboard_files.to_vec();
|
||||
self.sigs = sigs;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -290,128 +258,14 @@ pub fn read_file_contents(
|
||||
}
|
||||
|
||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
|
||||
// selected (a dir's own mtime doesn't change when a file inside it is edited).
|
||||
let current = fingerprint(files);
|
||||
let mut files_lock = CLIP_FILES.lock();
|
||||
if files_lock.files == files
|
||||
&& files_lock.sigs == current
|
||||
&& !current.iter().any(|sig| sig.is_dir)
|
||||
{
|
||||
if files_lock.files == files {
|
||||
return Ok(());
|
||||
}
|
||||
files_lock.sync_files(files, current)?;
|
||||
files_lock.sync_files(files)?;
|
||||
Ok(files_lock.build_file_list_pdu())
|
||||
}
|
||||
|
||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||
CLIP_FILES.lock().files_pdu.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sig_test {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
|
||||
struct TmpDir(PathBuf);
|
||||
impl TmpDir {
|
||||
fn new(tag: &str) -> Self {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let mut dir = std::env::temp_dir();
|
||||
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
TmpDir(dir)
|
||||
}
|
||||
fn join(&self, name: &str) -> PathBuf {
|
||||
self.0.join(name)
|
||||
}
|
||||
}
|
||||
impl Drop for TmpDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_str(p: &PathBuf) -> String {
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_missing_path_is_default() {
|
||||
let tmp = TmpDir::new("missing");
|
||||
let missing = path_str(&tmp.join("does_not_exist.bin"));
|
||||
let sigs = fingerprint(&[missing]);
|
||||
assert_eq!(sigs.len(), 1);
|
||||
// A path that can't be stat'd -> default sig, which forces a rebuild.
|
||||
assert_eq!(sigs[0], FileSig::default());
|
||||
assert_eq!(sigs[0].mtime, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_detects_inplace_edit() {
|
||||
let tmp = TmpDir::new("edit");
|
||||
let file = tmp.join("a.bin");
|
||||
fs::write(&file, b"small").unwrap();
|
||||
let p = path_str(&file);
|
||||
|
||||
let before = fingerprint(&[p.clone()]);
|
||||
// Same content, same path: fingerprint must be stable.
|
||||
let again = fingerprint(&[p.clone()]);
|
||||
assert_eq!(before, again);
|
||||
assert_eq!(before[0].size, 5);
|
||||
assert!(!before[0].is_dir);
|
||||
|
||||
// Edit in place so the file grows.
|
||||
fs::write(&file, b"much larger contents than before").unwrap();
|
||||
let after = fingerprint(&[p]);
|
||||
assert_ne!(before, after);
|
||||
assert!(after[0].size > before[0].size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_flags_directory() {
|
||||
let tmp = TmpDir::new("dir");
|
||||
let sub = tmp.join("subdir");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
let sigs = fingerprint(&[path_str(&sub)]);
|
||||
assert_eq!(sigs.len(), 1);
|
||||
assert!(sigs[0].is_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recopy_after_edit_refreshes_cached_size() {
|
||||
let tmp = TmpDir::new("recopy");
|
||||
let file = tmp.join("doc.bin");
|
||||
fs::write(&file, b"v1").unwrap(); // 2 bytes
|
||||
let files = vec![path_str(&file)];
|
||||
|
||||
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
|
||||
// reset first (this is the only test that touches the global).
|
||||
clear_files();
|
||||
|
||||
sync_files(&files).unwrap();
|
||||
{
|
||||
let cache = CLIP_FILES.lock();
|
||||
let idx = cache.first_file_index;
|
||||
assert_eq!(cache.file_list[idx].size, 2);
|
||||
}
|
||||
|
||||
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
|
||||
// path-only guard early-returned and left the cached size stale at 2.
|
||||
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
|
||||
sync_files(&files).unwrap();
|
||||
{
|
||||
let cache = CLIP_FILES.lock();
|
||||
let idx = cache.first_file_index;
|
||||
assert_eq!(cache.file_list[idx].size, 12);
|
||||
}
|
||||
|
||||
clear_files(); // leave the global clean for other tests
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,6 @@
|
||||
|
||||
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
|
||||
#define WF_CLIPRDR_MAX_STREAMS 16384
|
||||
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
|
||||
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
|
||||
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
|
||||
/* Registered format names are string atoms; cap the converted WCHAR name.
|
||||
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
|
||||
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
|
||||
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
|
||||
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
|
||||
|
||||
/* Validates the remote descriptor array size after cItems has been read safely. */
|
||||
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||
@@ -69,25 +61,6 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||
return size >= descriptors_size;
|
||||
}
|
||||
|
||||
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!value || !len)
|
||||
return FALSE;
|
||||
|
||||
for (i = 0; i <= max_len; i++)
|
||||
{
|
||||
if (value[i] == '\0')
|
||||
{
|
||||
*len = i;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clipboard Formats
|
||||
*/
|
||||
@@ -232,7 +205,6 @@ struct _CliprdrStream
|
||||
FILEDESCRIPTORW m_Dsc;
|
||||
void *m_pData;
|
||||
UINT32 m_connID;
|
||||
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
|
||||
};
|
||||
typedef struct _CliprdrStream CliprdrStream;
|
||||
|
||||
@@ -286,9 +258,6 @@ struct wf_clipboard
|
||||
char *req_fdata;
|
||||
HANDLE req_fevent;
|
||||
BOOL req_f_received;
|
||||
UINT32 req_f_conn_id_expected; // connID of the outstanding request
|
||||
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
|
||||
LONG req_f_stream_id_seq; // source of unique per-stream ids
|
||||
|
||||
size_t nFiles;
|
||||
size_t file_array_size;
|
||||
@@ -319,7 +288,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
|
||||
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
||||
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
|
||||
ULONG index, UINT32 flag, DWORD positionhigh,
|
||||
DWORD positionlow, ULONG request);
|
||||
|
||||
@@ -402,7 +371,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
|
||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
||||
ULONG *pcbRead)
|
||||
{
|
||||
UINT ret;
|
||||
int ret;
|
||||
CliprdrStream *instance = (CliprdrStream *)This;
|
||||
wfClipboard *clipboard;
|
||||
|
||||
@@ -415,23 +384,12 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
||||
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
||||
return S_FALSE;
|
||||
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
|
||||
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
||||
instance->m_lOffset.LowPart, cb);
|
||||
|
||||
if (ret != CHANNEL_RC_OK)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
if (ret < 0)
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
if (clipboard->req_fsize > cb)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
return STG_E_READFAULT;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata)
|
||||
{
|
||||
@@ -643,7 +601,6 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
instance->m_pData = pData;
|
||||
instance->m_lOffset.QuadPart = 0;
|
||||
instance->m_connID = connID;
|
||||
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
|
||||
|
||||
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
||||
{
|
||||
@@ -654,28 +611,16 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
||||
{
|
||||
/* get content size of this stream */
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
|
||||
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
||||
8) == CHANNEL_RC_OK)
|
||||
{
|
||||
success = TRUE;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
|
||||
{
|
||||
LONGLONG sz = 0;
|
||||
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
|
||||
if (sz < 0)
|
||||
success = FALSE;
|
||||
else
|
||||
instance->m_lSize.QuadPart = sz;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = FALSE;
|
||||
}
|
||||
if (clipboard->req_fdata)
|
||||
if (clipboard->req_fdata != NULL)
|
||||
{
|
||||
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
}
|
||||
@@ -1461,35 +1406,25 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
|
||||
return local_format;
|
||||
}
|
||||
|
||||
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
|
||||
static void map_ensure_capacity(wfClipboard *clipboard)
|
||||
{
|
||||
size_t old_size;
|
||||
formatMapping *new_map;
|
||||
|
||||
if (!clipboard)
|
||||
return FALSE;
|
||||
return;
|
||||
|
||||
if (!clipboard->format_mappings)
|
||||
return FALSE;
|
||||
if (clipboard->map_size >= clipboard->map_capacity)
|
||||
{
|
||||
size_t new_size;
|
||||
formatMapping *new_map;
|
||||
new_size = clipboard->map_capacity * 2;
|
||||
new_map =
|
||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
|
||||
|
||||
if (capacity <= clipboard->map_capacity)
|
||||
return TRUE;
|
||||
if (!new_map)
|
||||
return;
|
||||
|
||||
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
|
||||
capacity > ((size_t)-1) / sizeof(formatMapping))
|
||||
return FALSE;
|
||||
|
||||
old_size = clipboard->map_capacity;
|
||||
new_map =
|
||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
|
||||
|
||||
if (!new_map)
|
||||
return FALSE;
|
||||
|
||||
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
|
||||
clipboard->format_mappings = new_map;
|
||||
clipboard->map_capacity = capacity;
|
||||
return TRUE;
|
||||
clipboard->format_mappings = new_map;
|
||||
clipboard->map_capacity = new_size;
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL clear_format_map(wfClipboard *clipboard)
|
||||
@@ -1516,13 +1451,6 @@ static BOOL clear_format_map(wfClipboard *clipboard)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
|
||||
{
|
||||
clear_format_map(clipboard);
|
||||
clipboard->copied = FALSE;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||
{
|
||||
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
||||
@@ -1801,12 +1729,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||
}
|
||||
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
|
||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
||||
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
||||
ULONG nreq)
|
||||
{
|
||||
UINT rc;
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
|
||||
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
@@ -1817,11 +1745,12 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con
|
||||
return rc;
|
||||
}
|
||||
clipboard->req_f_received = FALSE;
|
||||
clipboard->req_f_conn_id_expected = connID;
|
||||
clipboard->req_f_stream_id_expected = streamId;
|
||||
|
||||
fileContentsRequest.connID = connID;
|
||||
fileContentsRequest.streamId = streamId;
|
||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
||||
// But it is OK, because it is only used to check if the stream is the same in
|
||||
// `wf_cliprdr_server_file_contents_request()` function.
|
||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||
fileContentsRequest.listIndex = index;
|
||||
fileContentsRequest.dwFlags = flag;
|
||||
fileContentsRequest.nPositionLow = positionlow;
|
||||
@@ -2514,16 +2443,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
|
||||
if (!clear_format_map(clipboard))
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
clipboard->copied = FALSE;
|
||||
|
||||
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (formatList->numFormats > 0 && !formatList->formats)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (!map_ensure_capacity(clipboard, formatList->numFormats))
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
clipboard->copied = TRUE;
|
||||
|
||||
@@ -2531,58 +2450,19 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
{
|
||||
format = &(formatList->formats[i]);
|
||||
mapping = &(clipboard->format_mappings[i]);
|
||||
/* Do not validate the peer-provided formatId as a Windows registered format.
|
||||
* It is only a remote protocol ID used when requesting data from the peer.
|
||||
* For named formats, RegisterClipboardFormatW creates the local Windows
|
||||
* clipboard ID below, and that local ID is checked before publishing. */
|
||||
mapping->remote_format_id = format->formatId;
|
||||
|
||||
if (format->formatName)
|
||||
{
|
||||
size_t name_len;
|
||||
int size;
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
|
||||
strlen(format->formatName), NULL, 0);
|
||||
mapping->name = calloc(size + 1, sizeof(WCHAR));
|
||||
|
||||
if (!wf_cliprdr_bounded_strlen(format->formatName,
|
||||
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
|
||||
if (mapping->name)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if (name_len == 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||
NULL, 0);
|
||||
if (size <= 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
|
||||
if (!mapping->name)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||
mapping->name, size) != size)
|
||||
{
|
||||
free(mapping->name);
|
||||
mapping->name = NULL;
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||
if (mapping->local_format_id == 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
|
||||
mapping->name, size);
|
||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2592,6 +2472,7 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
}
|
||||
|
||||
clipboard->map_size++;
|
||||
map_ensure_capacity(clipboard);
|
||||
}
|
||||
|
||||
if (file_transferring(clipboard))
|
||||
@@ -3042,7 +2923,6 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
BOOL bIsStreamFile = TRUE;
|
||||
static LPSTREAM pStreamStc = NULL;
|
||||
static UINT32 uStreamIdStc = 0;
|
||||
static UINT32 uConnIdStc = 0;
|
||||
wfClipboard *clipboard;
|
||||
UINT rc = ERROR_INTERNAL_ERROR;
|
||||
UINT sRc;
|
||||
@@ -3116,8 +2996,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
||||
vFormatEtc.ptd = NULL;
|
||||
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) ||
|
||||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
|
||||
{
|
||||
LPENUMFORMATETC pEnumFormatEtc;
|
||||
ULONG CeltFetched;
|
||||
@@ -3148,7 +3027,6 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
{
|
||||
pStreamStc = vStgMedium.pstm;
|
||||
uStreamIdStc = fileContentsRequest->streamId;
|
||||
uConnIdStc = fileContentsRequest->connID;
|
||||
bIsStreamFile = TRUE;
|
||||
}
|
||||
|
||||
@@ -3312,9 +3190,6 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
|
||||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
|
||||
return CHANNEL_RC_OK;
|
||||
clipboard->req_fsize = 0;
|
||||
clipboard->req_fdata = NULL;
|
||||
|
||||
@@ -3325,13 +3200,6 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
}
|
||||
|
||||
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
||||
/*
|
||||
* Keep the zero-size allocation: supported Windows builds use the Microsoft
|
||||
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
|
||||
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
|
||||
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
|
||||
* nonzero length cannot have a NULL data pointer on the normal call path.
|
||||
*/
|
||||
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
||||
if (!clipboard->req_fdata)
|
||||
{
|
||||
|
||||
@@ -113,11 +113,11 @@ pub enum MouseButton {
|
||||
|
||||
/// Scroll up button
|
||||
ScrollUp,
|
||||
/// Scroll down button
|
||||
/// Left right button
|
||||
ScrollDown,
|
||||
/// Scroll left button
|
||||
/// Left right button
|
||||
ScrollLeft,
|
||||
/// Scroll right button
|
||||
/// Left right button
|
||||
ScrollRight,
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
|
||||
// Windows uses uft-16 encoding. We need to check
|
||||
// for variable length characters. As such some
|
||||
// characters can be 32 bit long and those are
|
||||
// encoded in so-called high and low surrogates
|
||||
// encoded in such called hight and low surrogates
|
||||
// each 16 bit wide that needs to be send after
|
||||
// another to the SendInput function without
|
||||
// being interrupted by "keyup"
|
||||
|
||||
+1
-1
Submodule libs/hbb_common updated: a920d00945...387603f47c
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
|
||||
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
|
||||
@@ -79,10 +79,6 @@ mod webrtc {
|
||||
}
|
||||
}
|
||||
|
||||
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
|
||||
(threads as f64).log2().ceil() as _
|
||||
}
|
||||
|
||||
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
||||
use aom_superblock_size::*;
|
||||
let resolution = width * height;
|
||||
@@ -164,7 +160,8 @@ mod webrtc {
|
||||
} else {
|
||||
AV1E_SET_TILE_COLUMNS
|
||||
};
|
||||
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
|
||||
// Failed on android
|
||||
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
|
||||
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
||||
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
||||
@@ -200,23 +197,6 @@ mod webrtc {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::raw::c_uint;
|
||||
|
||||
#[test]
|
||||
fn tile_log2_uses_c_uint_and_rounds_up() {
|
||||
let one_thread: c_uint = tile_log2(1);
|
||||
let three_threads: c_uint = tile_log2(3);
|
||||
let max_threads: c_uint = tile_log2(64);
|
||||
|
||||
assert_eq!(one_thread, 0);
|
||||
assert_eq!(three_threads, 2);
|
||||
assert_eq!(max_threads, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderApi for AomEncoder {
|
||||
|
||||
@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
|
||||
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
|
||||
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
|
||||
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
||||
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
|
||||
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
|
||||
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
|
||||
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
|
||||
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
|
||||
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
|
||||
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
||||
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
||||
@@ -85,7 +85,7 @@ def make_parser():
|
||||
"-m",
|
||||
"--manufacturer",
|
||||
type=str,
|
||||
default="Purslane Tech Pte. Ltd.",
|
||||
default="PURSLANE",
|
||||
help="The app manufacturer.",
|
||||
)
|
||||
return parser
|
||||
@@ -499,7 +499,7 @@ def update_license_file(app_name):
|
||||
license_content = f.read()
|
||||
license_content = license_content.replace("website rustdesk.com and other ", "")
|
||||
license_content = license_content.replace("RustDesk", app_name)
|
||||
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
|
||||
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
|
||||
with open(license_file, "w", encoding="utf-8") as f:
|
||||
f.write(license_content)
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ pre_start()
|
||||
return 0
|
||||
}
|
||||
|
||||
# When logging out from the interactive shell, the execution sequence is:
|
||||
# When loging out from the interactive shell, the execution sequence is:
|
||||
#
|
||||
# IF ~/.bash_logout exists THEN
|
||||
# execute ~/.bash_logout
|
||||
|
||||
+8
-10
@@ -941,23 +941,21 @@ impl Client {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
fn try_stop_clipboard() {
|
||||
// Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
|
||||
// should block clipboard cleanup.
|
||||
// There's a bug here.
|
||||
// If session is closed by the peer, `has_sessions_running()` will always return true.
|
||||
// It's better to check if the active session number.
|
||||
// But it's not a problem, because the clipboard thread does not consume CPU.
|
||||
//
|
||||
// If we want to fix it, we can add a flag to indicate if session is active.
|
||||
// But I think it's not necessary to introduce complexity at this point.
|
||||
#[cfg(feature = "flutter")]
|
||||
if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
|
||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
|
||||
crate::clipboard::ClipboardSide::Client,
|
||||
0,
|
||||
) {
|
||||
log::error!("Failed to empty client clipboard files: {}", e);
|
||||
}
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
clipboard::platform::unix::fuse::uninit_fuse_context(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -360,8 +360,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
if self.handler.is_default() && _set_disconnected_ok {
|
||||
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
|
||||
// unmounted. Keep this async path for other file-clipboard platforms.
|
||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-40
@@ -151,50 +151,41 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
|
||||
log::error!("Failed to empty clipboard files: {}", e);
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
match ClipboardContext::new() {
|
||||
Ok(x) => {
|
||||
*ctx = Some(x);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create clipboard context: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
// No need to make sure the context is enabled.
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.empty_clipboard(_conn_id).ok();
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
match ClipboardContext::new() {
|
||||
Ok(x) => {
|
||||
*ctx = Some(x);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create clipboard context: {}", e);
|
||||
bail!("Failed to create clipboard context: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
// No need to make sure the context is enabled.
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
if !context.empty_clipboard(_conn_id)? {
|
||||
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
|
||||
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
|
||||
|
||||
+12
-20
@@ -332,16 +332,12 @@ pub mod unix_file_clip {
|
||||
log::debug!("format data response: msg_flags: {}", msg_flags);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
log::error!(
|
||||
"peer reported clipboard format data failure: {}",
|
||||
msg_flags
|
||||
);
|
||||
return vec![];
|
||||
// return failure message?
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
match fuse::init_fuse_context(side == ClipboardSide::Client) {
|
||||
Ok(()) => match fuse::format_data_response_to_urls(
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
match fuse::format_data_response_to_urls(
|
||||
side == ClipboardSide::Client,
|
||||
format_data,
|
||||
conn_id,
|
||||
@@ -352,10 +348,9 @@ pub mod unix_file_clip {
|
||||
Err(e) => {
|
||||
log::error!("failed to parse file descriptors: {:?}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
// send error message to server
|
||||
}
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
@@ -391,7 +386,6 @@ pub mod unix_file_clip {
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
..
|
||||
} => {
|
||||
log::debug!(
|
||||
@@ -399,15 +393,13 @@ pub mod unix_file_clip {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
);
|
||||
let response = ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
};
|
||||
if let Err(e) =
|
||||
fuse::handle_file_content_response(side == ClipboardSide::Client, response)
|
||||
{
|
||||
log::error!("failed to handle file contents response: {:?}", e);
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
hbb_common::allow_err!(fuse::handle_file_content_response(
|
||||
side == ClipboardSide::Client,
|
||||
clip
|
||||
));
|
||||
} else {
|
||||
// send error message to server
|
||||
}
|
||||
}
|
||||
ClipboardFile::NotifyCallback {
|
||||
|
||||
@@ -2297,16 +2297,6 @@ pub mod sessions {
|
||||
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
|
||||
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
|
||||
*r#type == conn_type
|
||||
&& s.session_handlers.read().unwrap().len() != 0
|
||||
&& s.connection_round_state.lock().unwrap().is_connected()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod async_tasks {
|
||||
|
||||
+2
-10
@@ -2852,16 +2852,8 @@ pub fn main_get_common(key: String) -> String {
|
||||
crate::platform::windows::is_msi_installed(),
|
||||
crate::common::is_custom_client(),
|
||||
) {
|
||||
(Ok(true), false) => match crate::platform::windows::release_arch_suffix() {
|
||||
Some(arch) => format!("rustdesk-{_version}-{arch}.msi"),
|
||||
None => "error:unsupported".to_owned(),
|
||||
},
|
||||
(Ok(true), true) | (Ok(false), _) => {
|
||||
match crate::platform::windows::release_arch_suffix() {
|
||||
Some(arch) => format!("rustdesk-{_version}-{arch}.exe"),
|
||||
None => "error:unsupported".to_owned(),
|
||||
}
|
||||
}
|
||||
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
(Ok(true), true) | (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")
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "تبديل الشاشة"),
|
||||
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
|
||||
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
|
||||
("All monitors", "جميع الشاشات"),
|
||||
("#{} monitor", "الشاشة رقم {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Пераключыць дысплэй"),
|
||||
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
|
||||
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
|
||||
("All monitors", "Усе манітори"),
|
||||
("#{} monitor", "Манітор {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Превключване на дисплея"),
|
||||
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
|
||||
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
|
||||
("All monitors", "Всички монитори"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Canvia de pantalla"),
|
||||
("Show monitor switch button on the main toolbar", "Mostra el botó de canvi de monitor a la barra d’eines principal"),
|
||||
("Show on the minimized toolbar", "Mostra a la barra d’eines minimitzada"),
|
||||
("All monitors", "Tots els monitors"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "切换显示器"),
|
||||
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
|
||||
("Show on the minimized toolbar", "在最小化工具栏上显示"),
|
||||
("All monitors", "所有显示器"),
|
||||
("#{} monitor", "{}号显示器"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Přepnout obrazovku"),
|
||||
("Show monitor switch button on the main toolbar", "Zobrazit tlačítko přepnutí monitoru na hlavním panelu nástrojů"),
|
||||
("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
|
||||
("All monitors", "Všechny monitory"),
|
||||
("#{} monitor", "Monitor č. {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Skift skærm"),
|
||||
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"),
|
||||
("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
|
||||
("All monitors", "Alle skærme"),
|
||||
("#{} monitor", "Skærm {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Anzeige wechseln"),
|
||||
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"),
|
||||
("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
|
||||
("All monitors", "Alle Bildschirme"),
|
||||
("#{} monitor", "Bildschirm {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Εναλλαγή οθόνης"),
|
||||
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
|
||||
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
|
||||
("All monitors", "Όλες οι οθόνες"),
|
||||
("#{} monitor", "Οθόνη {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ŝalti ekranon"),
|
||||
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"),
|
||||
("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
|
||||
("All monitors", "Ĉiuj monitoroj"),
|
||||
("#{} monitor", "Monitoro {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Cambiar de pantalla"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar el botón de cambio de monitor en la barra de herramientas principal"),
|
||||
("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
|
||||
("All monitors", "Todos los monitores"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Vaheta kuva"),
|
||||
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"),
|
||||
("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
|
||||
("All monitors", "Kõik kuvarid"),
|
||||
("#{} monitor", "Kuvar {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Aldatu pantaila"),
|
||||
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
|
||||
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
|
||||
("All monitors", "Monitore guztiak"),
|
||||
("#{} monitor", "{}. monitorea"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "تعویض نمایشگر"),
|
||||
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
|
||||
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچکشده"),
|
||||
("All monitors", "همه نمایشگرها"),
|
||||
("#{} monitor", "نمایشگر {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Vaihda näyttöä"),
|
||||
("Show monitor switch button on the main toolbar", "Näytä näytön vaihtopainike päätyökalurivillä"),
|
||||
("Show on the minimized toolbar", "Näytä pienennetyssä työkalurivissä"),
|
||||
("All monitors", "Kaikki näytöt"),
|
||||
("#{} monitor", "Näyttö {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Changer d’écran"),
|
||||
("Show monitor switch button on the main toolbar", "Afficher le bouton de changement d’écran dans la barre d’outils principale"),
|
||||
("Show on the minimized toolbar", "Afficher dans la barre d’outils réduite"),
|
||||
("All monitors", "Tous les moniteurs"),
|
||||
("#{} monitor", "Moniteur {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ეკრანის გადართვა"),
|
||||
("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"),
|
||||
("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"),
|
||||
("All monitors", "ყველა მონიტორი"),
|
||||
("#{} monitor", "მონიტორი {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ડિસ્પ્લે બદલો"),
|
||||
("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"),
|
||||
("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"),
|
||||
("All monitors", "બધા મોનિટર"),
|
||||
("#{} monitor", "મોનિટર {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "החלפת צג"),
|
||||
("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"),
|
||||
("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"),
|
||||
("All monitors", "כל המסכים"),
|
||||
("#{} monitor", "מסך {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "डिस्प्ले बदलें"),
|
||||
("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"),
|
||||
("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"),
|
||||
("All monitors", "सभी मॉनिटर"),
|
||||
("#{} monitor", "मॉनिटर {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Promijeni zaslon"),
|
||||
("Show monitor switch button on the main toolbar", "Prikaži gumb za prebacivanje monitora na glavnoj alatnoj traci"),
|
||||
("Show on the minimized toolbar", "Prikaži na minimiziranoj alatnoj traci"),
|
||||
("All monitors", "Svi monitori"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Kijelző váltása"),
|
||||
("Show monitor switch button on the main toolbar", "Monitorváltó gomb megjelenítése a fő eszköztáron"),
|
||||
("Show on the minimized toolbar", "Megjelenítés a kis méretű eszköztáron"),
|
||||
("All monitors", "Minden monitor"),
|
||||
("#{} monitor", "{}. monitor"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ganti tampilan"),
|
||||
("Show monitor switch button on the main toolbar", "Tampilkan tombol pengalih monitor di bilah alat utama"),
|
||||
("Show on the minimized toolbar", "Tampilkan di bilah alat yang diperkecil"),
|
||||
("All monitors", "Semua monitor"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Cambia schermo"),
|
||||
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio schermo"),
|
||||
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
|
||||
("All monitors", "Tutti gli schermi"),
|
||||
("#{} monitor", "Schermo {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ディスプレイを切り替え"),
|
||||
("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"),
|
||||
("Show on the minimized toolbar", "最小化したツールバーに表示"),
|
||||
("All monitors", "すべてのディスプレイ"),
|
||||
("#{} monitor", "ディスプレイ {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
+7
-9
@@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_change_tip", "a-z, A-Z, 0-9, -(대시) 및 _(밑줄) 문자만 허용됩니다. 첫 글자는 a-z, A-Z여야 합니다. 길이는 6에서 16 사이여야 합니다."),
|
||||
("Website", "웹사이트"),
|
||||
("About", "정보"),
|
||||
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다!"),
|
||||
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다! - 한국어 번역: 비너스걸"),
|
||||
("Privacy Statement", "개인정보 보호정책"),
|
||||
("Mute", "음소거"),
|
||||
("Build Date", "빌드 날짜"),
|
||||
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Share", "화면 공유"),
|
||||
("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."),
|
||||
("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("xdp-portal-unavailable", "Wayland 화면 캡처에 실패했습니다. XDG Desktop Portal이 충돌했거나 사용할 수 없는 상태일 수 있습니다. `systemctl --user restart xdg-desktop-portal` 명령으로 다시 시작해 보세요."),
|
||||
("JumpLink", "점프 링크"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"),
|
||||
("Show RustDesk", "RustDesk 표시"),
|
||||
@@ -749,19 +749,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Deploy", "배포"),
|
||||
("Custom ID (optional)", "사용자 지정 ID (선택 사항)"),
|
||||
("server_requires_deployment_tip", "서버에서 이 장치를 명시적으로 배포하도록 요구합니다. 지금 배포하시겠습니까?"),
|
||||
("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."),
|
||||
("The server does not require explicit deployment.", "서버에서 명시적인 배포를 요구하지 않습니다."),
|
||||
("Unknown response.", "알 수 없는 응답입니다."),
|
||||
("wayland-keyboard-input-disabled-tip", "키보드 입력을 허용하시겠습니까?"),
|
||||
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용 (비밀번호 포함)은 해당 컴퓨터의 다른 앱에서 읽을 수 있습니다."),
|
||||
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용(비밀번호 포함)은 해당 컴퓨터의 다른 앱이 읽을 수 있습니다."),
|
||||
("wayland-keyboard-input-applies-to-tip", "이 선택이 적용되는 대상:"),
|
||||
("wayland-soft-keyboard-input-label", "소프트 키보드 입력"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 재설정"),
|
||||
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않음"),
|
||||
("Why this happens", "이 현상이 발생하는 이유"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 초기화"),
|
||||
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않기"),
|
||||
("Why this happens", "이런 현상이 발생하는 이유"),
|
||||
("Switch display", "디스플레이 전환"),
|
||||
("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"),
|
||||
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
|
||||
("All monitors", "모든 모니터"),
|
||||
("#{} monitor", "#{} 모니터"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Дисплейді ауыстыру"),
|
||||
("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"),
|
||||
("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"),
|
||||
("All monitors", "Барлық мониторлар"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Perjungti ekraną"),
|
||||
("Show monitor switch button on the main toolbar", "Rodyti monitoriaus perjungimo mygtuką pagrindinėje įrankių juostoje"),
|
||||
("Show on the minimized toolbar", "Rodyti sumažintoje įrankių juostoje"),
|
||||
("All monitors", "Visi monitoriai"),
|
||||
("#{} monitor", "Monitorius {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Pārslēgt displeju"),
|
||||
("Show monitor switch button on the main toolbar", "Rādīt monitora pārslēgšanas pogu galvenajā rīkjoslā"),
|
||||
("Show on the minimized toolbar", "Rādīt minimizētajā rīkjoslā"),
|
||||
("All monitors", "Visi monitori"),
|
||||
("#{} monitor", "Monitors {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"),
|
||||
("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"),
|
||||
("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"),
|
||||
("All monitors", "എല്ലാ മോണിറ്ററുകളും"),
|
||||
("#{} monitor", "മോണിറ്റർ {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Bytt skjerm"),
|
||||
("Show monitor switch button on the main toolbar", "Vis knapp for skjermbytte på hovedverktøylinjen"),
|
||||
("Show on the minimized toolbar", "Vis på den minimerte verktøylinjen"),
|
||||
("All monitors", "Alle skjermer"),
|
||||
("#{} monitor", "Skjerm {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Beeldscherm wisselen"),
|
||||
("Show monitor switch button on the main toolbar", "Knop voor monitorwisseling weergeven op de hoofdwerkbalk"),
|
||||
("Show on the minimized toolbar", "Weergeven op de geminimaliseerde werkbalk"),
|
||||
("All monitors", "Alle monitoren"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Przełącz ekran"),
|
||||
("Show monitor switch button on the main toolbar", "Pokaż przycisk przełączania monitora na głównym pasku narzędzi"),
|
||||
("Show on the minimized toolbar", "Pokaż na zminimalizowanym pasku narzędzi"),
|
||||
("All monitors", "Wszystkie ekrany"),
|
||||
("#{} monitor", "Ekran {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Trocar de ecrã"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar o botão de troca de monitor na barra de ferramentas principal"),
|
||||
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
|
||||
("All monitors", "Todos os monitores"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
+2
-4
@@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Accept and Install", "Aceitar e Instalar"),
|
||||
("End-user license agreement", "Acordo de licença do usuário final"),
|
||||
("Generating ...", "Gerando ..."),
|
||||
("Your installation is lower version.", "Sua instalação está com uma versão desatualizada."),
|
||||
("Your installation is lower version.", "Instalação desatualizada"),
|
||||
("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"),
|
||||
("Listening ...", "Escutando ..."),
|
||||
("Remote Host", "Host Remoto"),
|
||||
@@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fullscreen", "Tela Cheia"),
|
||||
("Mobile Actions", "Ações móveis"),
|
||||
("Select Monitor", "Selecionar tela"),
|
||||
("Control Actions", "Ações de controle"),
|
||||
("Control Actions", "Controlar ações"),
|
||||
("Display Settings", "Configurações de exibição"),
|
||||
("Ratio", "Proporção"),
|
||||
("Image Quality", "Qualidade de imagem"),
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Trocar de tela"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar botão de troca de tela na barra de ferramentas"),
|
||||
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
|
||||
("All monitors", "Todas as telas"),
|
||||
("#{} monitor", "Tela {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Comută afișajul"),
|
||||
("Show monitor switch button on the main toolbar", "Afișează butonul de comutare a monitorului în bara de instrumente principală"),
|
||||
("Show on the minimized toolbar", "Afișează în bara de instrumente minimizată"),
|
||||
("All monitors", "Toate monitoarele"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Переключить дисплей"),
|
||||
("Show monitor switch button on the main toolbar", "Показывать кнопку переключения монитора на главной панели инструментов"),
|
||||
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
|
||||
("All monitors", "Все мониторы"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Càmbia ischermu"),
|
||||
("Show monitor switch button on the main toolbar", "Mustra su butone de càmbiu de monitor in sa barra de aina printzipale"),
|
||||
("Show on the minimized toolbar", "Mustra in sa barra de aina minimizada"),
|
||||
("All monitors", "Totu sos ischermos"),
|
||||
("#{} monitor", "Ischermu {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Prepnúť obrazovku"),
|
||||
("Show monitor switch button on the main toolbar", "Zobraziť tlačidlo prepnutia monitora na hlavnom paneli nástrojov"),
|
||||
("Show on the minimized toolbar", "Zobraziť na minimalizovanom paneli nástrojov"),
|
||||
("All monitors", "Všetky monitory"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Preklopi zaslon"),
|
||||
("Show monitor switch button on the main toolbar", "Pokaži gumb za preklop monitorja v glavni orodni vrstici"),
|
||||
("Show on the minimized toolbar", "Pokaži v pomanjšani orodni vrstici"),
|
||||
("All monitors", "Vsi zasloni"),
|
||||
("#{} monitor", "Zaslon {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ndërro ekranin"),
|
||||
("Show monitor switch button on the main toolbar", "Shfaq butonin e ndërrimit të monitorit te shiriti kryesor i veglave"),
|
||||
("Show on the minimized toolbar", "Shfaq te shiriti i minimizuar i veglave"),
|
||||
("All monitors", "Të gjithë monitorët"),
|
||||
("#{} monitor", "Monitori {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Промени екран"),
|
||||
("Show monitor switch button on the main toolbar", "Прикажи дугме за пребацивање монитора на главној траци са алаткама"),
|
||||
("Show on the minimized toolbar", "Прикажи на умањеној траци са алаткама"),
|
||||
("All monitors", "Svi monitori"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Växla skärm"),
|
||||
("Show monitor switch button on the main toolbar", "Visa knapp för skärmväxling i huvudverktygsfältet"),
|
||||
("Show on the minimized toolbar", "Visa i det minimerade verktygsfältet"),
|
||||
("All monitors", "Alla skärmar"),
|
||||
("#{} monitor", "Skärm {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "திரையை மாற்று"),
|
||||
("Show monitor switch button on the main toolbar", "முதன்மை கருவிப்பட்டையில் திரை மாற்று பொத்தானைக் காட்டு"),
|
||||
("Show on the minimized toolbar", "சிறிதாக்கப்பட்ட கருவிப்பட்டையில் காட்டு"),
|
||||
("All monitors", "அனைத்து மானிட்டர்களும்"),
|
||||
("#{} monitor", "மானிட்டர் {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", ""),
|
||||
("Show monitor switch button on the main toolbar", ""),
|
||||
("Show on the minimized toolbar", ""),
|
||||
("All monitors", ""),
|
||||
("#{} monitor", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "สลับจอแสดงผล"),
|
||||
("Show monitor switch button on the main toolbar", "แสดงปุ่มสลับจอภาพบนแถบเครื่องมือหลัก"),
|
||||
("Show on the minimized toolbar", "แสดงบนแถบเครื่องมือที่ย่อเล็กสุด"),
|
||||
("All monitors", "จอภาพทั้งหมด"),
|
||||
("#{} monitor", "จอภาพ {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ekranı değiştir"),
|
||||
("Show monitor switch button on the main toolbar", "Ana araç çubuğunda monitör değiştirme düğmesini göster"),
|
||||
("Show on the minimized toolbar", "Simge durumuna küçültülmüş araç çubuğunda göster"),
|
||||
("All monitors", "Tüm monitörler"),
|
||||
("#{} monitor", "Monitör {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "切換螢幕"),
|
||||
("Show monitor switch button on the main toolbar", "在主工具列上顯示螢幕切換按鈕"),
|
||||
("Show on the minimized toolbar", "在最小化工具列上顯示"),
|
||||
("All monitors", "所有顯示器"),
|
||||
("#{} monitor", "{}號顯示器"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Перемкнути дисплей"),
|
||||
("Show monitor switch button on the main toolbar", "Показувати кнопку перемикання монітора на головній панелі інструментів"),
|
||||
("Show on the minimized toolbar", "Показувати на згорнутій панелі інструментів"),
|
||||
("All monitors", "Усі монітори"),
|
||||
("#{} monitor", "Монітор {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -761,7 +761,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Chuyển màn hình"),
|
||||
("Show monitor switch button on the main toolbar", "Hiển thị nút chuyển đổi màn hình trên thanh công cụ chính"),
|
||||
("Show on the minimized toolbar", "Hiển thị trên thanh công cụ thu nhỏ"),
|
||||
("All monitors", "Tất cả màn hình"),
|
||||
("#{} monitor", "Màn hình {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ fn main() {
|
||||
);
|
||||
let matches = App::new("rustdesk")
|
||||
.version(crate::VERSION)
|
||||
.author("Purslane Tech Pte. Ltd.<info@rustdesk.com>")
|
||||
.author("Purslane Ltd<info@rustdesk.com>")
|
||||
.about("RustDesk command line tool")
|
||||
.args_from_usage(&args)
|
||||
.get_matches();
|
||||
|
||||
+4
-11
@@ -3647,9 +3647,10 @@ pub fn update_to(file: &str) -> ResultType<()> {
|
||||
// `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 quiet_args = if quiet { " /qn LAUNCH_TRAY_APP=N" } else { "" };
|
||||
let cmds =
|
||||
format!("chcp 65001 && msiexec /i \"{msi}\"{quiet_args} REBOOT=ReallySuppress /norestart");
|
||||
let cmds = format!(
|
||||
"chcp 65001 && msiexec /i {msi} {}",
|
||||
if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" }
|
||||
);
|
||||
run_cmds(cmds, false, "update-msi")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -3943,14 +3944,6 @@ pub fn is_x64() -> bool {
|
||||
unsafe { sys_info.u.s().wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 }
|
||||
}
|
||||
|
||||
pub fn release_arch_suffix() -> Option<&'static str> {
|
||||
match std::env::consts::ARCH {
|
||||
"x86_64" => Some("x86_64"),
|
||||
"aarch64" => Some("aarch64"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
|
||||
// Kill rustdesk.exe without extra arg, should only be called by --server
|
||||
// We can find the exact process which occupies the ipc, see more from https://github.com/winsiderss/systeminformer
|
||||
|
||||
+60
-43
@@ -28,21 +28,11 @@ use hbb_common::{
|
||||
|
||||
use crate::{
|
||||
check_port,
|
||||
server::{check_zombie, new as new_server, ConnectionMeta, ServerPtr},
|
||||
server::{check_zombie, new as new_server, ServerPtr},
|
||||
};
|
||||
|
||||
type Message = RendezvousMessage;
|
||||
|
||||
fn connection_meta(
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
controlled_context: Option<ControlledContext>,
|
||||
) -> ConnectionMeta {
|
||||
ConnectionMeta {
|
||||
control_permissions,
|
||||
controlled_context,
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
|
||||
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
||||
@@ -500,10 +490,6 @@ impl RendezvousMediator {
|
||||
if last.0 == addr && last.1.elapsed().as_millis() < 100 {
|
||||
return Ok(());
|
||||
}
|
||||
let meta = connection_meta(
|
||||
rr.control_permissions.into_option(),
|
||||
rr.controlled_context.into_option(),
|
||||
);
|
||||
|
||||
self.create_relay(
|
||||
rr.socket_addr.into(),
|
||||
@@ -513,7 +499,7 @@ impl RendezvousMediator {
|
||||
rr.secure,
|
||||
false,
|
||||
Default::default(),
|
||||
meta,
|
||||
rr.control_permissions.clone().into_option(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -527,7 +513,7 @@ impl RendezvousMediator {
|
||||
secure: bool,
|
||||
initiate: bool,
|
||||
socket_addr_v6: bytes::Bytes,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&socket_addr);
|
||||
log::info!(
|
||||
@@ -561,7 +547,7 @@ impl RendezvousMediator {
|
||||
peer_addr,
|
||||
secure,
|
||||
is_ipv4(&self.addr),
|
||||
meta,
|
||||
control_permissions,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
@@ -579,12 +565,14 @@ impl RendezvousMediator {
|
||||
let relay_server = self.get_relay_server(fla.relay_server.clone());
|
||||
let relay = use_ws() || Config::is_proxy();
|
||||
let mut socket_addr_v6 = Default::default();
|
||||
let meta = connection_meta(
|
||||
fla.control_permissions.clone().into_option(),
|
||||
fla.controlled_context.clone().into_option(),
|
||||
);
|
||||
if peer_addr_v6.port() > 0 && !relay {
|
||||
socket_addr_v6 = start_ipv6(peer_addr_v6, addr, server.clone(), meta.clone()).await;
|
||||
socket_addr_v6 = start_ipv6(
|
||||
peer_addr_v6,
|
||||
addr,
|
||||
server.clone(),
|
||||
fla.control_permissions.clone().into_option(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() {
|
||||
if let Err(err) = self
|
||||
@@ -593,7 +581,6 @@ impl RendezvousMediator {
|
||||
server.clone(),
|
||||
relay_server.clone(),
|
||||
socket_addr_v6.clone(),
|
||||
meta.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -611,7 +598,7 @@ impl RendezvousMediator {
|
||||
true,
|
||||
true,
|
||||
socket_addr_v6,
|
||||
meta,
|
||||
fla.control_permissions.into_option(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -622,7 +609,6 @@ impl RendezvousMediator {
|
||||
server: ServerPtr,
|
||||
relay_server: String,
|
||||
socket_addr_v6: bytes::Bytes,
|
||||
meta: ConnectionMeta,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
log::debug!("Handle intranet from {:?}", peer_addr);
|
||||
@@ -643,7 +629,14 @@ impl RendezvousMediator {
|
||||
});
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
socket.send_raw(bytes).await?;
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true, meta).await;
|
||||
crate::accept_connection(
|
||||
server.clone(),
|
||||
socket,
|
||||
peer_addr,
|
||||
true,
|
||||
fla.control_permissions.into_option(),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -658,13 +651,15 @@ impl RendezvousMediator {
|
||||
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
|
||||
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
|
||||
let mut socket_addr_v6 = Default::default();
|
||||
let meta = connection_meta(
|
||||
ph.control_permissions.into_option(),
|
||||
ph.controlled_context.into_option(),
|
||||
);
|
||||
let control_permissions = ph.control_permissions.into_option();
|
||||
if peer_addr_v6.port() > 0 && !relay {
|
||||
socket_addr_v6 =
|
||||
start_ipv6(peer_addr_v6, peer_addr, server.clone(), meta.clone()).await;
|
||||
socket_addr_v6 = start_ipv6(
|
||||
peer_addr_v6,
|
||||
peer_addr,
|
||||
server.clone(),
|
||||
control_permissions.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let relay_server = self.get_relay_server(ph.relay_server);
|
||||
// for ensure, websocket go relay directly
|
||||
@@ -683,7 +678,7 @@ impl RendezvousMediator {
|
||||
true,
|
||||
true,
|
||||
socket_addr_v6.clone(),
|
||||
meta,
|
||||
control_permissions,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -700,7 +695,7 @@ impl RendezvousMediator {
|
||||
};
|
||||
if ph.udp_port > 0 {
|
||||
peer_addr.set_port(ph.udp_port as u16);
|
||||
self.punch_udp_hole(peer_addr, server, msg_punch, meta)
|
||||
self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -717,7 +712,8 @@ impl RendezvousMediator {
|
||||
msg_out.set_punch_hole_sent(msg_punch);
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
socket.send_raw(bytes).await?;
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true, meta).await;
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -726,7 +722,7 @@ impl RendezvousMediator {
|
||||
peer_addr: SocketAddr,
|
||||
server: ServerPtr,
|
||||
msg_punch: PunchHoleSent,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(msg_punch);
|
||||
@@ -741,7 +737,14 @@ impl RendezvousMediator {
|
||||
socket.send_to(&data, addr).await.ok();
|
||||
}
|
||||
});
|
||||
udp_nat_listen(socket_cloned.clone(), peer_addr, peer_addr, server, meta).await?;
|
||||
udp_nat_listen(
|
||||
socket_cloned.clone(),
|
||||
peer_addr,
|
||||
peer_addr,
|
||||
server,
|
||||
control_permissions,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -898,7 +901,7 @@ async fn direct_server(server: ServerPtr) {
|
||||
hbb_common::Stream::from(stream, local_addr),
|
||||
addr,
|
||||
false,
|
||||
ConnectionMeta::default(), // Direct connections don't have server-side user context.
|
||||
None, // Direct connections don't have control_permissions
|
||||
)
|
||||
.await
|
||||
);
|
||||
@@ -930,14 +933,21 @@ async fn start_ipv6(
|
||||
peer_addr_v6: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> bytes::Bytes {
|
||||
crate::test_ipv6().await;
|
||||
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(
|
||||
udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server, meta).await
|
||||
udp_nat_listen(
|
||||
socket.clone(),
|
||||
peer_addr_v6,
|
||||
peer_addr_v4,
|
||||
server,
|
||||
control_permissions
|
||||
)
|
||||
.await
|
||||
);
|
||||
});
|
||||
return local_addr_v6;
|
||||
@@ -950,7 +960,7 @@ async fn udp_nat_listen(
|
||||
peer_addr: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let tm = Instant::now();
|
||||
let socket_cloned = socket.clone();
|
||||
@@ -963,7 +973,14 @@ async fn udp_nat_listen(
|
||||
res,
|
||||
)
|
||||
.await?;
|
||||
crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true, meta).await?;
|
||||
crate::server::create_tcp_connection(
|
||||
server,
|
||||
stream.1,
|
||||
peer_addr_v4,
|
||||
true,
|
||||
control_permissions,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
};
|
||||
func.await.map_err(|e: anyhow::Error| {
|
||||
|
||||
+17
-16
@@ -81,12 +81,6 @@ pub mod printer_service;
|
||||
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
|
||||
type ConnMap = HashMap<i32, ConnInner>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConnectionMeta {
|
||||
pub control_permissions: Option<ControlPermissions>,
|
||||
pub controlled_context: Option<ControlledContext>,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3;
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
@@ -169,7 +163,7 @@ async fn accept_connection_(
|
||||
server: ServerPtr,
|
||||
socket: Stream,
|
||||
secure: bool,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let local_addr = socket.local_addr();
|
||||
drop(socket);
|
||||
@@ -186,7 +180,7 @@ async fn accept_connection_(
|
||||
Stream::from(stream, stream_addr),
|
||||
addr,
|
||||
secure,
|
||||
meta,
|
||||
control_permissions,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -198,7 +192,7 @@ pub async fn create_tcp_connection(
|
||||
stream: Stream,
|
||||
addr: SocketAddr,
|
||||
secure: bool,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = stream;
|
||||
let id = server.write().unwrap().get_new_id();
|
||||
@@ -266,7 +260,14 @@ pub async fn create_tcp_connection(
|
||||
}
|
||||
log::info!("wake up macos");
|
||||
}
|
||||
Connection::start(addr, stream, id, Arc::downgrade(&server), meta).await;
|
||||
Connection::start(
|
||||
addr,
|
||||
stream,
|
||||
id,
|
||||
Arc::downgrade(&server),
|
||||
control_permissions,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -275,9 +276,9 @@ pub async fn accept_connection(
|
||||
socket: Stream,
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) {
|
||||
if let Err(err) = accept_connection_(server, socket, secure, meta).await {
|
||||
if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await {
|
||||
log::warn!("Failed to accept connection from {}: {}", peer_addr, err);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +290,7 @@ pub async fn create_relay_connection(
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
ipv4: bool,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) {
|
||||
if let Err(err) = create_relay_connection_(
|
||||
server,
|
||||
@@ -298,7 +299,7 @@ pub async fn create_relay_connection(
|
||||
peer_addr,
|
||||
secure,
|
||||
ipv4,
|
||||
meta,
|
||||
control_permissions,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -318,7 +319,7 @@ async fn create_relay_connection_(
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
ipv4: bool,
|
||||
meta: ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = socket_client::connect_tcp(
|
||||
socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4),
|
||||
@@ -333,7 +334,7 @@ async fn create_relay_connection_(
|
||||
..Default::default()
|
||||
});
|
||||
stream.send(&msg_out).await?;
|
||||
create_tcp_connection(server, stream, peer_addr, secure, meta).await?;
|
||||
create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+13
-112
@@ -240,36 +240,6 @@ pub enum AuthConnType {
|
||||
Terminal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(i64)]
|
||||
enum ConnAuditPrimaryAuth {
|
||||
None = 0,
|
||||
Click = 1,
|
||||
TemporaryPassword = 2,
|
||||
PermanentPassword = 3,
|
||||
SwitchSides = 4,
|
||||
}
|
||||
|
||||
impl ConnAuditPrimaryAuth {
|
||||
fn as_i64(self) -> i64 {
|
||||
self as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(i64)]
|
||||
enum ConnAuditTwoFactor {
|
||||
None = 0,
|
||||
Totp = 1,
|
||||
TrustedDevice = 2,
|
||||
}
|
||||
|
||||
impl ConnAuditTwoFactor {
|
||||
fn as_i64(self) -> i64 {
|
||||
self as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[derive(Clone, Debug)]
|
||||
enum TerminalUserToken {
|
||||
@@ -340,7 +310,6 @@ pub struct Connection {
|
||||
video_ack_required: bool,
|
||||
server_audit_conn: String,
|
||||
server_audit_file: String,
|
||||
controlled_context: Option<ControlledContext>,
|
||||
lr: LoginRequest,
|
||||
peer_argb: u32,
|
||||
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
|
||||
@@ -375,8 +344,6 @@ pub struct Connection {
|
||||
// For post requests that need to be sent sequentially.
|
||||
// eg. post_conn_audit
|
||||
tx_post_seq: mpsc::UnboundedSender<(String, Value)>,
|
||||
conn_audit_primary_auth: ConnAuditPrimaryAuth,
|
||||
conn_audit_two_factor: ConnAuditTwoFactor,
|
||||
// Tracks read job IDs delegated to CM process.
|
||||
// When a read job is delegated to CM (via FS::ReadFile), the job id is added here.
|
||||
// Used to filter stale responses (FileBlockFromCM, FileReadDone, etc.) for
|
||||
@@ -440,12 +407,8 @@ impl Connection {
|
||||
stream: super::Stream,
|
||||
id: i32,
|
||||
server: super::ServerPtrWeak,
|
||||
meta: super::ConnectionMeta,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) {
|
||||
let super::ConnectionMeta {
|
||||
control_permissions,
|
||||
controlled_context,
|
||||
} = meta;
|
||||
// Android is not supported yet, so we always set control_permissions to None.
|
||||
#[cfg(target_os = "android")]
|
||||
let control_permissions = None;
|
||||
@@ -532,7 +495,6 @@ impl Connection {
|
||||
video_ack_required: false,
|
||||
server_audit_conn: "".to_owned(),
|
||||
server_audit_file: "".to_owned(),
|
||||
controlled_context,
|
||||
lr: Default::default(),
|
||||
peer_argb: 0u32,
|
||||
session_last_recv_time: None,
|
||||
@@ -574,8 +536,6 @@ impl Connection {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
terminal_user_token: None,
|
||||
terminal_generic_service: None,
|
||||
conn_audit_primary_auth: ConnAuditPrimaryAuth::None,
|
||||
conn_audit_two_factor: ConnAuditTwoFactor::None,
|
||||
};
|
||||
let addr = hbb_common::try_into_v4(addr);
|
||||
if !conn.on_open(addr).await {
|
||||
@@ -659,7 +619,6 @@ impl Connection {
|
||||
Some(data) = rx_from_cm.recv() => {
|
||||
match data {
|
||||
ipc::Data::Authorize => {
|
||||
conn.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::Click);
|
||||
conn.require_2fa.take();
|
||||
if !conn.send_logon_response_and_keep_alive().await {
|
||||
break;
|
||||
@@ -1349,7 +1308,7 @@ impl Connection {
|
||||
{
|
||||
self.send_login_error("Your ip is blocked by the peer")
|
||||
.await;
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::IpWhitelist, //"ip whitelist",
|
||||
json!({ "ip":addr.ip() }),
|
||||
);
|
||||
@@ -1375,14 +1334,10 @@ impl Connection {
|
||||
msg_out.set_hash(self.hash.clone());
|
||||
self.send(msg_out).await;
|
||||
self.get_api_server();
|
||||
let mut audit = json!({
|
||||
self.post_conn_audit(json!({
|
||||
"ip": addr.ip(),
|
||||
"action": "new",
|
||||
});
|
||||
if let Some(audit_ref) = self.conn_audit_ref() {
|
||||
audit["conn_audit_ref"] = json!(audit_ref);
|
||||
}
|
||||
self.post_conn_audit(audit);
|
||||
}));
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1399,18 +1354,6 @@ impl Connection {
|
||||
);
|
||||
}
|
||||
|
||||
fn conn_audit_ref(&self) -> Option<&str> {
|
||||
let audit_ref = self
|
||||
.controlled_context
|
||||
.as_ref()
|
||||
.map(|c| c.conn_audit_ref.as_str())?;
|
||||
if audit_ref.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(audit_ref)
|
||||
}
|
||||
}
|
||||
|
||||
fn post_conn_audit(&self, v: Value) {
|
||||
if self.server_audit_conn.is_empty() {
|
||||
return;
|
||||
@@ -1465,7 +1408,6 @@ impl Connection {
|
||||
"id":json!(Config::get_id()),
|
||||
"uuid":json!(crate::encode64(hbb_common::get_uuid())),
|
||||
"peer_id":json!(self.lr.my_id),
|
||||
"conn_id":json!(self.inner.id()),
|
||||
"type": r#type as i8,
|
||||
"path":path,
|
||||
"is_file":is_file,
|
||||
@@ -1476,7 +1418,7 @@ impl Connection {
|
||||
});
|
||||
}
|
||||
|
||||
fn post_alarm_audit(&self, typ: AlarmAuditType, info: Value) {
|
||||
pub fn post_alarm_audit(typ: AlarmAuditType, info: Value) {
|
||||
let url = crate::get_audit_server(
|
||||
Config::get_option("api-server"),
|
||||
Config::get_option("custom-rendezvous-server"),
|
||||
@@ -1490,12 +1432,6 @@ impl Connection {
|
||||
v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
|
||||
v["typ"] = json!(typ as i8);
|
||||
v["info"] = serde_json::Value::String(info.to_string());
|
||||
v["conn_id"] = json!(self.inner.id());
|
||||
if typ == AlarmAuditType::IpWhitelist {
|
||||
if let Some(audit_ref) = self.conn_audit_ref() {
|
||||
v["conn_audit_ref"] = json!(audit_ref);
|
||||
}
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
allow_err!(Self::post_audit_async(url, v).await);
|
||||
});
|
||||
@@ -1506,23 +1442,6 @@ impl Connection {
|
||||
crate::post_request(url, v.to_string(), "").await
|
||||
}
|
||||
|
||||
fn set_conn_audit_primary_auth(&mut self, method: ConnAuditPrimaryAuth) {
|
||||
self.conn_audit_primary_auth = method;
|
||||
}
|
||||
|
||||
fn set_conn_audit_two_factor(&mut self, two_factor: ConnAuditTwoFactor) {
|
||||
self.conn_audit_two_factor = two_factor;
|
||||
}
|
||||
|
||||
fn normalize_conn_audit_auth_fields(&mut self) {
|
||||
if matches!(
|
||||
self.conn_audit_primary_auth,
|
||||
ConnAuditPrimaryAuth::Click | ConnAuditPrimaryAuth::SwitchSides
|
||||
) {
|
||||
self.conn_audit_two_factor = ConnAuditTwoFactor::None;
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
|
||||
let mut is_rdp = false;
|
||||
if pf.host == "RDP" && pf.port == 0 {
|
||||
@@ -1646,15 +1565,9 @@ impl Connection {
|
||||
.unwrap()
|
||||
.get(&self.session_key())
|
||||
.map(|s| s.last_recv_time.clone());
|
||||
self.normalize_conn_audit_auth_fields();
|
||||
let mut audit = json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type});
|
||||
if self.conn_audit_primary_auth != ConnAuditPrimaryAuth::None {
|
||||
audit["primary_auth"] = json!(self.conn_audit_primary_auth.as_i64());
|
||||
}
|
||||
if self.conn_audit_two_factor != ConnAuditTwoFactor::None {
|
||||
audit["two_factor"] = json!(self.conn_audit_two_factor.as_i64());
|
||||
}
|
||||
self.post_conn_audit(audit);
|
||||
self.post_conn_audit(
|
||||
json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}),
|
||||
);
|
||||
#[allow(unused_mut)]
|
||||
let mut username = crate::platform::get_active_username();
|
||||
let mut res = LoginResponse::new();
|
||||
@@ -2266,7 +2179,6 @@ impl Connection {
|
||||
if password::temporary_enabled() {
|
||||
let password = password::temporary_password();
|
||||
if self.validate_password_plain(&password) {
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::TemporaryPassword);
|
||||
raii::AuthedConnID::update_or_insert_session(
|
||||
self.session_key(),
|
||||
Some(password),
|
||||
@@ -2290,7 +2202,6 @@ impl Connection {
|
||||
if local_permanent_password_storage_is_usable_for_auth(&local_storage, &local_salt)
|
||||
&& self.validate_password_storage(&local_storage)
|
||||
{
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::PermanentPassword);
|
||||
print_fallback();
|
||||
return true;
|
||||
}
|
||||
@@ -2299,7 +2210,6 @@ impl Connection {
|
||||
if preset_permanent_password_storage_is_usable_for_auth(&hard, &salt)
|
||||
&& self.validate_preset_password_storage(&hard, &salt)
|
||||
{
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::PermanentPassword);
|
||||
print_fallback();
|
||||
return true;
|
||||
}
|
||||
@@ -2324,11 +2234,6 @@ impl Connection {
|
||||
&& (tfa && session.tfa
|
||||
|| !tfa && self.validate_password_plain(&session.random_password))
|
||||
{
|
||||
if tfa {
|
||||
self.set_conn_audit_two_factor(ConnAuditTwoFactor::Totp);
|
||||
} else {
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::TemporaryPassword);
|
||||
}
|
||||
log::info!("is recent session");
|
||||
return true;
|
||||
}
|
||||
@@ -2422,7 +2327,6 @@ impl Connection {
|
||||
&& device.platform == lr.my_platform
|
||||
{
|
||||
log::info!("2FA bypassed by trusted devices");
|
||||
self.set_conn_audit_two_factor(ConnAuditTwoFactor::TrustedDevice);
|
||||
self.require_2fa = None;
|
||||
}
|
||||
}
|
||||
@@ -2715,7 +2619,6 @@ impl Connection {
|
||||
if res {
|
||||
self.update_failure(failure, true, 1);
|
||||
self.require_2fa.take();
|
||||
self.set_conn_audit_two_factor(ConnAuditTwoFactor::Totp);
|
||||
raii::AuthedConnID::set_session_2fa(self.session_key());
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
@@ -2771,7 +2674,6 @@ impl Connection {
|
||||
if let Some((_instant, uuid_old)) = uuid_old {
|
||||
if uuid == uuid_old {
|
||||
self.from_switch = true;
|
||||
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::SwitchSides);
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
}
|
||||
@@ -3766,7 +3668,7 @@ impl Connection {
|
||||
);
|
||||
self.send_login_error("Please try 1 minute later").await;
|
||||
sleep(1.).await;
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::TerminalOsLoginConcurrency,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
@@ -3954,7 +3856,7 @@ impl Connection {
|
||||
prefix_num
|
||||
))
|
||||
.await;
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::ExceedIPv6PrefixAttempts,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
@@ -3999,7 +3901,7 @@ impl Connection {
|
||||
if let Some(audit) = decision.audit {
|
||||
// For OS blocked/backoff events, we currently emit one alarm report per blocked attempt.
|
||||
// TODO: Add unified cumulative/aggregation fields across alarm producers.
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
audit,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
@@ -4036,7 +3938,7 @@ impl Connection {
|
||||
|
||||
let res = if failure.2 > 30 {
|
||||
self.send_login_error("Too many wrong attempts").await;
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::ExceedThirtyAttempts,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
@@ -4047,7 +3949,7 @@ impl Connection {
|
||||
false
|
||||
} else if time == failure.0 && failure.1 > 6 {
|
||||
self.send_login_error("Please try 1 minute later").await;
|
||||
self.post_alarm_audit(
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::SixAttemptsWithinOneMinute,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
@@ -5588,7 +5490,6 @@ fn try_activate_screen() {
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AlarmAuditType {
|
||||
IpWhitelist = 0,
|
||||
ExceedThirtyAttempts = 1,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/// Url handler based on dbus
|
||||
///
|
||||
/// Note:
|
||||
/// On linux, we use dbus to communicate between multiple rustdesk processes.
|
||||
/// On linux, we use dbus to communicate multiple rustdesk process.
|
||||
/// [Flutter]: handle uni links for linux
|
||||
use dbus::blocking::Connection;
|
||||
use dbus_crossroads::{Crossroads, IfaceBuilder};
|
||||
|
||||
@@ -1857,33 +1857,15 @@ impl TerminalServiceProxy {
|
||||
// Process each session with its own lock
|
||||
for (terminal_id, session_arc) in sessions {
|
||||
if let Ok(mut session) = session_arc.try_lock() {
|
||||
// Check if the session has ended (reader thread finished or child exited).
|
||||
// On Linux, the PTY reader thread may not return EOF when the shell exits
|
||||
// (the cloned master fd keeps the read side open), so we also poll the child
|
||||
// process via try_wait() as a fallback detection mechanism.
|
||||
// Check if reader thread is still alive and we haven't sent closed message yet
|
||||
let mut should_send_closed = false;
|
||||
if !session.closed_message_sent {
|
||||
if let Some(thread) = &session.reader_thread {
|
||||
if thread.is_finished() {
|
||||
should_send_closed = true;
|
||||
session.closed_message_sent = true;
|
||||
}
|
||||
}
|
||||
if !should_send_closed {
|
||||
if let Some(child) = &mut session.child {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
should_send_closed = true;
|
||||
}
|
||||
Ok(None) => {} // still running
|
||||
Err(e) => {
|
||||
log::warn!("Terminal {} child wait error: {}", terminal_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if should_send_closed {
|
||||
session.closed_message_sent = true;
|
||||
}
|
||||
}
|
||||
// It's Ok to put the closed message here.
|
||||
// Because the `reader_thread` is joined in `stop()`,
|
||||
@@ -2036,8 +2018,7 @@ impl TerminalServiceProxy {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For persistent sessions, clear the child reference and remove the session
|
||||
// if the closed message has been sent (shell has exited).
|
||||
// For persistent sessions, just clear the child reference
|
||||
if let Some(session_arc) = sessions.get(&terminal_id) {
|
||||
let mut session = session_arc.lock().unwrap();
|
||||
if let Some(mut child) = session.child.take() {
|
||||
@@ -2047,12 +2028,6 @@ impl TerminalServiceProxy {
|
||||
}
|
||||
add_to_reaper(child);
|
||||
}
|
||||
if session.closed_message_sent {
|
||||
// Shell has exited, remove the dead session
|
||||
drop(session);
|
||||
sessions.remove(&terminal_id);
|
||||
service.lock().unwrap().sessions.remove(&terminal_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-13
@@ -143,21 +143,12 @@ fn make_tray() -> hbb_common::ResultType<()> {
|
||||
}
|
||||
// We create the icon once the event loop is actually running
|
||||
// to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90
|
||||
let mut builder = TrayIconBuilder::new()
|
||||
let tray = TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu.clone()))
|
||||
.with_tooltip(tooltip(0))
|
||||
.with_icon(icon.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.with_icon_as_template(true);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Required since tray-icon 0.17
|
||||
// Fixes #15215, #15222, #15410
|
||||
builder = builder.with_menu_on_left_click(false);
|
||||
}
|
||||
let tray = builder.build();
|
||||
.with_icon(icon.clone())
|
||||
.with_icon_as_template(true) // mac only
|
||||
.build();
|
||||
match tray {
|
||||
Ok(tray) => _tray_icon = Arc::new(Mutex::new(Some(tray))),
|
||||
Err(err) => {
|
||||
|
||||
+19
-72
@@ -72,48 +72,6 @@ function getExt(name) {
|
||||
class JobTable: Reactor.Component {
|
||||
this var jobs = [];
|
||||
this var job_map = {};
|
||||
this var next_conflict_batch_id = 1;
|
||||
this var remembered_write_strategy = {};
|
||||
|
||||
function nextConflictBatchId() {
|
||||
return this.next_conflict_batch_id++;
|
||||
}
|
||||
|
||||
function getRememberedWriteStrategy(conflict_batch_id) {
|
||||
var is_override = this.remembered_write_strategy[conflict_batch_id];
|
||||
if (is_override == true || is_override == false) return is_override;
|
||||
return null;
|
||||
}
|
||||
|
||||
function rememberWriteStrategy(conflict_batch_id, is_override) {
|
||||
this.remembered_write_strategy[conflict_batch_id] = is_override;
|
||||
}
|
||||
|
||||
function cancelTransferJob(job) {
|
||||
job.finished = true;
|
||||
job.err = "cancel";
|
||||
this.updateJob(job);
|
||||
}
|
||||
|
||||
function cancelTransferConflictBatch(id) {
|
||||
var job = this.job_map[id];
|
||||
if (!job) return;
|
||||
var conflict_batch_id = job.conflict_batch_id;
|
||||
if (conflict_batch_id == null) {
|
||||
this.cancelTransferJob(job);
|
||||
handler.cancel_job(job.id);
|
||||
refreshDir(!job.is_remote);
|
||||
return;
|
||||
}
|
||||
delete this.remembered_write_strategy[conflict_batch_id];
|
||||
for (var i = 0; i < this.jobs.length; ++i) {
|
||||
var current_job = this.jobs[i];
|
||||
if (current_job.conflict_batch_id != conflict_batch_id || current_job.finished) continue;
|
||||
this.cancelTransferJob(current_job);
|
||||
handler.cancel_job(current_job.id);
|
||||
}
|
||||
refreshDir(!job.is_remote);
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
@@ -151,12 +109,10 @@ class JobTable: Reactor.Component {
|
||||
function clearAllJobs() {
|
||||
this.jobs = [];
|
||||
this.job_map = {};
|
||||
this.next_conflict_batch_id = 1;
|
||||
this.remembered_write_strategy = {};
|
||||
this.update();
|
||||
}
|
||||
|
||||
function send(path, is_remote, conflict_batch_id = null) {
|
||||
function send(path, is_remote) {
|
||||
var to;
|
||||
var show_hidden;
|
||||
if (is_remote) {
|
||||
@@ -167,15 +123,13 @@ class JobTable: Reactor.Component {
|
||||
show_hidden = file_transfer.local_folder_view.show_hidden;
|
||||
}
|
||||
if (!to) return;
|
||||
if (conflict_batch_id == null) conflict_batch_id = this.nextConflictBatchId();
|
||||
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
|
||||
var id = handler.get_next_job_id();
|
||||
this.jobs.push({ type: "transfer",
|
||||
id: id, path: path, to: to,
|
||||
include_hidden: show_hidden,
|
||||
is_remote: is_remote,
|
||||
is_last: false,
|
||||
conflict_batch_id: conflict_batch_id
|
||||
is_last: false
|
||||
});
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.send_files(id, 0, path, to, 0, show_hidden, is_remote);
|
||||
@@ -187,8 +141,7 @@ class JobTable: Reactor.Component {
|
||||
var job = { type: "transfer",
|
||||
id: id, path: path, to: to,
|
||||
include_hidden: show_hidden,
|
||||
is_remote: is_remote, is_last: true, file_num: file_num,
|
||||
conflict_batch_id: this.nextConflictBatchId() };
|
||||
is_remote: is_remote, is_last: true, file_num: file_num };
|
||||
this.jobs.push(job);
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.update_next_job_id(id + 1);
|
||||
@@ -277,7 +230,6 @@ class JobTable: Reactor.Component {
|
||||
else return translate("Waiting");
|
||||
}
|
||||
}
|
||||
if (job.err == "cancel") return translate("Cancel");
|
||||
if (!job.entries) return translate("Waiting");
|
||||
var i = job.file_num + 1;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
@@ -310,8 +262,6 @@ class JobTable: Reactor.Component {
|
||||
|
||||
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
|
||||
var job = this.job_map[id];
|
||||
if (!job) return;
|
||||
if (job.finished && job.err == "cancel") return;
|
||||
if (job.type == "del-file"){
|
||||
job.finished = true;
|
||||
job.err = err;
|
||||
@@ -319,6 +269,7 @@ class JobTable: Reactor.Component {
|
||||
this.updateJob(job);
|
||||
return;
|
||||
}
|
||||
if (!job) return;
|
||||
if (file_num < job.file_num) return;
|
||||
job.file_num = file_num;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
@@ -650,9 +601,8 @@ class FolderView : Reactor.Component {
|
||||
event click $(.send) () {
|
||||
var rows = this.getCurrentRows();
|
||||
if (!rows || rows.length == 0) return;
|
||||
var conflict_batch_id = file_transfer.job_table.nextConflictBatchId();
|
||||
for (var i = 0; i < rows.length; ++i) {
|
||||
file_transfer.job_table.send(rows[i][0], this.is_remote, conflict_batch_id);
|
||||
file_transfer.job_table.send(rows[i][0], this.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,13 +780,6 @@ handler.confirmDeleteFiles = function(id, i, name) {
|
||||
|
||||
handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) {
|
||||
var jt = file_transfer.job_table;
|
||||
var job = jt.job_map[id];
|
||||
if (!job || job.finished) return;
|
||||
var remembered = jt.getRememberedWriteStrategy(job.conflict_batch_id);
|
||||
if (remembered == true || remembered == false) {
|
||||
handler.set_write_override(id, file_num, remembered, true, is_upload);
|
||||
return;
|
||||
}
|
||||
var identical_msg = is_identical ? translate("identical_file_tip"): "";
|
||||
msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \
|
||||
<div>" + translate('Overwrite') + " " + translate('files') + ".</div> \
|
||||
@@ -845,18 +788,22 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical
|
||||
<div>" + identical_msg + "</div> \
|
||||
<div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \
|
||||
</div>", "", function(res=null) {
|
||||
var current_job = jt.job_map[id];
|
||||
if (!current_job || current_job.finished) return;
|
||||
if (!res) {
|
||||
jt.cancelTransferConflictBatch(id);
|
||||
return;
|
||||
jt.updateJobStatus(id, -1, "cancel");
|
||||
handler.cancel_job(id);
|
||||
} else if (res.skip) {
|
||||
if (res.remember){
|
||||
handler.set_write_override(id,file_num,false,true, is_upload); //
|
||||
} else {
|
||||
handler.set_write_override(id,file_num,false,false,is_upload); //
|
||||
}
|
||||
} else {
|
||||
if (res.remember){
|
||||
handler.set_write_override(id,file_num,true,true,is_upload); //
|
||||
} else {
|
||||
handler.set_write_override(id,file_num,true,false,is_upload); //
|
||||
}
|
||||
}
|
||||
var is_override = !res.skip;
|
||||
var remember = res.remember ? true : false;
|
||||
if (remember) {
|
||||
jt.rememberWriteStrategy(current_job.conflict_batch_id, is_override);
|
||||
}
|
||||
handler.set_write_override(id, file_num, is_override, remember, is_upload);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -603,7 +603,7 @@ class MyIdMenu: Reactor.Component {
|
||||
<div>Fingerprint: " + handler.get_fingerprint() + " \
|
||||
<div .link .custom-event url='https://rustdesk.com/privacy.html'>" + translate("Privacy Statement") + "</div> \
|
||||
<div .link .custom-event url='https://rustdesk.com'>" + translate("Website") + "</div> \
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2026 Purslane Tech Pte. Ltd.\
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2025 Purslane Ltd.\
|
||||
<br />" + handler.get_license() + " \
|
||||
<p style='font-weight: bold'>" + translate("Slogan_tip") + "</p>\
|
||||
</div>\
|
||||
|
||||
@@ -128,10 +128,6 @@ impl ConnectionRoundState {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
matches!(self.state, ConnectionState::Connected)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConnectionRoundState {
|
||||
|
||||
+1
-8
@@ -136,17 +136,10 @@ fn check_update(manually: bool) -> ResultType<()> {
|
||||
let version = download_url.split('/').last().unwrap_or_default();
|
||||
#[cfg(target_os = "windows")]
|
||||
let download_url = if cfg!(feature = "flutter") {
|
||||
let Some(arch) = crate::platform::windows::release_arch_suffix() else {
|
||||
bail!(
|
||||
"Unsupported Windows release architecture: {}",
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
};
|
||||
format!(
|
||||
"{}/rustdesk-{}-{}.{}",
|
||||
"{}/rustdesk-{}-x86_64.{}",
|
||||
download_url,
|
||||
version,
|
||||
arch,
|
||||
if update_msi { "msi" } else { "exe" }
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include <check.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "../libs/clipboard/src/windows/wf_cliprdr.c"
|
||||
|
||||
static SIZE_T descriptor_size(UINT count)
|
||||
{
|
||||
return offsetof(FILEGROUPDESCRIPTORW, fgd) + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
|
||||
}
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_buffer_smaller_than_header)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(0, 1), FALSE);
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
offsetof(FILEGROUPDESCRIPTORW, fgd) - 1, 1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_zero_items)
|
||||
{
|
||||
ck_assert_int_eq(
|
||||
wf_cliprdr_file_group_descriptor_size_valid(offsetof(FILEGROUPDESCRIPTORW, fgd), 0),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_accepts_max_stream_count)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS),
|
||||
TRUE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_stream_count_above_limit)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
|
||||
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS + 1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_truncated_descriptor_array)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(descriptor_size(2) - 1, 2),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_descriptor_size_rejects_extreme_count)
|
||||
{
|
||||
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid((SIZE_T)-1, (UINT)-1),
|
||||
FALSE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
Suite *wf_cliprdr_invariant_suite(void)
|
||||
{
|
||||
Suite *s;
|
||||
TCase *tc_core;
|
||||
|
||||
s = suite_create("wf_cliprdr_invariants");
|
||||
tc_core = tcase_create("descriptor_size");
|
||||
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_buffer_smaller_than_header);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_zero_items);
|
||||
tcase_add_test(tc_core, test_descriptor_size_accepts_max_stream_count);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_stream_count_above_limit);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_truncated_descriptor_array);
|
||||
tcase_add_test(tc_core, test_descriptor_size_rejects_extreme_count);
|
||||
|
||||
suite_add_tcase(s, tc_core);
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int number_failed;
|
||||
Suite *s;
|
||||
SRunner *sr;
|
||||
|
||||
s = wf_cliprdr_invariant_suite();
|
||||
sr = srunner_create(s);
|
||||
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
number_failed = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
|
||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Reference in New Issue
Block a user