feat: remote printer (#11231)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-03-27 15:34:27 +08:00
committed by GitHub
parent 1cb53c1f7a
commit f4bbf82363
101 changed files with 3707 additions and 211 deletions

View File

@@ -159,14 +159,44 @@ jobs:
- name: Build rustdesk - name: Build rustdesk
run: | 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 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 . 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\Win32 -Recurse
Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" 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 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 - name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./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 # Runner.rc does not contain actual version, but Runner.res does

25
Cargo.lock generated
View File

@@ -5559,6 +5559,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "remote_printer"
version = "0.1.0"
dependencies = [
"hbb_common",
"winapi 0.3.9",
"windows-strings",
]
[[package]] [[package]]
name = "repng" name = "repng"
version = "0.2.2" version = "0.2.2"
@@ -5805,6 +5814,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"qrcode-generator", "qrcode-generator",
"rdev", "rdev",
"remote_printer",
"repng", "repng",
"reqwest", "reqwest",
"ringbuf", "ringbuf",
@@ -7833,6 +7843,12 @@ dependencies = [
"syn 2.0.98", "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]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"
@@ -7853,6 +7869,15 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"

View File

@@ -116,10 +116,12 @@ winapi = { version = "0.3", features = [
"cguid", "cguid",
"cfgmgr32", "cfgmgr32",
"ioapiset", "ioapiset",
"winspool",
] } ] }
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" } virtual_display = { path = "libs/virtual_display" }
remote_printer = { path = "libs/remote_printer" }
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1.2" tauri-winrt-notification = "0.1.2"
@@ -177,7 +179,7 @@ jni = "0.21"
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace] [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"] exclude = ["vdi/host", "examples/custom_plugin"]
[package.metadata.winres] [package.metadata.winres]

View File

@@ -3789,3 +3789,29 @@ void updateTextAndPreserveSelection(
baseOffset: 0, extentOffset: controller.value.text.length); baseOffset: 0, extentOffset: controller.value.text.length);
} }
} }
List<String> getPrinterNames() {
final printerNamesJson = bind.mainGetPrinterNames();
if (printerNamesJson.isEmpty) {
return [];
}
try {
final List<dynamic> 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;
}

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';

View File

@@ -98,6 +98,7 @@ const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode"; const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard"; const String kOptionEnableKeyboard = "enable-keyboard";
// "Settings -> Security -> Permissions" // "Settings -> Security -> Permissions"
const String kOptionEnableRemotePrinter = "enable-remote-printer";
const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio"; const String kOptionEnableAudio = "enable-audio";
@@ -219,6 +220,14 @@ const double kDefaultQuality = 50;
const double kMaxQuality = 100; const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000; 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 double kNewWindowOffset = isWindows
? 56.0 ? 56.0
: isLinux : isLinux

View File

@@ -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/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.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/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/manager.dart';
@@ -55,6 +56,7 @@ enum SettingsTabKey {
display, display,
plugin, plugin,
account, account,
printer,
about, about,
} }
@@ -74,6 +76,7 @@ class DesktopSettingPage extends StatefulWidget {
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin, SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account, if (!bind.isDisableAccount()) SettingsTabKey.account,
if (isWindows) SettingsTabKey.printer,
SettingsTabKey.about, SettingsTabKey.about,
]; ];
@@ -198,6 +201,10 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
settingTabs.add( settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); _TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break; break;
case SettingsTabKey.printer:
settingTabs
.add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print));
break;
case SettingsTabKey.about: case SettingsTabKey.about:
settingTabs settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
@@ -229,6 +236,9 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
case SettingsTabKey.account: case SettingsTabKey.account:
children.add(const _Account()); children.add(const _Account());
break; break;
case SettingsTabKey.printer:
children.add(const _Printer());
break;
case SettingsTabKey.about: case SettingsTabKey.about:
children.add(const _About()); children.add(const _About());
break; break;
@@ -963,6 +973,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox( _OptionCheckBox(
context, 'Enable keyboard/mouse', kOptionEnableKeyboard, context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
if (isWindows)
_OptionCheckBox(
context, 'Enable remote printer', kOptionEnableRemotePrinter,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard, _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _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<Widget> 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 { class _About extends StatefulWidget {
const _About({Key? key}) : super(key: key); const _About({Key? key}) : super(key: key);

View File

@@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
late final TextEditingController controller; late final TextEditingController controller;
final RxBool startmenu = true.obs; final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs; final RxBool desktopicon = true.obs;
final RxBool printer = true.obs;
final RxBool showProgress = false.obs; final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs; final RxBool btnEnabled = true.obs;
@@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
final installOptions = jsonDecode(bind.installInstallOptions()); final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
printer.value = installOptions['PRINTER'] != '0';
} }
@override @override
@@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
).marginSymmetric(vertical: 2 * em), ).marginSymmetric(vertical: 2 * em),
Option(startmenu, label: 'Create start menu shortcuts') Option(startmenu, label: 'Create start menu shortcuts')
.marginOnly(bottom: 7), .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( Container(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
String args = ''; String args = '';
if (startmenu.value) args += ' startmenu'; if (startmenu.value) args += ' startmenu';
if (desktopicon.value) args += ' desktopicon'; if (desktopicon.value) args += ' desktopicon';
if (printer.value) args += ' printer';
bind.installInstallMe(options: args, path: controller.text); bind.installInstallMe(options: args, path: controller.text);
} }

View File

@@ -30,8 +30,15 @@ enum SortBy {
class JobID { class JobID {
int _count = 0; int _count = 0;
int next() { int next() {
_count++; String v = bind.mainGetCommonSync(key: 'transfer-job-id');
return _count; 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;
}
} }
} }

View File

@@ -9,7 +9,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.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/group_model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_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/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -412,12 +412,186 @@ class FfiModel with ChangeNotifier {
isMobile) { isMobile) {
parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
} }
} else if (name == "printer_request") {
_handlePrinterRequest(evt, sessionId, peerId);
} else { } else {
debugPrint('Event is not handled in the fixed branch: $name'); debugPrint('Event is not handled in the fixed branch: $name');
} }
}; };
} }
_handlePrinterRequest(
Map<String, dynamic> 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<String> 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<String>(
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<String>(
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( _handleUseTextureRender(
Map<String, dynamic> evt, SessionID sessionId, String peerId) { Map<String, dynamic> evt, SessionID sessionId, String peerId) {
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');

View File

@@ -60,14 +60,14 @@ class PlatformFFI {
} }
bool registerEventHandler( bool registerEventHandler(
String eventName, String handlerName, HandleEvent handler) { String eventName, String handlerName, HandleEvent handler, {bool replace = false}) {
debugPrint('registerEventHandler $eventName $handlerName'); debugPrint('registerEventHandler $eventName $handlerName');
var handlers = _eventHandlers[eventName]; var handlers = _eventHandlers[eventName];
if (handlers == null) { if (handlers == null) {
_eventHandlers[eventName] = {handlerName: handler}; _eventHandlers[eventName] = {handlerName: handler};
return true; return true;
} else { } else {
if (handlers.containsKey(handlerName)) { if (!replace && handlers.containsKey(handlerName)) {
return false; return false;
} else { } else {
handlers[handlerName] = handler; handlers[handlerName] = handler;

View File

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

View File

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

View File

@@ -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<u16> {
format!("{} Printer", app_name)
.encode_utf16()
.chain(Some(0))
.collect()
}
#[cfg(target_os = "windows")]
fn get_driver_name() -> Vec<u16> {
"RustDesk v4 Printer Driver"
.encode_utf16()
.chain(Some(0))
.collect()
}
#[cfg(target_os = "windows")]
fn get_port_name(app_name: &str) -> Vec<u16> {
format!("{} Printer", app_name)
.encode_utf16()
.chain(Some(0))
.collect()
}

View File

@@ -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<Option<DWORDLONG>> {
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<Vec<u16>> {
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<u16>) -> 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(())
}

View File

@@ -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<bool> {
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<u16> {
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<T, R: Sized>(
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<R>,
on_no_data: impl Fn() -> Option<R>,
) -> ResultType<Option<R>> {
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())
}

View File

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

View File

@@ -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<bool> {
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<Option<Vec<u16>>> {
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(())
}

View File

@@ -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<PathBuf> {
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<u16> = 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));
}

View File

@@ -23,7 +23,8 @@ remote = open('src/ui/remote.html').read() \
.replace('include "grid.tis";', open('src/ui/grid.tis').read()) \ .replace('include "grid.tis";', open('src/ui/grid.tis').read()) \
.replace('include "header.tis";', open('src/ui/header.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 "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() chatbox = open('src/ui/chatbox.html').read()
install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read()) install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read())

View File

@@ -15,3 +15,9 @@ bool MyStopServiceW(LPCWSTR serviceName);
std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key); std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key);
void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired); void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired);
namespace RemotePrinter
{
VOID installUpdatePrinter(const std::wstring& installFolder);
VOID uninstallPrinter();
}

View File

@@ -878,3 +878,55 @@ void TryStopDeleteServiceByShell(LPWSTR svcName)
WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); 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);
}

View File

@@ -12,3 +12,5 @@ EXPORTS
SetPropertyFromConfig SetPropertyFromConfig
AddRegSoftwareSASGeneration AddRegSoftwareSASGeneration
RemoveAmyuniIdd RemoveAmyuniIdd
InstallPrinter
UninstallPrinter

View File

@@ -67,6 +67,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="ReadConfig.cpp" /> <ClCompile Include="ReadConfig.cpp" />
<ClCompile Include="RemotePrinter.cpp" />
<ClCompile Include="ServiceUtils.cpp" /> <ClCompile Include="ServiceUtils.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,517 @@
#include "pch.h"
#include <Windows.h>
#include <winspool.h>
#include <setupapi.h>
#include <memory>
#include <string>
#include <functional>
#include <vector>
#include <iostream>
#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<BOOL(DWORD level, LPBYTE pDriverInfo, DWORD cbBuf, LPDWORD pcbNeeded, LPDWORD pcReturned)>;
template <typename T, typename R>
using FuncOnData = std::function<std::shared_ptr<R>(const T &)>;
template <typename R>
using FuncOnNoData = std::function<std::shared_ptr<R>()>;
template <class T, class R>
std::shared_ptr<R> commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData<T, R> onData, FuncOnNoData<R> onNoData)
{
DWORD needed = 0;
DWORD returned = 0;
func(level, NULL, 0, &needed, &returned);
if (needed == 0)
{
return onNoData();
}
std::vector<BYTE> buffer(needed);
if (!func(level, buffer.data(), needed, &needed, &returned))
{
return nullptr;
}
T *pPortInfo = reinterpret_cast<T *>(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<BOOL>(new BOOL(TRUE));
}
else {
return std::shared_ptr<BOOL>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PORT_INFO_2, BOOL>(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<LPWSTR>(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<DWORDLONG>(new DWORDLONG(info.dwlDriverVersion));
}
else
{
return std::shared_ptr<DWORDLONG>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<DRIVER_INFO_6W, DWORDLONG>(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<std::wstring>(new std::wstring(info.pszInfPath));
}
else
{
return std::shared_ptr<std::wstring>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<DRIVER_INFO_8W, std::wstring>(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<LPWSTR>(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<BOOL>(new BOOL(TRUE));
}
else
{
return std::shared_ptr<BOOL>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PRINTER_INFO_1W, BOOL>(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<std::wstring>(new std::wstring(info.pPrinterName));
}
else
{
return std::shared_ptr<std::wstring>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PRINTER_INFO_2W, std::wstring>(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<LPWSTR>(name);
printerInfo.pPortName = const_cast<LPWSTR>(port);
printerInfo.pDriverName = const_cast<LPWSTR>(driver);
printerInfo.pPrintProcessor = const_cast<LPWSTR>(L"WinPrint");
printerInfo.pDatatype = const_cast<LPWSTR>(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<LPWSTR>(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");
}
}

View File

@@ -30,6 +30,7 @@
<CustomAction Id="SetPropertyServiceStop.SetParam.PropertyName" Return="check" Property="PropertyName" Value="STOP_SERVICE" /> <CustomAction Id="SetPropertyServiceStop.SetParam.PropertyName" Return="check" Property="PropertyName" Value="STOP_SERVICE" />
<CustomAction Id="TryDeleteStartupShortcut.SetParam" Return="check" Property="ShortcutName" Value="$(var.Product) Tray" /> <CustomAction Id="TryDeleteStartupShortcut.SetParam" Return="check" Property="ShortcutName" Value="$(var.Product) Tray" />
<CustomAction Id="RemoveAmyuniIdd.SetParam" Return="check" Property="RemoveAmyuniIdd" Value="[INSTALLFOLDER_INNER]" /> <CustomAction Id="RemoveAmyuniIdd.SetParam" Return="check" Property="RemoveAmyuniIdd" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="InstallPrinter.SetParam" Return="check" Property="InstallPrinter" Value="[INSTALLFOLDER_INNER]" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="SetPropertyIsServiceRunning" After="InstallInitialize" Condition="Installed" /> <Custom Action="SetPropertyIsServiceRunning" After="InstallInitialize" Condition="Installed" />
@@ -57,6 +58,18 @@
<Custom Action="LaunchApp" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) "/> <Custom Action="LaunchApp" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) "/>
<Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/> <Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<!-- https://learn.microsoft.com/en-us/windows/win32/msi/operating-system-property-values -->
<!-- We have to use `VersionNT` to instead of `IsWindows10OrGreater()` in the custom action.
Because `IsWindows10OrGreater()` requires the manifest file to be embedded in the executable/dll file.
Even I have embedded the manifest file, it still does not work correctly in my case.
https://learn.microsoft.com/en-us/windows/win32/sysinfo/version-helper-apis -->
<!-- VersionNT >= 603 means can't differentiate between Windows 8.1 and Windows 10.
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
<!-- Remote printer also works on Win8.1 in my test. -->
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;" />
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT &gt;= 603" />
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.--> <!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
<!--ExecFirewallExceptions: Error 0x80070057: failed to add app to the authorized apps list--> <!--ExecFirewallExceptions: Error 0x80070057: failed to add app to the authorized apps list-->
<Custom Action="AddFirewallRules" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)"/> <Custom Action="AddFirewallRules" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)"/>
@@ -72,6 +85,8 @@
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/> <Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/> <Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/> <Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/> <Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/> <Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>

View File

@@ -17,5 +17,7 @@
<CustomAction Id="SetPropertyServiceStop" DllEntry="SetPropertyFromConfig" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/> <CustomAction Id="SetPropertyServiceStop" DllEntry="SetPropertyFromConfig" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddRegSoftwareSASGeneration" DllEntry="AddRegSoftwareSASGeneration" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/> <CustomAction Id="AddRegSoftwareSASGeneration" DllEntry="AddRegSoftwareSASGeneration" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveAmyuniIdd" DllEntry="RemoveAmyuniIdd" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/> <CustomAction Id="RemoveAmyuniIdd" DllEntry="RemoveAmyuniIdd" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="InstallPrinter" DllEntry="InstallPrinter" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="UninstallPrinter" DllEntry="UninstallPrinter" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
</Fragment> </Fragment>
</Wix> </Wix>

View File

@@ -14,6 +14,7 @@
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property> <Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="PRINTER" Value="1" Secure="yes"></Property>
<!-- These properties get set from either the command line, bundle or registry value, <!-- These properties get set from either the command line, bundle or registry value,
if set they update the properties above with their value. --> if set they update the properties above with their value. -->
@@ -23,6 +24,9 @@
<Property Id="CREATEDESKTOPSHORTCUTS" Secure="yes"> <Property Id="CREATEDESKTOPSHORTCUTS" Secure="yes">
<RegistrySearch Id="CreateDesktopShortcutsSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="DESKTOPSHORTCUTS" Type="raw" /> <RegistrySearch Id="CreateDesktopShortcutsSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="DESKTOPSHORTCUTS" Type="raw" />
</Property> </Property>
<Property Id="INSTALLPRINTER" Secure="yes">
<RegistrySearch Id="InstallPrinterSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="PRINTER" Type="raw" />
</Property>
<!-- Component that persists the property values to the registry so they are available during an upgrade/modify --> <!-- Component that persists the property values to the registry so they are available during an upgrade/modify -->
<DirectoryRef Id="INSTALLFOLDER_INNER"> <DirectoryRef Id="INSTALLFOLDER_INNER">
@@ -46,6 +50,16 @@
<RegistryValue Type="string" Name="DESKTOPSHORTCUTS" Value="0" /> <RegistryValue Type="string" Name="DESKTOPSHORTCUTS" Value="0" />
</RegistryKey> </RegistryKey>
</Component> </Component>
<Component Id="Product.Registry.PersistedPrinterProperties1" Guid="AF617116-2502-EB3D-5B52-B47AA89EB4B0" Condition="PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="PRINTER" Value="1" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedPrinterProperties0" Guid="51F944D3-AAEB-F167-03A1-081A38E9468A" Condition="NOT (PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;)">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="PRINTER" Value="0" />
</RegistryKey>
</Component>
</DirectoryRef> </DirectoryRef>
<!-- If a property value has been passed via the command line (which includes when set from the bundle), the registry search will <!-- If a property value has been passed via the command line (which includes when set from the bundle), the registry search will
@@ -53,14 +67,17 @@
is performed so they can be restored after the registry search is complete --> is performed so they can be restored after the registry search is complete -->
<SetProperty Id="SavedStartMenuShortcutsCmdLineValue" Value="[CREATESTARTMENUSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS" /> <SetProperty Id="SavedStartMenuShortcutsCmdLineValue" Value="[CREATESTARTMENUSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS" />
<SetProperty Id="SavedDesktopShortcutsCmdLineValue" Value="[CREATEDESKTOPSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS" /> <SetProperty Id="SavedDesktopShortcutsCmdLineValue" Value="[CREATEDESKTOPSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS" />
<SetProperty Id="SavedPrinterCmdLineValue" Value="[INSTALLPRINTER]" Before="AppSearch" Sequence="first" Condition="INSTALLPRINTER" />
<!-- If a command line value was stored, restore it after the registry search has been performed --> <!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedStartMenuShortcutsValue" Id="CREATESTARTMENUSHORTCUTS" Value="[SavedStartMenuShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedStartMenuShortcutsCmdLineValue" /> <SetProperty Action="RestoreSavedStartMenuShortcutsValue" Id="CREATESTARTMENUSHORTCUTS" Value="[SavedStartMenuShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedStartMenuShortcutsCmdLineValue" />
<SetProperty Action="RestoreSavedDesktopShortcutsValue" Id="CREATEDESKTOPSHORTCUTS" Value="[SavedDesktopShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedDesktopShortcutsCmdLineValue" /> <SetProperty Action="RestoreSavedDesktopShortcutsValue" Id="CREATEDESKTOPSHORTCUTS" Value="[SavedDesktopShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedDesktopShortcutsCmdLineValue" />
<SetProperty Action="RestoreSavedPrinterValue" Id="INSTALLPRINTER" Value="[SavedPrinterCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedPrinterCmdLineValue" />
<!-- If a command line value or registry value was set, update the main properties with the value --> <!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" /> <SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" /> <SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="PRINTER" Value="" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</Fragment> </Fragment>
</Wix> </Wix>

View File

@@ -51,5 +51,6 @@ This file contains the declaration of all the localizable strings.
<String Id="MyInstallDirDlgDesktopShortcuts" Value="Create desktop icon" /> <String Id="MyInstallDirDlgDesktopShortcuts" Value="Create desktop icon" />
<String Id="MyInstallDirDlgStartMenuShortcuts" Value="Create start menu shortcuts" /> <String Id="MyInstallDirDlgStartMenuShortcuts" Value="Create start menu shortcuts" />
<String Id="MyInstallDirDlgPrinter" Value="Install RustDesk Printer" />
</WixLocalization> </WixLocalization>

View File

@@ -51,6 +51,8 @@
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" /> <ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" />
<ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties1" /> <ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties0" /> <ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties0" />
<ComponentRef Id="Product.Registry.PersistedPrinterProperties1" />
<ComponentRef Id="Product.Registry.PersistedPrinterProperties0" />
</Feature> </Feature>
<!--https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set--> <!--https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set-->

View File

@@ -25,6 +25,7 @@
<Control Id="ChkBoxStartMenuShortcuts" Type="CheckBox" X="20" Y="140" Width="290" Height="17" Property="STARTMENUSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgStartMenuShortcuts)" /> <Control Id="ChkBoxStartMenuShortcuts" Type="CheckBox" X="20" Y="140" Width="290" Height="17" Property="STARTMENUSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgStartMenuShortcuts)" />
<Control Id="ChkBoxDesktopShortcuts" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="DESKTOPSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgDesktopShortcuts)" /> <Control Id="ChkBoxDesktopShortcuts" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="DESKTOPSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgDesktopShortcuts)" />
<Control Id="ChkBoxInstallPrinter" Type="CheckBox" X="20" Y="180" Width="290" Height="17" Property="PRINTER" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgPrinter)" />
</Dialog> </Dialog>
</UI> </UI>
</Fragment> </Fragment>

View File

@@ -8,7 +8,9 @@ import argparse
import datetime import datetime
import subprocess import subprocess
import re import re
import platform
from pathlib import Path from pathlib import Path
from itertools import chain
import shutil import shutil
g_indent_unit = "\t" 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: with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines) 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 gen_upgrade_info():
def func(lines, index_start): def func(lines, index_start):
@@ -542,3 +555,4 @@ if __name__ == "__main__":
sys.exit(-1) sys.exit(-1)
replace_app_name_in_langs(args.app_name) replace_app_name_in_langs(args.app_name)
replace_app_name_in_custom_actions(args.app_name)

View File

@@ -49,6 +49,7 @@ use hbb_common::{
self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT,
READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS,
}, },
fs::JobType,
get_version_number, log, get_version_number, log,
message_proto::{option_message::BoolOption, *}, message_proto::{option_message::BoolOption, *},
protobuf::{Message as _, MessageField}, protobuf::{Message as _, MessageField},
@@ -3297,7 +3298,7 @@ pub enum Data {
Close, Close,
Login((String, String, String, bool)), Login((String, String, String, bool)),
Message(Message), Message(Message),
SendFiles((i32, String, String, i32, bool, bool)), SendFiles((i32, JobType, String, String, i32, bool, bool)),
RemoveDirAll((i32, String, bool, bool)), RemoveDirAll((i32, String, bool, bool)),
ConfirmDeleteFiles((i32, i32)), ConfirmDeleteFiles((i32, i32)),
SetNoConfirm(i32), SetNoConfirm(i32),
@@ -3311,7 +3312,7 @@ pub enum Data {
ToggleClipboardFile, ToggleClipboardFile,
NewRDP, NewRDP,
SetConfirmOverrideFile((i32, i32, bool, bool, bool)), SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
AddJob((i32, String, String, i32, bool, bool)), AddJob((i32, JobType, String, String, i32, bool, bool)),
ResumeJob((i32, bool)), ResumeJob((i32, bool)),
RecordScreen(bool), RecordScreen(bool),
ElevateDirect, ElevateDirect,

View File

@@ -7,6 +7,14 @@ pub trait FileManager: Interface {
fs::get_home_as_string() 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( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
@@ -98,6 +106,7 @@ pub trait FileManager: Interface {
fn send_files( fn send_files(
&self, &self,
id: i32, id: i32,
r#type: i32,
path: String, path: String,
to: String, to: String,
file_num: i32, file_num: i32,
@@ -106,6 +115,7 @@ pub trait FileManager: Interface {
) { ) {
self.send(Data::SendFiles(( self.send(Data::SendFiles((
id, id,
r#type.into(),
path, path,
to, to,
file_num, file_num,
@@ -117,6 +127,7 @@ pub trait FileManager: Interface {
fn add_job( fn add_job(
&self, &self,
id: i32, id: i32,
r#type: i32,
path: String, path: String,
to: String, to: String,
file_num: i32, file_num: i32,
@@ -125,6 +136,7 @@ pub trait FileManager: Interface {
) { ) {
self.send(Data::AddJob(( self.send(Data::AddJob((
id, id,
r#type.into(),
path, path,
to, to,
file_num, file_num,

View File

@@ -46,6 +46,7 @@ use std::{
collections::HashMap, collections::HashMap,
ffi::c_void, ffi::c_void,
num::NonZeroI64, num::NonZeroI64,
path::PathBuf,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, RwLock, Arc, RwLock,
@@ -549,13 +550,20 @@ impl<T: InvokeUiSession> Remote<T> {
} }
allow_err!(peer.send(&msg).await); 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); log::info!("send files, is remote {}", is_remote);
let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version);
if is_remote { if is_remote {
log::debug!("New job {}, write to {} from remote {}", id, to, path); 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( self.write_jobs.push(fs::TransferJob::new_write(
id, id,
r#type,
path.clone(), path.clone(),
to, to,
file_num, file_num,
@@ -565,14 +573,15 @@ impl<T: InvokeUiSession> Remote<T> {
od, od,
)); ));
allow_err!( 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 .await
); );
} else { } else {
match fs::TransferJob::new_read( match fs::TransferJob::new_read(
id, id,
r#type,
to.clone(), to.clone(),
path.clone(), fs::DataSource::FilePath(PathBuf::from(&path)),
file_num, file_num,
include_hidden, include_hidden,
is_remote, is_remote,
@@ -616,7 +625,7 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
} }
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); let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version);
if is_remote { if is_remote {
log::debug!( log::debug!(
@@ -627,8 +636,9 @@ impl<T: InvokeUiSession> Remote<T> {
); );
let mut job = fs::TransferJob::new_write( let mut job = fs::TransferJob::new_write(
id, id,
r#type,
path.clone(), path.clone(),
to, fs::DataSource::FilePath(PathBuf::from(&to)),
file_num, file_num,
include_hidden, include_hidden,
is_remote, is_remote,
@@ -640,8 +650,9 @@ impl<T: InvokeUiSession> Remote<T> {
} else { } else {
match fs::TransferJob::new_read( match fs::TransferJob::new_read(
id, id,
r#type,
to.clone(), to.clone(),
path.clone(), fs::DataSource::FilePath(PathBuf::from(&path)),
file_num, file_num,
include_hidden, include_hidden,
is_remote, is_remote,
@@ -679,6 +690,7 @@ impl<T: InvokeUiSession> Remote<T> {
allow_err!( allow_err!(
peer.send(&fs::new_send( peer.send(&fs::new_send(
id, id,
fs::JobType::Generic,
job.remote.clone(), job.remote.clone(),
job.file_num, job.file_num,
job.show_hidden job.show_hidden
@@ -688,17 +700,25 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} else { } else {
if let Some(job) = get_job(id, &mut self.read_jobs) { if let Some(job) = get_job(id, &mut self.read_jobs) {
job.is_last_job = false; match &job.data_source {
allow_err!( fs::DataSource::FilePath(p) => {
peer.send(&fs::new_receive( job.is_last_job = false;
id, allow_err!(
job.path.to_string_lossy().to_string(), peer.send(&fs::new_receive(
job.file_num, id,
job.files.clone(), p.to_string_lossy().to_string(),
job.total_size(), job.file_num,
)) job.files.clone(),
.await job.total_size(),
); ))
.await
);
}
fs::DataSource::MemoryCursor(_) => {
// unreachable!()
log::error!("Resume job with memory cursor");
}
}
} }
} }
} }
@@ -803,11 +823,10 @@ impl<T: InvokeUiSession> Remote<T> {
}); });
msg_out.set_file_action(file_action); msg_out.set_file_action(file_action);
allow_err!(peer.send(&msg_out).await); 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(); 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); self.remove_jobs.remove(&id);
} }
Data::RemoveDir((id, path)) => { Data::RemoveDir((id, path)) => {
@@ -1402,92 +1421,105 @@ impl<T: InvokeUiSession> Remote<T> {
if digest.is_upload { if digest.is_upload {
if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { 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) { if let Some(file) = job.files().get(digest.file_num as usize) {
let read_path = get_string(&job.join(&file.name)); if let fs::DataSource::FilePath(p) = &job.data_source {
let overwrite_strategy = job.default_overwrite_strategy(); let read_path =
if let Some(overwrite) = overwrite_strategy { get_string(&fs::TransferJob::join(p, &file.name));
let req = FileTransferSendConfirmRequest { let overwrite_strategy =
id: digest.id, job.default_overwrite_strategy();
file_num: digest.file_num, if let Some(overwrite) = overwrite_strategy {
union: Some(if overwrite { let req = FileTransferSendConfirmRequest {
file_transfer_send_confirm_request::Union::OffsetBlk(0) id: digest.id,
} else { file_num: digest.file_num,
file_transfer_send_confirm_request::Union::Skip( union: Some(if overwrite {
true, file_transfer_send_confirm_request::Union::OffsetBlk(0)
) } else {
}), file_transfer_send_confirm_request::Union::Skip(
..Default::default() true,
}; )
job.confirm(&req); }),
let msg = new_send_confirm(req); ..Default::default()
allow_err!(peer.send(&msg).await); };
} else { job.confirm(&req);
self.handler.override_file_confirm( let msg = new_send_confirm(req);
digest.id, allow_err!(peer.send(&msg).await);
digest.file_num, } else {
read_path, self.handler.override_file_confirm(
true, digest.id,
digest.is_identical, digest.file_num,
); read_path,
true,
digest.is_identical,
);
}
} }
} }
} }
} else { } else {
if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { 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) { if let Some(file) = job.files().get(digest.file_num as usize) {
let write_path = get_string(&job.join(&file.name)); if let fs::DataSource::FilePath(p) = &job.data_source {
let overwrite_strategy = job.default_overwrite_strategy(); let write_path =
match fs::is_write_need_confirmation(&write_path, &digest) { get_string(&fs::TransferJob::join(p, &file.name));
Ok(res) => match res { let overwrite_strategy =
DigestCheckResult::IsSame => { job.default_overwrite_strategy();
let req = FileTransferSendConfirmRequest { match fs::is_write_need_confirmation(
&write_path,
&digest,
) {
Ok(res) => match res {
DigestCheckResult::IsSame => {
let req = FileTransferSendConfirmRequest {
id: digest.id, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
union: Some(file_transfer_send_confirm_request::Union::Skip(true)), union: Some(file_transfer_send_confirm_request::Union::Skip(true)),
..Default::default() ..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); job.confirm(&req);
let msg = new_send_confirm(req); let msg = new_send_confirm(req);
allow_err!(peer.send(&msg).await); allow_err!(peer.send(&msg).await);
} else {
self.handler.override_file_confirm(
digest.id,
digest.file_num,
write_path,
false,
digest.is_identical,
);
} }
} DigestCheckResult::NeedConfirm(digest) => {
DigestCheckResult::NoSuchFile => { if let Some(overwrite) = overwrite_strategy
let req = FileTransferSendConfirmRequest { {
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, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
..Default::default() ..Default::default()
}; };
job.confirm(&req); job.confirm(&req);
let msg = new_send_confirm(req); let msg = new_send_confirm(req);
allow_err!(peer.send(&msg).await); 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<T: InvokeUiSession> Remote<T> {
if let Err(_err) = job.write(block).await { if let Err(_err) = job.write(block).await {
// to-do: add "skip" for writing job // 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)) => { Some(file_response::Union::Done(d)) => {
let mut err: Option<String> = None; let mut err: Option<String> = 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(); job.modify_time();
err = job.job_error(); 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)) => { Some(file_response::Union::Error(e)) => {
if let Some(_job) = fs::get_job(e.id, &mut self.write_jobs) { let job_type = fs::remove_job(e.id, &mut self.write_jobs)
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<T: InvokeUiSession> Remote<T> {
} }
} }
Some(message::Union::FileAction(action)) => match action.union { 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)) => { Some(file_action::Union::SendConfirm(c)) => {
if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) {
job.confirm(&c); job.confirm(&c);
@@ -2004,7 +2104,7 @@ impl<T: InvokeUiSession> Remote<T> {
async fn handle_cliprdr_msg( async fn handle_cliprdr_msg(
&mut self, &mut self,
clip: hbb_common::message_proto::Cliprdr, clip: hbb_common::message_proto::Cliprdr,
peer: &mut Stream, _peer: &mut Stream,
) { ) {
log::debug!("handling cliprdr msg from server peer"); log::debug!("handling cliprdr msg from server peer");
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
@@ -2074,7 +2174,7 @@ impl<T: InvokeUiSession> Remote<T> {
} }
if let Some(msg) = out_msg { if let Some(msg) = out_msg {
allow_err!(peer.send(&msg).await); allow_err!(_peer.send(&msg).await);
} }
} }
} }

View File

@@ -139,6 +139,10 @@ pub fn is_support_file_copy_paste_num(ver: i64) -> bool {
ver >= hbb_common::get_version_number("1.3.8") 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 { 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") hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9")
} }

View File

@@ -196,12 +196,11 @@ pub fn core_main() -> Option<Vec<String>> {
if config::is_disable_installation() { if config::is_disable_installation() {
return None; return None;
} }
let res = platform::install_me( #[cfg(not(windows))]
"desktopicon startmenu", let options = "desktopicon startmenu";
"".to_owned(), #[cfg(windows)]
true, let options = "desktopicon startmenu printer";
args.len() > 1, let res = platform::install_me(options, "".to_owned(), true, args.len() > 1);
);
let text = match res { let text = match res {
Ok(_) => translate("Installation Successful!".to_string()), Ok(_) => translate("Installation Successful!".to_string()),
Err(err) => { Err(err) => {

View File

@@ -1059,6 +1059,14 @@ impl InvokeUiSession for FlutterHandler {
fn update_record_status(&self, start: bool) { fn update_record_status(&self, start: bool) {
self.push_event("record_status", &[("start", &start.to_string())], &[]); 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 { impl FlutterHandler {

View File

@@ -624,7 +624,15 @@ pub fn session_send_files(
_is_dir: bool, _is_dir: bool,
) { ) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { 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, is_remote: bool,
) { ) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { 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) { pub fn main_set_home_dir(_home: String) {
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
{ {
@@ -2362,6 +2389,68 @@ pub fn main_audio_support_loopback() -> SyncReturn<bool> {
SyncReturn(is_surpport) SyncReturn(is_surpport)
} }
pub fn main_get_printer_names() -> SyncReturn<String> {
#[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<String> {
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")] #[cfg(target_os = "android")]
pub mod server_side { pub mod server_side {
use hbb_common::{config, log}; use hbb_common::{config, log};

View File

@@ -272,6 +272,8 @@ pub enum Data {
HwCodecConfig(Option<String>), HwCodecConfig(Option<String>),
RemoveTrustedDevices(Vec<Bytes>), RemoveTrustedDevices(Vec<Bytes>),
ClearTrustedDevices, ClearTrustedDevices,
#[cfg(all(target_os = "windows", feature = "flutter"))]
PrinterData(Vec<u8>),
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
@@ -461,7 +463,7 @@ async fn handle(data: Data, stream: &mut Connection) {
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|x| x.1 == crate::server::AuthConnType::Remote) .filter(|x| x.conn_type == crate::server::AuthConnType::Remote)
.count(); .count();
allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await);
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "没有摄像头"), ("No cameras", "没有摄像头"),
("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"),
("Use D3D rendering", "使用 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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Keine Kameras"), ("No cameras", "Keine Kameras"),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -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!"), ("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."), ("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."), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "No hay cámaras"), ("No cameras", "No hay cámaras"),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Aucune caméra"), ("No cameras", "Aucune caméra"),
("d3d_render_tip", "Sur certaines machines, lécran du contrôle à distance peut rester noir lors de lutilisation du rendu D3D."), ("d3d_render_tip", "Sur certaines machines, lécran du contrôle à distance peut rester noir lors de lutilisation du rendu D3D."),
("Use D3D rendering", "Utiliser le 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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Nessuna camera"), ("No cameras", "Nessuna camera"),
("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."),
("Use D3D rendering", "Usa rendering D3D"), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Nav kameru"), ("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."), ("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"), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Geen camera's"), ("No cameras", "Geen camera's"),
("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."),
("Use D3D rendering", "Gebruik D3D-rendering"), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Камера отсутствует"), ("No cameras", "Камера отсутствует"),
("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."),
("Use D3D rendering", "Использовать визуализацию 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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "Peruna càmera"), ("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"), ("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"), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", "沒有鏡頭"), ("No cameras", "沒有鏡頭"),
("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"),
("Use D3D rendering", "使用 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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No cameras", ""), ("No cameras", ""),
("d3d_render_tip", ""), ("d3d_render_tip", ""),
("Use D3D rendering", ""), ("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(); ].iter().cloned().collect();
} }

View File

@@ -1,6 +1,8 @@
#include <windows.h> #include <windows.h>
#include <wtsapi32.h> #include <wtsapi32.h>
#include <tlhelp32.h> #include <tlhelp32.h>
#include <comdef.h>
#include <xpsprint.h>
#include <cstdio> #include <cstdio>
#include <cstdint> #include <cstdint>
#include <intrin.h> #include <intrin.h>
@@ -11,6 +13,7 @@
#include <versionhelpers.h> #include <versionhelpers.h>
#include <vector> #include <vector>
#include <sddl.h> #include <sddl.h>
#include <memory>
extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id);
@@ -859,4 +862,114 @@ extern "C"
return isRunning; return isRunning;
} }
} // end of extern "C" } // 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<int> coInitGuard(nullptr, [isCoInitializeOk](int *) {
if (isCoInitializeOk) CoUninitialize();
});
IXpsOMObjectFactory *xpsFactory = nullptr;
hr = CoCreateInstance(
__uuidof(XpsOMObjectFactory),
nullptr,
CLSCTX_INPROC_SERVER,
__uuidof(IXpsOMObjectFactory),
reinterpret_cast<LPVOID *>(&xpsFactory));
PRINT_XPS_CHECK_HR(hr, "Failed to create XPS object factory.");
std::shared_ptr<IXpsOMObjectFactory> 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<HANDLE> 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<IXpsPrintJobStream> jobStreamGuard(jobStream, [](IXpsPrintJobStream *jobStream) {
jobStream->Release();
});
BOOL jobOk = FALSE;
std::shared_ptr<IXpsPrintJob> 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)
}

View File

@@ -21,23 +21,22 @@ use std::{
fs, fs,
io::{self, prelude::*}, io::{self, prelude::*},
mem, mem,
os::windows::process::CommandExt, os::{raw::c_ulong, windows::process::CommandExt},
path::*, path::*,
ptr::null_mut, ptr::null_mut,
sync::{atomic::Ordering, Arc, Mutex}, sync::{atomic::Ordering, Arc, Mutex},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use wallpaper; use wallpaper;
#[cfg(not(debug_assertions))]
use winapi::um::libloaderapi::{LoadLibraryExW, LOAD_LIBRARY_SEARCH_USER_DIRS};
use winapi::{ use winapi::{
ctypes::c_void, ctypes::c_void,
shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*},
um::{ um::{
errhandlingapi::GetLastError, errhandlingapi::GetLastError,
handleapi::CloseHandle, handleapi::CloseHandle,
libloaderapi::{ libloaderapi::{GetProcAddress, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32},
GetProcAddress, LoadLibraryExA, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32,
LOAD_LIBRARY_SEARCH_USER_DIRS,
},
minwinbase::STILL_ACTIVE, minwinbase::STILL_ACTIVE,
processthreadsapi::{ processthreadsapi::{
GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess,
@@ -54,6 +53,10 @@ use winapi::{
TOKEN_ELEVATION, TOKEN_QUERY, TOKEN_ELEVATION, TOKEN_QUERY,
}, },
winreg::HKEY_CURRENT_USER, winreg::HKEY_CURRENT_USER,
winspool::{
EnumPrintersW, GetDefaultPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL,
PRINTER_INFO_1W,
},
winuser::*, 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_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS";
const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS";
const REG_NAME_INSTALL_PRINTER: &str = "PRINTER";
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> { pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
unsafe { unsafe {
@@ -1011,6 +1015,10 @@ pub fn get_install_options() -> String {
if let Some(start_menu_shortcuts) = start_menu_shortcuts { if let Some(start_menu_shortcuts) = start_menu_shortcuts {
opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, 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()) serde_json::to_string(&opts).unwrap_or("{}".to_owned())
} }
@@ -1136,6 +1144,7 @@ fn get_after_install(
exe: &str, exe: &str,
reg_value_start_menu_shortcuts: Option<String>, reg_value_start_menu_shortcuts: Option<String>,
reg_value_desktop_shortcuts: Option<String>, reg_value_desktop_shortcuts: Option<String>,
reg_value_printer: Option<String>,
) -> String { ) -> String {
let app_name = crate::get_app_name(); let app_name = crate::get_app_name();
let ext = app_name.to_lowercase(); let ext = app_name.to_lowercase();
@@ -1159,12 +1168,20 @@ fn get_after_install(
) )
}) })
.unwrap_or_default(); .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!(" format!("
chcp 65001 chcp 65001
reg add HKEY_CLASSES_ROOT\\.{ext} /f reg add HKEY_CLASSES_ROOT\\.{ext} /f
{desktop_shortcuts} {desktop_shortcuts}
{start_menu_shortcuts} {start_menu_shortcuts}
{reg_printer}
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f 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}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\"
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f
@@ -1249,6 +1266,7 @@ oLink.Save
let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?;
let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_desktop_shortcuts = "0".to_owned();
let mut reg_value_start_menu_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(); let mut shortcuts = Default::default();
if options.contains("desktopicon") { if options.contains("desktopicon") {
shortcuts = format!( shortcuts = format!(
@@ -1268,6 +1286,10 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
); );
reg_value_start_menu_shortcuts = "1".to_owned(); 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 meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
let size = meta.len() / 1024; let size = meta.len() / 1024;
@@ -1338,7 +1360,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
after_install = get_after_install( after_install = get_after_install(
&exe, &exe,
Some(reg_value_start_menu_shortcuts), 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 { "" }, sleep = if debug { "timeout 300" } else { "" },
dels = if debug { "" } else { &dels }, dels = if debug { "" } else { &dels },
@@ -1346,13 +1369,22 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
import_config = get_import_config(&exe), import_config = get_import_config(&exe),
); );
run_cmds(cmds, debug, "install")?; run_cmds(cmds, debug, "install")?;
if install_printer {
allow_err!(remote_printer::install_update_printer(
&crate::get_app_name()
));
}
run_after_run_cmds(silent); run_after_run_cmds(silent);
Ok(()) Ok(())
} }
pub fn run_after_install() -> ResultType<()> { pub fn run_after_install() -> ResultType<()> {
let (_, _, _, exe) = get_install_info(); 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<()> { 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<()> { 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") 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(); *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 { fn set_safe_load_dll() -> bool {
if !unsafe { set_default_dll_directories() } { if !unsafe { set_default_dll_directories() } {
return false; 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 // https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories
#[cfg(not(debug_assertions))]
unsafe fn set_default_dll_directories() -> bool { unsafe fn set_default_dll_directories() -> bool {
let module = LoadLibraryExW( let module = LoadLibraryExW(
wide_string("Kernel32.dll").as_ptr(), wide_string("Kernel32.dll").as_ptr(),
@@ -2728,3 +2773,119 @@ pub mod reg_display_settings {
} }
} }
} }
pub fn get_printer_names() -> ResultType<Vec<String>> {
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<String>, data: Vec<u8>) -> 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(())
}

View File

@@ -70,6 +70,9 @@ mod service;
mod video_qos; mod video_qos;
pub mod video_service; pub mod video_service;
#[cfg(all(target_os = "windows", feature = "flutter"))]
pub mod printer_service;
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>; pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
type ConnMap = HashMap<i32, ConnInner>; type ConnMap = HashMap<i32, ConnInner>;
@@ -129,6 +132,20 @@ pub fn new() -> ServerPtr {
server.add_service(Box::new(input_service::new_window_focus())); 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)) Arc::new(RwLock::new(server))
} }

View File

@@ -28,7 +28,7 @@ use hbb_common::platform::linux::run_cmds;
use hbb_common::protobuf::EnumOrUnknown; use hbb_common::protobuf::EnumOrUnknown;
use hbb_common::{ use hbb_common::{
config::{self, keys, Config, TrustedDevice}, config::{self, keys, Config, TrustedDevice},
fs::{self, can_enable_overwrite_detection}, fs::{self, can_enable_overwrite_detection, JobType},
futures::{SinkExt, StreamExt}, futures::{SinkExt, StreamExt},
get_time, get_version_number, get_time, get_version_number,
message_proto::{option_message::BoolOption, permission_info::Permission}, message_proto::{option_message::BoolOption, permission_info::Permission},
@@ -67,7 +67,7 @@ lazy_static::lazy_static! {
static ref LOGIN_FAILURES: [Arc::<Mutex<HashMap<String, (i32, i32, i32)>>>; 2] = Default::default(); static ref LOGIN_FAILURES: [Arc::<Mutex<HashMap<String, (i32, i32, i32)>>>; 2] = Default::default();
static ref SESSIONS: Arc::<Mutex<HashMap<SessionKey, Session>>> = Default::default(); static ref SESSIONS: Arc::<Mutex<HashMap<SessionKey, Session>>> = Default::default();
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default(); static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
pub static ref AUTHED_CONNS: Arc::<Mutex<Vec<(i32, AuthConnType, SessionKey)>>> = Default::default(); pub static ref AUTHED_CONNS: Arc::<Mutex<Vec<AuthedConn>>> = Default::default();
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread())); static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread()));
} }
@@ -245,6 +245,8 @@ pub struct Connection {
follow_remote_cursor: bool, follow_remote_cursor: bool,
follow_remote_window: bool, follow_remote_window: bool,
multi_ui_session: bool, multi_ui_session: bool,
tx_from_authed: mpsc::UnboundedSender<ipc::Data>,
printer_data: Vec<(Instant, String, Vec<u8>)>,
} }
impl ConnInner { impl ConnInner {
@@ -309,6 +311,7 @@ impl Connection {
let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
let (tx_input, _rx_input) = std_mpsc::channel(); let (tx_input, _rx_input) = std_mpsc::channel();
let (tx_from_authed, mut rx_from_authed) = mpsc::unbounded_channel::<ipc::Data>();
let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver();
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1);
@@ -396,6 +399,8 @@ impl Connection {
delayed_read_dir: None, delayed_read_dir: None,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
retina: Retina::default(), retina: Retina::default(),
tx_from_authed,
printer_data: Vec::new(),
}; };
let addr = hbb_common::try_into_v4(addr); let addr = hbb_common::try_into_v4(addr);
if !conn.on_open(addr).await { if !conn.on_open(addr).await {
@@ -758,6 +763,19 @@ impl Connection {
break; 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() => { _ = second_timer.tick() => {
#[cfg(windows)] #[cfg(windows)]
conn.portable_check(); conn.portable_check();
@@ -1104,6 +1122,22 @@ impl Connection {
}); });
} }
fn get_files_for_audit(job_type: fs::JobType, mut files: Vec<FileEntry>) -> 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( fn post_file_audit(
&self, &self,
r#type: FileAuditType, r#type: FileAuditType,
@@ -1212,6 +1246,8 @@ impl Connection {
self.inner.id(), self.inner.id(),
auth_conn_type, auth_conn_type,
self.session_key(), self.session_key(),
self.tx_from_authed.clone(),
self.lr.clone(),
)); ));
self.session_last_recv_time = SESSIONS self.session_last_recv_time = SESSIONS
.lock() .lock()
@@ -2318,7 +2354,15 @@ impl Connection {
} }
} }
Some(message::Union::FileAction(fa)) => { 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 self.delayed_read_dir.is_some() {
if let Some(file_action::Union::ReadDir(rd)) = fa.union { if let Some(file_action::Union::ReadDir(rd)) = fa.union {
self.delayed_read_dir = Some((rd.path, rd.include_hidden)); self.delayed_read_dir = Some((rd.path, rd.include_hidden));
@@ -2375,10 +2419,32 @@ impl Connection {
&self.lr.version, &self.lr.version,
)); ));
let path = s.path.clone(); 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( match fs::TransferJob::new_read(
id, id,
r#type,
"".to_string(), "".to_string(),
path.clone(), data_source,
s.file_num, s.file_num,
s.include_hidden, s.include_hidden,
false, false,
@@ -2390,19 +2456,21 @@ impl Connection {
Ok(mut job) => { Ok(mut job) => {
self.send(fs::new_dir(id, path, job.files().to_vec())) self.send(fs::new_dir(id, path, job.files().to_vec()))
.await; .await;
let mut files = job.files().to_owned(); let files = job.files().to_owned();
job.is_remote = true; job.is_remote = true;
job.conn_id = self.inner.id(); job.conn_id = self.inner.id();
let job_type = job.r#type;
self.read_jobs.push(job); self.read_jobs.push(job);
self.file_timer = self.file_timer =
crate::rustdesk_interval(time::interval(MILLI1)); crate::rustdesk_interval(time::interval(MILLI1));
self.post_file_audit( self.post_file_audit(
FileAuditType::RemoteSend, FileAuditType::RemoteSend,
&s.path, if job_type == fs::JobType::Printer {
files "Remote print"
.drain(..) } else {
.map(|f| (f.name, f.size as _)) &s.path
.collect(), },
Self::get_files_for_audit(job_type, files),
json!({}), json!({}),
); );
} }
@@ -2433,11 +2501,7 @@ impl Connection {
self.post_file_audit( self.post_file_audit(
FileAuditType::RemoteReceive, FileAuditType::RemoteReceive,
&r.path, &r.path,
r.files Self::get_files_for_audit(fs::JobType::Generic, r.files),
.to_vec()
.drain(..)
.map(|f| (f.name, f.size as _))
.collect(),
json!({}), json!({}),
); );
self.file_transferred = true; self.file_transferred = true;
@@ -2476,13 +2540,12 @@ impl Connection {
} }
Some(file_action::Union::Cancel(c)) => { Some(file_action::Union::Cancel(c)) => {
self.send_fs(ipc::FS::CancelWrite { id: c.id }); 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(( self.send_to_cm(ipc::Data::FileTransferLog((
"transfer".to_string(), "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)) => { Some(file_action::Union::SendConfirm(r)) => {
if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { 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) { fn try_empty_file_clipboard(&mut self) {
try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); 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<u8>) {
// 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) { 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 tx
} }
#[cfg(all(target_os = "windows", feature = "flutter"))]
pub fn on_printer_data(data: Vec<u8>) {
crate::server::AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|c| c.printer)
.next()
.map(|c| {
c.sender.send(Data::PrinterData(data)).ok();
});
}
#[cfg(windows)] #[cfg(windows)]
pub struct PortableState { pub struct PortableState {
pub last_uac: bool, 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<Data>,
pub printer: bool,
}
mod raii { mod raii {
// ALIVE_CONNS: all connections, including unauthorized connections // ALIVE_CONNS: all connections, including unauthorized connections
// AUTHED_CONNS: all authorized connections // AUTHED_CONNS: all authorized connections
@@ -4127,11 +4237,23 @@ mod raii {
pub struct AuthedConnID(i32, AuthConnType); pub struct AuthedConnID(i32, AuthConnType);
impl AuthedConnID { impl AuthedConnID {
pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { pub fn new(
AUTHED_CONNS conn_id: i32,
.lock() conn_type: AuthConnType,
.unwrap() session_key: SessionKey,
.push((conn_id, conn_type, session_key)); sender: mpsc::UnboundedSender<Data>,
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(); Self::check_wake_lock();
use std::sync::Once; use std::sync::Once;
static _ONCE: Once = Once::new(); static _ONCE: Once = Once::new();
@@ -4153,7 +4275,7 @@ mod raii {
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 == AuthConnType::Remote) .filter(|c| c.conn_type == AuthConnType::Remote)
.count(); .count();
allow_err!(WAKELOCK_SENDER allow_err!(WAKELOCK_SENDER
.lock() .lock()
@@ -4166,7 +4288,7 @@ mod raii {
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 != AuthConnType::PortForward) .filter(|c| c.conn_type != AuthConnType::PortForward)
.count() .count()
} }
@@ -4179,16 +4301,16 @@ mod raii {
.lock() .lock()
.unwrap() .unwrap()
.iter() .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 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 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 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. // 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 let another_remote = AUTHED_CONNS.lock().unwrap().iter().any(|c| {
.lock() c.conn_id != conn_id
.unwrap() && c.session_key == key
.iter() && c.conn_type == AuthConnType::Remote
.any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); });
if is_remote || !another_remote { if is_remote || !another_remote {
lock.remove(&key); lock.remove(&key);
log::info!("remove session"); log::info!("remove session");
@@ -4256,12 +4378,12 @@ mod raii {
.unwrap() .unwrap()
.on_connection_close(self.0); .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 let remote_count = AUTHED_CONNS
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 == AuthConnType::Remote) .filter(|c| c.conn_type == AuthConnType::Remote)
.count(); .count();
if remote_count == 0 { if remote_count == 0 {
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "windows", target_os = "linux"))]

View File

@@ -505,7 +505,7 @@ pub fn try_stop_record_cursor_pos() {
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 == AuthConnType::Remote) .filter(|c| c.conn_type == AuthConnType::Remote)
.count(); .count();
if remote_count > 0 { if remote_count > 0 {
return; return;

View File

@@ -812,7 +812,7 @@ pub mod client {
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 == crate::server::AuthConnType::Remote) .filter(|c| c.conn_type == crate::server::AuthConnType::Remote)
.count(); .count();
stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok();
} }

View File

@@ -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<Library>,
$($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<Mutex<LibWrapper>> = Default::default();
}
fn get_lib_name() -> ResultType<String> {
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<Vec<u8>> {
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(())
}

View File

@@ -69,8 +69,6 @@ function getExt(name) {
return ""; return "";
} }
var jobIdCounter = 1;
class JobTable: Reactor.Component { class JobTable: Reactor.Component {
this var jobs = []; this var jobs = [];
this var job_map = {}; this var job_map = {};
@@ -126,8 +124,7 @@ class JobTable: Reactor.Component {
} }
if (!to) return; if (!to) return;
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
var id = jobIdCounter; var id = handler.get_next_job_id();
jobIdCounter += 1;
this.jobs.push({ type: "transfer", this.jobs.push({ type: "transfer",
id: id, path: path, to: to, id: id, path: path, to: to,
include_hidden: show_hidden, include_hidden: show_hidden,
@@ -135,7 +132,7 @@ class JobTable: Reactor.Component {
is_last: false is_last: false
}); });
this.job_map[id] = this.jobs[this.jobs.length - 1]; 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; var self = this;
self.timer(30ms, function() { self.update(); }); 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 }; is_remote: is_remote, is_last: true, file_num: file_num };
this.jobs.push(job); this.jobs.push(job);
this.job_map[id] = this.jobs[this.jobs.length - 1]; this.job_map[id] = this.jobs[this.jobs.length - 1];
jobIdCounter = id + 1; handler.update_next_job_id(id + 1);
handler.add_job(id, path, to, file_num, show_hidden, is_remote); handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote);
stdout.println(JSON.stringify(job)); stdout.println(JSON.stringify(job));
} }
@@ -162,16 +159,14 @@ class JobTable: Reactor.Component {
} }
function addDelDir(path, is_remote) { function addDelDir(path, is_remote) {
var id = jobIdCounter; var id = handler.get_next_job_id();
jobIdCounter += 1;
this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); 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.job_map[id] = this.jobs[this.jobs.length - 1];
this.update(); this.update();
} }
function addDelFile(path, is_remote) { function addDelFile(path, is_remote) {
var id = jobIdCounter; var id = handler.get_next_job_id();
jobIdCounter += 1;
this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); 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.job_map[id] = this.jobs[this.jobs.length - 1];
this.update(); this.update();
@@ -552,9 +547,9 @@ class FolderView : Reactor.Component {
return; return;
} }
var path = me.joinPath(name); var path = me.joinPath(name);
handler.create_dir(jobIdCounter, path, me.is_remote); var id = handler.get_next_job_id();
create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; handler.create_dir(id, path, me.is_remote);
jobIdCounter += 1; create_dir_jobs[id] = { is_remote: me.is_remote, path: path };
}); });
} }

View File

@@ -317,6 +317,9 @@ class MsgboxComponent: Reactor.Component {
if (this.type == "multiple-sessions-nocancel") { if (this.type == "multiple-sessions-nocancel") {
values.sid = (this.$$(select))[0].value; values.sid = (this.$$(select))[0].value;
} }
if (this.type == "remote-printer-selector") {
values.name = (this.$$(select))[0].value;
}
return values; return values;
} }

41
src/ui/printer.tis Normal file
View File

@@ -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", <PrinterComponent names={names} />, "", 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 <div>
<div>{translate("print-incoming-job-confirm-tip")}</div>
<div style="margin-top: 1em;" />
<div>
<select style="width: 450; margin: 1em 0; font-size: 1.15em;">
{this.names.map(function(name) {
return <option value={name}>{name}</option>;
})}
</select>
</div>
<div style="margin-top: 1em;" />
</div>;
}
}

View File

@@ -15,6 +15,7 @@
include "port_forward.tis"; include "port_forward.tis";
include "grid.tis"; include "grid.tis";
include "header.tis"; include "header.tis";
include "printer.tis";
</script> </script>
</head> </head>
<header> <header>

View File

@@ -379,6 +379,10 @@ impl InvokeUiSession for SciterHandler {
fn update_record_status(&self, start: bool) { fn update_record_status(&self, start: bool) {
self.call("updateRecordStatus", &make_args!(start)); 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<SciterHandler>); pub struct SciterSession(Session<SciterHandler>);
@@ -491,6 +495,8 @@ impl sciter::EventHandler for SciterSession {
fn get_chatbox(); fn get_chatbox();
fn get_icon(); fn get_icon();
fn get_home_dir(); fn get_home_dir();
fn get_next_job_id();
fn update_next_job_id(i32);
fn read_dir(String, bool); fn read_dir(String, bool);
fn remove_dir(i32, String, bool); fn remove_dir(i32, String, bool);
fn create_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 confirm_delete_files(i32, i32);
fn set_no_confirm(i32); fn set_no_confirm(i32);
fn cancel_job(i32); fn cancel_job(i32);
fn send_files(i32, String, String, i32, bool, bool); fn send_files(i32, i32, String, String, i32, bool, bool);
fn add_job(i32, String, String, i32, bool, bool); fn add_job(i32, i32, String, String, i32, bool, bool);
fn resume_job(i32, bool); fn resume_job(i32, bool);
fn get_platform(bool); fn get_platform(bool);
fn get_path_sep(bool); fn get_path_sep(bool);
@@ -541,6 +547,8 @@ impl sciter::EventHandler for SciterSession {
fn set_selected_windows_session_id(String); fn set_selected_windows_session_id(String);
fn is_recording(); fn is_recording();
fn has_file_clipboard(); 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 { fn version_cmp(&self, v1: String, v2: String) -> i32 {
(hbb_common::get_version_number(&v1) - hbb_common::get_version_number(&v2)) as 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<String> = 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<FileEntry>, only_count: bool) -> Value { pub fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {

View File

@@ -744,6 +744,8 @@ async fn handle_fs(
tx: &UnboundedSender<Data>, tx: &UnboundedSender<Data>,
tx_log: Option<&UnboundedSender<String>>, tx_log: Option<&UnboundedSender<String>>,
) { ) {
use std::path::PathBuf;
use hbb_common::fs::serialize_transfer_job; use hbb_common::fs::serialize_transfer_job;
match fs { match fs {
@@ -785,8 +787,9 @@ async fn handle_fs(
// dummy remote, show_hidden, is_remote // dummy remote, show_hidden, is_remote
let mut job = fs::TransferJob::new_write( let mut job = fs::TransferJob::new_write(
id, id,
fs::JobType::Generic,
"".to_string(), "".to_string(),
path, fs::DataSource::FilePath(PathBuf::from(&path)),
file_num, file_num,
false, false,
false, false,
@@ -805,27 +808,24 @@ async fn handle_fs(
write_jobs.push(job); write_jobs.push(job);
} }
ipc::FS::CancelWrite { id } => { 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(); job.remove_download_file();
tx_log.map(|tx: &UnboundedSender<String>| { tx_log.map(|tx: &UnboundedSender<String>| {
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 } => { 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(); job.modify_time();
send_raw(fs::new_done(id, file_num), tx); send_raw(fs::new_done(id, file_num), tx);
tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, ""))); tx_log.map(|tx| tx.send(serialize_transfer_job(&job, true, false, "")));
fs::remove_job(id, write_jobs);
} }
} }
ipc::FS::WriteError { id, file_num, err } => { ipc::FS::WriteError { id, file_num, err } => {
if let Some(job) = fs::get_job(id, write_jobs) { if let Some(job) = fs::remove_job(id, write_jobs) {
tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err))); tx_log.map(|tx| tx.send(serialize_transfer_job(&job, false, false, &err)));
send_raw(fs::new_error(job.id(), err, file_num), tx); send_raw(fs::new_error(job.id(), err, file_num), tx);
fs::remove_job(job.id(), write_jobs);
} }
} }
ipc::FS::WriteBlock { ipc::FS::WriteBlock {
@@ -871,32 +871,34 @@ async fn handle_fs(
..Default::default() ..Default::default()
}; };
if let Some(file) = job.files().get(file_num as usize) { if let Some(file) = job.files().get(file_num as usize) {
let path = get_string(&job.join(&file.name)); if let fs::DataSource::FilePath(p) = &job.data_source {
match is_write_need_confirmation(&path, &digest) { let path = get_string(&fs::TransferJob::join(p, &file.name));
Ok(digest_result) => { match is_write_need_confirmation(&path, &digest) {
match digest_result { Ok(digest_result) => {
DigestCheckResult::IsSame => { match digest_result {
req.set_skip(true); DigestCheckResult::IsSame => {
let msg_out = new_send_confirm(req); req.set_skip(true);
send_raw(msg_out, &tx); 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 DigestCheckResult::NeedConfirm(mut digest) => {
digest.is_upload = is_upload; // upload to server, but server has the same file, request
let mut msg_out = Message::new(); digest.is_upload = is_upload;
let mut fr = FileResponse::new(); let mut msg_out = Message::new();
fr.set_digest(digest); let mut fr = FileResponse::new();
msg_out.set_file_response(fr); fr.set_digest(digest);
send_raw(msg_out, &tx); msg_out.set_file_response(fr);
} send_raw(msg_out, &tx);
DigestCheckResult::NoSuchFile => { }
let msg_out = new_send_confirm(req); DigestCheckResult::NoSuchFile => {
send_raw(msg_out, &tx); let msg_out = new_send_confirm(req);
send_raw(msg_out, &tx);
}
} }
} }
} Err(err) => {
Err(err) => { send_raw(fs::new_error(id, err, file_num), &tx);
send_raw(fs::new_error(id, err, file_num), &tx); }
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More