diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index f9dfee5c6..4850efd7b 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -426,6 +426,37 @@ jobs: throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'" } + - name: Verify template payload signatures + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: powershell + run: | + Get-ChildItem ./rustdesk -Recurse -File -ErrorAction Stop | + Where-Object { $_.Extension -eq '.exe' -or ($_.Extension -eq '.dll' -and $_.FullName -notmatch 'RustDeskPrinterDriver|usbmmidd_v2') } | + ForEach-Object { + $signature = Get-AuthenticodeSignature -LiteralPath $_.FullName -ErrorAction Stop + if ($signature.Status -ne 'Valid') { throw "Invalid template payload signature: $($_.FullName) ($($signature.Status))" } + } + + - name: Build template packer + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -e rustdesk.exe -l 11 -t "${{ matrix.job.target }}" --exclude-exe + popd + mkdir -p ./packer-template + cp ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe ./packer-template/rustdesk-template-${{ matrix.job.arch }}.exe + + - name: Upload unsigned template packer + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rustdesk-unsigned-packer-template-${{ matrix.job.arch }} + path: ./packer-template + if-no-files-found: error + - name: Build self-extracted executable shell: bash if: env.UPLOAD_ARTIFACT == 'true' @@ -690,6 +721,37 @@ jobs: throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'" } + - name: Verify template payload signatures + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: powershell + run: | + Get-ChildItem ./Release -Recurse -File -ErrorAction Stop | + Where-Object { $_.Extension -eq '.exe' -or ($_.Extension -eq '.dll' -and $_.FullName -notmatch 'RustDeskPrinterDriver|usbmmidd_v2') } | + ForEach-Object { + $signature = Get-AuthenticodeSignature -LiteralPath $_.FullName -ErrorAction Stop + if ($signature.Status -ne 'Valid') { throw "Invalid template payload signature: $($_.FullName) ($($signature.Status))" } + } + + - name: Build template packer + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../Release/ -e rustdesk.exe -l 11 -t "${{ matrix.job.target }}" + popd + mkdir -p ./packer-template + cp ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe ./packer-template/rustdesk-template-${{ matrix.job.arch }}.exe + + - name: Upload unsigned template packer + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rustdesk-unsigned-packer-template-${{ matrix.job.arch }} + path: ./packer-template + if-no-files-found: error + - name: Build self-extracted executable shell: bash run: | @@ -1301,9 +1363,34 @@ jobs: name: rustdesk-unsigned-msi-template-aarch64 path: ./msi-template/ + - name: Download Artifacts + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: rustdesk-unsigned-packer-template-x86_64 + path: ./packer-template/ + + - name: Download Artifacts + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: rustdesk-unsigned-packer-template-aarch64 + path: ./packer-template/ + + - name: Download Artifacts + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: rustdesk-unsigned-packer-template-x86 + path: ./packer-template/ + - name: Combine unsigned app run: | - tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template + packer_template=() + if [[ "$SIGN_BASE_URL" != '-2' ]]; then + packer_template=(packer-template) + fi + tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template "${packer_template[@]}" - name: Publish unsigned app uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index cc40e1422..1f3e027d3 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -40,7 +40,7 @@ prepare_flutter() { # Patch flutter - if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then + if dpkg --compare-versions "${VERSION}" ge "3.24.4" && dpkg --compare-versions "${VERSION}" lt "3.44.0"; then git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" fi @@ -324,11 +324,7 @@ prebuild) # Install Flutter bridge version - # Unbraced on purpose: F-Droid recipes rewrite the braced form with sed to - # pin a version, but the bridge generator only works with the Flutter - # version bridge.yml uses - - prepare_flutter "$FLUTTER_BRIDGE_VERSION" "${HOME}/flutter" + prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" # Save changes @@ -346,7 +342,7 @@ prebuild) pushd flutter flutter clean - flutter packages pub get --enforce-lockfile + flutter packages pub get popd # flutter diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index 0a6c9e459..edf2b9f39 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 5CE7AD012EB4000000A1B2C3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -40,6 +41,7 @@ 4151ACC476A12FDC49BC7860 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -104,6 +106,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -279,6 +282,7 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 5CE7AD012EB4000000A1B2C3 /* SceneDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 1c9e71b50..5d177b2db 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -8,7 +8,6 @@ import AVFoundation _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) do { // Mix remote audio with other apps while still respecting silent mode. try AVAudioSession.sharedInstance().setCategory(.ambient) @@ -18,6 +17,19 @@ import AVFoundation dummyMethodToEnforceBundling(); return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + // With the UIScene life cycle, the FlutterViewController only exists once the scene + // connects, and FlutterAppDelegate reaches its engine through `window`. A launch URL + // also arrives with the scene instead of in launchOptions, so forward it to plugins here. + func sceneWillConnect(window: UIWindow?, launchURL: URL?) { + self.window = window + GeneratedPluginRegistrant.register(with: self) + var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:] + if let launchURL = launchURL { + launchOptions[.url] = launchURL + } + _ = super.application(UIApplication.shared, didFinishLaunchingWithOptions: launchOptions) + } public func dummyMethodToEnforceBundling() { dummy_method_to_enforce_bundling(); diff --git a/flutter/ios/Runner/Info.plist b/flutter/ios/Runner/Info.plist index 2d655a922..78fd51dd0 100644 --- a/flutter/ios/Runner/Info.plist +++ b/flutter/ios/Runner/Info.plist @@ -47,6 +47,27 @@ This app needs camera access to scan QR codes NSPhotoLibraryUsageDescription This app needs photo library access to get QR codes from image + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UIFileSharingEnabled @@ -71,7 +92,7 @@ UISupportsDocumentBrowser UIViewControllerBasedStatusBarAppearance - + io.flutter.embedded_views_preview diff --git a/flutter/ios/Runner/SceneDelegate.swift b/flutter/ios/Runner/SceneDelegate.swift new file mode 100644 index 000000000..811717650 --- /dev/null +++ b/flutter/ios/Runner/SceneDelegate.swift @@ -0,0 +1,21 @@ +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + (UIApplication.shared.delegate as? AppDelegate)?.sceneWillConnect( + window: window, launchURL: connectionOptions.urlContexts.first?.url) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } + for context in URLContexts { + _ = appDelegate.application(UIApplication.shared, open: context.url, options: [:]) + } + } +} diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 8e72f8d31..58ff028f2 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/install_directory_confirmation.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -164,27 +165,46 @@ class _InstallPageBodyState extends State<_InstallPageBody> children: [ Text(translate('Installation'), style: Theme.of(context).textTheme.headlineMedium), - Row( + Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 2: IntrinsicColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - Text('${translate('Installation Path')}:') - .marginOnly(right: 10), - Expanded( - child: TextField( - controller: controller, - readOnly: true, - decoration: InputDecoration( - contentPadding: EdgeInsets.all(0.75 * em), + TableRow( + children: [ + Text('${translate('Installation Path')}:') + .marginOnly(right: 10), + TextField( + controller: controller, + readOnly: true, + decoration: InputDecoration( + contentPadding: EdgeInsets.all(0.75 * em), + ), + ).workaroundFreezeLinuxMint().marginOnly(right: 10), + Obx( + () => OutlinedButton.icon( + icon: Icon(Icons.folder_outlined, size: 16), + onPressed: + btnEnabled.value ? selectInstallPath : null, + style: buttonStyle, + label: Text(translate('Change Path')), + ), ), - ).workaroundFreezeLinuxMint().marginOnly(right: 10), + ], + ), + TableRow( + children: [ + const SizedBox.shrink(), + ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) => + InstallDirectoryWarning(directoryPath: value.text), + ).marginOnly(right: 10), + const SizedBox.shrink(), + ], ), - Obx( - () => OutlinedButton.icon( - icon: Icon(Icons.folder_outlined, size: 16), - onPressed: btnEnabled.value ? selectInstallPath : null, - style: buttonStyle, - label: Text(translate('Change Path')), - ), - ) ], ).marginSymmetric(vertical: 2 * em), Option(startmenu, label: 'Create start menu shortcuts') @@ -277,7 +297,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> )); } - void install() { + Future install() async { + if (!btnEnabled.value) return; + final installPath = controller.text; do_install() { btnEnabled.value = false; cancelEnabled.value = false; @@ -286,10 +308,18 @@ class _InstallPageBodyState extends State<_InstallPageBody> if (startmenu.value) args += ' startmenu'; if (desktopicon.value) args += ' desktopicon'; if (printer.value) args += ' printer'; - bind.installInstallMe(options: args, path: controller.text); + bind.installInstallMe(options: args, path: installPath); } - do_install(); + btnEnabled.value = false; + try { + final confirmed = + await confirmInstallDirectory(this.context, targetPath: installPath); + if (!mounted || !confirmed) return; + do_install(); + } finally { + if (mounted && !showProgress.value) btnEnabled.value = true; + } } void selectInstallPath() async { diff --git a/flutter/lib/desktop/widgets/install_directory_confirmation.dart b/flutter/lib/desktop/widgets/install_directory_confirmation.dart new file mode 100644 index 000000000..d48b203a9 --- /dev/null +++ b/flutter/lib/desktop/widgets/install_directory_confirmation.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:path/path.dart' as path; + +const _nonemptyDirectoryWarning = 'install-directory-not-empty-tip'; + +Future confirmInstallDirectory(BuildContext context, + {required String targetPath}) async { + try { + final needsWarning = await _needsDirectoryWarning(targetPath); + if (!context.mounted) return false; + if (!needsWarning) return true; + return await _showDirectoryDialog( + context, '${translate(_nonemptyDirectoryWarning)}\n$targetPath'); + } on FileSystemException catch (error) { + debugPrint('Failed to inspect installation directory: $error'); + if (context.mounted) { + await _showDirectoryDialog(context, error.toString(), isError: true); + } + return false; + } +} + +Future _needsDirectoryWarning(String directoryPath) async { + if (bind.mainIsInstalled() && + path.equals(directoryPath, bind.installInstallPath())) { + return false; + } + final directory = Directory(directoryPath); + return await directory.exists() && + !await directory.list(followLinks: false).isEmpty; +} + +Future _showDirectoryDialog(BuildContext context, String message, + {bool isError = false}) async { + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + scrollable: true, + title: Text(translate(isError ? 'Error' : 'Warning')), + content: SelectableText(message), + actions: [ + dialogButton(isError ? 'OK' : 'Cancel', + isOutline: !isError, + onPressed: () => Navigator.of(dialogContext).pop(false)), + if (!isError) + dialogButton('Continue', + onPressed: () => Navigator.of(dialogContext).pop(true)), + ], + ), + ); + return confirmed == true; +} + +class InstallDirectoryWarning extends StatefulWidget { + InstallDirectoryWarning({required this.directoryPath}) + : super(key: ValueKey(directoryPath)); + + final String directoryPath; + + @override + State createState() => + _InstallDirectoryWarningState(); +} + +class _InstallDirectoryWarningState extends State { + static const _spacing = 8.0; + late final Future _needsWarning = + _needsDirectoryWarning(widget.directoryPath); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _needsWarning, + builder: (context, snapshot) { + if (!snapshot.hasError && snapshot.data != true) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: _spacing), + child: Text( + snapshot.hasError + ? '${translate('Error')}: ${snapshot.error}' + : translate(_nonemptyDirectoryWarning), + style: TextStyle( + color: snapshot.hasError + ? Theme.of(context).colorScheme.error + : Colors.orange.shade700)), + ); + }, + ); + } +} diff --git a/res/job.py b/res/job.py index e53105fd3..22703f86e 100755 --- a/res/job.py +++ b/res/job.py @@ -6,8 +6,48 @@ import time import argparse import logging import shutil +import sys import zipfile + +def _file_stream_positions(files): + if not files: + return [] + values = files.values() if hasattr(files, "values") else (item[1] for item in files) + positions = [] + for value in values: + stream = value[1] if isinstance(value, tuple) and len(value) > 1 else value + if hasattr(stream, "tell") and hasattr(stream, "seek"): + try: + positions.append((stream, stream.tell())) + except (OSError, ValueError): + pass + return positions + + +def _with_simple_retries(fn, max_attempts=8, sleep_secs=1.0): + def _wrapper(*args, **kwargs): + file_positions = _file_stream_positions(kwargs.get("files")) + for i in range(max_attempts): + try: + if i: + for stream, position in file_positions: + stream.seek(position) + return fn(*args, **kwargs) + except Exception as e: + logging.warning(f"{fn.__name__} attempt {i+1}/{max_attempts} failed: {e}") + if i == max_attempts - 1: + raise + time.sleep(sleep_secs) + return _wrapper + + +HTTP_RETRY_MAX_TRIES = int(os.getenv("HTTP_RETRY_MAX_TRIES") or "8") +HTTP_RETRY_SLEEP_SECS = float(os.getenv("HTTP_RETRY_SLEEP_SECS") or "1") + +requests.get = _with_simple_retries(requests.get, max_attempts=HTTP_RETRY_MAX_TRIES, sleep_secs=HTTP_RETRY_SLEEP_SECS) +requests.post = _with_simple_retries(requests.post, max_attempts=HTTP_RETRY_MAX_TRIES, sleep_secs=HTTP_RETRY_SLEEP_SECS) + logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]", @@ -164,10 +204,13 @@ def sign_one_file(file_path): n += 1 status = get_status(task_id) if status and status.get("state") == "done": - download_one_file( + downloaded = download_one_file( task_id, os.path.basename(file_path), os.path.dirname(file_path) ) delete_task(task_id) + if not downloaded: + logging.error(f"Failed to download signed {file_path}") + return False logging.info(f"Signed {file_path}") return True return False @@ -217,7 +260,7 @@ def sign_files(dir_path, only_ext=None): if ext in SIGN_EXTENSIONS: if not sign_one_file(file_path): logging.error(f"Failed to sign {file_path}") - break + sys.exit(1) def main(): diff --git a/src/lang/ar.rs b/src/lang/ar.rs index be896959f..cfc6d72b2 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "مدة الثقة (بالأيام)"), ("Renew trust", "تجديد الثقة"), ("Request approval", ""), + ("install-directory-not-empty-tip", "المجلد المحدد ليس فارغًا. قد يؤدي التثبيت إلى حذف البيانات الموجودة. يُنصح باختيار مجلد آخر."), ].iter().cloned().collect(); } diff --git a/src/lang/az.rs b/src/lang/az.rs index 315d498ef..06d1fea57 100644 --- a/src/lang/az.rs +++ b/src/lang/az.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Etibar müddəti (gün)"), ("Renew trust", "Etibarı yenilə"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Seçilmiş qovluq boş deyil. Quraşdırma mövcud məlumatları silə bilər. Başqa qovluq seçməyiniz tövsiyə olunur."), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 442d4fd53..7e81bcd31 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Тэрмін даверу (у днях)"), ("Renew trust", "Падоўжыць давер"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Выбраная папка не пустая. Усталёўванне можа выдаліць наяўныя даныя. Лепш выберыце іншую папку."), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index f13ffaaad..e36a9f3eb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Срок на доверие (дни)"), ("Renew trust", "Подновяване на доверието"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Избраната папка не е празна. Инсталацията може да изтрие съществуващите данни. Препоръчително е да изберете друга папка."), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 3696f4fa5..9535bc68b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durada de la confiança (dies)"), ("Renew trust", "Renova la confiança"), ("Request approval", ""), + ("install-directory-not-empty-tip", "La carpeta seleccionada no és buida. La instal·lació pot suprimir les dades existents. Considereu triar una altra carpeta."), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7c8d90b2b..2840b1301 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "信任有效期(天)"), ("Renew trust", "延长信任有效期"), ("Request approval", "请求批准"), + ("install-directory-not-empty-tip", "所选文件夹不为空。安装可能删除现有数据。建议选择其他文件夹。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 86d2f0fb4..71c675257 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Doba důvěry (dny)"), ("Renew trust", "Obnovit důvěru"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Vybraná složka není prázdná. Instalace může odstranit existující data. Zvažte výběr jiné složky."), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 71f3db823..c1ae3a997 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Tillidsperiode (dage)"), ("Renew trust", "Forny tillid"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Den valgte mappe er ikke tom. Installationen kan slette eksisterende data. Overvej at vælge en anden mappe."), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c3a945d35..070bf46c5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Vertrauensdauer (Tage)"), ("Renew trust", "Vertrauen erneuern"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Der ausgewählte Ordner ist nicht leer. Bei der Installation können vorhandene Daten gelöscht werden. Erwägen Sie, einen anderen Ordner auszuwählen."), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index fbb422edd..c122433a8 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Διάρκεια εμπιστοσύνης (ημέρες)"), ("Renew trust", "Ανανέωση εμπιστοσύνης"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Ο επιλεγμένος φάκελος δεν είναι κενός. Η εγκατάσταση ενδέχεται να διαγράψει υπάρχοντα δεδομένα. Εξετάστε το ενδεχόμενο να επιλέξετε διαφορετικό φάκελο."), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index f3c654f0f..72b666174 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -291,5 +291,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("api_server_connection_tip", "Your custom server uses unencrypted HTTP. Contact your administrator to enable HTTPS."), ("api_server_empty_tip", "With API Server empty, your custom server defaults to HTTP. Contact your administrator to enable HTTPS."), ("android-logs-folder-tip", "Choose Android → media to store log files."), + ("install-directory-not-empty-tip", "The selected folder is not empty. Installation may delete existing data. Consider choosing a different folder."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 849e816ac..2b2af68b9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Daŭro de fido (tagoj)"), ("Renew trust", "Renovigi fidon"), ("Request approval", ""), + ("install-directory-not-empty-tip", "La elektita dosierujo ne estas malplena. La instalado povas forigi ekzistantajn datumojn. Konsideru elekti alian dosierujon."), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 78514e687..8dbe7f9f9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Duración de la confianza (días)"), ("Renew trust", "Renovar confianza"), ("Request approval", ""), + ("install-directory-not-empty-tip", "La carpeta seleccionada no está vacía. La instalación puede eliminar los datos existentes. Considera elegir otra carpeta."), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 2a75be6d9..a0ee25c53 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Usalduse kestus (päevades)"), ("Renew trust", "Uuenda usaldust"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Valitud kaust ei ole tühi. Paigaldamine võib olemasolevad andmed kustutada. Kaalu teise kausta valimist."), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 283e325cc..4987d0bff 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Konfiantzaren iraupena (egunak)"), ("Renew trust", "Berritu konfiantza"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Hautatutako karpeta ez dago hutsik. Instalazioak lehendik dauden datuak ezaba ditzake. Beste karpeta bat aukeratzea gomendatzen da."), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5064f50d3..cf47ae4a0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "مدت اعتماد (روز)"), ("Renew trust", "تمدید اعتماد"), ("Request approval", ""), + ("install-directory-not-empty-tip", "پوشه انتخاب‌شده خالی نیست. نصب ممکن است داده‌های موجود را حذف کند. بهتر است پوشه دیگری انتخاب کنید."), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 0f2315a66..71eac5864 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Luottamuksen kesto (päivää)"), ("Renew trust", "Jatka luottamusta"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Valittu kansio ei ole tyhjä. Asennus voi poistaa olemassa olevia tietoja. Harkitse toisen kansion valitsemista."), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index adc75a0f2..c3a29150d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durée de confiance (jours)"), ("Renew trust", "Renouveler la confiance"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Le dossier sélectionné n’est pas vide. L’installation peut supprimer des données existantes. Envisagez de choisir un autre dossier."), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 7085cf831..7ed8c7b54 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "ნდობის ხანგრძლივობა (დღე)"), ("Renew trust", "ნდობის განახლება"), ("Request approval", ""), + ("install-directory-not-empty-tip", "არჩეული საქაღალდე ცარიელი არ არის. ინსტალაციამ შეიძლება წაშალოს არსებული მონაცემები. რეკომენდებულია სხვა საქაღალდის არჩევა."), ].iter().cloned().collect(); } diff --git a/src/lang/gl.rs b/src/lang/gl.rs index aa7563416..474ea2833 100644 --- a/src/lang/gl.rs +++ b/src/lang/gl.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Duración da confianza (días)"), ("Renew trust", "Renovar a confianza"), ("Request approval", ""), + ("install-directory-not-empty-tip", "O cartafol seleccionado non está baleiro. A instalación pode eliminar os datos existentes. Considera escoller outro cartafol."), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index 59bcc5680..f7020caf2 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "વિશ્વાસ અવધિ (દિવસો)"), ("Renew trust", "વિશ્વાસ નવીકરણ કરો"), ("Request approval", ""), + ("install-directory-not-empty-tip", "પસંદ કરેલ ફોલ્ડર ખાલી નથી. ઇન્સ્ટોલેશન હાલનો ડેટા કાઢી નાખી શકે છે. બીજું ફોલ્ડર પસંદ કરવાનું વિચારો."), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index e71211e85..4afac336b 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "משך האמון (ימים)"), ("Renew trust", "חידוש אמון"), ("Request approval", ""), + ("install-directory-not-empty-tip", "התיקייה שנבחרה אינה ריקה. ההתקנה עלולה למחוק נתונים קיימים. מומלץ לבחור תיקייה אחרת."), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index ebe6b1a35..18a63723c 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "विश्वास अवधि (दिन)"), ("Renew trust", "विश्वास नवीनीकृत करें"), ("Request approval", ""), + ("install-directory-not-empty-tip", "चयनित फ़ोल्डर खाली नहीं है। इंस्टॉलेशन से मौजूदा डेटा हट सकता है। कोई दूसरा फ़ोल्डर चुनने पर विचार करें।"), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 3d5dcac17..c3b0e8d32 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Trajanje povjerenja (dani)"), ("Renew trust", "Obnovi povjerenje"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Odabrana mapa nije prazna. Instalacija može izbrisati postojeće podatke. Razmislite o odabiru druge mape."), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9842052ab..82bd3de61 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Megbízhatóság időtartama (nap)"), ("Renew trust", "Megbízhatóság megújítása"), ("Request approval", ""), + ("install-directory-not-empty-tip", "A kiválasztott mappa nem üres. A telepítés törölheti a meglévő adatokat. Fontolja meg egy másik mappa kiválasztását."), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 81d591f3a..f76d0f15e 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durasi kepercayaan (hari)"), ("Renew trust", "Perbarui kepercayaan"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Folder yang dipilih tidak kosong. Instalasi dapat menghapus data yang ada. Pertimbangkan untuk memilih folder lain."), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b382f1ee7..8ecb659be 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durata attendibilità (giorni)"), ("Renew trust", "Rinnova attendibilità"), ("Request approval", ""), + ("install-directory-not-empty-tip", "La cartella selezionata non è vuota. L'installazione potrebbe eliminare i dati esistenti. Valuta la possibilità di scegliere una cartella diversa."), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c7e0e029c..001e529fd 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "信頼期間 (日)"), ("Renew trust", "信頼を更新"), ("Request approval", ""), + ("install-directory-not-empty-tip", "選択したフォルダーは空ではありません。インストールにより既存のデータが削除される可能性があります。別のフォルダーを選択することをおすすめします。"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ec44f4d50..a2ca12757 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "신뢰 유지 기간 (일)"), ("Renew trust", "신뢰 갱신"), ("Request approval", ""), + ("install-directory-not-empty-tip", "선택한 폴더가 비어 있지 않습니다. 설치 시 기존 데이터가 삭제될 수 있습니다. 다른 폴더를 선택하는 것이 좋습니다."), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1cb2c9a98..cc4576fad 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Сенім мерзімі (күн)"), ("Renew trust", "Сенімді жаңарту"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Таңдалған бума бос емес. Орнату бар деректерді жоюы мүмкін. Басқа буманы таңдаған жөн."), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 17cf13ad2..303f8cc1b 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Pasitikėjimo trukmė (dienomis)"), ("Renew trust", "Atnaujinti pasitikėjimą"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Pasirinktas aplankas nėra tuščias. Diegimas gali ištrinti esamus duomenis. Apsvarstykite galimybę pasirinkti kitą aplanką."), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index d3cb2f47d..0220f101a 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Uzticēšanās ilgums (dienas)"), ("Renew trust", "Atjaunot uzticēšanos"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Izvēlētā mape nav tukša. Instalēšana var izdzēst esošos datus. Apsveriet iespēju izvēlēties citu mapi."), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index dcc2191e7..1a3a096ee 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "വിശ്വാസ കാലാവധി (ദിവസം)"), ("Renew trust", "വിശ്വാസം പുതുക്കുക"), ("Request approval", ""), + ("install-directory-not-empty-tip", "തിരഞ്ഞെടുത്ത ഫോൾഡർ ശൂന്യമല്ല. ഇൻസ്റ്റാളേഷൻ നിലവിലുള്ള ഡാറ്റ ഇല്ലാതാക്കിയേക്കാം. മറ്റൊരു ഫോൾഡർ തിരഞ്ഞെടുക്കുന്നത് പരിഗണിക്കുക."), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 37e7a1724..d536a093d 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Klareringsperiode (dager)"), ("Renew trust", "Forny klarering"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Den valgte mappen er ikke tom. Installasjonen kan slette eksisterende data. Vurder å velge en annen mappe."), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 20be8aaff..9414e8202 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Vertrouwensperiode (dagen)"), ("Renew trust", "Vertrouwen hernieuwen"), ("Request approval", ""), + ("install-directory-not-empty-tip", "De geselecteerde map is niet leeg. De installatie kan bestaande gegevens verwijderen. Overweeg een andere map te kiezen."), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 08e756a8b..d8dc12074 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Okres zaufania (dni)"), ("Renew trust", "Odnów zaufanie"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Wybrany folder nie jest pusty. Instalacja może usunąć istniejące dane. Rozważ wybranie innego folderu."), ].iter().cloned().collect(); } diff --git a/src/lang/pt_BR.rs b/src/lang/pt_BR.rs index ac739c1b2..29db22ff1 100644 --- a/src/lang/pt_BR.rs +++ b/src/lang/pt_BR.rs @@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Informe sua senha"), ("Logging in...", "Fazendo Login..."), ("Enable RDP session sharing", "Habilitar compartilhamento de sessão RDP"), - ("Auto Login", "Login Automático (Somente válido se você habilitou \"Bloquear após o fim da sessão\")"), + ("Auto Login", "Login Automático (Para que o recurso funcione corretamente, é necessário habilitar \"Bloquear após o fim da sessão\" nas configurações do sistema.)"), ("Enable direct IP access", "Habilitar Acesso IP Direto"), ("Rename", "Renomear"), ("Space", "Espaço"), @@ -686,7 +686,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("New tab", "Nova aba"), ("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"), ("Terminal (Run as administrator)", "Terminal (Executar como administrador)"), - ("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."), + ("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo."), ("Failed to get user token.", "Falha ao obter token do usuário."), ("Incorrect username or password.", "Usuário ou senha incorretos"), ("The user is not an administrator.", "O usuário não é administrador"), @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Duração da confiança (dias)"), ("Renew trust", "Renovar confiança"), ("Request approval", ""), + ("install-directory-not-empty-tip", "A pasta selecionada não está vazia. A instalação pode excluir dados existentes. Considere escolher outra pasta."), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 9e8b41eb4..f70e68ad9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Duração da confiança (dias)"), ("Renew trust", "Renovar confiança"), ("Request approval", ""), + ("install-directory-not-empty-tip", "A pasta selecionada não está vazia. A instalação pode eliminar dados existentes. Considere escolher outra pasta."), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index f2c963535..04f5f4cd5 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durata încrederii (zile)"), ("Renew trust", "Reînnoiește încrederea"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Folderul selectat nu este gol. Instalarea poate șterge datele existente. Ia în considerare alegerea altui folder."), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e16940b67..dce05df2f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Срок доверия (в днях)"), ("Renew trust", "Продлить доверие"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Выбранная папка не пуста. При установке существующие данные могут быть удалены. Рекомендуется выбрать другую папку."), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 07fa01e9f..b3e1586c1 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Durada de sa fidùtzia (dies)"), ("Renew trust", "Annoa sa fidùtzia"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Sa cartella seletzionada non est bòida. S'installatzione diat pòdere iscantzellare sos datos chi bi sunt. Ti cussigiamus de isseberare un'àtera cartella."), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9647d69b4..18ee9f5db 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Doba dôvery (dni)"), ("Renew trust", "Obnoviť dôveru"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Vybraný priečinok nie je prázdny. Inštalácia môže odstrániť existujúce údaje. Zvážte výber iného priečinka."), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bd0162c0c..22520b9c6 100644 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Trajanje zaupanja (dni)"), ("Renew trust", "Obnovi zaupanje"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Izbrana mapa ni prazna. Namestitev lahko izbriše obstoječe podatke. Razmislite o izbiri druge mape."), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 588176f0e..7f8c39b08 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Kohëzgjatja e besimit (ditë)"), ("Renew trust", "Rinovo besimin"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Dosja e zgjedhur nuk është bosh. Instalimi mund të fshijë të dhënat ekzistuese. Konsideroni të zgjidhni një dosje tjetër."), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 2316f64b4..fa05b4d0f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Trajanje poverenja (dani)"), ("Renew trust", "Obnovi poverenje"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Izabrani direktorijum nije prazan. Instalacija može da obriše postojeće podatke. Razmislite o izboru drugog direktorijuma."), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a766a3847..65e8be5a6 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Förtroendeperiod (dagar)"), ("Renew trust", "Förnya förtroende"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Den valda mappen är inte tom. Installationen kan radera befintliga data. Överväg att välja en annan mapp."), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 855a570bb..5ee2f9f6a 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "நம்பிக்கைக் காலம் (நாட்கள்)"), ("Renew trust", "நம்பிக்கையைப் புதுப்பி"), ("Request approval", ""), + ("install-directory-not-empty-tip", "தேர்ந்தெடுக்கப்பட்ட கோப்புறை காலியாக இல்லை. நிறுவல் ஏற்கனவே உள்ள தரவை நீக்கக்கூடும். வேறு கோப்புறையைத் தேர்ந்தெடுப்பது நல்லது."), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b5a6d96ce..be574f50f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", ""), ("Renew trust", ""), ("Request approval", ""), + ("install-directory-not-empty-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 7c3831334..20af035cb 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "ระยะเวลาการเชื่อถือ (วัน)"), ("Renew trust", "ต่ออายุการเชื่อถือ"), ("Request approval", ""), + ("install-directory-not-empty-tip", "โฟลเดอร์ที่เลือกไม่ว่างเปล่า การติดตั้งอาจลบข้อมูลที่มีอยู่ ควรพิจารณาเลือกโฟลเดอร์อื่น"), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f3c28860d..6034bf656 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Güven süresi (gün)"), ("Renew trust", "Güveni yenile"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Seçilen klasör boş değil. Kurulum mevcut verileri silebilir. Farklı bir klasör seçmeyi düşünün."), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index fa745d923..eb82e510e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "信任有效期(天)"), ("Renew trust", "延長信任期限"), ("Request approval", "請求批准"), + ("install-directory-not-empty-tip", "所選資料夾不是空的。安裝可能刪除現有資料。建議選擇其他資料夾。"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index f61eff3c2..a5cd37afc 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Термін довіри (у днях)"), ("Renew trust", "Подовжити довіру"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Вибрана тека не порожня. Встановлення може видалити наявні дані. Радимо вибрати іншу теку."), ].iter().cloned().collect(); } diff --git a/src/lang/ur.rs b/src/lang/ur.rs index 0ffd93df8..f565492b7 100644 --- a/src/lang/ur.rs +++ b/src/lang/ur.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "اعتماد کی مدت (دن)"), ("Renew trust", "اعتماد کی تجدید کریں"), ("Request approval", ""), + ("install-directory-not-empty-tip", "منتخب فولڈر خالی نہیں ہے۔ انسٹالیشن موجودہ ڈیٹا کو حذف کر سکتی ہے۔ کوئی دوسرا فولڈر منتخب کرنے پر غور کریں۔"), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index ccd2a730f..565e83069 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trust duration (days)", "Thời hạn tin cậy (ngày)"), ("Renew trust", "Gia hạn tin cậy"), ("Request approval", ""), + ("install-directory-not-empty-tip", "Thư mục đã chọn không trống. Quá trình cài đặt có thể xóa dữ liệu hiện có. Hãy cân nhắc chọn một thư mục khác."), ].iter().cloned().collect(); } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index ddc190ccc..d321d12f5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1015,6 +1015,11 @@ fn update_me_from_source(update_source: UpdateSource) -> ResultType<()> { } Ok(output) => log_update_cleanup_warning(&output), } + if crate::common::is_server_running() { + // This process hosts the backend. A new GUI could reuse our IPC and + // skip starting its own backend, leaving it "Not ready" when we exit. + return relaunch_after_update_exit(&app_name); + } } match Command::new("open") .arg("-n") @@ -1034,6 +1039,44 @@ fn update_me_from_source(update_source: UpdateSource) -> ResultType<()> { Ok(()) } +fn relaunch_after_update_exit(app_name: &str) -> ResultType<()> { + // The update caller cleans up and quits this GUI after we return. Waiting + // in a separate process lets that caller finish instead of blocking its exit. + // The shell's kill -0 only checks whether our PID exists; it does not kill us. + const RELAUNCH: &str = r#" +poll_interval=0.1 +while /bin/kill -0 "$1" 2>/dev/null; do + /bin/sleep "$poll_interval" +done +if ! output=$(/usr/bin/open -n "$2" 2>&1); then + /usr/bin/logger -t "$0" "Failed to relaunch updated app: $output" + exit 1 +fi +"#; + Command::new("/bin/sh") + .args(["-c", RELAUNCH, app_name]) + .arg(std::process::id().to_string()) + .arg(format!("/Applications/{}.app", app_name)) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + // Children normally inherit their parent's process group, allowing a + // supervisor to signal the whole group. If launchd cleans up the old + // app's group on exit, it could kill this helper before it runs `open`. + // Passing 0 creates a new group whose ID is the child's own PID, so + // cleanup of the old group does not affect the helper or its children. + // It remains our child; ordinary parent exit alone does not kill it. + .process_group(0) + .spawn() + .map_err(|err| { + anyhow!( + "The new version is installed, but automatic restart failed. Please quit and reopen the application. Error: {}", + err + ) + })?; + Ok(()) +} + pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { let update_temp_dir = get_update_temp_dir_string(); println!("Starting update from DMG: {}", dmg_path); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 042d62b6c..b1e523da3 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -23,7 +23,7 @@ use hbb_common::{ socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for}, tokio::{ self, select, - sync::{mpsc, Mutex}, + sync::{mpsc, Mutex, Notify}, time::interval, }, udp::FramedSocket, @@ -208,6 +208,7 @@ impl IceRoute { static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); +static MANUAL_RESTART_NOTIFY: Notify = Notify::const_new(); static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false); #[cfg(target_os = "android")] @@ -256,6 +257,13 @@ pub(crate) fn reset_needs_deploy_notification() { NOTIFIED_NEEDS_DEPLOY.store(false, Ordering::SeqCst); } +async fn wait_for_retry(delay: f32, restarted: tokio::sync::futures::Notified<'_>) { + select! { + _ = sleep(delay) => {} + _ = restarted => {} + } +} + #[derive(Clone)] pub struct RendezvousMediator { addr: TargetAddr<'static>, @@ -268,6 +276,7 @@ impl RendezvousMediator { pub fn restart() { SHOULD_EXIT.store(true, Ordering::SeqCst); MANUAL_RESTARTED.store(true, Ordering::SeqCst); + MANUAL_RESTART_NOTIFY.notify_waiters(); log::info!("server restart"); } @@ -339,10 +348,12 @@ impl RendezvousMediator { } Config::reset_online(); let timeout = *timeout.read().unwrap(); + // Subscribe before checking the flag so a restart cannot miss the wait. + let restarted = MANUAL_RESTART_NOTIFY.notified(); if !MANUAL_RESTARTED.load(Ordering::SeqCst) { let elapsed = conn_start_time.elapsed().as_millis() as u64; if elapsed < timeout { - sleep(((timeout - elapsed) / 1000) as _).await; + wait_for_retry(((timeout - elapsed) / 1000) as _, restarted).await; } } else { // https://github.com/rustdesk/rustdesk/issues/12233 @@ -1719,11 +1730,33 @@ impl Drop for CheckIfResendPk { #[cfg(test)] mod tests { use super::{ - connection_meta, mpsc, tokio, AnswererSlot, IceRoute, IntoTargetAddr, PunchHoleSent, - RendezvousMediator, RendezvousMessage, MAX_WEBRTC_ANSWERERS, TCP_PUNCHES, UDP_PUNCHES, + connection_meta, mpsc, tokio, wait_for_retry, AnswererSlot, Duration, IceRoute, + IntoTargetAddr, Ordering, PunchHoleSent, RendezvousMediator, RendezvousMessage, + CONNECT_TIMEOUT, MANUAL_RESTARTED, MANUAL_RESTART_NOTIFY, MAX_WEBRTC_ANSWERERS, + SHOULD_EXIT, TCP_PUNCHES, UDP_PUNCHES, }; use hbb_common::protobuf::Message as _; + #[tokio::test] + async fn manual_restart_interrupts_retry_wait() { + const WAKE_TIMEOUT: Duration = Duration::from_secs(1); + let waiting = wait_for_retry( + (CONNECT_TIMEOUT / 1000) as _, + MANUAL_RESTART_NOTIFY.notified(), + ); + tokio::pin!(waiting); + assert!(hbb_common::futures::poll!(waiting.as_mut()).is_pending()); + + RendezvousMediator::restart(); + let result = tokio::time::timeout(WAKE_TIMEOUT, waiting).await; + MANUAL_RESTARTED.store(false, Ordering::SeqCst); + SHOULD_EXIT.store(false, Ordering::SeqCst); + assert!( + result.is_ok(), + "restart must interrupt the pending retry delay" + ); + } + // The places are the bound and they come back on drop: a punch past the limit is declined // while the ones in flight are left alone, and the next one in is admitted only once a // place has actually been given back. diff --git a/src/server/connection/trusted_devices.rs b/src/server/connection/trusted_devices.rs index d058a20ff..2f70374d3 100644 --- a/src/server/connection/trusted_devices.rs +++ b/src/server/connection/trusted_devices.rs @@ -9,7 +9,7 @@ use base::message_proto::LoginResponse; impl Connection { pub(super) async fn begin_trusted_device(&mut self) -> bool { if self.conn_audit_primary_auth != ConnAuditPrimaryAuth::PermanentPassword - || !self.stream.is_secured() + || (!self.stream.is_secured() && !crate::common::is_direct_ip_access(&self.lr.username)) || self.require_2fa.is_none() || self.from_switch || !hbb_common::config::Config::get_bool_option( diff --git a/src/trusted_devices/client.rs b/src/trusted_devices/client.rs index 3a52e175a..96a3ae92c 100644 --- a/src/trusted_devices/client.rs +++ b/src/trusted_devices/client.rs @@ -52,6 +52,12 @@ impl Session { secured: bool, key: Option<&[u8]>, ) -> ResultType { + if crate::common::is_direct_ip_access(login.get_id()) { + return Ok(Self { + credentials: Some(Credentials::direct_ip(login.get_id())), + ..Default::default() + }); + } let Some(key) = key.filter(|_| secured) else { return Ok(Self::default()); }; @@ -137,7 +143,7 @@ impl Session { #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] struct Issuer { namespace: String, - public_key: [u8; TOKEN_BYTES], + public_key: Option<[u8; TOKEN_BYTES]>, } #[derive(Serialize, Deserialize)] @@ -153,14 +159,26 @@ pub(super) struct Credentials { } impl Credentials { + fn direct_ip(peer_id: &str) -> Self { + Self { + peer_id: peer_id.to_owned(), + issuer: Issuer { + namespace: format!("direct-ip:{peer_id}"), + public_key: None, + }, + } + } + pub fn new(peer_id: String, namespace: String, public_key: &[u8]) -> ResultType { Ok(Self { peer_id, issuer: Issuer { namespace, - public_key: public_key - .try_into() - .map_err(|_| anyhow!("Invalid trusted host public key"))?, + public_key: Some( + public_key + .try_into() + .map_err(|_| anyhow!("Invalid trusted host public key"))?, + ), }, }) } diff --git a/src/trusted_devices/tests.rs b/src/trusted_devices/tests.rs index 6f1715c8f..109b3adbe 100644 --- a/src/trusted_devices/tests.rs +++ b/src/trusted_devices/tests.rs @@ -11,6 +11,7 @@ use hbb_common::{ use std::fs; mod configuration; +mod credentials; mod recovery; const PASSWORD: &str = "permanent-test-password"; diff --git a/src/trusted_devices/tests/credentials.rs b/src/trusted_devices/tests/credentials.rs new file mode 100644 index 000000000..312fc990a --- /dev/null +++ b/src/trusted_devices/tests/credentials.rs @@ -0,0 +1,23 @@ +use super::*; +use hbb_common::password_security::symmetric_crypt; + +#[test] +fn id_credentials_saved_before_direct_ip_support_remain_readable() { + const NAMESPACE: &str = "[\"rs.example.com\"]"; + const PUBLIC_KEY: [u8; TOKEN_BYTES] = [1; TOKEN_BYTES]; + + initialize_config(); + let peer_id = uuid::Uuid::new_v4().to_string(); + let token = random_token().unwrap(); + let saved = serde_json::json!({ + "issuer": { + "namespace": NAMESPACE, + "public_key": PUBLIC_KEY, + }, + "ciphertext": symmetric_crypt(&token, true).unwrap(), + }); + PeerConfig::set_trusted_device_v2(&peer_id, saved.to_string(), Default::default()).unwrap(); + + let credentials = Credentials::new(peer_id, NAMESPACE.into(), &PUBLIC_KEY).unwrap(); + assert_eq!(credentials.load().unwrap(), Some(token.to_vec())); +}