使用 C# 通过 Windows 服务打印 PDF
PDF Print Through Windows Service with C#
我正在使用此代码在 windows 服务中使用 C# 在本地打印机上打印 PDF 文件。
Process process = new Process();
PrinterSettings printerSettings = new PrinterSettings();
if (!string.IsNullOrWhiteSpace(basePrint))
printerSettings.PrinterName = basePrint;
process.StartInfo.FileName = fileName;
process.StartInfo.Verb = "printto";
process.StartInfo.Arguments = "\"" + printerSettings.PrinterName + "\"";
process.Start();
process.WaitForInputIdle();
当我将用户设置为 运行 windows 服务时一切正常。
每当我在 LocalSystem 凭证下 运行 此代码时,我都会收到错误消息“没有与此操作关联的应用程序”,这通常表示我没有准备好处理打印的程序操作具有 .pdf 扩展名的文件。
我的问题是我确实有程序 (Foxit Reader) 来处理这个操作,这一点得到了证实,这个代码适用于服务上的特定用户集,而且我能够通过右键单击文件并选择打印选项将文件发送到打印机。
有什么我可以更改的,以便能够在没有特定用户的情况下从服务中在本地打印机上打印?
问题可能是 SYSTEM (LocalSystem) 帐户的用户界面功能有限,并且 shell 或 shell 扩展可能已被删除或禁用。动词是 shell 子系统的一项功能,具体来说,是 Explorer。
您可以手动调用该程序以查看是否属于这种情况,或者是否存在安全问题或缺少用户配置文件详细信息。
为此,您需要深入研究注册表,您会发现许多 shell 执行扩展动词都有命令行。
例如,查找 HKEY_CLASSES_ROOT.pdf\shell\printto\command 并使用该命令。
此外,您可以检查 SYSTEM 帐户是否可以访问此密钥和父密钥。 (很少见,但值得检查)
您也许可以执行您的工作代码,但使用当前活动的会话用户令牌(但没有活动会话这不应该工作)
For brevity, this code won't compile : you have to adapt and add some P/Invoke first.
您必须找到活动会话 ID。对于本地打开的会话,使用这个:
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern int WTSEnumerateSessions(
IntPtr hServer,
int Reserved,
int Version,
ref IntPtr ppSessionInfo,
ref int pCount);
然后搜索打开的session id:
var typeSessionInfo = typeof(WTSApi32.WTSSessionInfo);
var sizeSessionInfo = Marshal.SizeOf(typeSessionInfo);
var current = handleSessionInfo;
for (var i = 0; i < sessionCount; i++)
{
var sessionInfo = (WTSApi32.WTSSessionInfo)Marshal.PtrToStructure(current, typeSessionInfo);
current += sizeSessionInfo;
if (sessionInfo.State == WTSApi32.WTSConnectStateClass.WTSActive)
return sessionInfo.SessionID;
}
如果未找到,请搜索 rdp 会话:
[DllImport("kernel32.dll")]
public static extern uint WTSGetActiveConsoleSessionId();
得到一个令牌
private static IntPtr GetUserImpersonatedToken(uint activeSessionId)
{
if (!WTSApi32.WTSQueryUserToken(activeSessionId, out var handleImpersonationToken))
Win32Helper.RaiseInvalidOperation("WTSQueryUserToken");
try
{
return DuplicateToken(handleImpersonationToken, AdvApi32.TokenType.TokenPrimary);
}
finally
{
Kernel32.CloseHandle(handleImpersonationToken);
}
}
有了它,您可以在打开的用户会话中从本地系统服务执行 exe。
public static void ExecuteAsUserFromService(string appExeFullPath, uint activeSessionId, bool isVisible = false, string cmdLine = null, string workDir = null)
{
var tokenUser = GetUserImpersonatedToken(activeSessionId);
try
{
if (!AdvApi32.SetTokenInformation(tokenUser, AdvApi32.TokenInformationClass.TokenSessionId, ref activeSessionId, sizeof(UInt32)))
Win32Helper.RaiseInvalidOperation("SetTokenInformation");
ExecuteAsUser(tokenUser, appExeFullPath, isVisible, cmdLine, workDir);
}
finally
{
Kernel32.CloseHandle(tokenUser);
}
}
现在看看您是否可以调整您的代码以适应 CreateProcessAsUSer(...)
private static void ExecuteAsUser(IntPtr token, string appExeFullPath, bool isVisible, string cmdLine, string workDir)
{
PrepareExecute(appExeFullPath, isVisible, ref workDir, out var creationFlags, out var startInfo, out var procInfo);
try
{
startInfo.lpDesktop = "WinSta0\Default";
var processAttributes = new AdvApi32.SecurityAttributes
{
lpSecurityDescriptor = IntPtr.Zero
};
var threadAttributes = new AdvApi32.SecurityAttributes
{
lpSecurityDescriptor = IntPtr.Zero
};
if (!AdvApi32.CreateProcessAsUser(token,
appExeFullPath, // Application Name
cmdLine, // Command Line
ref processAttributes,
ref threadAttributes,
true,
creationFlags,
IntPtr.Zero,
workDir, // Working directory
ref startInfo,
out procInfo))
{
throw Win32Helper.RaiseInvalidOperation("CreateProcessAsUser");
}
}
finally
{
Kernel32.CloseHandle(procInfo.hThread);
Kernel32.CloseHandle(procInfo.hProcess);
}
}
希望此代码对您或其他人有用!
我最终使用 pdfium 来完成这项工作。使用该代码,即使 windows 服务在 LocalService 用户下 运行,PDF 文件也会正确发送到打印机。
PrinterSettings printerSettings = new PrinterSettings()
{
PrinterName = printerName,
Copies = 1
};
PageSettings pageSettings = new PageSettings(printerSettings)
{
Margins = new Margins(0, 0, 0, 0)
};
foreach (PaperSize paperSize in printerSettings.PaperSizes)
{
if (paperSize.PaperName == "A4")
{
pageSettings.PaperSize = paperSize;
break;
}
}
using (PdfDocument pdfDocument = PdfDocument.Load(filePath))
{
using (PrintDocument printDocument = pdfDocument.CreatePrintDocument())
{
printDocument.PrinterSettings = printerSettings;
printDocument.DefaultPageSettings = pageSettings;
printDocument.PrintController = (PrintController) new StandardPrintController();
printDocument.Print();
}
}
谢谢大家的回答。
难道 PDF 应用程序不在系统范围的 PATH 变量中,而是仅在您的特定用户下?
我认为你的问题是因为 "local system" 用户没有找到合适的应用程序,所以你必须为他注册。由于您已经接受了另一个答案,我不会再花时间讨论这个问题,但是如果对此还有其他问题,请提出。
我正在使用此代码在 windows 服务中使用 C# 在本地打印机上打印 PDF 文件。
Process process = new Process();
PrinterSettings printerSettings = new PrinterSettings();
if (!string.IsNullOrWhiteSpace(basePrint))
printerSettings.PrinterName = basePrint;
process.StartInfo.FileName = fileName;
process.StartInfo.Verb = "printto";
process.StartInfo.Arguments = "\"" + printerSettings.PrinterName + "\"";
process.Start();
process.WaitForInputIdle();
当我将用户设置为 运行 windows 服务时一切正常。
每当我在 LocalSystem 凭证下 运行 此代码时,我都会收到错误消息“没有与此操作关联的应用程序”,这通常表示我没有准备好处理打印的程序操作具有 .pdf 扩展名的文件。
我的问题是我确实有程序 (Foxit Reader) 来处理这个操作,这一点得到了证实,这个代码适用于服务上的特定用户集,而且我能够通过右键单击文件并选择打印选项将文件发送到打印机。
有什么我可以更改的,以便能够在没有特定用户的情况下从服务中在本地打印机上打印?
问题可能是 SYSTEM (LocalSystem) 帐户的用户界面功能有限,并且 shell 或 shell 扩展可能已被删除或禁用。动词是 shell 子系统的一项功能,具体来说,是 Explorer。
您可以手动调用该程序以查看是否属于这种情况,或者是否存在安全问题或缺少用户配置文件详细信息。
为此,您需要深入研究注册表,您会发现许多 shell 执行扩展动词都有命令行。
例如,查找 HKEY_CLASSES_ROOT.pdf\shell\printto\command 并使用该命令。
此外,您可以检查 SYSTEM 帐户是否可以访问此密钥和父密钥。 (很少见,但值得检查)
您也许可以执行您的工作代码,但使用当前活动的会话用户令牌(但没有活动会话这不应该工作)
For brevity, this code won't compile : you have to adapt and add some P/Invoke first.
您必须找到活动会话 ID。对于本地打开的会话,使用这个:
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern int WTSEnumerateSessions(
IntPtr hServer,
int Reserved,
int Version,
ref IntPtr ppSessionInfo,
ref int pCount);
然后搜索打开的session id:
var typeSessionInfo = typeof(WTSApi32.WTSSessionInfo);
var sizeSessionInfo = Marshal.SizeOf(typeSessionInfo);
var current = handleSessionInfo;
for (var i = 0; i < sessionCount; i++)
{
var sessionInfo = (WTSApi32.WTSSessionInfo)Marshal.PtrToStructure(current, typeSessionInfo);
current += sizeSessionInfo;
if (sessionInfo.State == WTSApi32.WTSConnectStateClass.WTSActive)
return sessionInfo.SessionID;
}
如果未找到,请搜索 rdp 会话:
[DllImport("kernel32.dll")]
public static extern uint WTSGetActiveConsoleSessionId();
得到一个令牌
private static IntPtr GetUserImpersonatedToken(uint activeSessionId)
{
if (!WTSApi32.WTSQueryUserToken(activeSessionId, out var handleImpersonationToken))
Win32Helper.RaiseInvalidOperation("WTSQueryUserToken");
try
{
return DuplicateToken(handleImpersonationToken, AdvApi32.TokenType.TokenPrimary);
}
finally
{
Kernel32.CloseHandle(handleImpersonationToken);
}
}
有了它,您可以在打开的用户会话中从本地系统服务执行 exe。
public static void ExecuteAsUserFromService(string appExeFullPath, uint activeSessionId, bool isVisible = false, string cmdLine = null, string workDir = null)
{
var tokenUser = GetUserImpersonatedToken(activeSessionId);
try
{
if (!AdvApi32.SetTokenInformation(tokenUser, AdvApi32.TokenInformationClass.TokenSessionId, ref activeSessionId, sizeof(UInt32)))
Win32Helper.RaiseInvalidOperation("SetTokenInformation");
ExecuteAsUser(tokenUser, appExeFullPath, isVisible, cmdLine, workDir);
}
finally
{
Kernel32.CloseHandle(tokenUser);
}
}
现在看看您是否可以调整您的代码以适应 CreateProcessAsUSer(...)
private static void ExecuteAsUser(IntPtr token, string appExeFullPath, bool isVisible, string cmdLine, string workDir)
{
PrepareExecute(appExeFullPath, isVisible, ref workDir, out var creationFlags, out var startInfo, out var procInfo);
try
{
startInfo.lpDesktop = "WinSta0\Default";
var processAttributes = new AdvApi32.SecurityAttributes
{
lpSecurityDescriptor = IntPtr.Zero
};
var threadAttributes = new AdvApi32.SecurityAttributes
{
lpSecurityDescriptor = IntPtr.Zero
};
if (!AdvApi32.CreateProcessAsUser(token,
appExeFullPath, // Application Name
cmdLine, // Command Line
ref processAttributes,
ref threadAttributes,
true,
creationFlags,
IntPtr.Zero,
workDir, // Working directory
ref startInfo,
out procInfo))
{
throw Win32Helper.RaiseInvalidOperation("CreateProcessAsUser");
}
}
finally
{
Kernel32.CloseHandle(procInfo.hThread);
Kernel32.CloseHandle(procInfo.hProcess);
}
}
希望此代码对您或其他人有用!
我最终使用 pdfium 来完成这项工作。使用该代码,即使 windows 服务在 LocalService 用户下 运行,PDF 文件也会正确发送到打印机。
PrinterSettings printerSettings = new PrinterSettings()
{
PrinterName = printerName,
Copies = 1
};
PageSettings pageSettings = new PageSettings(printerSettings)
{
Margins = new Margins(0, 0, 0, 0)
};
foreach (PaperSize paperSize in printerSettings.PaperSizes)
{
if (paperSize.PaperName == "A4")
{
pageSettings.PaperSize = paperSize;
break;
}
}
using (PdfDocument pdfDocument = PdfDocument.Load(filePath))
{
using (PrintDocument printDocument = pdfDocument.CreatePrintDocument())
{
printDocument.PrinterSettings = printerSettings;
printDocument.DefaultPageSettings = pageSettings;
printDocument.PrintController = (PrintController) new StandardPrintController();
printDocument.Print();
}
}
谢谢大家的回答。
难道 PDF 应用程序不在系统范围的 PATH 变量中,而是仅在您的特定用户下?
我认为你的问题是因为 "local system" 用户没有找到合适的应用程序,所以你必须为他注册。由于您已经接受了另一个答案,我不会再花时间讨论这个问题,但是如果对此还有其他问题,请提出。