VB.NET – Start process in console session from windows service on Windows 7

June 10, 2011 — 37 Comments

Launching a process that the currently logged on user can see on their desktop session (and interact with) from a Windows service is a popular topic – and there are a wide variety of answers out there when someone asks how to do this, some people say it is not even possible on Windows Vista or Windows 7. Turns out it is actually very easy…

Firstly, I do not really recommend this method because ideally you should have a process running in the user’s session already that communicates with your service via Named Pipes, TCP/IP, or whatever – this process can receive a command from your service and launch/do whatever you want and it will already be running in the user’s session and security context. The only reason for posting this method is because in some scenarios that ideal method is not always possible or not warranted. I think of this as a quick and dirty way of doing it if you can’t do it a more ‘proper’ way. I’m using it as part of an installation package for a program that we deploy via SCCM (which therefore gets run from the SCCM Agent service) to run a one off job that needs to be executed as the currently logged on user to avoid them needing to log out and back on before the newly deployed application will work correctly (and no its not just HKCU registry edits or anything like that which could be done in an easier way).

I’ve seen plenty of other different ways of doing this, some unnecessarily complex, but this method I am going to demonstrate seems to be by far the most simple and elegant. Oh and you don’t need to mark your service as an Interactive service and the user will not get switched to the ‘services’ desktop when they try to interact with the newly launched process.

Here is the most basic example I could come up with (needs error handling etc):

Dim UserTokenHandle As IntPtr = IntPtr.Zero
WindowsApi.WTSQueryUserToken(WindowsApi.WTSGetActiveConsoleSessionId, UserTokenHandle)

Dim ProcInfo As New WindowsApi.PROCESS_INFORMATION
Dim StartInfo As New WindowsApi.STARTUPINFOW
StartInfo.cb = CUInt(Runtime.InteropServices.Marshal.SizeOf(StartInfo))

WindowsApi.CreateProcessAsUser(UserTokenHandle, "C:\Windows\System32\cmd.exe", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, False, 0, IntPtr.Zero, Nothing, StartInfo, ProcInfo)
If Not UserTokenHandle = IntPtr.Zero Then
    WindowsApi.CloseHandle(UserTokenHandle)
End If

Obviously this example launches command prompt but you can replace C:\Windows\System32\cmd.exe with whatever application you want to launch. As soon as CreateProcessAsUser is called, the process will be started and will appear on the user’s screen and act just like any other program. Well with one exception, with that basic example the user’s environmental variables will not be set – so for example if from that newly launched command prompt process you type “Echo %USERNAME%” then it will show that the %USERNAME% variable is currently set to the local system account. I’m sure you could get the environmental variables to get setup correctly by calling CreateEnvironmentBlock and passing the result in to the lpEnvironment argument of CreateProcessAsUser (instead of passing in Nothing as I do in this example).

Also note that this method assumes your service is running as Local System – other accounts wont have the required permissions for this to work.

For anyone that can’t be bothered (or doesn’t know how) to define the Windows APIs used in this example, here you go (I have also included each of these in my Windows API library)

Imports System.Runtime.InteropServices

