diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 825c67ec4..9d18f3bb1 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -159,14 +159,44 @@ jobs: - name: Build rustdesk run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip Expand-Archive usbmmidd_v2.zip -DestinationPath . - python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack Remove-Item -Path usbmmidd_v2\Win32 -Recurse Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" - mv ./flutter/build/windows/x64/runner/Release ./rustdesk mv -Force .\usbmmidd_v2 ./rustdesk + # Download printer driver files and extract them to ./rustdesk + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums + + # Check and move the files + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256 + $checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { + Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . + mkdir ./rustdesk/drivers + mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver + Expand-Archive printer_driver_adapter.zip -DestinationPath . + mv -Force .\printer_driver_adapter.dll ./rustdesk + } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { + Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file." + } else { + Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." + } + } catch { + Write-Host "Ingore the printer driver error." + } + - name: find Runner.res # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res # Runner.rc does not contain actual version, but Runner.res does diff --git a/Cargo.lock b/Cargo.lock index f4f61833d..eded3d37f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5559,6 +5559,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "remote_printer" +version = "0.1.0" +dependencies = [ + "hbb_common", + "winapi 0.3.9", + "windows-strings", +] + [[package]] name = "repng" version = "0.2.2" @@ -5805,6 +5814,7 @@ dependencies = [ "percent-encoding", "qrcode-generator", "rdev", + "remote_printer", "repng", "reqwest", "ringbuf", @@ -7833,6 +7843,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.1.2" @@ -7853,6 +7869,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 317ab123c..54550d654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,10 +116,12 @@ winapi = { version = "0.3", features = [ "cguid", "cfgmgr32", "ioapiset", + "winspool", ] } winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display" } +remote_printer = { path = "libs/remote_printer" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } shared_memory = "0.12" tauri-winrt-notification = "0.1.2" @@ -177,7 +179,7 @@ jni = "0.21" android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] exclude = ["vdi/host", "examples/custom_plugin"] [package.metadata.winres] diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5d43d7ae1..f97276662 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3789,3 +3789,29 @@ void updateTextAndPreserveSelection( baseOffset: 0, extentOffset: controller.value.text.length); } } + +List getPrinterNames() { + final printerNamesJson = bind.mainGetPrinterNames(); + if (printerNamesJson.isEmpty) { + return []; + } + try { + final List printerNamesList = jsonDecode(printerNamesJson); + final appPrinterName = '$appName Printer'; + return printerNamesList + .map((e) => e.toString()) + .where((name) => name != appPrinterName) + .toList(); + } catch (e) { + debugPrint('failed to parse printer names, err: $e'); + return []; + } +} + +String _appName = ''; +String get appName { + if (_appName.isEmpty) { + _appName = bind.mainGetAppNameSync(); + } + return _appName; +} diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 345a140ad..302b6b440 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 011709cc5..63023443e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -98,6 +98,7 @@ const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; // "Settings -> Security -> Permissions" +const String kOptionEnableRemotePrinter = "enable-remote-printer"; const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; @@ -219,6 +220,14 @@ const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; +const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action'; +const String kValuePrinterIncomingJobDismiss = 'dismiss'; +const String kValuePrinterIncomingJobDefault = ''; +const String kValuePrinterIncomingJobSelected = 'selected'; +const String kKeyPrinterSelected = 'printer-selected-name'; +const String kKeyPrinterSave = 'allow-printer-dialog-save'; +const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print'; + double kNewWindowOffset = isWindows ? 56.0 : isLinux diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5d95c8d0c..d21e7d347 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; @@ -55,6 +56,7 @@ enum SettingsTabKey { display, plugin, account, + printer, about, } @@ -74,6 +76,7 @@ class DesktopSettingPage extends StatefulWidget { if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) SettingsTabKey.plugin, if (!bind.isDisableAccount()) SettingsTabKey.account, + if (isWindows) SettingsTabKey.printer, SettingsTabKey.about, ]; @@ -198,6 +201,10 @@ class _DesktopSettingPageState extends State settingTabs.add( _TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); break; + case SettingsTabKey.printer: + settingTabs + .add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print)); + break; case SettingsTabKey.about: settingTabs .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); @@ -229,6 +236,9 @@ class _DesktopSettingPageState extends State case SettingsTabKey.account: children.add(const _Account()); break; + case SettingsTabKey.printer: + children.add(const _Printer()); + break; case SettingsTabKey.about: children.add(const _About()); break; @@ -963,6 +973,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox( context, 'Enable keyboard/mouse', kOptionEnableKeyboard, enabled: enabled, fakeValue: fakeValue), + if (isWindows) + _OptionCheckBox( + context, 'Enable remote printer', kOptionEnableRemotePrinter, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard, enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( @@ -1881,6 +1895,153 @@ class _PluginState extends State<_Plugin> { } } +class _Printer extends StatefulWidget { + const _Printer({super.key}); + + @override + State<_Printer> createState() => __PrinterState(); +} + +class __PrinterState extends State<_Printer> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + outgoing(context), + incomming(context), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget outgoing(BuildContext context) { + final isSupportPrinterDriver = + bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true'; + + Widget tipOsNotSupported() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-os-requirement-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipClientNotInstalled() { + return Align( + alignment: Alignment.topLeft, + child: + Text(translate('printer-requires-installed-{$appName}-client-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipPrinterNotInstalled() { + final failedMsg = ''.obs; + platformFFI.registerEventHandler( + 'install-printer-res', 'install-printer-res', (evt) async { + if (evt['success'] as bool) { + setState(() {}); + } else { + failedMsg.value = evt['msg'] as String; + } + }, replace: true); + return Column(children: [ + Obx( + () => failedMsg.value.isNotEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-not-installed-tip')) + .marginOnly(bottom: 10.0), + ), + ), + Obx( + () => failedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(failedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button('Install {$appName} Printer', () { + failedMsg.value = ''; + bind.mainSetCommon(key: 'install-printer', value: ''); + }) + ]).marginOnly(left: _kCardLeftMargin, bottom: 2.0); + } + + Widget tipReady() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-ready-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + final installed = bind.mainIsInstalled(); + // `is-printer-installed` may fail, but it's rare case. + // Add additional error message here if it's really needed. + final driver_installed = + bind.mainGetCommonSync(key: 'is-printer-installed') == 'true'; + + final List children = []; + if (!isSupportPrinterDriver) { + children.add(tipOsNotSupported()); + } else { + children.addAll([ + if (!installed) tipClientNotInstalled(), + if (installed && !driver_installed) tipPrinterNotInstalled(), + if (installed && driver_installed) tipReady() + ]); + } + return _Card(title: 'Outgoing Print Jobs', children: children); + } + + Widget incomming(BuildContext context) { + onRadioChanged(String value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, value: value); + setState(() {}); + } + + PrinterOptions printerOptions = PrinterOptions.load(); + return _Card(title: 'Incomming Print Jobs', children: [ + _Radio(context, + value: kValuePrinterIncomingJobDismiss, + groupValue: printerOptions.action, + label: 'Dismiss', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobDefault, + groupValue: printerOptions.action, + label: 'use-the-default-printer-tip', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobSelected, + groupValue: printerOptions.action, + label: 'use-the-selected-printer-tip', + onChanged: onRadioChanged), + if (printerOptions.printerNames.isNotEmpty) + ComboBox( + initialKey: printerOptions.printerName, + keys: printerOptions.printerNames, + values: printerOptions.printerNames, + enabled: printerOptions.action == kValuePrinterIncomingJobSelected, + onChanged: (value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: value); + setState(() {}); + }, + ).marginOnly(left: 10), + _OptionCheckBox( + context, + 'auto-print-tip', + kKeyPrinterAllowAutoPrint, + isServer: false, + enabled: printerOptions.action != kValuePrinterIncomingJobDismiss, + ) + ]); + } +} + class _About extends StatefulWidget { const _About({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 756367c21..5bf6bafee 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; + final RxBool printer = true.obs; final RxBool showProgress = false.obs; final RxBool btnEnabled = true.obs; @@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> final installOptions = jsonDecode(bind.installInstallOptions()); startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; + printer.value = installOptions['PRINTER'] != '0'; } @override @@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> ).marginSymmetric(vertical: 2 * em), Option(startmenu, label: 'Create start menu shortcuts') .marginOnly(bottom: 7), - Option(desktopicon, label: 'Create desktop icon'), + Option(desktopicon, label: 'Create desktop icon') + .marginOnly(bottom: 7), + Option(printer, label: 'Install {$appName} Printer'), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( @@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> String args = ''; if (startmenu.value) args += ' startmenu'; if (desktopicon.value) args += ' desktopicon'; + if (printer.value) args += ' printer'; bind.installInstallMe(options: args, path: controller.text); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4a00b803e..fabbcc00c 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -30,8 +30,15 @@ enum SortBy { class JobID { int _count = 0; int next() { - _count++; - return _count; + String v = bind.mainGetCommonSync(key: 'transfer-job-id'); + try { + return int.parse(v); + } catch (e) { + // unreachable. But we still handle it to make it safe. + // If we return -1, we have to check it in the caller. + _count++; + return _count; + } } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dd7927abc..8091846c6 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -9,7 +9,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; @@ -19,6 +18,7 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -412,12 +412,186 @@ class FfiModel with ChangeNotifier { isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); } + } else if (name == "printer_request") { + _handlePrinterRequest(evt, sessionId, peerId); } else { debugPrint('Event is not handled in the fixed branch: $name'); } }; } + _handlePrinterRequest( + Map evt, SessionID sessionId, String peerId) { + final id = evt['id']; + final path = evt['path']; + final dialogManager = parent.target!.dialogManager; + dialogManager.show((setState, close, context) { + PrinterOptions printerOptions = PrinterOptions.load(); + final saveSettings = mainGetLocalBoolOptionSync(kKeyPrinterSave).obs; + final dontShowAgain = false.obs; + final Rx selectedPrinterName = printerOptions.printerName.obs; + final printerNames = printerOptions.printerNames; + final defaultOrSelectedGroupValue = + (printerOptions.action == kValuePrinterIncomingJobDismiss + ? kValuePrinterIncomingJobDefault + : printerOptions.action) + .obs; + + onRatioChanged(String? value) { + defaultOrSelectedGroupValue.value = + value ?? kValuePrinterIncomingJobDefault; + } + + onSubmit() { + final printerName = defaultOrSelectedGroupValue.isEmpty + ? '' + : selectedPrinterName.value; + bind.sessionPrinterResponse( + sessionId: sessionId, id: id, path: path, printerName: printerName); + if (saveSettings.value || dontShowAgain.value) { + bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName); + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: defaultOrSelectedGroupValue.value); + } + if (dontShowAgain.value) { + mainSetLocalBoolOption(kKeyPrinterAllowAutoPrint, true); + } + close(); + } + + onCancel() { + if (dontShowAgain.value) { + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: kValuePrinterIncomingJobDismiss); + } + close(); + } + + final printerItemHeight = 30.0; + final selectionAreaHeight = + printerItemHeight * min(8.0, max(printerNames.length, 3.0)); + final content = Column( + children: [ + Text(translate('print-incoming-job-confirm-tip')), + Row( + children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobDefault, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-default-printer-tip')), + onTap: () => onRatioChanged(kValuePrinterIncomingJobDefault)), + ], + ), + Column( + children: [ + Row(children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobSelected, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-selected-printer-tip')), + onTap: () => + onRatioChanged(kValuePrinterIncomingJobSelected)), + ]), + SizedBox( + height: selectionAreaHeight, + width: 500, + child: ListView.builder( + itemBuilder: (context, index) { + return Obx(() => GestureDetector( + child: Container( + decoration: BoxDecoration( + color: selectedPrinterName.value == + printerNames[index] + ? (defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? MyTheme.button + : MyTheme.button.withOpacity(0.5)) + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + key: ValueKey(printerNames[index]), + height: printerItemHeight, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + printerNames[index], + style: TextStyle(fontSize: 14), + ), + ), + ), + ), + onTap: defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? () { + selectedPrinterName.value = + printerNames[index]; + } + : null, + )); + }, + itemCount: printerNames.length), + ), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: saveSettings.value, + onChanged: (value) { + if (value != null) { + saveSettings.value = value; + mainSetLocalBoolOption(kKeyPrinterSave, value); + } + })), + GestureDetector( + child: Text(translate('save-settings-tip')), + onTap: () { + saveSettings.value = !saveSettings.value; + mainSetLocalBoolOption(kKeyPrinterSave, saveSettings.value); + }), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: dontShowAgain.value, + onChanged: (value) { + if (value != null) { + dontShowAgain.value = value; + } + })), + GestureDetector( + child: Text(translate('dont-show-again-tip')), + onTap: () { + dontShowAgain.value = !dontShowAgain.value; + }), + ], + ), + ], + ); + return CustomAlertDialog( + title: Text(translate('Incoming Print Job')), + content: content, + actions: [ + dialogButton('OK', onPressed: onSubmit), + dialogButton('Cancel', onPressed: onCancel), + ], + onSubmit: onSubmit, + onCancel: onCancel, + ); + }); + } + _handleUseTextureRender( Map evt, SessionID sessionId, String peerId) { parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index c8d5085e8..337f53278 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -60,14 +60,14 @@ class PlatformFFI { } bool registerEventHandler( - String eventName, String handlerName, HandleEvent handler) { + String eventName, String handlerName, HandleEvent handler, {bool replace = false}) { debugPrint('registerEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers == null) { _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handlerName)) { + if (!replace && handlers.containsKey(handlerName)) { return false; } else { handlers[handlerName] = handler; diff --git a/flutter/lib/models/printer_model.dart b/flutter/lib/models/printer_model.dart new file mode 100644 index 000000000..df8f67cff --- /dev/null +++ b/flutter/lib/models/printer_model.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +class PrinterOptions { + String action; + List printerNames; + String printerName; + + PrinterOptions( + {required this.action, + required this.printerNames, + required this.printerName}); + + static PrinterOptions load() { + var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction); + if (![ + kValuePrinterIncomingJobDismiss, + kValuePrinterIncomingJobDefault, + kValuePrinterIncomingJobSelected + ].contains(action)) { + action = kValuePrinterIncomingJobDefault; + } + + final printerNames = getPrinterNames(); + var selectedPrinterName = bind.mainGetLocalOption(key: kKeyPrinterSelected); + if (!printerNames.contains(selectedPrinterName)) { + if (action == kValuePrinterIncomingJobSelected) { + action = kValuePrinterIncomingJobDefault; + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: kValuePrinterIncomingJobDefault); + if (printerNames.isEmpty) { + selectedPrinterName = ''; + } else { + selectedPrinterName = printerNames.first; + } + bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: selectedPrinterName); + } + } + + return PrinterOptions( + action: action, + printerNames: printerNames, + printerName: selectedPrinterName); + } +} diff --git a/libs/hbb_common b/libs/hbb_common index 181987547..9ede5d49f 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 1819875476d487612a654099881b8a16f4337599 +Subproject commit 9ede5d49f65b8c513fe613dbfea2daf7694cba3e diff --git a/libs/remote_printer/Cargo.toml b/libs/remote_printer/Cargo.toml new file mode 100644 index 000000000..30f8aff33 --- /dev/null +++ b/libs/remote_printer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "remote_printer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[target.'cfg(target_os = "windows")'.dependencies] +hbb_common = { version = "0.1.0", path = "../hbb_common" } +winapi = { version = "0.3" } +windows-strings = "0.3.1" diff --git a/libs/remote_printer/src/lib.rs b/libs/remote_printer/src/lib.rs new file mode 100644 index 000000000..51ee3721a --- /dev/null +++ b/libs/remote_printer/src/lib.rs @@ -0,0 +1,34 @@ +#[cfg(target_os = "windows")] +mod setup; +#[cfg(target_os = "windows")] +pub use setup::{ + is_rd_printer_installed, + setup::{install_update_printer, uninstall_printer}, +}; + +#[cfg(target_os = "windows")] +const RD_DRIVER_INF_PATH: &str = "drivers/RustDeskPrinterDriver/RustDeskPrinterDriver.inf"; + +#[cfg(target_os = "windows")] +fn get_printer_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_driver_name() -> Vec { + "RustDesk v4 Printer Driver" + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_port_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} diff --git a/libs/remote_printer/src/setup/driver.rs b/libs/remote_printer/src/setup/driver.rs new file mode 100644 index 000000000..81226c5cc --- /dev/null +++ b/libs/remote_printer/src/setup/driver.rs @@ -0,0 +1,202 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, log, ResultType}; +use std::{io, ptr::null_mut, time::Duration}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD, MAX_PATH}, + ntdef::{DWORDLONG, LPCWSTR}, + winerror::{ERROR_UNKNOWN_PRINTER_DRIVER, S_OK}, + }, + um::{ + winspool::{ + DeletePrinterDriverExW, DeletePrinterDriverPackageW, EnumPrinterDriversW, + InstallPrinterDriverFromPackageW, UploadPrinterDriverPackageW, DPD_DELETE_ALL_FILES, + DRIVER_INFO_6W, DRIVER_INFO_8W, IPDFP_COPY_ALL_FILES, UPDP_SILENT_UPLOAD, + UPDP_UPLOAD_ALWAYS, + }, + winuser::GetForegroundWindow, + }, +}; +use windows_strings::PCWSTR; + +const HRESULT_ERR_ELEMENT_NOT_FOUND: u32 = 0x80070490; + +fn enum_printer_driver( + level: DWORD, + p_driver_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrinterDriversW( + null_mut(), + null_mut(), + level, + p_driver_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +pub fn get_installed_driver_version(name: &PCWSTR) -> ResultType> { + common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 6, + |info: &DRIVER_INFO_6W| { + if is_name_equal(name, info.pName) { + Some(info.dwlDriverVersion) + } else { + None + } + }, + || None, + ) +} + +fn find_inf(name: &PCWSTR) -> ResultType> { + let r = common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 8, + |info: &DRIVER_INFO_8W| { + if is_name_equal(name, info.pName) { + Some(get_wstr_bytes(info.pszInfPath)) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(vec![])) +} + +fn delete_printer_driver(name: &PCWSTR) -> ResultType<()> { + unsafe { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + if FALSE + == DeletePrinterDriverExW( + null_mut(), + null_mut(), + name.as_ptr() as _, + DPD_DELETE_ALL_FILES, + 0, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_UNKNOWN_PRINTER_DRIVER as _) { + return Ok(()); + } else { + bail!("Failed to delete the printer driver, {}", err) + } + } + } + Ok(()) +} + +// https://github.com/dvalter/chromium-android-ext-dev/blob/dab74f7d5bc5a8adf303090ee25c611b4d54e2db/cloud_print/virtual_driver/win/install/setup.cc#L190 +fn delete_printer_driver_package(inf: Vec) -> ResultType<()> { + if inf.is_empty() { + return Ok(()); + } + let slen = if inf[inf.len() - 1] == 0 { + inf.len() - 1 + } else { + inf.len() + }; + let inf_path = String::from_utf16_lossy(&inf[..slen]); + if !std::path::Path::new(&inf_path).exists() { + return Ok(()); + } + + let mut retries = 3; + loop { + unsafe { + let res = DeletePrinterDriverPackageW(null_mut(), inf.as_ptr(), null_mut()); + if res == S_OK || res == HRESULT_ERR_ELEMENT_NOT_FOUND as i32 { + return Ok(()); + } + log::error!("Failed to delete the printer driver, result: {}", res); + } + retries -= 1; + if retries <= 0 { + bail!("Failed to delete the printer driver"); + } + std::thread::sleep(Duration::from_secs(2)); + } +} + +pub fn uninstall_driver(name: &PCWSTR) -> ResultType<()> { + // Note: inf must be found before `delete_printer_driver()`. + let inf = find_inf(name)?; + delete_printer_driver(name)?; + delete_printer_driver_package(inf) +} + +pub fn install_driver(name: &PCWSTR, inf: LPCWSTR) -> ResultType<()> { + let mut size = (MAX_PATH * 10) as u32; + let mut package_path = [0u16; MAX_PATH * 10]; + unsafe { + let mut res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, + null_mut(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + log::error!( + "Failed to upload the printer driver package to the driver cache silently, {}. Will try with user UI.", + res + ); + + res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + bail!( + "Failed to upload the printer driver package to the driver cache with UI, {}", + res + ); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/printdocs/installprinterdriverfrompackage + res = InstallPrinterDriverFromPackageW( + null_mut(), + package_path.as_ptr(), + name.as_ptr(), + null_mut(), + IPDFP_COPY_ALL_FILES, + ); + if res != S_OK { + bail!("Failed to install the printer driver from package, {}", res); + } + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/mod.rs b/libs/remote_printer/src/setup/mod.rs new file mode 100644 index 000000000..ddf386de3 --- /dev/null +++ b/libs/remote_printer/src/setup/mod.rs @@ -0,0 +1,99 @@ +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::{LPCWSTR, LPWSTR}, + }, + um::winbase::{lstrcmpiW, lstrlenW}, +}; +use windows_strings::PCWSTR; + +mod driver; +mod port; +pub(crate) mod printer; +pub(crate) mod setup; + +#[inline] +pub fn is_rd_printer_installed(app_name: &str) -> ResultType { + let printer_name = crate::get_printer_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + printer::is_printer_added(&rd_printer_name) +} + +fn get_wstr_bytes(p: LPWSTR) -> Vec { + let mut vec_bytes = vec![]; + unsafe { + let len: isize = lstrlenW(p) as _; + if len > 0 { + for i in 0..len + 1 { + vec_bytes.push(*p.offset(i)); + } + } + } + vec_bytes +} + +fn is_name_equal(name: &PCWSTR, name_from_api: LPCWSTR) -> bool { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + unsafe { lstrcmpiW(name.as_ptr(), name_from_api) == 0 } +} + +fn common_enum( + enum_name: &str, + enum_fn: fn( + Level: DWORD, + pDriverInfo: LPBYTE, + cbBuf: DWORD, + pcbNeeded: LPDWORD, + pcReturned: LPDWORD, + ) -> BOOL, + level: DWORD, + on_data: impl Fn(&T) -> Option, + on_no_data: impl Fn() -> Option, +) -> ResultType> { + let mut needed = 0; + let mut returned = 0; + enum_fn(level, null_mut(), 0, &mut needed, &mut returned); + if needed == 0 { + return Ok(on_no_data()); + } + + let mut buffer = vec![0u8; needed as usize]; + if FALSE + == enum_fn( + level, + buffer.as_mut_ptr(), + needed, + &mut needed, + &mut returned, + ) + { + bail!( + "Failed to call {}, error: {}", + enum_name, + io::Error::last_os_error() + ) + } + + // to-do: how to free the buffers in *const T? + + let p_enum_info = buffer.as_ptr() as *const T; + unsafe { + for i in 0..returned { + let enum_info = p_enum_info.offset(i as isize); + let r = on_data(&*enum_info); + if r.is_some() { + return Ok(r); + } + } + } + + Ok(on_no_data()) +} diff --git a/libs/remote_printer/src/setup/port.rs b/libs/remote_printer/src/setup/port.rs new file mode 100644 index 000000000..d5ab0bace --- /dev/null +++ b/libs/remote_printer/src/setup/port.rs @@ -0,0 +1,128 @@ +use super::{common_enum, is_name_equal, printer::get_printer_installed_on_port}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + um::{ + winnt::HANDLE, + winspool::{ + ClosePrinter, EnumPortsW, OpenPrinterW, XcvDataW, PORT_INFO_2W, PRINTER_DEFAULTSW, + SERVER_WRITE, + }, + }, +}; +use windows_strings::{w, PCWSTR}; + +const XCV_MONITOR_LOCAL_PORT: PCWSTR = w!(",XcvMonitor Local Port"); + +fn enum_printer_port( + level: DWORD, + p_port_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPortsW( + null_mut(), + level, + p_port_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +fn is_port_exists(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPortsW", + enum_printer_port, + 2, + |info: &PORT_INFO_2W| { + if is_name_equal(name, info.pPortName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +unsafe fn execute_on_local_port(port: &PCWSTR, command: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: SERVER_WRITE, + }; + let mut h_monitor: HANDLE = null_mut(); + if FALSE + == OpenPrinterW( + XCV_MONITOR_LOCAL_PORT.as_ptr() as _, + &mut h_monitor, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + bail!(format!( + "Failed to open Local Port monitor. Error: {}", + io::Error::last_os_error() + )) + } + + let mut output_needed: u32 = 0; + let mut status: u32 = 0; + if FALSE + == XcvDataW( + h_monitor, + command.as_ptr(), + port.as_ptr() as *mut u8, + (port.len() + 1) as u32 * 2, + null_mut(), + 0, + &mut output_needed, + &mut status, + ) + { + ClosePrinter(h_monitor); + bail!(format!( + "Failed to execute the command on the printer port, Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_monitor); + + Ok(()) +} + +fn add_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("AddPort")) } +} + +fn delete_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("DeletePort")) } +} + +pub fn check_add_local_port(port: &PCWSTR) -> ResultType<()> { + if !is_port_exists(port)? { + return add_local_port(port); + } + Ok(()) +} + +pub fn check_delete_local_port(port: &PCWSTR) -> ResultType<()> { + if is_port_exists(port)? { + if get_printer_installed_on_port(port)?.is_some() { + bail!("The printer is installed on the port. Please remove the printer first."); + } + return delete_local_port(port); + } + Ok(()) +} diff --git a/libs/remote_printer/src/setup/printer.rs b/libs/remote_printer/src/setup/printer.rs new file mode 100644 index 000000000..9882b8f38 --- /dev/null +++ b/libs/remote_printer/src/setup/printer.rs @@ -0,0 +1,161 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::HANDLE, + winerror::ERROR_INVALID_PRINTER_NAME, + }, + um::winspool::{ + AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW, + PRINTER_ALL_ACCESS, PRINTER_ATTRIBUTE_LOCAL, PRINTER_CONTROL_PURGE, PRINTER_DEFAULTSW, + PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_2W, + }, +}; +use windows_strings::{w, PCWSTR}; + +fn enum_local_printer( + level: DWORD, + p_printer_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrintersW( + PRINTER_ENUM_LOCAL, + null_mut(), + level, + p_printer_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +#[inline] +pub fn is_printer_added(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPrintersW", + enum_local_printer, + 1, + |info: &PRINTER_INFO_1W| { + if is_name_equal(name, info.pName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +// Only return the first matched printer +pub fn get_printer_installed_on_port(port: &PCWSTR) -> ResultType>> { + common_enum( + "EnumPrintersW", + enum_local_printer, + 2, + |info: &PRINTER_INFO_2W| { + if is_name_equal(port, info.pPortName) { + Some(get_wstr_bytes(info.pPrinterName)) + } else { + None + } + }, + || None, + ) +} + +pub fn add_printer(name: &PCWSTR, driver: &PCWSTR, port: &PCWSTR) -> ResultType<()> { + let mut printer_info = PRINTER_INFO_2W { + pServerName: null_mut(), + pPrinterName: name.as_ptr() as _, + pShareName: null_mut(), + pPortName: port.as_ptr() as _, + pDriverName: driver.as_ptr() as _, + pComment: null_mut(), + pLocation: null_mut(), + pDevMode: null_mut(), + pSepFile: null_mut(), + pPrintProcessor: w!("WinPrint").as_ptr() as _, + pDatatype: w!("RAW").as_ptr() as _, + pParameters: null_mut(), + pSecurityDescriptor: null_mut(), + Attributes: PRINTER_ATTRIBUTE_LOCAL, + Priority: 0, + DefaultPriority: 0, + StartTime: 0, + UntilTime: 0, + Status: 0, + cJobs: 0, + AveragePPM: 0, + }; + unsafe { + let h_printer = AddPrinterW( + null_mut(), + 2, + &mut printer_info as *mut PRINTER_INFO_2W as _, + ); + if h_printer.is_null() { + bail!(format!( + "Failed to add printer. Error: {}", + io::Error::last_os_error() + )) + } + } + Ok(()) +} + +pub fn delete_printer(name: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: PRINTER_ALL_ACCESS, + }; + let mut h_printer: HANDLE = null_mut(); + unsafe { + if FALSE + == OpenPrinterW( + name.as_ptr() as _, + &mut h_printer, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_INVALID_PRINTER_NAME as _) { + return Ok(()); + } else { + bail!(format!("Failed to open printer. Error: {}", err)) + } + } + + if FALSE == SetPrinterW(h_printer, 0, null_mut(), PRINTER_CONTROL_PURGE) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to purge printer queue. Error: {}", + io::Error::last_os_error() + )) + } + + if FALSE == DeletePrinter(h_printer) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to delete printer. Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_printer); + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/setup.rs b/libs/remote_printer/src/setup/setup.rs new file mode 100644 index 000000000..f461ab75c --- /dev/null +++ b/libs/remote_printer/src/setup/setup.rs @@ -0,0 +1,94 @@ +use super::{ + driver::{get_installed_driver_version, install_driver, uninstall_driver}, + port::{check_add_local_port, check_delete_local_port}, + printer::{add_printer, delete_printer}, +}; +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{path::PathBuf, sync::Mutex}; +use windows_strings::PCWSTR; + +lazy_static::lazy_static!( + static ref SETUP_MTX: Mutex<()> = Mutex::new(()); +); + +fn get_driver_inf_abs_path() -> ResultType { + use crate::RD_DRIVER_INF_PATH; + + let exe_file = std::env::current_exe()?; + let abs_path = match exe_file.parent() { + Some(parent) => parent.join(RD_DRIVER_INF_PATH), + None => bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + }; + if !abs_path.exists() { + bail!( + "The driver inf file \"{}\" does not exists", + RD_DRIVER_INF_PATH + ) + } + Ok(abs_path) +} + +// Note: This function must be called in a separate thread. +// Because many functions in this module are blocking or synchronous. +// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. +// Steps: +// 1. Add the local port. +// 2. Check if the driver is installed. +// Uninstall the existing driver if it is installed. +// We should not check the driver version because the driver is deployed with the application. +// It's better to uninstall the existing driver and install the driver from the application. +// 3. Add the printer. +pub fn install_update_printer(app_name: &str) -> ResultType<()> { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let inf_file = get_driver_inf_abs_path()?; + let inf_file: Vec = inf_file + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + let _lock = SETUP_MTX.lock().unwrap(); + + check_add_local_port(&rd_printer_port)?; + + let should_install_driver = match get_installed_driver_version(&rd_printer_driver_name)? { + Some(_version) => { + delete_printer(&rd_printer_name)?; + allow_err!(uninstall_driver(&rd_printer_driver_name)); + true + } + None => true, + }; + + if should_install_driver { + allow_err!(install_driver(&rd_printer_driver_name, inf_file.as_ptr())); + } + + add_printer(&rd_printer_name, &rd_printer_driver_name, &rd_printer_port)?; + + Ok(()) +} + +pub fn uninstall_printer(app_name: &str) { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let _lock = SETUP_MTX.lock().unwrap(); + + allow_err!(delete_printer(&rd_printer_name)); + allow_err!(uninstall_driver(&rd_printer_driver_name)); + allow_err!(check_delete_local_port(&rd_printer_port)); +} diff --git a/res/inline-sciter.py b/res/inline-sciter.py index 26cf1a754..4c81bd621 100644 --- a/res/inline-sciter.py +++ b/res/inline-sciter.py @@ -23,7 +23,8 @@ remote = open('src/ui/remote.html').read() \ .replace('include "grid.tis";', open('src/ui/grid.tis').read()) \ .replace('include "header.tis";', open('src/ui/header.tis').read()) \ .replace('include "file_transfer.tis";', open('src/ui/file_transfer.tis').read()) \ - .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) + .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) \ + .replace('include "printer.tis";', open('src/ui/printer.tis').read()) chatbox = open('src/ui/chatbox.html').read() install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read()) diff --git a/res/msi/CustomActions/Common.h b/res/msi/CustomActions/Common.h index 5dcd529c0..08302d98c 100644 --- a/res/msi/CustomActions/Common.h +++ b/res/msi/CustomActions/Common.h @@ -15,3 +15,9 @@ bool MyStopServiceW(LPCWSTR serviceName); std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key); void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired); + +namespace RemotePrinter +{ + VOID installUpdatePrinter(const std::wstring& installFolder); + VOID uninstallPrinter(); +} diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index afe06fdbb..fafbab6b5 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -878,3 +878,55 @@ void TryStopDeleteServiceByShell(LPWSTR svcName) WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); } } + +UINT __stdcall InstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + hr = WcaInitialize(hInstall, "InstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + WcaLog(LOGMSG_STANDARD, "Try to install RD printer in : %ls", installFolder); + RemotePrinter::installUpdatePrinter(installFolder); + WcaLog(LOGMSG_STANDARD, "Install RD printer done"); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall UninstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "UninstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Try to uninstall RD printer"); + RemotePrinter::uninstallPrinter(); + WcaLog(LOGMSG_STANDARD, "Uninstall RD printer done"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index 557bfaf18..01b03490c 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -12,3 +12,5 @@ EXPORTS SetPropertyFromConfig AddRegSoftwareSASGeneration RemoveAmyuniIdd + InstallPrinter + UninstallPrinter diff --git a/res/msi/CustomActions/CustomActions.vcxproj b/res/msi/CustomActions/CustomActions.vcxproj index 1bff7b154..2e704fbb5 100644 --- a/res/msi/CustomActions/CustomActions.vcxproj +++ b/res/msi/CustomActions/CustomActions.vcxproj @@ -67,6 +67,7 @@ Create + diff --git a/res/msi/CustomActions/RemotePrinter.cpp b/res/msi/CustomActions/RemotePrinter.cpp new file mode 100644 index 000000000..767c8c82c --- /dev/null +++ b/res/msi/CustomActions/RemotePrinter.cpp @@ -0,0 +1,517 @@ +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common.h" + +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "winspool.lib") + +namespace RemotePrinter +{ +#define HRESULT_ERR_ELEMENT_NOT_FOUND 0x80070490 + + LPCWCH RD_DRIVER_INF_PATH = L"drivers\\RustDeskPrinterDriver\\RustDeskPrinterDriver.inf"; + LPCWCH RD_PRINTER_PORT = L"RustDesk Printer"; + LPCWCH RD_PRINTER_NAME = L"RustDesk Printer"; + LPCWCH RD_PRINTER_DRIVER_NAME = L"RustDesk v4 Printer Driver"; + LPCWCH XCV_MONITOR_LOCAL_PORT = L",XcvMonitor Local Port"; + + using FuncEnum = std::function; + template + using FuncOnData = std::function(const T &)>; + template + using FuncOnNoData = std::function()>; + + template + std::shared_ptr commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData onData, FuncOnNoData onNoData) + { + DWORD needed = 0; + DWORD returned = 0; + func(level, NULL, 0, &needed, &returned); + if (needed == 0) + { + return onNoData(); + } + + std::vector buffer(needed); + if (!func(level, buffer.data(), needed, &needed, &returned)) + { + return nullptr; + } + + T *pPortInfo = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < returned; i++) + { + auto r = onData(pPortInfo[i]); + if (r) + { + return r; + } + } + return onNoData(); + } + + BOOL isNameEqual(LPCWSTR lhs, LPCWSTR rhs) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + return lstrcmpiW(lhs, rhs) == 0 ? TRUE : FALSE; + } + + BOOL enumPrinterPort( + DWORD level, + LPBYTE pPortInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPortsW(NULL, level, pPortInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPortExists(LPCWSTR port) + { + auto onData = [port](const PORT_INFO_2 &info) + { + if (isNameEqual(info.pPortName, port) == TRUE) { + return std::shared_ptr(new BOOL(TRUE)); + } + else { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPortsW", enumPrinterPort, 2, onData, onNoData); + if (res == nullptr) + { + return false; + } + else + { + return *res; + } + } + + BOOL executeOnLocalPort(LPCWSTR port, LPCWSTR command) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = SERVER_WRITE; + HANDLE hMonitor = NULL; + if (OpenPrinterW(const_cast(XCV_MONITOR_LOCAL_PORT), &hMonitor, &dft) == FALSE) + { + return FALSE; + } + + DWORD outputNeeded = 0; + DWORD status = 0; + if (XcvDataW(hMonitor, command, (LPBYTE)port, (lstrlenW(port) + 1) * 2, NULL, 0, &outputNeeded, &status) == FALSE) + { + ClosePrinter(hMonitor); + return FALSE; + } + + ClosePrinter(hMonitor); + return TRUE; + } + + BOOL addLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"AddPort"); + } + + BOOL deleteLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"DeletePort"); + } + + BOOL checkAddLocalPort(LPCWSTR port) + { + if (!isPortExists(port)) + { + return addLocalPort(port); + } + return TRUE; + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port); + + BOOL checkDeleteLocalPort(LPCWSTR port) + { + if (isPortExists(port)) + { + if (getPrinterInstalledOnPort(port) != L"") + { + WcaLog(LOGMSG_STANDARD, "The printer is installed on the port. Please remove the printer first.\n"); + return FALSE; + } + return deleteLocalPort(port); + } + return TRUE; + } + + BOOL enumPrinterDriver( + DWORD level, + LPBYTE pDriverInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrinterDriversW( + NULL, + NULL, + level, + pDriverInfo, + cbBuf, + pcbNeeded, + pcReturned); + } + + DWORDLONG getInstalledDriverVersion(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_6W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new DWORDLONG(info.dwlDriverVersion)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 6, onData, onNoData); + if (res == nullptr) + { + return 0; + } + else + { + return *res; + } + } + + std::wstring findInf(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_8W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pszInfPath)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 8, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL deletePrinterDriver(LPCWSTR name) + { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + BOOL res = DeletePrinterDriverExW(NULL, NULL, const_cast(name), DPD_DELETE_ALL_FILES, 0); + if (res == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_UNKNOWN_PRINTER_DRIVER) + { + return TRUE; + } + else + { + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver. Error (%d)\n", error); + } + } + return res; + } + + BOOL deletePrinterDriverPackage(const std::wstring &inf) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/deleteprinterdriverpackage + // This function is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + int tries = 3; + HRESULT result = S_FALSE; + while ((result = DeletePrinterDriverPackage(NULL, inf.c_str(), NULL)) != S_OK) + { + if (result == HRESULT_ERR_ELEMENT_NOT_FOUND) + { + return TRUE; + } + + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver package. HRESULT (%d)\n", result); + tries--; + if (tries <= 0) + { + return FALSE; + } + Sleep(2000); + } + return S_OK; + } + + BOOL uninstallDriver(LPCWSTR name) + { + auto infFile = findInf(name); + if (!deletePrinterDriver(name)) + { + return FALSE; + } + if (infFile != L"" && !deletePrinterDriverPackage(infFile)) + { + return FALSE; + } + return TRUE; + } + + BOOL installDriver(LPCWSTR name, LPCWSTR inf) + { + DWORD size = MAX_PATH * 10; + wchar_t package_path[MAX_PATH * 10] = {0}; + HRESULT result = UploadPrinterDriverPackage( + NULL, inf, NULL, + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, NULL, package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache silently, failed. Will retry with user UI. HRESULT (%d)\n", result); + result = UploadPrinterDriverPackage( + NULL, inf, NULL, UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache failed with user UI. Aborting...\n"); + return FALSE; + } + } + + result = InstallPrinterDriverFromPackage( + NULL, package_path, name, NULL, IPDFP_COPY_ALL_FILES); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Installing the printer driver failed. HRESULT (%d)\n", result); + } + return result == S_OK; + } + + BOOL enumLocalPrinter( + DWORD level, + LPBYTE pPrinterInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPrinterAdded(LPCWSTR name) + { + auto onData = [name](const PRINTER_INFO_1W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new BOOL(TRUE)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 1, onData, onNoData); + if (res == nullptr) + { + return FALSE; + } + else + { + return *res; + } + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port) + { + auto onData = [port](const PRINTER_INFO_2W &info) + { + if (isNameEqual(port, info.pPortName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pPrinterName)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 2, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL addPrinter(LPCWSTR name, LPCWSTR driver, LPCWSTR port) + { + PRINTER_INFO_2W printerInfo = {0}; + printerInfo.pPrinterName = const_cast(name); + printerInfo.pPortName = const_cast(port); + printerInfo.pDriverName = const_cast(driver); + printerInfo.pPrintProcessor = const_cast(L"WinPrint"); + printerInfo.pDatatype = const_cast(L"RAW"); + printerInfo.Attributes = PRINTER_ATTRIBUTE_LOCAL; + HANDLE hPrinter = AddPrinterW(NULL, 2, (LPBYTE)&printerInfo); + return hPrinter == NULL ? FALSE : TRUE; + } + + VOID deletePrinter(LPCWSTR name) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = PRINTER_ALL_ACCESS; + HANDLE hPrinter = NULL; + if (OpenPrinterW(const_cast(name), &hPrinter, &dft) == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_INVALID_PRINTER_NAME) + { + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to open printer. error (%d)\n", error); + return; + } + + if (SetPrinterW(hPrinter, 0, NULL, PRINTER_CONTROL_PURGE) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to purge printer queue. error (%d)\n", GetLastError()); + return; + } + + if (DeletePrinter(hPrinter) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to delete printer. error (%d)\n", GetLastError()); + return; + } + + ClosePrinter(hPrinter); + } + + bool FileExists(const std::wstring &filePath) + { + DWORD fileAttributes = GetFileAttributes(filePath.c_str()); + return (fileAttributes != INVALID_FILE_ATTRIBUTES && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY)); + } + + // Steps: + // 1. Add the local port. + // 2. Check if the driver is installed. + // Uninstall the existing driver if it is installed. + // We should not check the driver version because the driver is deployed with the application. + // It's better to uninstall the existing driver and install the driver from the application. + // 3. Add the printer. + VOID installUpdatePrinter(const std::wstring &installFolder) + { + const std::wstring infFile = installFolder + L"\\" + RemotePrinter::RD_DRIVER_INF_PATH; + if (!FileExists(infFile)) + { + WcaLog(LOGMSG_STANDARD, "Printer driver INF file not found, aborting...\n"); + return; + } + + if (!checkAddLocalPort(RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to check add local port, error (%d)\n", GetLastError()); + return; + } + else + { + WcaLog(LOGMSG_STANDARD, "Local port added successfully\n"); + } + + if (getInstalledDriverVersion(RD_PRINTER_DRIVER_NAME) > 0) + { + deletePrinter(RD_PRINTER_NAME); + if (FALSE == uninstallDriver(RD_PRINTER_DRIVER_NAME)) + { + WcaLog(LOGMSG_STANDARD, "Failed to uninstall previous printer driver, error (%d)\n", GetLastError()); + } + } + + if (FALSE == installDriver(RD_PRINTER_DRIVER_NAME, infFile.c_str())) + { + WcaLog(LOGMSG_STANDARD, "Driver installation failed, still try to add the printer\n"); + } + else + { + WcaLog(LOGMSG_STANDARD, "Driver installed successfully\n"); + } + + if (FALSE == addPrinter(RD_PRINTER_NAME, RD_PRINTER_DRIVER_NAME, RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to add printer, error (%d)\n", GetLastError()); + } + else + { + WcaLog(LOGMSG_STANDARD, "Printer installed successfully\n"); + } + } + + VOID uninstallPrinter() + { + deletePrinter(RD_PRINTER_NAME); + WcaLog(LOGMSG_STANDARD, "Deleted the printer\n"); + uninstallDriver(RD_PRINTER_DRIVER_NAME); + WcaLog(LOGMSG_STANDARD, "Uninstalled the printer driver\n"); + checkDeleteLocalPort(RD_PRINTER_PORT); + WcaLog(LOGMSG_STANDARD, "Deleted the local port\n"); + } +} diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index ff39f8443..4093a0189 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -30,6 +30,7 @@ + @@ -57,6 +58,18 @@ + + + + + + + @@ -72,6 +85,8 @@ + + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index b443eff52..3727c0dd3 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -17,5 +17,7 @@ + + diff --git a/res/msi/Package/Fragments/ShortcutProperties.wxs b/res/msi/Package/Fragments/ShortcutProperties.wxs index 95bd4aaa3..dafd7dea4 100644 --- a/res/msi/Package/Fragments/ShortcutProperties.wxs +++ b/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -14,6 +14,7 @@ + @@ -23,6 +24,9 @@ + + + @@ -46,6 +50,16 @@ + + + + + + + + + + + + + diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 1bd3986dd..c65a5126d 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -51,5 +51,6 @@ This file contains the declaration of all the localizable strings. + diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs index bdd8471cf..e11756a65 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -51,6 +51,8 @@ + + diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs index 6e27e2b28..e4bad9197 100644 --- a/res/msi/Package/UI/MyInstallDirDlg.wxs +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -25,6 +25,7 @@ + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 9a43e9da6..670091ad8 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -8,7 +8,9 @@ import argparse import datetime import subprocess import re +import platform from pathlib import Path +from itertools import chain import shutil g_indent_unit = "\t" @@ -187,6 +189,17 @@ def replace_app_name_in_langs(app_name): with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) +def replace_app_name_in_custom_actions(app_name): + custion_actions_dir = Path(sys.argv[0]).parent.joinpath("CustomActions") + for file_path in chain(custion_actions_dir.glob("*.cpp"), custion_actions_dir.glob("*.h")): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines): + line = re.sub(r"\bRustDesk\b", app_name, line) + line = line.replace(f"{app_name} v4 Printer Driver", "RustDesk v4 Printer Driver") + lines[i] = line + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) def gen_upgrade_info(): def func(lines, index_start): @@ -542,3 +555,4 @@ if __name__ == "__main__": sys.exit(-1) replace_app_name_in_langs(args.app_name) + replace_app_name_in_custom_actions(args.app_name) diff --git a/src/client.rs b/src/client.rs index c056a64f3..def76a0e1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -49,6 +49,7 @@ use hbb_common::{ self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, + fs::JobType, get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::{Message as _, MessageField}, @@ -3297,7 +3298,7 @@ pub enum Data { Close, Login((String, String, String, bool)), Message(Message), - SendFiles((i32, String, String, i32, bool, bool)), + SendFiles((i32, JobType, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), SetNoConfirm(i32), @@ -3311,7 +3312,7 @@ pub enum Data { ToggleClipboardFile, NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), - AddJob((i32, String, String, i32, bool, bool)), + AddJob((i32, JobType, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool), ElevateDirect, diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 88f0b14a5..e6b977818 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,6 +7,14 @@ pub trait FileManager: Interface { fs::get_home_as_string() } + fn get_next_job_id(&self) -> i32 { + fs::get_next_job_id() + } + + fn update_next_job_id(&self, id: i32) { + fs::update_next_job_id(id); + } + #[cfg(not(any( target_os = "android", target_os = "ios", @@ -98,6 +106,7 @@ pub trait FileManager: Interface { fn send_files( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -106,6 +115,7 @@ pub trait FileManager: Interface { ) { self.send(Data::SendFiles(( id, + r#type.into(), path, to, file_num, @@ -117,6 +127,7 @@ pub trait FileManager: Interface { fn add_job( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -125,6 +136,7 @@ pub trait FileManager: Interface { ) { self.send(Data::AddJob(( id, + r#type.into(), path, to, file_num, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 4f084151f..5f06c23fe 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -46,6 +46,7 @@ use std::{ collections::HashMap, ffi::c_void, num::NonZeroI64, + path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, RwLock, @@ -549,13 +550,20 @@ impl Remote { } allow_err!(peer.send(&msg).await); } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + Data::SendFiles((id, r#type, path, to, file_num, include_hidden, is_remote)) => { log::info!("send files, is remote {}", is_remote); let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!("New job {}, write to {} from remote {}", id, to, path); + let to = match r#type { + fs::JobType::Generic => fs::DataSource::FilePath(PathBuf::from(&to)), + fs::JobType::Printer => { + fs::DataSource::MemoryCursor(std::io::Cursor::new(Vec::new())) + } + }; self.write_jobs.push(fs::TransferJob::new_write( id, + r#type, path.clone(), to, file_num, @@ -565,14 +573,15 @@ impl Remote { od, )); allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) + peer.send(&fs::new_send(id, r#type, path, file_num, include_hidden)) .await ); } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -616,7 +625,7 @@ impl Remote { } } } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + Data::AddJob((id, r#type, path, to, file_num, include_hidden, is_remote)) => { let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!( @@ -627,8 +636,9 @@ impl Remote { ); let mut job = fs::TransferJob::new_write( id, + r#type, path.clone(), - to, + fs::DataSource::FilePath(PathBuf::from(&to)), file_num, include_hidden, is_remote, @@ -640,8 +650,9 @@ impl Remote { } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -679,6 +690,7 @@ impl Remote { allow_err!( peer.send(&fs::new_send( id, + fs::JobType::Generic, job.remote.clone(), job.file_num, job.show_hidden @@ -688,17 +700,25 @@ impl Remote { } } else { if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone(), - job.total_size(), - )) - .await - ); + match &job.data_source { + fs::DataSource::FilePath(p) => { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + p.to_string_lossy().to_string(), + job.file_num, + job.files.clone(), + job.total_size(), + )) + .await + ); + } + fs::DataSource::MemoryCursor(_) => { + // unreachable!() + log::error!("Resume job with memory cursor"); + } + } } } } @@ -803,11 +823,10 @@ impl Remote { }); msg_out.set_file_action(file_action); allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); } - fs::remove_job(id, &mut self.read_jobs); + let _ = fs::remove_job(id, &mut self.read_jobs); self.remove_jobs.remove(&id); } Data::RemoveDir((id, path)) => { @@ -1402,92 +1421,105 @@ impl Remote { if digest.is_upload { if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handler.override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - digest.is_identical, - ); + if let fs::DataSource::FilePath(p) = &job.data_source { + let read_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + digest.is_identical, + ); + } } } } } else { if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let req = FileTransferSendConfirmRequest { + if let fs::DataSource::FilePath(p) = &job.data_source { + let write_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + match fs::is_write_need_confirmation( + &write_path, + &digest, + ) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }; job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); - } else { - self.handler.override_file_confirm( - digest.id, - digest.file_num, - write_path, - false, - digest.is_identical, - ); } - } - DigestCheckResult::NoSuchFile => { - let req = FileTransferSendConfirmRequest { + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy + { + let req = + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + digest.is_identical, + ); + } + } + DigestCheckResult::NoSuchFile => { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error receiving digest: {}", err); } - }, - Err(err) => { - println!("error receiving digest: {}", err); } } } @@ -1499,23 +1531,56 @@ impl Remote { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job } - self.update_jobs_status(); + if job.r#type == fs::JobType::Generic { + self.update_jobs_status(); + } } } Some(file_response::Union::Done(d)) => { let mut err: Option = None; - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + let mut job_type = fs::JobType::Generic; + let mut printer_data = None; + if let Some(job) = fs::remove_job(d.id, &mut self.write_jobs) { job.modify_time(); err = job.job_error(); - fs::remove_job(d.id, &mut self.write_jobs); + job_type = job.r#type; + printer_data = job.get_buf_data(); + } + match job_type { + fs::JobType::Generic => { + self.handle_job_status(d.id, d.file_num, err); + } + fs::JobType::Printer => + { + #[cfg(target_os = "windows")] + if let Some(data) = printer_data { + let printer_name = self + .handler + .printer_names + .write() + .unwrap() + .remove(&d.id); + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + .ok(); + } + } } - self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { - if let Some(_job) = fs::get_job(e.id, &mut self.write_jobs) { - fs::remove_job(e.id, &mut self.write_jobs); + let job_type = fs::remove_job(e.id, &mut self.write_jobs) + .map(|j| j.r#type) + .unwrap_or(fs::JobType::Generic); + match job_type { + fs::JobType::Generic => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + fs::JobType::Printer => { + log::error!("Printer job error: {}", e.error); + } } - self.handle_job_status(e.id, e.file_num, Some(e.error)); } _ => {} } @@ -1739,6 +1804,41 @@ impl Remote { } } Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::Send(_s)) => match _s.file_type.enum_value() { + #[cfg(target_os = "windows")] + Ok(file_transfer_send_request::FileType::Printer) => { + #[cfg(feature = "flutter")] + let action = LocalConfig::get_option( + config::keys::OPTION_PRINTER_INCOMING_JOB_ACTION, + ); + #[cfg(not(feature = "flutter"))] + let action = ""; + if action == "dismiss" { + // Just ignore the incoming print job. + } else { + let id = fs::get_next_job_id(); + #[cfg(feature = "flutter")] + let allow_auto_print = LocalConfig::get_bool_option( + config::keys::OPTION_PRINTER_ALLOW_AUTO_PRINT, + ); + #[cfg(not(feature = "flutter"))] + let allow_auto_print = false; + if allow_auto_print { + let printer_name = if action == "" { + "".to_string() + } else { + LocalConfig::get_option( + config::keys::OPTION_PRINTER_SELECTED_NAME, + ) + }; + self.handler.printer_response(id, _s.path, printer_name); + } else { + self.handler.printer_request(id, _s.path); + } + } + } + _ => {} + }, Some(file_action::Union::SendConfirm(c)) => { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { job.confirm(&c); @@ -2004,7 +2104,7 @@ impl Remote { async fn handle_cliprdr_msg( &mut self, clip: hbb_common::message_proto::Cliprdr, - peer: &mut Stream, + _peer: &mut Stream, ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] @@ -2074,7 +2174,7 @@ impl Remote { } if let Some(msg) = out_msg { - allow_err!(peer.send(&msg).await); + allow_err!(_peer.send(&msg).await); } } } diff --git a/src/common.rs b/src/common.rs index 83272a710..26d25f789 100644 --- a/src/common.rs +++ b/src/common.rs @@ -139,6 +139,10 @@ pub fn is_support_file_copy_paste_num(ver: i64) -> bool { ver >= hbb_common::get_version_number("1.3.8") } +pub fn is_support_remote_print(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + pub fn is_support_file_paste_if_macos(ver: &str) -> bool { hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") } diff --git a/src/core_main.rs b/src/core_main.rs index 4e1fc1159..90c1261bb 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -196,12 +196,11 @@ pub fn core_main() -> Option> { if config::is_disable_installation() { return None; } - let res = platform::install_me( - "desktopicon startmenu", - "".to_owned(), - true, - args.len() > 1, - ); + #[cfg(not(windows))] + let options = "desktopicon startmenu"; + #[cfg(windows)] + let options = "desktopicon startmenu printer"; + let res = platform::install_me(options, "".to_owned(), true, args.len() > 1); let text = match res { Ok(_) => translate("Installation Successful!".to_string()), Err(err) => { diff --git a/src/flutter.rs b/src/flutter.rs index 0ea552254..13c7bdd15 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1059,6 +1059,14 @@ impl InvokeUiSession for FlutterHandler { fn update_record_status(&self, start: bool) { self.push_event("record_status", &[("start", &start.to_string())], &[]); } + + fn printer_request(&self, id: i32, path: String) { + self.push_event( + "printer_request", + &[("id", json!(id)), ("path", json!(path))], + &[], + ); + } } impl FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e66a11a2..84b39dc85 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -624,7 +624,15 @@ pub fn session_send_files( _is_dir: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.send_files(act_id, path, to, file_num, include_hidden, is_remote); + session.send_files( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -749,7 +757,15 @@ pub fn session_add_job( is_remote: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.add_job(act_id, path, to, file_num, include_hidden, is_remote); + session.add_job( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -1668,6 +1684,17 @@ pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: boo } } +pub fn session_printer_response( + session_id: SessionID, + id: i32, + path: String, + printer_name: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.printer_response(id, path, printer_name); + } +} + pub fn main_set_home_dir(_home: String) { #[cfg(any(target_os = "android", target_os = "ios"))] { @@ -2362,6 +2389,68 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } +pub fn main_get_printer_names() -> SyncReturn { + #[cfg(target_os = "windows")] + return SyncReturn( + serde_json::to_string(&crate::platform::windows::get_printer_names().unwrap_or_default()) + .unwrap_or_default(), + ); + #[cfg(not(target_os = "windows"))] + return SyncReturn("".to_owned()); +} + +pub fn main_get_common(key: String) -> String { + if key == "is-printer-installed" { + #[cfg(target_os = "windows")] + { + return match remote_printer::is_rd_printer_installed(&get_app_name()) { + Ok(r) => r.to_string(), + Err(e) => e.to_string(), + }; + } + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "is-support-printer-driver" { + #[cfg(target_os = "windows")] + return crate::platform::is_win_10_or_greater().to_string(); + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "transfer-job-id" { + return hbb_common::fs::get_next_job_id().to_string(); + } else { + "".to_owned() + } +} + +pub fn main_get_common_sync(key: String) -> SyncReturn { + SyncReturn(main_get_common(key)) +} + +pub fn main_set_common(_key: String, _value: String) { + #[cfg(target_os = "windows")] + if _key == "install-printer" && crate::platform::is_win_10_or_greater() { + std::thread::spawn(move || { + let (success, msg) = match remote_printer::install_update_printer(&get_app_name()) { + Ok(_) => (true, "".to_owned()), + Err(e) => { + let err = e.to_string(); + log::error!("Failed to install/update rd printer: {}", &err); + (false, err) + } + }; + let data = HashMap::from([ + ("name", serde_json::json!("install-printer-res")), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/ipc.rs b/src/ipc.rs index 7e447576b..07b011740 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -272,6 +272,8 @@ pub enum Data { HwCodecConfig(Option), RemoveTrustedDevices(Vec), ClearTrustedDevices, + #[cfg(all(target_os = "windows", feature = "flutter"))] + PrinterData(Vec), } #[tokio::main(flavor = "current_thread")] @@ -461,7 +463,7 @@ async fn handle(data: Data, stream: &mut Connection) { .lock() .unwrap() .iter() - .filter(|x| x.1 == crate::server::AuthConnType::Remote) + .filter(|x| x.conn_type == crate::server::AuthConnType::Remote) .count(); allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0cd8b3e6c..8b6cbd0c6 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 5c4907130..b618d21f8 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index b153b0358..e11e7db4a 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 7a524f21c..8120ace84 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index dd8d6d10a..0297af774 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "没有摄像头"), ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "打印机"), + ("printer-os-requirement-tip", "打印机的传出功能需要 Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "请先安装 {} 客户端。"), + ("printer-{}-not-installed-tip", "未安装 {} 打印机。"), + ("printer-{}-ready-tip", "{} 打印机已安装,您可以使用打印功能了。"), + ("Install {} Printer", "安装 {} 打印机"), + ("Outgoing Print Jobs", "传出的打印任务"), + ("Incomming Print Jobs", "传入的打印任务"), + ("Incoming Print Job", "传入的打印任务"), + ("use-the-default-printer-tip", "使用默认的打印机执行"), + ("use-the-selected-printer-tip", "使用选择的打印机执行"), + ("auto-print-tip", "使用选择的打印机自动执行"), + ("print-incoming-job-confirm-tip", "您收到一个远程打印任务,您想在本地执行它吗?"), + ("remote-printing-disallowed-tile-tip", "不允许远程打印"), + ("remote-printing-disallowed-text-tip", "被控端的权限设置拒绝了远程打印。"), + ("save-settings-tip", "保存设置"), + ("dont-show-again-tip", "不再显示此信息"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f64a5df23..6f287a03c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3f8f14cfb..32bd52881 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f9f8994ed..a1c69481a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Keine Kameras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 505a211c8..dc23cd03d 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 26754a0a5..04d661ec9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -241,5 +241,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), ("d3d_render_tip", "When D3D rendering is enabled, the remote control screen may be black on some machines."), + ("printer-requires-installed-{}-client-tip", "In order to use remote printing, {} needs to be installed on this device."), + ("printer-os-requirement-tip", "The printer outgoing function requires Windows 10 or higher."), + ("printer-{}-not-installed-tip", "The {} Printer is not installed."), + ("printer-{}-ready-tip", "The {} Printer is installed and ready to use."), + ("auto-print-tip", "Print automatically using the selected printer."), + ("print-incoming-job-confirm-tip", "You received a print job from remote. Do you want to execute it at your side?"), + ("use-the-default-printer-tip", "Use the default printer"), + ("use-the-selected-printer-tip", "Use the selected printer"), + ("remote-printing-disallowed-tile-tip", "Remote Printing disallowed"), + ("remote-printing-disallowed-text-tip", "The permission settings of the controlled side deny Remote Printing."), + ("save-settings-tip", "Save settings"), + ("dont-show-again-tip", "Don't show this again"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 22a6c73f5..a92c36449 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d889dba74..51f1f3043 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "No hay cámaras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index f4db11cc3..292271185 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 5d47f36c7..851aa25fd 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9fc72dc75..948e005e4 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6c5761437..5750768b3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Aucune caméra"), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index b602d91ff..5799d0b60 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d9ff9b481..46c9795b4 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1ea2152b5..8d14cdcd2 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f6fac7053..f397442af 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 58fda3f7e..e2b5857e0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nessuna camera"), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("Use D3D rendering", "Usa rendering D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index bdd9b41b4..a18d6d8e6 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 08e27f8af..d104dd452 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b820e2f39..82b095e50 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 97b4a205c..07bbf2ac9 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 45942447d..474a6cbdb 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nav kameru"), ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), ("Use D3D rendering", "Izmantot D3D renderēšanu"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 73dae6028..a6d909a9b 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 7bfd50d54..55b44acd6 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Geen camera's"), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("Use D3D rendering", "Gebruik D3D-rendering"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 37e634467..6c03abdc7 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6246bce81..155c8e161 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e7c0ee5d1..9d250cd21 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 95040f109..9703a4d15 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6c0698d0a..c431e91fd 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Камера отсутствует"), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("Use D3D rendering", "Использовать визуализацию D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f5f57d3d1..b583655ad 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Peruna càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 531ccbf71..9f96cb60e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index c3fb9ec4f..405208f52 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3142b2317..b1467f661 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90007d7eb..8e469d9be 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 4493f7ab4..5d3135719 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 87ced6bf7..d4d176090 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ccf3b9bcd..064a2f657 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c1b41d2df..010bd3c2e 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e248898f9..1e36b85fd 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4a2647bb1..66ebf00f1 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "沒有鏡頭"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 578e889e5..475dd071c 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 61a63f2af..e44627495 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.cc b/src/platform/windows.cc index cbf80c497..682f5bbf3 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); @@ -859,4 +862,114 @@ extern "C" return isRunning; } -} // end of extern "C" \ No newline at end of file +} // end of extern "C" + +// Remote printing +extern "C" +{ +#pragma comment(lib, "XpsPrint.lib") +#pragma warning(push) +#pragma warning(disable : 4995) + +#define PRINT_XPS_CHECK_HR(hr, msg) \ + if (FAILED(hr)) \ + { \ + _com_error err(hr); \ + flog("%s Error: %s\n", msg, err.ErrorMessage()); \ + return -1; \ + } + + int PrintXPSRawData(LPWSTR printerName, BYTE *rawData, ULONG dataSize) + { + BOOL isCoInitializeOk = FALSE; + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (hr == RPC_E_CHANGED_MODE) + { + hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + } + if (hr == S_OK) + { + isCoInitializeOk = TRUE; + } + std::shared_ptr coInitGuard(nullptr, [isCoInitializeOk](int *) { + if (isCoInitializeOk) CoUninitialize(); + }); + + IXpsOMObjectFactory *xpsFactory = nullptr; + hr = CoCreateInstance( + __uuidof(XpsOMObjectFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IXpsOMObjectFactory), + reinterpret_cast(&xpsFactory)); + PRINT_XPS_CHECK_HR(hr, "Failed to create XPS object factory."); + std::shared_ptr xpsFactoryGuard( + xpsFactory, + [](IXpsOMObjectFactory *xpsFactory) { + xpsFactory->Release(); + }); + + HANDLE completionEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (completionEvent == nullptr) + { + flog("Failed to create completion event. Last error: %d\n", GetLastError()); + return -1; + } + std::shared_ptr completionEventGuard( + &completionEvent, + [](HANDLE *completionEvent) { + CloseHandle(*completionEvent); + }); + + IXpsPrintJob *job = nullptr; + IXpsPrintJobStream *jobStream = nullptr; + // `StartXpsPrintJob()` is deprecated, but we still use it for compatibility. + // We may change to use the `Print Document Package API` in the future. + // https://learn.microsoft.com/en-us/windows/win32/printdocs/xpsprint-functions + hr = StartXpsPrintJob( + printerName, + L"Print Job 1", + nullptr, + nullptr, + completionEvent, + nullptr, + 0, + &job, + &jobStream, + nullptr); + PRINT_XPS_CHECK_HR(hr, "Failed to start XPS print job."); + + std::shared_ptr jobStreamGuard(jobStream, [](IXpsPrintJobStream *jobStream) { + jobStream->Release(); + }); + BOOL jobOk = FALSE; + std::shared_ptr jobGuard(job, [&jobOk](IXpsPrintJob* job) { + if (jobOk == FALSE) + { + job->Cancel(); + } + job->Release(); + }); + + DWORD bytesWritten = 0; + hr = jobStream->Write(rawData, dataSize, &bytesWritten); + PRINT_XPS_CHECK_HR(hr, "Failed to write data to print job stream."); + + hr = jobStream->Close(); + PRINT_XPS_CHECK_HR(hr, "Failed to close print job stream."); + + // Wait about 5 minutes for the print job to complete. + DWORD waitMillis = 300 * 1000; + DWORD waitResult = WaitForSingleObject(completionEvent, waitMillis); + if (waitResult != WAIT_OBJECT_0) + { + flog("Wait for print job completion failed. Last error: %d\n", GetLastError()); + return -1; + } + jobOk = TRUE; + + return 0; + } + +#pragma warning(pop) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6c0136128..ed2b71c5d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -21,23 +21,22 @@ use std::{ fs, io::{self, prelude::*}, mem, - os::windows::process::CommandExt, + os::{raw::c_ulong, windows::process::CommandExt}, path::*, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, time::{Duration, Instant}, }; use wallpaper; +#[cfg(not(debug_assertions))] +use winapi::um::libloaderapi::{LoadLibraryExW, LOAD_LIBRARY_SEARCH_USER_DIRS}; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, - libloaderapi::{ - GetProcAddress, LoadLibraryExA, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, - LOAD_LIBRARY_SEARCH_USER_DIRS, - }, + libloaderapi::{GetProcAddress, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32}, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, @@ -54,6 +53,10 @@ use winapi::{ TOKEN_ELEVATION, TOKEN_QUERY, }, winreg::HKEY_CURRENT_USER, + winspool::{ + EnumPrintersW, GetDefaultPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, + PRINTER_INFO_1W, + }, winuser::*, }, }; @@ -73,6 +76,7 @@ pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; +const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; pub fn get_focused_display(displays: Vec) -> Option { unsafe { @@ -1011,6 +1015,10 @@ pub fn get_install_options() -> String { if let Some(start_menu_shortcuts) = start_menu_shortcuts { opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); } + let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER); + if let Some(printer) = printer { + opts.insert(REG_NAME_INSTALL_PRINTER, printer); + } serde_json::to_string(&opts).unwrap_or("{}".to_owned()) } @@ -1136,6 +1144,7 @@ fn get_after_install( exe: &str, reg_value_start_menu_shortcuts: Option, reg_value_desktop_shortcuts: Option, + reg_value_printer: Option, ) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); @@ -1159,12 +1168,20 @@ fn get_after_install( ) }) .unwrap_or_default(); + let reg_printer = reg_value_printer + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_PRINTER} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); format!(" chcp 65001 reg add HKEY_CLASSES_ROOT\\.{ext} /f {desktop_shortcuts} {start_menu_shortcuts} + {reg_printer} reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f @@ -1249,6 +1266,7 @@ oLink.Save let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_start_menu_shortcuts = "0".to_owned(); + let mut reg_value_printer = "0".to_owned(); let mut shortcuts = Default::default(); if options.contains("desktopicon") { shortcuts = format!( @@ -1268,6 +1286,10 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" ); reg_value_start_menu_shortcuts = "1".to_owned(); } + let install_printer = options.contains("printer") && crate::platform::is_win_10_or_greater(); + if install_printer { + reg_value_printer = "1".to_owned(); + } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1338,7 +1360,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" after_install = get_after_install( &exe, Some(reg_value_start_menu_shortcuts), - Some(reg_value_desktop_shortcuts) + Some(reg_value_desktop_shortcuts), + Some(reg_value_printer) ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, @@ -1346,13 +1369,22 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" import_config = get_import_config(&exe), ); run_cmds(cmds, debug, "install")?; + if install_printer { + allow_err!(remote_printer::install_update_printer( + &crate::get_app_name() + )); + } run_after_run_cmds(silent); Ok(()) } pub fn run_after_install() -> ResultType<()> { let (_, _, _, exe) = get_install_info(); - run_cmds(get_after_install(&exe, None, None), true, "after_install") + run_cmds( + get_after_install(&exe, None, None, None), + true, + "after_install", + ) } pub fn run_before_uninstall() -> ResultType<()> { @@ -1413,6 +1445,9 @@ fn get_uninstall(kill_self: bool) -> String { } pub fn uninstall_me(kill_self: bool) -> ResultType<()> { + if crate::platform::is_win_10_or_greater() { + remote_printer::uninstall_printer(&crate::get_app_name()); + } run_cmds(get_uninstall(kill_self), true, "uninstall") } @@ -1570,9 +1605,18 @@ pub fn bootstrap() -> bool { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); } - set_safe_load_dll() + #[cfg(debug_assertions)] + { + true + } + #[cfg(not(debug_assertions))] + { + // This function will cause `'sciter.dll' was not found neither in PATH nor near the current executable.` when debugging RustDesk. + set_safe_load_dll() + } } +#[cfg(not(debug_assertions))] fn set_safe_load_dll() -> bool { if !unsafe { set_default_dll_directories() } { return false; @@ -1589,6 +1633,7 @@ fn set_safe_load_dll() -> bool { } // https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories +#[cfg(not(debug_assertions))] unsafe fn set_default_dll_directories() -> bool { let module = LoadLibraryExW( wide_string("Kernel32.dll").as_ptr(), @@ -2728,3 +2773,119 @@ pub mod reg_display_settings { } } } + +pub fn get_printer_names() -> ResultType> { + let mut needed_bytes = 0; + let mut returned_count = 0; + + unsafe { + // First call to get required buffer size + EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + std::ptr::null_mut(), + 0, + &mut needed_bytes, + &mut returned_count, + ); + + let mut buffer = vec![0u8; needed_bytes as usize]; + + if EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + buffer.as_mut_ptr() as *mut _, + needed_bytes, + &mut needed_bytes, + &mut returned_count, + ) == 0 + { + return Err(anyhow!("Failed to enumerate printers")); + } + + let ptr = buffer.as_ptr() as *const PRINTER_INFO_1W; + let printers = std::slice::from_raw_parts(ptr, returned_count as usize); + + Ok(printers + .iter() + .filter_map(|p| { + let name = p.pName; + if !name.is_null() { + let mut len = 0; + while len < 500 { + if name.add(len).is_null() || *name.add(len) == 0 { + break; + } + len += 1; + } + if len > 0 && len < 500 { + Some(String::from_utf16_lossy(std::slice::from_raw_parts( + name, len, + ))) + } else { + None + } + } else { + None + } + }) + .collect()) + } +} + +extern "C" { + fn PrintXPSRawData(printer_name: *const u16, raw_data: *const u8, data_size: c_ulong) -> DWORD; +} + +pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> ResultType<()> { + let mut printer_name = printer_name.unwrap_or_default(); + if printer_name.is_empty() { + // use GetDefaultPrinter to get the default printer name + let mut needed_bytes = 0; + unsafe { + GetDefaultPrinterW(std::ptr::null_mut(), &mut needed_bytes); + } + if needed_bytes > 0 { + let mut default_printer_name = vec![0u16; needed_bytes as usize]; + unsafe { + GetDefaultPrinterW( + default_printer_name.as_mut_ptr() as *mut _, + &mut needed_bytes, + ); + } + printer_name = String::from_utf16_lossy(&default_printer_name[..needed_bytes as usize]); + } + } else { + if let Ok(names) = crate::platform::windows::get_printer_names() { + if !names.contains(&printer_name) { + // Don't set the first printer as current printer. + // It may not be the desired printer. + log::error!( + "Printer name \"{}\" not found, ignore the print job", + printer_name + ); + bail!("Printer name \"{}\" not found", &printer_name); + } + } + } + if printer_name.is_empty() { + log::error!("Failed to get printer name"); + return Err(anyhow!("Failed to get printer name")); + } + + let printer_name = wide_string(&printer_name); + unsafe { + let res = PrintXPSRawData( + printer_name.as_ptr(), + data.as_ptr() as *const u8, + data.len() as c_ulong, + ); + if res != 0 { + bail!("Failed to send file to printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + } + } + + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index dcb7023f5..87e6f390f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -70,6 +70,9 @@ mod service; mod video_qos; pub mod video_service; +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub mod printer_service; + pub type Childs = Arc>>; type ConnMap = HashMap; @@ -129,6 +132,20 @@ pub fn new() -> ServerPtr { server.add_service(Box::new(input_service::new_window_focus())); } } + #[cfg(all(target_os = "windows", feature = "flutter"))] + { + match printer_service::init(&crate::get_app_name()) { + Ok(()) => { + log::info!("printer service initialized"); + server.add_service(Box::new(printer_service::new( + printer_service::NAME.to_owned(), + ))); + } + Err(e) => { + log::error!("printer service init failed: {}", e); + } + } + } Arc::new(RwLock::new(server)) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 95f4b88ef..a225acb02 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -28,7 +28,7 @@ use hbb_common::platform::linux::run_cmds; use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::{self, keys, Config, TrustedDevice}, - fs::{self, can_enable_overwrite_detection}, + fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, @@ -67,7 +67,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } @@ -245,6 +245,8 @@ pub struct Connection { follow_remote_cursor: bool, follow_remote_window: bool, multi_ui_session: bool, + tx_from_authed: mpsc::UnboundedSender, + printer_data: Vec<(Instant, String, Vec)>, } impl ConnInner { @@ -309,6 +311,7 @@ impl Connection { let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, _rx_input) = std_mpsc::channel(); + let (tx_from_authed, mut rx_from_authed) = mpsc::unbounded_channel::(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); @@ -396,6 +399,8 @@ impl Connection { delayed_read_dir: None, #[cfg(target_os = "macos")] retina: Retina::default(), + tx_from_authed, + printer_data: Vec::new(), }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -758,6 +763,19 @@ impl Connection { break; } }, + Some(data) = rx_from_authed.recv() => { + match data { + #[cfg(all(target_os = "windows", feature = "flutter"))] + ipc::Data::PrinterData(data) => { + if config::Config::get_bool_option(config::keys::OPTION_ENABLE_REMOTE_PRINTER) { + conn.send_printer_request(data).await; + } else { + conn.send_remote_printing_disallowed().await; + } + } + _ => {} + } + } _ = second_timer.tick() => { #[cfg(windows)] conn.portable_check(); @@ -1104,6 +1122,22 @@ impl Connection { }); } + fn get_files_for_audit(job_type: fs::JobType, mut files: Vec) -> Vec<(String, i64)> { + files + .drain(..) + .map(|f| { + ( + if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + f.name + }, + f.size as _, + ) + }) + .collect() + } + fn post_file_audit( &self, r#type: FileAuditType, @@ -1212,6 +1246,8 @@ impl Connection { self.inner.id(), auth_conn_type, self.session_key(), + self.tx_from_authed.clone(), + self.lr.clone(), )); self.session_last_recv_time = SESSIONS .lock() @@ -2318,7 +2354,15 @@ impl Connection { } } Some(message::Union::FileAction(fa)) => { - if self.file_transfer.is_some() { + let mut handle_fa = self.file_transfer.is_some(); + if !handle_fa { + if let Some(file_action::Union::Send(s)) = fa.union.as_ref() { + if JobType::from_proto(s.file_type) == JobType::Printer { + handle_fa = true; + } + } + } + if handle_fa { if self.delayed_read_dir.is_some() { if let Some(file_action::Union::ReadDir(rd)) = fa.union { self.delayed_read_dir = Some((rd.path, rd.include_hidden)); @@ -2375,10 +2419,32 @@ impl Connection { &self.lr.version, )); let path = s.path.clone(); + let r#type = JobType::from_proto(s.file_type); + let data_source; + match r#type { + JobType::Generic => { + data_source = + fs::DataSource::FilePath(PathBuf::from(&path)); + } + JobType::Printer => { + if let Some(pd) = + self.printer_data.iter().find(|(_, p, _)| *p == path) + { + data_source = fs::DataSource::MemoryCursor( + std::io::Cursor::new(pd.2.clone()), + ); + self.printer_data.retain(|f| f.1 != path); + } else { + // Ignore this message if the printer data is not found + return true; + } + } + }; match fs::TransferJob::new_read( id, + r#type, "".to_string(), - path.clone(), + data_source, s.file_num, s.include_hidden, false, @@ -2390,19 +2456,21 @@ impl Connection { Ok(mut job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; - let mut files = job.files().to_owned(); + let files = job.files().to_owned(); job.is_remote = true; job.conn_id = self.inner.id(); + let job_type = job.r#type; self.read_jobs.push(job); self.file_timer = crate::rustdesk_interval(time::interval(MILLI1)); self.post_file_audit( FileAuditType::RemoteSend, - &s.path, - files - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + if job_type == fs::JobType::Printer { + "Remote print" + } else { + &s.path + }, + Self::get_files_for_audit(job_type, files), json!({}), ); } @@ -2433,11 +2501,7 @@ impl Connection { self.post_file_audit( FileAuditType::RemoteReceive, &r.path, - r.files - .to_vec() - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + Self::get_files_for_audit(fs::JobType::Generic, r.files), json!({}), ); self.file_transferred = true; @@ -2476,13 +2540,12 @@ impl Connection { } Some(file_action::Union::Cancel(c)) => { self.send_fs(ipc::FS::CancelWrite { id: c.id }); - if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) { + if let Some(job) = fs::remove_job(c.id, &mut self.read_jobs) { self.send_to_cm(ipc::Data::FileTransferLog(( "transfer".to_string(), - fs::serialize_transfer_job(job, false, true, ""), + fs::serialize_transfer_job(&job, false, true, ""), ))); } - fs::remove_job(c.id, &mut self.read_jobs); } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { @@ -3635,6 +3698,32 @@ impl Connection { fn try_empty_file_clipboard(&mut self) { try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_printer_request(&mut self, data: Vec) { + // This path is only used to identify the printer job. + let path = format!("RustDesk://FsJob//Printer/{}", get_time()); + + let msg = fs::new_send(0, fs::JobType::Printer, path.clone(), 1, false); + self.send(msg).await; + self.printer_data + .retain(|(t, _, _)| t.elapsed().as_secs() < 60); + self.printer_data.push((Instant::now(), path, data)); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_remote_printing_disallowed(&mut self) { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "custom-nook-nocancel-hasclose".to_owned(), + title: "remote-printing-disallowed-tile-tip".to_owned(), + text: "remote-printing-disallowed-text-tip".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3970,6 +4059,19 @@ fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { tx } +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub fn on_printer_data(data: Vec) { + crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.printer) + .next() + .map(|c| { + c.sender.send(Data::PrinterData(data)).ok(); + }); +} + #[cfg(windows)] pub struct PortableState { pub last_uac: bool, @@ -4103,6 +4205,14 @@ impl Retina { } } +pub struct AuthedConn { + pub conn_id: i32, + pub conn_type: AuthConnType, + pub session_key: SessionKey, + pub sender: mpsc::UnboundedSender, + pub printer: bool, +} + mod raii { // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections @@ -4127,11 +4237,23 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { - AUTHED_CONNS - .lock() - .unwrap() - .push((conn_id, conn_type, session_key)); + pub fn new( + conn_id: i32, + conn_type: AuthConnType, + session_key: SessionKey, + sender: mpsc::UnboundedSender, + lr: LoginRequest, + ) -> Self { + let printer = conn_type == crate::server::AuthConnType::Remote + && crate::is_support_remote_print(&lr.version) + && lr.my_platform == whoami::Platform::Windows.to_string(); + AUTHED_CONNS.lock().unwrap().push(AuthedConn { + conn_id, + conn_type, + session_key, + sender, + printer, + }); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); @@ -4153,7 +4275,7 @@ mod raii { .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); allow_err!(WAKELOCK_SENDER .lock() @@ -4166,7 +4288,7 @@ mod raii { .lock() .unwrap() .iter() - .filter(|c| c.1 != AuthConnType::PortForward) + .filter(|c| c.conn_type != AuthConnType::PortForward) .count() } @@ -4179,16 +4301,16 @@ mod raii { .lock() .unwrap() .iter() - .any(|c| c.0 == conn_id && c.1 == AuthConnType::Remote); + .any(|c| c.conn_id == conn_id && c.conn_type == AuthConnType::Remote); // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, // If any of the connections is closed allowing retry, this will not be called; // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; // If the remote connection is closed with no retry, keep the session is not reasonable in case there is a retry button in the remote side, and ignore network fluctuations. - let another_remote = AUTHED_CONNS - .lock() - .unwrap() - .iter() - .any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); + let another_remote = AUTHED_CONNS.lock().unwrap().iter().any(|c| { + c.conn_id != conn_id + && c.session_key == key + && c.conn_type == AuthConnType::Remote + }); if is_remote || !another_remote { lock.remove(&key); log::info!("remove session"); @@ -4256,12 +4378,12 @@ mod raii { .unwrap() .on_connection_close(self.0); } - AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); if remote_count == 0 { #[cfg(any(target_os = "windows", target_os = "linux"))] diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 257f4d71b..06391ad7c 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -505,7 +505,7 @@ pub fn try_stop_record_cursor_pos() { .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); if remote_count > 0 { return; diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index b8cba71dd..4a4eaaad1 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -812,7 +812,7 @@ pub mod client { .lock() .unwrap() .iter() - .filter(|c| c.1 == crate::server::AuthConnType::Remote) + .filter(|c| c.conn_type == crate::server::AuthConnType::Remote) .count(); stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); } diff --git a/src/server/printer_service.rs b/src/server/printer_service.rs new file mode 100644 index 000000000..268429a58 --- /dev/null +++ b/src/server/printer_service.rs @@ -0,0 +1,163 @@ +use super::service::{EmptyExtraFieldService, GenericService, Service}; +use hbb_common::{bail, dlopen::symbor::Library, log, ResultType}; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +pub const NAME: &'static str = "remote-printer"; + +const LIB_NAME_PRINTER_DRIVER_ADAPTER: &str = "printer_driver_adapter"; + +// Return 0 if success, otherwise return error code. +pub type Init = fn(tag_name: *const i8) -> i32; +pub type Uninit = fn(); +// dur_mills: Get the file generated in the last `dur_mills` milliseconds. +// data: The raw prn data, xps format. +// data_len: The length of the raw prn data. +pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib_name = match get_lib_name() { + Ok(name) => name, + Err(e) => { + log::warn!("Failed to get lib name, {}", e); + return Self { + _lib: None, + $( $field: None ),+ + }; + } + }; + let lib = match Library::open(&lib_name) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", &lib_name, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + log::info!("method found {}", stringify!($field)); + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + init: Init, + uninit: Uninit, + get_prn_data: GetPrnData +); + +lazy_static::lazy_static! { + static ref LIB_WRAPPER: Arc> = Default::default(); +} + +fn get_lib_name() -> ResultType { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + let dll_name = format!("{}.dll", LIB_NAME_PRINTER_DRIVER_ADAPTER); + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + bail!("{} not found", full_path.to_string_lossy().as_ref()); + } else { + Ok(full_path.to_string_lossy().into_owned()) + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } +} + +pub fn init(app_name: &str) -> ResultType<()> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + let Some(fn_init) = lib_wrapper.init.as_ref() else { + bail!("Failed to load func init"); + }; + + let tag_name = std::ffi::CString::new(app_name)?; + let ret = fn_init(tag_name.as_ptr()); + if ret != 0 { + bail!("Failed to init printer driver"); + } + Ok(()) +} + +pub fn uninit() { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_uninit) = lib_wrapper.uninit.as_ref() { + fn_uninit(); + } +} + +fn get_prn_data(dur_mills: u32) -> ResultType> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_get_prn_data) = lib_wrapper.get_prn_data.as_ref() { + let mut data = std::ptr::null_mut(); + let mut data_len = 0u32; + fn_get_prn_data(dur_mills, &mut data, &mut data_len); + if data.is_null() || data_len == 0 { + return Ok(Vec::new()); + } + let bytes = + Vec::from(unsafe { std::slice::from_raw_parts(data as *const u8, data_len as usize) }); + unsafe { + hbb_common::libc::free(data as *mut std::ffi::c_void); + } + Ok(bytes) + } else { + bail!("Failed to load func get_prn_file"); + } +} + +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + let bytes = get_prn_data(1000)?; + if !bytes.is_empty() { + log::info!("Got prn data, data len: {}", bytes.len()); + crate::server::on_printer_data(bytes); + } + thread::sleep(Duration::from_millis(300)); + } + Ok(()) +} diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 6c741b31f..0b60cf748 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -69,8 +69,6 @@ function getExt(name) { return ""; } -var jobIdCounter = 1; - class JobTable: Reactor.Component { this var jobs = []; this var job_map = {}; @@ -126,8 +124,7 @@ class JobTable: Reactor.Component { } if (!to) return; to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, @@ -135,7 +132,7 @@ class JobTable: Reactor.Component { is_last: false }); this.job_map[id] = this.jobs[this.jobs.length - 1]; - handler.send_files(id, path, to, 0, show_hidden, is_remote); + handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); var self = this; self.timer(30ms, function() { self.update(); }); } @@ -147,8 +144,8 @@ class JobTable: Reactor.Component { 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]; - jobIdCounter = id + 1; - handler.add_job(id, path, to, file_num, show_hidden, is_remote); + handler.update_next_job_id(id + 1); + handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); stdout.println(JSON.stringify(job)); } @@ -162,16 +159,14 @@ class JobTable: Reactor.Component { } function addDelDir(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); } function addDelFile(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); @@ -552,9 +547,9 @@ class FolderView : Reactor.Component { return; } var path = me.joinPath(name); - handler.create_dir(jobIdCounter, path, me.is_remote); - create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; - jobIdCounter += 1; + var id = handler.get_next_job_id(); + handler.create_dir(id, path, me.is_remote); + create_dir_jobs[id] = { is_remote: me.is_remote, path: path }; }); } diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 34f6b0443..da557e312 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -317,6 +317,9 @@ class MsgboxComponent: Reactor.Component { if (this.type == "multiple-sessions-nocancel") { values.sid = (this.$$(select))[0].value; } + if (this.type == "remote-printer-selector") { + values.name = (this.$$(select))[0].value; + } return values; } diff --git a/src/ui/printer.tis b/src/ui/printer.tis new file mode 100644 index 000000000..c28482601 --- /dev/null +++ b/src/ui/printer.tis @@ -0,0 +1,41 @@ +include "sciter:reactor.tis"; + +handler.printerRequest = function(id, path) { + show_printer_selector(id, path); +}; + +function show_printer_selector(id, path) +{ + var names = handler.get_printer_names(); + msgbox("remote-printer-selector", "Incoming Print Job", , "", function(res=null) { + if (res && res.name) { + handler.on_printer_selected(id, path, res.name); + } + }, 180); +} + +class PrinterComponent extends Reactor.Component { + this var names = []; + this var jobTip = translate("print-incoming-job-confirm-tip"); + + function this(params) { + if (params && params.names) { + this.names = params.names; + } + } + + function render() { + return
+
{translate("print-incoming-job-confirm-tip")}
+
+
+ +
+
+
; + } +} diff --git a/src/ui/remote.html b/src/ui/remote.html index d58c3449b..70e909d17 100644 --- a/src/ui/remote.html +++ b/src/ui/remote.html @@ -15,6 +15,7 @@ include "port_forward.tis"; include "grid.tis"; include "header.tis"; + include "printer.tis";
diff --git a/src/ui/remote.rs b/src/ui/remote.rs index d4d89b64e..fffc51b86 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -379,6 +379,10 @@ impl InvokeUiSession for SciterHandler { fn update_record_status(&self, start: bool) { self.call("updateRecordStatus", &make_args!(start)); } + + fn printer_request(&self, id: i32, path: String) { + self.call("printerRequest", &make_args!(id, path)); + } } pub struct SciterSession(Session); @@ -491,6 +495,8 @@ impl sciter::EventHandler for SciterSession { fn get_chatbox(); fn get_icon(); fn get_home_dir(); + fn get_next_job_id(); + fn update_next_job_id(i32); fn read_dir(String, bool); fn remove_dir(i32, String, bool); fn create_dir(i32, String, bool); @@ -502,8 +508,8 @@ impl sciter::EventHandler for SciterSession { fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); - fn send_files(i32, String, String, i32, bool, bool); - fn add_job(i32, String, String, i32, bool, bool); + fn send_files(i32, i32, String, String, i32, bool, bool); + fn add_job(i32, i32, String, String, i32, bool, bool); fn resume_job(i32, bool); fn get_platform(bool); fn get_path_sep(bool); @@ -541,6 +547,8 @@ impl sciter::EventHandler for SciterSession { fn set_selected_windows_session_id(String); fn is_recording(); fn has_file_clipboard(); + fn get_printer_names(); + fn on_printer_selected(i32, String, String); } } @@ -842,6 +850,22 @@ impl SciterSession { fn version_cmp(&self, v1: String, v2: String) -> i32 { (hbb_common::get_version_number(&v1) - hbb_common::get_version_number(&v2)) as i32 } + + fn get_printer_names(&self) -> Value { + #[cfg(target_os = "windows")] + let printer_names = crate::platform::windows::get_printer_names().unwrap_or_default(); + #[cfg(not(target_os = "windows"))] + let printer_names: Vec = vec![]; + let mut v = Value::array(0); + for name in printer_names { + v.push(name); + } + v + } + + fn on_printer_selected(&self, id: i32, path: String, printer_name: String) { + self.printer_response(id, path, printer_name); + } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index b36c37be7..1da297a4e 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -744,6 +744,8 @@ async fn handle_fs( tx: &UnboundedSender, tx_log: Option<&UnboundedSender>, ) { + use std::path::PathBuf; + use hbb_common::fs::serialize_transfer_job; match fs { @@ -785,8 +787,9 @@ async fn handle_fs( // dummy remote, show_hidden, is_remote let mut job = fs::TransferJob::new_write( id, + fs::JobType::Generic, "".to_string(), - path, + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, false, false, @@ -805,27 +808,24 @@ async fn handle_fs( write_jobs.push(job); } ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.remove_download_file(); tx_log.map(|tx: &UnboundedSender| { - tx.send(serialize_transfer_job(job, false, true, "")) + tx.send(serialize_transfer_job(&job, false, true, "")) }); - fs::remove_job(id, write_jobs); } } ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.modify_time(); send_raw(fs::new_done(id, file_num), tx); - tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, ""))); - fs::remove_job(id, write_jobs); + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, true, false, ""))); } } ipc::FS::WriteError { id, file_num, err } => { - if let Some(job) = fs::get_job(id, write_jobs) { - tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err))); + if let Some(job) = fs::remove_job(id, write_jobs) { + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, false, false, &err))); send_raw(fs::new_error(job.id(), err, file_num), tx); - fs::remove_job(job.id(), write_jobs); } } ipc::FS::WriteBlock { @@ -871,32 +871,34 @@ async fn handle_fs( ..Default::default() }; if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - send_raw(msg_out, &tx); - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); + if let fs::DataSource::FilePath(p) = &job.data_source { + let path = get_string(&fs::TransferJob::join(p, &file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } } } - } - Err(err) => { - send_raw(fs::new_error(id, err, file_num), &tx); + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d4d9e1022..fb06493fb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -58,6 +58,7 @@ pub struct Session { pub server_clipboard_enabled: Arc>, pub last_change_display: Arc>, pub connection_round_state: Arc>, + pub printer_names: Arc>>, } #[derive(Clone)] @@ -1505,6 +1506,20 @@ impl Session { pub fn get_conn_token(&self) -> Option { self.lc.read().unwrap().get_conn_token() } + + pub fn printer_response(&self, id: i32, path: String, printer_name: String) { + self.printer_names.write().unwrap().insert(id, printer_name); + let to = std::env::temp_dir().join(format!("rustdesk_printer_{id}")); + self.send(Data::SendFiles(( + id, + hbb_common::fs::JobType::Printer, + path, + to.to_string_lossy().to_string(), + 0, + false, + true, + ))); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1570,6 +1585,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn is_multi_ui_session(&self) -> bool; fn update_record_status(&self, start: bool); fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} + fn printer_request(&self, id: i32, path: String); } impl Deref for Session {