Fix/preserve deleted printer (#16500)

* fix(windows): preserve manually deleted printers

Signed-off-by: fufesou <[email protected]>

* fix(windows): distinguish printer detection failures

Signed-off-by: fufesou <[email protected]>

* fix(windows): skip printer detection when disabled

Signed-off-by: fufesou <[email protected]>

* fix(flutter): load printer status asynchronously

Signed-off-by: fufesou <[email protected]>

* refactor(flutter): remove printer status mounted checks

Signed-off-by: fufesou <[email protected]>

* fix(msi): preserve deleted printers in direct upgrades

Signed-off-by: fufesou <[email protected]>

* fix(windows): preserve deleted printers in silent EXE installs

Signed-off-by: fufesou <[email protected]>

* fix(windows): skip printer installation when detection fails

Signed-off-by: fufesou <[email protected]>

* fix(installer): default printer off when detection fails

Signed-off-by: fufesou <[email protected]>

* fix(printer): limit checked enumeration to presence queries

Signed-off-by: fufesou <[email protected]>

* fix(installer): allow cancellation during printer detection

Inline MSI update command construction without changing its arguments.

Signed-off-by: fufesou <[email protected]>

* fix(flutter): time out installer printer detection

Signed-off-by: fufesou <[email protected]>

* chore

Signed-off-by: fufesou <[email protected]>

* test(windows): cover multilingual install app names

Signed-off-by: fufesou <[email protected]>

---------

Signed-off-by: fufesou <[email protected]>
This commit is contained in:
fufesou
2026-10-10 00:21:17 +08:00
committed by GitHub
parent 0bc2473ffe
commit 001b0e9617
6 changed files with 186 additions and 22 deletions
+30 -2
View File
@@ -62,12 +62,15 @@ class _InstallPageBody extends StatefulWidget {
class _InstallPageBodyState extends State<_InstallPageBody>
with WindowListener {
static const _printerStatusTimeout = Duration(seconds: 5);
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
final RxBool printer = false.obs;
final RxBool showProgress = false.obs;
final RxBool btnEnabled = true.obs;
final RxBool cancelEnabled = true.obs;
// todo move to theme.
final buttonStyle = OutlinedButton.styleFrom(
@@ -83,10 +86,33 @@ class _InstallPageBodyState extends State<_InstallPageBody>
printer.value = installOptions['PRINTER'] == '1';
}
Future<void> _loadPrinterStatus() async {
btnEnabled.value = false;
showProgress.value = true;
try {
final status = await bind
.mainGetCommon(key: 'is-printer-installed')
.timeout(_printerStatusTimeout);
printer.value = status == 'true';
if (status != 'true' && status != 'false') {
debugPrint('Failed to check printer installation status: $status');
}
} catch (e) {
printer.value = false;
debugPrint('Failed to check printer installation status: $e');
} finally {
showProgress.value = false;
btnEnabled.value = true;
}
}
@override
void initState() {
windowManager.addListener(this);
super.initState();
if (printer.value) {
_loadPrinterStatus();
}
}
@override
@@ -217,8 +243,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
() => OutlinedButton.icon(
icon: Icon(Icons.close_rounded, size: 16),
label: Text(translate('Cancel')),
onPressed:
btnEnabled.value ? () => windowManager.close() : null,
onPressed: cancelEnabled.value
? () => windowManager.close()
: null,
style: buttonStyle,
).marginOnly(right: 10),
),
@@ -253,6 +280,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
void install() {
do_install() {
btnEnabled.value = false;
cancelEnabled.value = false;
showProgress.value = true;
String args = '';
if (startmenu.value) args += ' startmenu';
+44 -15
View File
@@ -5,7 +5,7 @@ use winapi::{
shared::{
minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD},
ntdef::HANDLE,
winerror::ERROR_INVALID_PRINTER_NAME,
winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PRINTER_NAME},
},
um::winspool::{
AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW,
@@ -42,20 +42,49 @@ fn enum_local_printer(
#[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))
const PRINTER_INFO_LEVEL: DWORD = 1;
let mut needed = 0;
let mut returned = 0;
if enum_local_printer(
PRINTER_INFO_LEVEL,
null_mut(),
0,
&mut needed,
&mut returned,
) == FALSE
{
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) || needed == 0 {
bail!("Failed to query EnumPrintersW buffer size: {error}");
}
}
if needed == 0 {
return Ok(false);
}
let mut buffer = vec![0u8; needed as usize];
if enum_local_printer(
PRINTER_INFO_LEVEL,
buffer.as_mut_ptr(),
needed,
&mut needed,
&mut returned,
) == FALSE
{
bail!(
"Failed to call EnumPrintersW, error: {}",
io::Error::last_os_error()
);
}
let printers = buffer.as_ptr() as *const PRINTER_INFO_1W;
for index in 0..returned {
let printer = unsafe { &*printers.add(index as usize) };
if is_name_equal(name, printer.pName) {
return Ok(true);
}
}
Ok(false)
}
// Only return the first matched printer
+1
View File
@@ -14,3 +14,4 @@ EXPORTS
RemoveAmyuniIdd
InstallPrinter
UninstallPrinter
SetPrinterInstallDefault
+66
View File
@@ -3,6 +3,7 @@
#include <Windows.h>
#include <winspool.h>
#include <setupapi.h>
#include <strutil.h>
#include <memory>
#include <string>
#include <functional>
@@ -356,6 +357,42 @@ namespace RemotePrinter
return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned);
}
static HRESULT queryPrinterPresence(LPCWSTR name)
{
constexpr DWORD printerInfoLevel = 1;
DWORD needed = 0;
DWORD returned = 0;
if (!enumLocalPrinter(printerInfoLevel, NULL, 0, &needed, &returned))
{
const DWORD error = GetLastError();
if (error != ERROR_INSUFFICIENT_BUFFER || needed == 0)
{
return error == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(error);
}
}
if (needed == 0)
{
return S_FALSE;
}
std::vector<BYTE> buffer(needed);
if (!enumLocalPrinter(printerInfoLevel, buffer.data(), needed, &needed, &returned))
{
const DWORD error = GetLastError();
return error == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(error);
}
const auto printers = reinterpret_cast<const PRINTER_INFO_1W *>(buffer.data());
for (DWORD i = 0; i < returned; i++)
{
if (isNameEqual(name, printers[i].pName))
{
return S_OK;
}
}
return S_FALSE;
}
BOOL isPrinterAdded(LPCWSTR name)
{
auto onData = [name](const PRINTER_INFO_1W &info)
@@ -528,3 +565,32 @@ namespace RemotePrinter
WcaLog(LOGMSG_STANDARD, "Deleted the local port\n");
}
}
extern "C" UINT __stdcall SetPrinterInstallDefault(__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
LPWSTR appName = NULL;
hr = WcaInitialize(hInstall, "SetPrinterInstallDefault");
ExitOnFailure(hr, "Failed to initialize printer detection");
hr = WcaGetProperty(L"ProductName", &appName);
ExitOnFailure(hr, "Failed to get the printer's application name");
hr = RemotePrinter::queryPrinterPresence(RemotePrinter::printerNameOf(appName).c_str());
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to detect the existing printer (0x%08X); skipping printer installation.", static_cast<UINT>(hr));
}
if (hr != S_OK)
{
hr = WcaSetProperty(L"INSTALLPRINTER", L"0");
ExitOnFailure(hr, "Failed to disable printer installation");
WcaLog(LOGMSG_STANDARD, "The application's printer was not confirmed present; leaving printer installation disabled by default.");
}
LExit:
ReleaseStr(appName);
return WcaFinalize(SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE);
}
@@ -74,13 +74,22 @@
<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" />
<!-- Check before the old product removes its printer. firstSequence preserves later UI choices. -->
<CustomAction Id="SetPrinterInstallDefault" DllEntry="SetPrinterInstallDefault" Execute="firstSequence" Return="check" BinaryRef="Custom_Actions_Dll" />
<InstallUISequence>
<Custom Action="SetPrinterInstallDefault" After="RestoreSavedPrinterValue" Condition="WIX_UPGRADE_DETECTED AND NOT SavedPrinterCmdLineValue AND (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</InstallUISequence>
<InstallExecuteSequence>
<Custom Action="SetPrinterInstallDefault" After="RestoreSavedPrinterValue" Condition="WIX_UPGRADE_DETECTED AND NOT SavedPrinterCmdLineValue AND (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</InstallExecuteSequence>
<!-- 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="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" />
<!-- PRINTER defaults to empty now, so a saved or command-line INSTALLPRINTER=1
must explicitly enable the main PRINTER property. Non-truthy INSTALLPRINTER
values still clear PRINTER so upgrades preserve an explicit disabled choice. -->
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;" />
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="SetPrinterInstallDefault" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;" />
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</Fragment>
+35 -4
View File
@@ -1354,6 +1354,10 @@ pub fn get_install_options() -> String {
}
pub fn get_silent_install_options(printer_override: Option<bool>) -> &'static str {
if !is_win_10_or_greater() {
return "desktopicon startmenu";
}
let install_printer = match printer_override {
Some(override_value) => override_value,
None => {
@@ -1361,9 +1365,13 @@ pub fn get_silent_install_options(printer_override: Option<bool>) -> &'static st
let subkey = format!(".{}", app_name.to_lowercase());
let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER);
printer.as_deref() == Some("1")
&& remote_printer::is_rd_printer_installed(&app_name).unwrap_or_else(|err| {
log::warn!("Failed to check printer installation status: {err}; skipping printer installation");
false
})
}
};
if install_printer && is_win_10_or_greater() {
if install_printer {
"desktopicon startmenu printer"
} else {
"desktopicon startmenu"
@@ -3911,8 +3919,22 @@ pub fn update_to(file: &str) -> ResultType<()> {
// We need also to handle the command line parsing to find the tray processes.
pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
let quiet_args = if quiet { " /qn LAUNCH_TRAY_APP=N" } else { "" };
let cmds =
format!("chcp 65001 && msiexec /i \"{msi}\"{quiet_args} REBOOT=ReallySuppress /norestart");
let app_name = crate::get_app_name();
let subkey = format!(".{}", app_name.to_lowercase());
let printer_installed = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER).as_deref()
!= Some("0")
&& remote_printer::is_rd_printer_installed(&app_name).unwrap_or_else(|err| {
log::warn!(
"Failed to check printer installation status: {err}; skipping printer installation"
);
false
});
let printer_args = if printer_installed {
""
} else {
" INSTALLPRINTER=0"
};
let cmds = format!("chcp 65001 && msiexec /i \"{msi}\"{quiet_args}{printer_args} REBOOT=ReallySuppress /norestart");
run_cmds(cmds, false, "update-msi")?;
Ok(())
}
@@ -4846,7 +4868,16 @@ mod tests {
#[test]
fn install_app_names_enforce_ascii_command_safety() {
assert!(validate_install_app_name("RustDesk-Admin1").is_ok());
for app_name in ["", "RustDesk_Admin", "RustDesk&whoami", "RustDesk应用"] {
for app_name in [
"",
"RustDesk_Admin",
"RustDesk&whoami",
"RustDesk应用",
"RustDesk앱",
"RustDeskBüro",
"RustDeskFrançais",
"RustDeskアプリ",
] {
assert!(
validate_install_app_name(app_name).is_err(),
"unsafe application name was accepted: {app_name}"