Caio Sant'Anna
Caio Sant'Anna

Reputation: 342

PDF Print Through Windows Service with C#

I'm using this code to print a PDF file on a local printer with C# within a windows service.

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();

Everything works fine when I set a user to run the windows service.

Whenever I run this code under the LocalSystem credential, I get the error "there are no application associated to this operation" which usually indicates that I don't have a program ready to deal with a print operation of a file with the .pdf extension.

My problem is that I do have the program (Foxit Reader) to deal with this operation as confirmed by the fact that this code works with specific user set on the service and that I'm able to send files to the printer by right clicking them and selecting the print option.

Is there anything I can change to be able to print on a local printer from within a service without an specific user?

Upvotes: 8

Views: 2789

Answers (4)

thetillhoff
thetillhoff

Reputation: 554

Could it be, that the PDF-application is not in the system-wide PATH variable, but only under your specific user?

I think your problem occurs because the "local system" user doesn't find a proper application, so you would have to register it for him. As you have already accepted another answer, I won't put more time into this, but if there are further questions to this, please ask.

Upvotes: 0

Caio Sant'Anna
Caio Sant'Anna

Reputation: 342

I ended up using pdfium to do the job. With that code, the PDF file is sent to the printer correctly even when the windows service is running under the LocalService user.

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();
    }
}

Thanks for the answers guys.

Upvotes: 4

Dmo
Dmo

Reputation: 161

You can perhaps execute your working code, but using a currently active session user token (but with no active session this should not work)

For brevity, this code won't compile : you have to adapt and add some P/Invoke first.

You have to find the active session id. For local opened session, use this :

    [DllImport("wtsapi32.dll", SetLastError = true)]
    public static extern int WTSEnumerateSessions(
        IntPtr hServer,
        int Reserved,
        int Version,
        ref IntPtr ppSessionInfo,
        ref int pCount);

Then search for an opened 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;
        }

If not found, search an rdp session with :

    [DllImport("kernel32.dll")]
    public static extern uint WTSGetActiveConsoleSessionId();

With that get a token

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

With that you can execute an exe from Local System service, on an opened user session.

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

Now see if you can adapt your code to 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);
        }
    }

Hoping this code will serve to you or someone else !

Upvotes: 0

Bahram Ardalan
Bahram Ardalan

Reputation: 280

The problem may be that SYSTEM (LocalSystem) account has limited user interface capabilities and possibly shell or shell extensions are removed or disabled. And verbs are a capability of the shell subsystem, specifically, the Explorer.

You may invoke the program manually to see if that's the case or it's a security issue or lack of user profile details.

To do that, you need to dig in the Registry and you'll find that many shell execution extension verbs have a command line.

For example, Find HKEY_CLASSES_ROOT.pdf\shell\printto\command and use that command.

Also, you may check that SYSTEM account has access to this and parent keys. (Rarely the case but worth checking)

Upvotes: 0

Related Questions