Public Class WindowsApi

    <DllImport("kernel32.dll", EntryPoint:="WTSGetActiveConsoleSessionId", SetLastError:=True)> _
    Public Shared Function WTSGetActiveConsoleSessionId() As UInteger
    End Function

    <DllImport("Wtsapi32.dll", EntryPoint:="WTSQueryUserToken", SetLastError:=True)> _
    Public Shared Function WTSQueryUserToken(ByVal SessionId As UInteger, ByRef phToken As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("kernel32.dll", EntryPoint:="CloseHandle", SetLastError:=True)> _
    Public Shared Function CloseHandle(<InAttribute()> ByVal hObject As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("advapi32.dll", EntryPoint:="CreateProcessAsUserW", SetLastError:=True)> _
    Public Shared Function CreateProcessAsUser(<InAttribute()> ByVal hToken As IntPtr, _
                                                    <InAttribute(), MarshalAs(UnmanagedType.LPWStr)> ByVal lpApplicationName As String, _
                                                    ByVal lpCommandLine As System.IntPtr, _
                                                    <InAttribute()> ByVal lpProcessAttributes As IntPtr, _
                                                    <InAttribute()> ByVal lpThreadAttributes As IntPtr, _
                                                    <MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandles As Boolean, _
                                                    ByVal dwCreationFlags As UInteger, _
                                                    <InAttribute()> ByVal lpEnvironment As IntPtr, _
                                                    <InAttribute(), MarshalAsAttribute(UnmanagedType.LPWStr)> ByVal lpCurrentDirectory As String, _
                                                    <InAttribute()> ByRef lpStartupInfo As STARTUPINFOW, _
                                                    <OutAttribute()> ByRef lpProcessInformation As PROCESS_INFORMATION) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <StructLayout(LayoutKind.Sequential)> _
    Public Structure SECURITY_ATTRIBUTES
        Public nLength As UInteger
        Public lpSecurityDescriptor As IntPtr
        <MarshalAs(UnmanagedType.Bool)> _
        Public bInheritHandle As Boolean
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Public Structure STARTUPINFOW
        Public cb As UInteger
        <MarshalAs(UnmanagedType.LPWStr)> _
        Public lpReserved As String
        <MarshalAs(UnmanagedType.LPWStr)> _
        Public lpDesktop As String
        <MarshalAs(UnmanagedType.LPWStr)> _
        Public lpTitle As String
        Public dwX As UInteger
        Public dwY As UInteger
        Public dwXSize As UInteger
        Public dwYSize As UInteger
        Public dwXCountChars As UInteger
        Public dwYCountChars As UInteger
        Public dwFillAttribute As UInteger
        Public dwFlags As UInteger
        Public wShowWindow As UShort
        Public cbReserved2 As UShort
        Public lpReserved2 As IntPtr
        Public hStdInput As IntPtr
        Public hStdOutput As IntPtr
        Public hStdError As IntPtr
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Public Structure PROCESS_INFORMATION
        Public hProcess As IntPtr
        Public hThread As IntPtr
        Public dwProcessId As UInteger
        Public dwThreadId As UInteger
    End Structure

End Class

Hope it helps someone out

Chris

37 responses to VB.NET – Start process in console session from windows service on Windows 7

  1. 

    Works good, however it leaves a ntvdm.exe process hanging out there each time.

    • 

      Does it? I’ve never noticed that and it seems a bit odd as ntvdm is used for the 16 bit program emulation environment… are you sure its not just the program that you are telling it to launch that is causing this?

  2. 

    Thats the problem 🙂

    Thanks

  3. 

    Helped me TREMENDOUSLY!

  4. 

    Hi,
    There is a very interesting article.
    But I have one question: I tried to launch Adobe Acrobat Reader, by replacing C:\Windows\System32\cmd.exe (that works very well) with the entire path of Acrobat (C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe), but I have an Acrobat error : An internal error occurred. If a try to launch notepad, it works also very well.

    Did you ever try to launch other applications (Windows applications by example) or do you know how could I do that ? (What I’m trying to do is to create a windows service in C# 3.5 Visual Studio 2010 that launch the Adobe Reader with the parameters /t/n to print some PDF files). Have you any idea ? (Operating System is Windows 7 Enterprise Ed).

    Thx a lot.

    • 

      I did try launching quite a few apps yeah and they all worked fine, but I think some only worked correctly when you setup the environmental variable block properly using CreateEnvironmentBlock before creating the process (mentioned in this article but code example not included). Did you do that?
      I have to ask though, if all you want to do is print some PDFs, why does that need to be run from the user’s session? Why can’t you just print them directly from your service?

      • 

        I found the printing PDFs files from C# is not so easy (all the articles I found say that). If I simply send to printer the file (as stream), the printer doesn’t know how to print the file, it will print the file as text(ascii) file. So, the easiest way I found is to launch another application from windows service that will be able to print the file. One of these applications is Adobe Reader launched is command line with parameters (/t/n), and as separate process (Process p = new Process(); proc.StartInfo.FileName = pathAdobeReader;p.Start();).
        But, when I tried this, the error I discussed appears. For others windows applications (notepad, calc etc), your method works very well. I will try as you said.
        Thx a lot.

      • 

        I don’t think you understood what I meant, either that or you don’t understand what this code does. This code is to launch a process in the user’s session, rather than in the services session, and what I’m asking is if your user doesn’t need to interact with it then why can’t you just use your exact same code (ie the code that launches Adobe Reader and prints through it) from your service directly rather than needing to launch it in the user’s session? You said originally that you get the error “An internal error occurred” when you use my code to launch it in the user’s session, but then you said that you get that error when you just try and launch the process directly from the service… so I’m confused as to when exactly you get this error and what actually happens when you try just launching the process in the services session using Process.Start

  5. 

    Hi,
    Sorry for my bad English and for my not very good explanation. I’ll try again : what I have to do is to create a windows service that looks every X seconds into a directory from local filesystem (c:\temp) on the same machine (computer)where the service is installed. If the service find a file PDF, it has to print it (send it to a printer). I looked for how can I print a PDF file and the easiest way I founded it is to launch Adobe Reader in the command line with the “/t/n” parameters. (adobe.exe /t/n ). So, I tried to launch Adobe as a separate process directly from my windows service, with : Process p = new Process(); p.StartInfo.FileName=; p. StartInfo.Arguments=”/t/n” <printername”. But, here one problem : the Adobe is not launched as separate process. The explanation I found – the windows services can’t launch an external application; the windows services run only in background and it is designed not to have an interaction with the external applications. After that, I tried to find a way to launch an external app from a win service and I found your code (among others). When a tried it, it’s ok for windows applications (command prompt, notepad, calculator etc), but when I tried to launch Adobe Reader I have the error : “An internal error occurred”.
    I also tried something else : I’ve created another c# win application (with MS Visual Studio 2010) that is very well launched by my service with your code; and, after that, I put the launch of Adobe Reader into this new application, with new Process, as a separate thread. Also this time I have the same problem : my win service launch very well my new win application with your code, but I have the same error “An internal error occurred” when the win app is trying to launch the Adobe Reader. On the other hand, if I launch the win application separately (not from the service), the Adobe is launched without any problem. So I think that the reason of the error is only because the service launches the windows application. If you have any idea, please let me know. Thx

    Hope I succeed a better explanation this time 🙂

    • 

      Services CAN launch external applications/processes – you should not need to launch the adobe reader in the user’s session. The only difference when launching another process from a service is that the process will run in the services session instead of the console session, so it will not have access to things in the user’s session such as their printers. This is probably what is causing you a problem – you try adding the printer from the service first before attempting to print the PDF. If you want to stick with the way you are doing it though and launch it in the user’s session, to fix your error I think you just need to use the CreateEnvironmentBlock API as I mentioned before to setup the environmental variables for Adobe Reader.

      • 

        Hi,
        You had wrights. I did do like you said (use CreateEnvironmentBlock) and works. Thx a lot ! 🙂
        On the other hand, did you ever try to modify (from c# or VB.net) the parameters of a printer? I have to continue my application with the Adobe that is launched in command mode, with the parameters /t/n to print a file, but also I have to change the paper size. That means the user says : it has to print this pdf file with this kind of paper (by paper size). So, I have to sent the paper size (height and width, in mm or inch, doesn’t matter) to the printer. There is a way to do that ? Some way to communicate with the printer’s driver … ? Do you know something about this ?

      • 

        glad it worked 🙂 and no I’m afraid I don’t know how you would modify printer properties but I’m sure if you google it you’ll find some tips

  6. 

    how can you close the application if you want to

    • 

      I think one of the parameters in CreateProcessAsUser receives either the process ID or a handle to the process once the function completes successfully, so you can use that to bind a regular .NET System.Diagnostics.Process object to it and then terminate it using that (I think there’s a method named Kill or Terminate or something like that). So basically yeah look at the MSDN documentation for CreateProcessAsUser

      • 

        Thanks sorted that, I used the process id like this
        Process.GetProcessById((int)ProcInfo.dwProcessId).Kill();

        Another thing is I tried another application i developed, which is on my desktop on windows XP, it comes back as access denied in event viewer. any help would be appreciated

  7. 

    Hi Chris– Thanks for posting the example. I tried using it, but I’m receiving error 5, access denied, from the call to createprocessasuser. Similar to your conditions with an SCCM agent, my application is getting delivered by CA Software Delivery, which the agent also runs as SYSTEM and launches my VB.NET application in the SYSTEM context. As part of my application, I need to start a process for each logged in user, although I will settle for a working example that can start a process for just the console user. Can you think if anything I might have missed using the same code from your example that will cause the access denied error? Is there some tool I can use to find out more information about the access denied error to see what exactly is causing the problem? Appreciate your words of wisdom. Thanks, Brian

    • 

      Hi Brian,

      What OS are you running it on? i only ever tested it on Windows 7, but if you are on Windows 7 then I can’t think of any reason why you would get access denied if it the calling process is definitely running as Local System. I know it sounds daft but have you double checked that the process that runs this code is DEFINITELY running as Local System? Maybe just make it output Environment.Username to a text file or something right before the CreateProcessAsUser call just as a 100% guarantee that this piece of code is running as system.

  8. 

    Thank you very much! This article was very helpful!
    However I still got problems with starting an application with parameters. Couldn’t find anything useful about CreateEnvironmentBlock, can you help me?

    • 

      This is my definition of CreateEnvironmentBlock:

      <DllImport("Userenv.dll", EntryPoint:="CreateEnvironmentBlock", SetLastError:=True)> _
      Public Shared Function CreateEnvironmentBlock( ByRef lpEnvironment As IntPtr, ByVal hToken As IntPtr, ByVal bInherit As Boolean) As Boolean
      End Function
      Public Shared Function CreateEnvironmentBlock( ByRef lpEnvironment As IntPtr, ByVal hToken As IntPtr, ByVal bInherit As Boolean) As Boolean
      End Function

      Then to use it you just call it:

      CreateEnvironmentBlock(EnvironmentBlock, UserTokenHandle, False)

      Then pass that pointer in to CreateProcessAsUser (the 4th to last argument) :

      CreateProcessAsUser(UserTokenHandle, Nothing, CommandLine, IntPtr.Zero, IntPtr.Zero, False, ApiDefinitions.CREATE_UNICODE_ENVIRONMENT Or ApiDefinitions.CREATE_NEW_CONSOLE, EnvironmentBlock, Nothing, StartInfo, ProcInfo)

      • 

        Hmm, exe file with arguments doesn’t work at all 😦 I think I made a mistake somewhere in my function. Is there a chance to get a full example of starting an exe with some parameters?

  9. 

    On building the project, following errors are shown:
    Error 1 ‘WindowsApi.CreateProcessAsUser(System.IntPtr, string, System.IntPtr, System.IntPtr, System.IntPtr, bool, uint, System.IntPtr, string, ref WindowsApi.STARTUPINFOW, ref WindowsApi.PROCESS_INFORMATION)’ cannot specify only Out attribute on a ref parameter. Use both In and Out attributes, or neither
    Error 2
    The name “UserTokenHandle” does not exist in the current context
    I am using a 64 bit, Windows 7 system and Microsoft Visual Studio 2010.

    • 

      Hi AJ,

      I’m only guessing without viewing your code, but did you initialize the PROCESS_INFORMATION and STARTUPINFO variables?

      Here’s a snippet from my code to do the very same..

      First the Windows API declaration for CreateProcessAsUser..

      _
      Private Shared Function CreateProcessAsUser( _
      ByVal hToken As SafeTokenHandle, _
      ByVal lpApplicationName As String, _
      ByVal lpCommandLine As String, _
      ByRef lpProcessAttributes As SECURITY_ATTRIBUTES, _
      ByRef lpThreadAttributes As SECURITY_ATTRIBUTES, _
      ByVal bInheritHandles As Boolean, _
      ByVal dwCreationFlags As UInteger, _
      ByVal lpEnvironment As IntPtr, _
      ByVal lpCurrentDirectory As String, _
      ByRef lpStartupInfo As STARTUPINFO, _
      ByRef lpProcessInformation As PROCESS_INFORMATION) _
      As Boolean
      End Function

      Then here’s a snippet of code that uses it..

      ‘ Initialize process and startup info
      pi = New PROCESS_INFORMATION
      si = New STARTUPINFO
      si.cb = Marshal.SizeOf(si)
      si.lpDesktop = Nothing

      ‘ Launch the process in the client’s logon session
      If Not CreateProcessAsUser(phNewToken, _
      Nothing, _
      CmdLine + Arguments, _
      Nothing, _
      Nothing, _
      False, _
      CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT, _
      Nothing, _
      Nothing, _
      si, _
      pi) Then

      ‘ Write debug
      System.Console.WriteLine(“Error: Windows API CreateProcessAsUser function returns an error.”)
      System.Console.WriteLine(“Windows API error code: ” + Marshal.GetLastWin32Error.ToString)
      System.Console.WriteLine(“”)

      Else

      ‘ Write debug
      System.Console.WriteLine(“Created new process: ” + pi.dwProcessId.ToString)

      End If

      Hope this helps,
      Brian

  10. 

    Hi All! I’ve the problem to print PDF files too, using some code in a service instead of a “normal” exe application (Windows 7):

    Dim sReport as string
    Dim pathToExecutable As String = “AcroRd32.exe”
    sReport = “\\Wipnet\customer\example.pdf” ‘ obviously just an example
    Dim starter As New ProcessStartInfo(pathToExecutable, “/t ” + sReport + ” ” + SPrinter + “”)
    Dim Process As New Process()

    Process.StartInfo = starter
    Try
    Process.Start()
    Catch
    EventLog.WriteEntry(“PDFPrintService”, “error”)
    End Try
    Process.CloseMainWindow()

    I’m not using a local user in my service, because I previously access the pdf file for some modifications, and I need a full access user to correctly open files which are in a net folder (\\wipdb\ecc ecc)
    With this code, using the service (it WORKS in a exe application) nothing is printed, with no error..the code is simply ignored :(((..in this blog I discovered the problem is due to windows 7 and its new way to manage services…

    Reading your messages, it seems I need to use a CreateEnvironmentBlock API to correctly manage my code to print PDF files from my Acrobat Read application ..erm..how can I do it? That is: in which way I can use that API in my code ? I like it, it’s very small!!

    Thanks,
    Luca

    ps. sorry my bad english 😛

    • 

      If you look at the other comments on this blog post you’ll see I already showed how to define the CreateEnvironmentBlock API and how to use it 🙂

  11. 

    oh my god, you are amazing! you helped me to get out from 2 days trouble! thanks a lot!

  12. 

    Well, for those who are looking for c++ code. BCB6 tested. Solution given as
    “copy&paste” to your .cpp since huge amount of errors appearing if you trying to do that for the first time as task is not very ordinary. Very short and elegant code, CREDITS to Chris.

    #define _WIN32_WINNT 0x0501
    #include
    #include
    #pragma comment(lib, “wtsapi32.lib”)
    #include “Unit1.h”
    //—————————————————————————
    #pragma package(smart_init)
    #pragma resource “*.dfm”
    TForm1 *Form1;
    //—————————————————————————
    __fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
    {
    }
    //—————————————————————————

    void __fastcall TForm1::Button1Click(TObject *Sender)
    {
    String A = “C:\\Windows\\Notepad.exe”;

    HANDLE UserTokenHandle = NULL;
    PROCESS_INFORMATION ProcInfo = { 0 };
    STARTUPINFOA StartInfo = { 0 };

    WTSQueryUserToken(WTSGetActiveConsoleSessionId(), &UserTokenHandle);
    StartInfo.cb = sizeof(STARTUPINFO);

    CreateProcessAsUser(UserTokenHandle, A.c_str() , NULL, NULL, NULL, FALSE, 0, NULL, NULL, &StartInfo, &ProcInfo);

    if (!UserTokenHandle == NULL) {CloseHandle(UserTokenHandle);}
    }

  13. 

    And the whole solution for the application to be able to start from the service call in Windows Vista, 7 etc. (Win8 not tested yet) so there is no need to use additional program “loaders”. On Start application detects active session id, and then restarts itself with correct parameters. For the compatibility with older OS:If version of windows is lower than Vista, application starts within default sessionid: 0.
    Create and Copy processid.h file to your …Borland/CBuilder/Include directory. Then use directive #include Processid.h in your Project.cpp file. In WinMain command use “ReloadInProcess()” function. N.B. Compiler may give a warning about precompiled header…

    Processid.h code
    //——————————————————————————–
    #ifndef ProcessidH
    #define ProcessidH
    //——————————————————————————–
    #define _WIN32_WINNT 0x0501
    #include Windows.h
    #include Wtsapi32.h
    #pragma comment(lib, “wtsapi32.lib”)
    #include vcl.h
    //——————————————————————————–

    void __fastcall ReloadInProcess(){
    OSVERSIONINFO verinfo;
    ZeroMemory(&verinfo, sizeof(OSVERSIONINFO));
    verinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    GetVersionEx(&verinfo);
    if (_argv[1] == NULL && verinfo.dwMajorVersion > 5){
    String A = _argv[0]; A = A + ” /1″;

    HANDLE UserTokenHandle = NULL;
    PROCESS_INFORMATION ProcInfo = { 0 };
    STARTUPINFOA StartInfo = { 0 };
    WTSQueryUserToken(WTSGetActiveConsoleSessionId(), &UserTokenHandle);
    StartInfo.cb = sizeof(STARTUPINFO);
    CreateProcessAsUser(UserTokenHandle, NULL, A.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &StartInfo, &ProcInfo);
    if (!UserTokenHandle == NULL) {CloseHandle(UserTokenHandle);}
    exit(0);
    }
    }
    //——————————————————————————–
    #endif

    Project.cpp code
    //——————————————————————————–
    #include Processid.h
    #include vcl.h
    #pragma hdrstop
    //——————————————————————————–
    USEFORM(“..\qwe2\Unit1.cpp”, Form1);
    //——————————————————————————–
    WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
    {
    try
    {
    ReloadInProcess();
    Application->Initialize();
    Application->Title = “InProcLaunch”;
    Application->CreateForm(__classid(TForm1), &Form1);
    Application->Run();
    }
    catch (Exception &exception)
    {
    Application->ShowException(&exception);
    }
    catch (…)
    {
    try
    {
    throw Exception(“”);
    }
    catch (Exception &exception)
    {
    Application->ShowException(&exception);
    }
    }
    return 0;
    }
    //—————————————————————————–

    Be sure to use bcb brackets in #include directives before&after filename & happy coding!

  14. 

    Not work win 8 or 8.1 (64bit) Please

  15. 

    it’s ok on Win 8.1.Thank you very much!

  16. 

    How can I add this to the code “CreateEnvironmentBlock”. I want to open uvnc from a service. Do you have an example?

  17. 

    Can you explain why your code doesn’t work with rdp sessions, please?

    • 

      because this is for starting a process in the console session, i.e. the session that you see if you log in physically standing in front of the computer. RDP sessions are completely separate from the console session. If there were multiple RDP sessions running on the computer, how would the program know which session to launch the process in?

  18. 

    Hi, good question 😉
    And when i want to start a certain process under all established rdp sessions? Is that possible?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s