19

We have a service which starts a process and waits for process to exit when service is stopped/ user of service calls stop (to stop/kill process started by service).

Sporadically, process.waitForExit(TimeSpan) hangs.

Please note that process started by Service is native process (C++/CLI) process and service is in C#.

Following is the code snippet we are using

public class ApplicationProcessControl : IProcessControl
 {  
    private Process _proc;
    private const int ProcessIdleTimeout = 5000;

    public bool Start(string arguments)
    {
        if (IsAlive)
        {
            Log.TraceInfo("Application process already running. Killing it now...");
            _proc.Kill();
        }

        var eProcStarted = new Mutex(false, "Mutex111");

        _proc = new Process { EnableRaisingEvents = true, StartInfo = new ProcessStartInfo(_exePath, arguments) { RedirectStandardOutput = false,RedirectStandardError = false};
        _proc.Exited += OnProcessExited;

        _proc.Start();
        bool started;
        if(_proc == null)
        {
            Log.TraceInfo("Unable to start application process");
            started = false;
        }
        else
        {
            started = eProcStarted.WaitOne(ProcessIdleTimeout);

            if(started)
            {
                Log.TraceInfo($"Application process with id {_proc.Id} started successfully");
            }
        }
        eProcStarted.Dispose();
        return started;
    } 

    public void Kill()
    {
        _proc.Kill();
    }

    public bool WaitForProcessToExit(TimeSpan timeout)
    {
        return _proc.WaitForExit((int) timeout.TotalMilliseconds);
    }

    public event Action ProcessExited;

    private void OnProcessExited(object sender, EventArgs e)
    {
        var proc = sender as Process;

        if(proc != null)
        {
            proc.Exited -= OnProcessExited;

            if(proc.ExitCode == 0)
            {
                Log.TraceInfo("Application process exited gracefully");
            }
            else
            {
                Log.DeveloperWarning("Application process exited unexpectedly with code {0}", proc.ExitCode);
                OnProcessExited();
            }
        }
    }

    private void OnProcessExited()
    {
        Action handler = ProcessExited;
        handler?.Invoke();
    }
}

public interface IProcessControl
{
    bool IsAlive { get; }

    bool Start(string arguments);

    bool WaitForProcessToExit(TimeSpan timeout);

    void Kill();

    event Action ProcessExited;
}

public class ApplicationClientService: DisposableObject, IComponentService, ITaskControl, IUIControl,
        IDataProvider<AngleFlavors>, IApplicationCloseNotifier
{
    //...
    private readonly IProcessControl _procCtrl;

    public ApplicationClientService(IObjectProvider objPro)
    {
        //...
        _procCtrl.ProcessExited += OnApplicationProcessExited;              
    }

    public void Stop()
    {
        //...
        CleanUpAppProcess();
        //...
    }


    private void CleanUpAppProcess()
    {
        //...

        if(!_procCtrl.WaitForProcessToExit(TimeSpan.FromSeconds(5)))
        {
            _procCtrl.Kill();
        }
    }

    private void OnApplicationProcessExited()
    {
        if(!_isAppRunning)
        {
            return;
        }

        _isAppRunning = false;
        _autoLaunchRequested = false;
        RaiseApplicationClosed();
        Log.DeveloperWarning("Application process closed unexpectedly");
        Log.UserMessageApplicationClosedUnexpectedly();
        ...
    }

    protected virtual void RaiseApplicationClosed()
    {
        //AuditApplicationStop();
        //ApplicationClosed?.Invoke();
    }

}
Hiran Patel
  • 229
  • 1
  • 5
  • Is it possible that this [question](https://stackoverflow.com/questions/139593/processstartinfo-hanging-on-waitforexit-why) and its answers can be applied to your problem too? – Ciaran_McCarthy May 14 '18 at 10:25
  • @Ciaran_McCarthy: As we are not using redirection of Standard Error or output as mentioned in other question. Hence is not related. – Hiran Patel May 14 '18 at 11:25
  • Sorry. I saw ``new ProcessStartInfo(_exePath, arguments) { RedirectStandardOutput ,};`` and thought it might have been related. – Ciaran_McCarthy May 14 '18 at 13:44
  • @Ciaran_McCarthy: I have edited question to update { RedirectStandardOutput = false, RedirectStandardError = false } – Hiran Patel May 15 '18 at 04:21
  • @sɐunıɔןɐqɐp: I do not see C++ process in task manager at all when this hang issue happens – Hiran Patel May 17 '18 at 06:32
  • Have a look at OnApplicationProcessExited and OnProcessExited. They are missing in your post. Maybe you built some lock there and think about using 'using' statements to be sure your process objects get disposed. After that i would review the mutex thingy. Its easy to build deadlocks with threads by accident. – stefan.seeland May 30 '18 at 14:27
  • @stefan.seeland : I have updated the code with missing functions. I'm not using any lock in those functions. – Hiran Patel Sep 18 '18 at 08:29
  • 1
    There is no well-known scenario where Process.WaitForExit() could hang when used with a non-infinite timeout. You have to look for an environmental problem. On the top of that list forever is aggressive anti-malware, the kind that gets its underwear in a bundle on a programmer's machine that makes .exe files appear from seemingly no-where. – Hans Passant Sep 18 '18 at 09:12
  • @HansPassant : On further investigation (with WaitForExit() without any parameter), we found that real issue is even though the un-managed process is not present in task manager, _process.hasExited property returns false. May be some process handle is not released in operating system. – Hiran Patel Sep 19 '18 at 04:25
  • 2
    I am a bit puzzled by your mutex thing. You start a process and only then wait for the mutex. It doesn't stop your code from spawning many instances of the the process simultaneously. Moreover in case of concurrent Start() calls _proc variable will be rewritten. In this case you will wait for a second instance of the process. Does your usage pattern include this scenario? – Bennie Tamir Oct 31 '18 at 20:47

1 Answers1

0

Don't know if this can answer your question (I have myself more questions than answers on this), but this code:

    private void CleanUpAppProcess()
    {
        //...

        if(!_procCtrl.WaitForProcessToExit(TimeSpan.FromSeconds(5)))
        {
            _procCtrl.Kill();
        }
    }

calls WaitForExit before the Kill command. Are you expecting the process to terminate by itself / to be terminated by a user in between 5 seconds? As Bennie Zhitomirsky pointed out in his comment, the mutex is not owned when it should be (if I understood correctly what you want to achieve, if not, sorry). What about the implementation of IsAlive?

Anyway, I put down some lines for the ApplicationProcessControl class. I just tested it a bit with some native process and seems to work (but still, I'm not sure this is what you're trying to achive):

    public class ApplicationProcessControl : IProcessControl
    {
        /// <summary>
        /// Process instance variable.
        /// </summary>
        private Process _proc;
        /// <summary>
        /// The thread will try to acquire the mutex for a maximum of _mutexAcquireTimeout ms.
        /// </summary>
        private const int _mutexAcquireTimeout = 5000;
        /// <summary>
        /// Global static named mutex, seen by all threads.
        /// </summary>
        private static Mutex SpawnProcessMutex = new Mutex(false, "Mutex111");
        /// <summary>
        /// The state of the process.
        /// </summary>
        public bool IsAlive
        {
            get { return !(_proc is null) && !_proc.HasExited; }
        }
        /// <summary>
        /// Spawns a new process.
        /// </summary>
        public bool Start(string arguments)
        {
            // Try to acquire the mutex for _mutexAcquireTimeout ms.
            if (!SpawnProcessMutex.WaitOne(_mutexAcquireTimeout) || IsAlive)
            {
                // Mutex is still owned (another thread got it and is trying to run the process) 
                // OR the process is already running.
                // DO NOT start a new process.
                return false;
            }

            try
            {
                // Mutex is acquired by this thread.
                // Create a new instance of the Process class.
                _proc = new Process
                {
                    EnableRaisingEvents = true,
                    StartInfo = new ProcessStartInfo("the_process_to_be_run", arguments)
                    {
                        RedirectStandardOutput = false,
                        RedirectStandardError = false
                    }
                };
                // Subscription to the ProcessExited event.
                _proc.Exited += OnProcessExited;
                // Run the process.
                var haveAnHandle = _proc.Start();

                // *******
                // TODO: The process started but we may not have an handle to it. What to do?
                // *******

                //Log.TraceInfo($"Application process with id {_proc.Id} started successfully");
                return true;
            }
            catch (Exception) // TODO: [Catch the specific exceptions here]
            {
                // The process failed to start, still we have an instance of Process with no use.
                if (!(_proc is null))
                {
                    _proc.Dispose();
                    _proc = null;
                }
                //Log.TraceInfo("Unable to start application process");
                return false;
            }
            finally 
            {
                // Release the mutex, another thread may be waiting to acquire it.
                SpawnProcessMutex.ReleaseMutex();
            }
        }
        /// <summary>
        /// Kills the process started by the Start method.
        /// </summary>
        public void Kill()
        {
            if (IsAlive) _proc.Kill();
        }
        /// <summary>
        /// Can't see a real reason to block the thread waiting synchronously for the process to
        /// exit, we are already subscribed to the Exited event.
        /// Kept here to avoid breaking the interface contract.
        /// </summary>
        public bool WaitForProcessToExit(TimeSpan timeout)
        {
            return _proc.WaitForExit((int)timeout.TotalMilliseconds);
        }
        /// <summary>
        /// Client class consumable event to know the the process actually terminated.
        /// </summary>
        public event Action ProcessExited;
        /// <summary>
        /// Process Exited event handler.
        /// </summary>
        private void OnProcessExited(object sender, EventArgs e)
        {
            // Get a reference to the actual Process object.
            var proc = sender as Process;
            if (proc is null) return;

            proc.Exited -= OnProcessExited;

            if (proc.ExitCode == 0)
            {
                // Log.TraceInfo("Application process exited gracefully");
            }
            else
            {
                // Log.DeveloperWarning("Application process exited unexpectedly with code {0}", proc.ExitCode);
                ProcessExited?.Invoke();
            }
        }
    }
FandangoOnCore
  • 269
  • 2
  • 9