1

I'm attempting to launch a service using CreateProcessAsUser but for some reason multiple (30+) instances of the EXE are being created when debugging. The processes begin to spawn on this line of code:

ret = CreateProcessAsUser(DupedToken, Path, null, ref sa, ref sa, false, 0, (IntPtr)0, "c:\\", ref si, out pi);

I used code from this example - http://support.microsoft.com/default.aspx?scid=kb;EN-US;889251.

    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public int cb;
        public String lpReserved;
        public String lpDesktop;
        public String lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public int Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
        String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

    [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
    public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
        int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);





      string curFile2 = AppDomain.CurrentDomain.BaseDirectory + "OnStart.txt";

   public void createProcessAsUser()
   {
       IntPtr Token = new IntPtr(0);
        IntPtr DupedToken = new IntPtr(0);
        bool      ret;
        //Label2.Text+=WindowsIdentity.GetCurrent().Name.ToString();


        SECURITY_ATTRIBUTES sa  = new SECURITY_ATTRIBUTES();
        sa.bInheritHandle       = false;
        sa.Length               = Marshal.SizeOf(sa);
        sa.lpSecurityDescriptor = (IntPtr)0;

        Token = WindowsIdentity.GetCurrent().Token;

        const uint GENERIC_ALL = 0x10000000;

        const int SecurityImpersonation = 2;
        const int TokenType = 1;

        ret = DuplicateTokenEx(Token, GENERIC_ALL, ref sa, SecurityImpersonation, TokenType, ref DupedToken);

        if (ret == false)
             File.AppendAllText(curFile2, "DuplicateTokenEx failed with " + Marshal.GetLastWin32Error());

        else
             File.AppendAllText(curFile2,  "DuplicateTokenEx SUCCESS");

        STARTUPINFO si          = new STARTUPINFO();
        si.cb                   = Marshal.SizeOf(si);
        si.lpDesktop            = "";

        string Path;
        Path = @"C:\myEXEpath";

        PROCESS_INFORMATION pi  = new PROCESS_INFORMATION();
        ret = CreateProcessAsUser(DupedToken, Path, null, ref sa, ref sa, false, 0, (IntPtr)0, "c:\\", ref si, out pi);

        if (ret == false)
             File.AppendAllText(curFile2, "CreateProcessAsUser failed with " + Marshal.GetLastWin32Error());
        else
        {
             File.AppendAllText(curFile2, "CreateProcessAsUser SUCCESS.  The child PID is" + pi.dwProcessId);

            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }

        ret = CloseHandle(DupedToken);
        if (ret == false)
             File.AppendAllText(curFile2, Marshal.GetLastWin32Error().ToString() );
        else
             File.AppendAllText(curFile2, "CloseHandle SUCCESS");
    }

enter image description here

Blake
  • 1,067
  • 14
  • 25
  • Hmm, no, that's unlikely. I'd just assume you've been testing your code for a while and simply forgot to kill the process you started. A service is started with ServiceController btw, the user account it is uses is controlled by config. – Hans Passant Jan 17 '14 at 18:02
  • Ok, I'm trying to launch the .exe in the user session... I'm I going down the right path? – Blake Jan 17 '14 at 18:18

1 Answers1

1

The steps you outlined above will generate one process per execution of the method createProcessAsUser(). Now this method does not contain any code to terminate or kill the process so repeatidly calling this method will generate more than one process. As your code is displayed the method will inface generate only one process.

I think the real answer is how are you calling this method. As you stated in the comment

I'm trying to launch the .exe in the user session

I can only assume you may be calling this process from the Session start, Application_BeginRequest or another method that may be executed multiple times depending on how your application is designed (the calling code for this method would be great as an edit).

As I stated earlier the exe is being executed every time the method is called and not terminated. If you only ever want one instance of the application running you will have to examine the process tree to identify if the process is already running. Now if you should have one process running per user you will need to do the above but also maintain a reference the process ID that was created the first time the application started.

Review the code below for the changes (simplified)

public void createProcessAsUser()
{
    //one process per session
    object sessionPID = Session["_servicePID"];
    if (sessionPID != null && sessionPID is int && Process.GetProcessById((int)sessionPID) != null)
        return; //<-- Return process already running for session
    else
        Session.Remove("_servicePID");

    //one process per application
    object applicationPID = Application["_applicationPID"];
    if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)applicationPID) != null)
        return; //<-- Process running for application
    else
        Application.Remove("_applicationPID");

    //omitted starting code

    if (ret == false)
        // omitted log failed
    else
    {
        // omitted log started

        //for one process per session
        Session["_servicePID"] = Convert.ToInt32(pi.dwProcessId);

        //for one process per application
        Application["_applicationPID"] = Convert.ToInt32(pi.dwProcessId);

        //close handles
    }

    // omitted the rest of the method
}

This simple saves a reference to the Process ID that was created for the application into either the Session state for one process per user or the Application state for one process per application instance.

Now if this is the intended result you may also want to look at Terminating the process either when the application shutdown (gracefully) or the session ends. That would be very similar to our first check but can be done as seen below. *note this doesn't take into account the worker process shutting down without calling the session \ application end events those should be handled as well possibly in the application start.

