Search code examples
c#.netmultithreadingsessionfreeze

SystemEvents.SessionSwitch causing my Windows Forms Application to freeze


I have a C# Windows Forms Application on .NET 4.5.

This application connects to a USB Device.

I want to support multiple sessions at the same time.

In order to do so, I need to disconnect from that device on session lock to allow the new session to connect to it.

I used SystemEvents.SessionSwitchEventArgs.Reason to detect such events: - SessionSwitchReason.ConsoleDisconnect on session switch - SessionSwitchReason.ConsoleConnect on unlock after session switch

This event seemed like the perfect solution but sometimes at random times (after a number of locks or unlocks), the event doesn't get fired and the UI freezes. It's worth noting that this doesn't happen when the application is running in the debugger.

I know from the logs that some other background threads are still working normally but the UI freezes and the subscribed function to the event doesn't get called.

A sample of my code:

Program.cs:

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MyProgram
{
    static class Program
    {
        private static Mutex mutex = null;
    [STAThread]
    static void Main()
    {
        const string appName = "MyProgram";
        bool createdNew;

        mutex = new Mutex(true, appName, out createdNew);

        if (!createdNew)
        {
            //app is already running! Exiting the application  
            return;
        }

        Application.EnableVisualStyles();

        //This was one attempt to solve the UI deadlock Microsoft.Win32.SystemEvents.UserPreferenceChanged += delegate { };

        Application.SetCompatibleTextRenderingDefault(false);
        MyProgramEngine MyProgramEngine = new MyProgram.MyProgramEngine();
        Application.Run(MyProgramEngine.getForm());
    }
}

}

MyProgramEngine:

using log4net;
using log4net.Config;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using WindowsInput;
using System.Windows.Forms;
using Microsoft.Win32;


namespace MyProgram
{
    class MyProgramEngine
    {
        private MainForm mainForm;
    public MyProgramEngine()
    {
        XmlConfigurator.Configure();
        Utility.logger.Info(string.Format("MyProgram Started. Version: {0}", Application.ProductVersion));
        SystemEvents.SessionSwitch += new SessionSwitchEventHandler(SystemEvents_SessionSwitch);
        if (!GlobalSettings.getInstance().isProperlyConfigured())
        {
            WarningForm warningForm = new WarningForm("MyProgram is not properly configured. Please contact support");
            warningForm.ShowDialog();
            Application.Exit();
            Environment.Exit(0);
        }
        mainForm = new MainForm();
        initArandomBackgroundThread();
        initDeviceThread();
    }

    private void initDeviceThread()
    {
        Thread detectAndStartReader = new Thread(initDevice);
        detectAndStartReader.IsBackground = true;
        detectAndStartReader.Start();
    }

    public void initDevice()
    {
        //Connect to device
        //Start device thread
    }

    public MainForm getForm()
    {
        return mainForm;
    }


    //Handles session switching events
    internal void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
    {
        try
        {
            if (e.Reason.Equals(SessionSwitchReason.ConsoleDisconnect))
            {
                DisconnectFromDevice();
                TerminateDeviceThread();
            }
            else if (e.Reason.Equals(SessionSwitchReason.ConsoleConnect))
            {
                initDeviceThread();
            }
            else
            {
                Utility.logger.Info("The following SesseionSwitchReason has been caught: " + e.Reason + " , No action!");
            }
        }
        catch (Exception ex)
        {
            Utility.logger.Error("Something bad happened while managing session switching events", ex);
        }

    }

}

Note: I have not interest in SessionSwitchReason.SessionUnlock or SessionSwitchReason.SessionLock because both I don't want any action on session lock and unlock on the same session.

Thanks for the support!


Solution

  • I found out where the bug was.

    In brief,

    Don't ever create a control on a background worker thread.

    In my code if I removed the SessionSwitch event subscription, the hang would still occur. I was able to trace back the wait on the main thread to SystemSettingsChanging which is also a SystemEvent but which I do not control.

    After I almost gave up on trying to figure out this hang, I went to reading the code line by line which led me to discover that a Form (pop up) was being created on a background thread.

    This part of the code didn't get my attention as showed in the sample given above.

    initArandomBackgroundThread();

    To know more about this freeze you can head to Microsoft Support for extensive explanation.

    The low level cause of such freeze as Microsoft claims is

    This occurs if a control is created on a thread which doesn't pump messages and the UI thread receives a WM_SETTINGCHANGE message.

    Common causes are a splash screens created on a secondary UI thread or any controls created on worker threads.

    The fix

    Applications should never leave Control objects on threads without an active message pump. If Controls cannot be created on the main UI thread, they should be created on a dedicated secondary UI thread and Disposed as soon as they are no longer needed.

    Debugging

    One way to identify which windows are created on which thread is with Spy++ in the Processes view (Spy.Processes menu). Select the hung process and expand its threads to see if there are any unexpected windows. This will find the native window if it still exists; however, the problem can occur even if the native window has been destroyed, so long as the managed Control has not yet been Disposed.