-1

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

Ash K
  • 1,802
  • 17
  • 44
  • 1
    Maybe poll for a window with that tittle. Also if this is a .net app and your supplier is totally unaccommodating (and your legally allowed to), there are disassemble and recompilation options – TheGeneral Jun 25 '21 at 22:34
  • @TheGeneral Do you have any example snippet on how to poll for a window and read messages on it in .NET? Thank you! – Ash K Jun 28 '21 at 13:10
  • 1
    You can use UI Automation https://learn.microsoft.com/en-us/dotnet/framework/ui-automation/ui-automation-overview, for example a similar question: https://stackoverflow.com/questions/24480596/how-do-i-get-access-to-a-messagebox-through-wpf-automation-api – Simon Mourier Jul 01 '21 at 05:23
  • 1
    Also https://stackoverflow.com/questions/7926107/force-to-close-messagebox-programmatically – Simon Mourier Jul 01 '21 at 05:28
  • @SimonMourier Thank you so much for those awesome examples. Can you take a look at my updated answer to see if it looks good. Any suggestions would be greatly appreciated. – Ash K Jul 01 '21 at 19:33

1 Answers1

0

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; }
}
Ash K
  • 1,802
  • 17
  • 44
  • 1
    That will work for traditional Win32 apps using e.g. `MessageBox()` to display the error. I would suggest using the `System.Windows.Automation` namespace instead... it doesn't require p/invoke and should work with apps that use newer UI frameworks like WPF. There's an example here: https://learn.microsoft.com/en-us/archive/msdn-magazine/2008/february/test-run-the-microsoft-ui-automation-library – Ben Voigt Jun 30 '21 at 20:54
  • @BenVoigt Thank you so much for the link. Can you take a look at my updated answer to see if it looks good. Any suggestions would be greatly appreciated. – Ash K Jul 01 '21 at 19:36
  • 1
    That looks like the right idea. You may wish to loop across all text elements in the error popup in case there's more than one. And you probably also want to check the title of the popup window, either to log it or to differentiate errors from normal dialog boxes. But I would say you're doing it right. – Ben Voigt Jul 01 '21 at 19:40
  • @BenVoigt: I have a follow up kind of question to this. Can you please take a look? https://stackoverflow.com/questions/68323725/identify-type-of-messagebox-window-shown-by-winform-app-using-net-core – Ash K Jul 13 '21 at 19:52