//session end
void Session_End(object sender, EventArgs e)
{
    object sessionPID = Session["_servicePID"];
    if (sessionPID != null && sessionPID is int)
    {
        Process runningProcess = Process.GetProcessById((int)sessionPID);
        if (runningProcess != null)
            runningProcess.Kill();
    }
}

//application end
void Application_End(object sender, EventArgs e)
{
    object applicationPID = Application["_applicationPID"];
    if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)applicationPID) != null)
    {
        Process runningProcess = Process.GetProcessById((int)applicationPID);
        if (runningProcess != null)
            runningProcess.Kill();
    }
}

Again, back to the original question how do you stop the multiple instances. The answer is simply stop the ability to spawn multiple instances by examining how you start the instances (I.e. the calling code to the method createProcessAsUser()) and adjust your method accordingly to avoid multiple calls.

Please post an edit if this inst helpful with details on how the createProcessAsUser() method is called.

Update 1:

Session \ Application does not exist in the context. This will happen if the method createProcessUser() is in a different class than an ASPX page (as it is on the tutorial).

Because of this you will need to change for the existance of an HttpContext this can simply done by calling

HttpContext.Currrent

I have adapted the method above to include checks to the HttpContext

public void createProcessAsUser()
{
    //find the http context
    var ctx = HttpContext.Current;
    if (ctx == null)
        throw new Exception("No Http Context");

    //use the following code for 1 process per user session
    object sessionPID = ctx.Session["_servicePID"];
    if (sessionPID != null && sessionPID is int && Process.GetProcessById((int)sessionPID) != null)
        return; //<-- Return process already running for session
    else
        ctx.Session.Remove("_servicePID");

    //use the following code for 1 process per application instance
    object applicationPID = ctx.Application["_applicationPID"];
    if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)sessionPID) != null)
        return; //<-- Process running for application
    else
        ctx.Application.Remove("_applicationPID");

    // omitted code

    if (ret == false)
    {
        //omitted logging
    }
    else
    {
        //omitted logging

        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);


        //for one process per session
        ctx.Session["_servicePID"] = Convert.ToInt32(pi.dwProcessId);

        //for one process per application
        ctx.Application["_applicationPID"] = Convert.ToInt32(pi.dwProcessId);
    }

    //omitted the rest
}

You will not the changes are in the first few lines where it gets the current HttpContext (you must add using System.Web) by calling var ctx = HttpContext.Current

Next we just check that the ctx variable is not null. If it is null I am throwing an exception, however you can handle this anyway you wish.

From there instead of directly calling Session and Application I have changed the references to ctx.Session... and ctx.Application...

Update 2:

This is a Windows Application calling the method above. Now this changes the ball game as the code above is really meant to start a process as the impersonated windows identity. Now Impersonation is typcially done in WebApplications not WinForms (can be done though).

If you are not impersonating a different user than the user who is running the application. Meaning the user logged in is the user that is running the application. If this is so then your code becomes ALOT easier.

Below is an example of how this can be achieved.

/// <summary>
/// static process ID value
/// </summary>
static int? processID = null;

public void startProcess()
{
    //check if the processID has a value and if the process ID is active
    if (processID.HasValue && Process.GetProcessById(processID.Value) != null)
        return;

    //start a new process
    var process = new Process();
    var processStartInfo = new ProcessStartInfo(@"C:\myProg.exe");
    processStartInfo.CreateNoWindow = true;
    processStartInfo.UseShellExecute = false;
    process.StartInfo = processStartInfo;
    process.Start();
    //set the process id
    processID = process.Id;
}

Again as this is a Win Forms application you can use the Process object to launch a process, this windows application will run as the user running the Windows Forms application. In this example we also hold a static reference to the processID and check the if the processID (if found) is already running.

Nico
  • 12,493
  • 5
  • 42
  • 62
  • Using your session and application code I get an error 'session/application' does not exist in current context – Blake Jan 28 '14 at 15:01
  • @Blake, I was assuming that you were following the guide where the method is part of a ASPX page (cs)? The session \ application objects are part of the `HttpContext` class. If you are no in a ASPX page you will have to capture the current HttpContext in your class. I have added an edit to do so. – Nico Jan 28 '14 at 20:52
  • I'm creating a windows service not a web application. What edit are you referring to? – Blake Jan 28 '14 at 20:57
  • @Blake, I just posted the edit. What type of application is calling the method `createProcessAsUser()`? I am assuming from the reference you are using a webApp. – Nico Jan 28 '14 at 21:02
  • I'm attempting to call it from a windows application with little success... perhaps I'm taking the wrong approach? – Blake Jan 28 '14 at 21:09
  • Ok, you stated Session on your comment so assumed webapplication (win forms doesnt have a session). However why are you launching the process using the code above? The user that it is being launched as is the same user that is running the windows form? The reason for using the method above is to launch a program for an impersonated user. Impersonation is not standard in Win Forms. Can you answer a questions and I may be able to put you on a better track. Are you doing any sort of Windows Impersonation in your windows forms application? – Nico Jan 28 '14 at 21:13
  • An example of what I'm trying to accomplish might be easier. I have 3 users on my server using application AppName.exe - I want to check if that process is responding for each user using Process.Responding and then log it to a file. Thanks for all your help! – Blake Jan 28 '14 at 21:51
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/46285/discussion-between-nico-and-blake) – Nico Jan 28 '14 at 21:54