使用 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" 用户没有找到合适的应用程序,所以你必须为他注册。由于您已经接受了另一个答案,我不会再花时间讨论这个问题,但是如果对此还有其他问题,请提出。