Search code examples
c#.net-coreui-automation.net-5microsoft-ui-automation

Detect and restart a crashed windows app using .NET core


We have few 3rd party vendor supplied apps that crash quite often and since we don't have source code for it, we can't really fix it properly. So I have decided to create a .NET core 5 worker service that would monitor those apps and restart them as needed.

How do I detect if this app has crashed because the app itself doesn't close but an error window comes up. The app still shows up on the Processes tab in Task manager. The indication of it being crashed comes from the message in the window dialog.

I just need to grab the error message on the error window dialog for logging, close both error window and the app and finally start the app again. The app is old; possibly a winforms app.

Any guidance on how to do it in .NET core would be greatly appreciated.

Thank You!

enter image description here


Solution

  • Updated to use UIAutomation

    as suggested by @Simon Mourier and @Ben Voigt in the comments. Much Thanks!

    This is how I made it to work. Please feel free to offer suggestions if it can be made better.

    Make sure to add this in the .NET core .csproj file to be able to use using System.Windows.Automation; namespace:

      <ItemGroup>
        <FrameworkReference Include="Microsoft.WindowsDesktop.App" />
      </ItemGroup>
    

    Now the main Program.cs:

    class Program
    {
        private const int ThreadDelay = 5000;
        public static async Task Main(string[] args)
        {
            var appsFromAppSettings = new List<WatchedApp>
            {
                new WatchedApp()
                {
                    AppName = "TMWMPoll",
                    NumberOfInstances = 1,
                    AppWindowName = "(4650) Test PNET Poller (3) ELogs",
                    ErrorWindowName = "TMW MobileComm Xfc",
                    AppLocation = @"C:\Users\source\repos\TMWMPoll\publish\setup.exe"
                }
            };
    
            // I'm using Hashset, because I do not want to add duplicate items to the list.
            var appsToRestart = new HashSet<WatchedApp>();
    
            while (true)
            {
                var processArray = Process.GetProcesses();
    
                //Step 1: Handle the errored out apps
                foreach (var app in appsFromAppSettings)
                {
                    var process = processArray.FirstOrDefault(p => p.ProcessName == app.AppName);
    
                    // See if the app is even running:
                    if (process == null)
                    {
                        Console.WriteLine($"Couldn't find the app: '{app.AppName}' to be running. A new instance will be opened for it.");
                        appsToRestart.Add(app);
                        continue;
                    }
    
                    // Get the main window of the process we're interested in:
                    AutomationElement appMainWindow = AutomationElement.FromHandle(process.MainWindowHandle);
                    if (appMainWindow == null)
                    {
                        Console.WriteLine($"Couldn't find the app window for: {app.AppName}.");
                        continue;
                    }
    
                    // Check if it is being opened as a Window. If it is, then it should implement the Window pattern.
                    object pattern;
                    if (!appMainWindow.TryGetCurrentPattern(WindowPattern.Pattern, out pattern))
                    {
                        continue;
                    }
    
                    // Cast the pattern object to WindowPattern
                    var window = (WindowPattern)pattern;
    
                    // Get all the child windows.
                    // Because if there is a child window, the app could have errored out so we'll be restarting the app to be safe.
                    var childElements = appMainWindow.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window));
                    if (childElements.Count > 0)
                    {
                        foreach (AutomationElement childElement in childElements)
                        {
                            // Check if it is being opened as a Window. If it is, then it should implement the Window pattern.
                            if (!childElement.TryGetCurrentPattern(WindowPattern.Pattern, out pattern))
                            {
                                continue;
                            }
    
                            // // Cast the pattern object to WindowPattern
                            var childWindow = (WindowPattern)pattern;
    
                            // Now read the error message in there:
                            var errorMessage = childElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Text))?.Current.Name;
                            Console.WriteLine($"This is the error to log: {errorMessage}");
                            childWindow.Close();
                        }
    
                        // This app will need to be restarted, so make note of it.
                        appsToRestart.Add(app);
                        // Finally kill the process after all that logging from those child windows.
                        process.Kill();
                    }
                }
    
                //Step 2: Handle the apps that didn't start or were crashed (by comparing with the processArray)
                var notRunningApps = appsFromAppSettings
                                    .Where(aps => !processArray
                                                    .Select(pa => pa.ProcessName)
                                                    .Contains(aps.AppName))
                                    .ToList();
    
                // Now create the final list of apps for us to open:
                appsToRestart.UnionWith(notRunningApps);
    
                // Now open all those apps.
                if (appsToRestart.Any())
                {
                    Console.WriteLine("Some required apps either crashed or were not running, so starting them now.");
                    foreach (var notRunningApp in appsToRestart)
                    {
                        //Start the app now
                        for (int i = 1; i <= notRunningApp.NumberOfInstances; i++)
                        {
                            Process.Start(notRunningApp.AppLocation);
                        }
                    }
                }
    
                // Now clear the hashset for appsToRestart before the next run
                appsToRestart.Clear();
    
                // Poll every ThreadDelay microseconds.
                await Task.Delay(ThreadDelay);
            };
        }
    }
    

    The WatchedApp record:

    //In a record type, you can't change the value of value-type properties or the reference of reference-type properties. 
    public record WatchedApp
    {
        public string AppName { get; init; }
        public sbyte NumberOfInstances { get; init; }
        public string AppWindowName { get; init; }
        public string ErrorWindowName { get; init; }
        public string AppLocation { get; init; }
    }