How to make FileSystemWatcher events run in an STA Thread in a console application?

101 Views Asked by At

I'm writing a console application that scans a directory for files and uploads the data in them to a database. The files are a proprietary type that require a specific library that requires the program to be run on an STA thread. When I call my function to upload the files in an OnCreated FileSystemWatcher method, I get an error warning about needing to run in an STA Thread. This doesn't show up when I call the upload function directly in the Main() function using a direct file path.

I've done a bit of research into this and found that the FileSystemWatcher calls its events in a separate thread. It seems if you are using Winforms or WPF you can set the SynchronizingObject property to your UI element, and it will run on the main UI thread. I couldn't find any information on how this would apply to a console application, however. So basically I'm wondering if there's any way to get FileSystemWatcher to run on the main thread, or to make the secondary thread it runs the functions I've made in, run as an STA thread.

3

There are 3 best solutions below

2
Simon Mourier On BEST ANSWER

Here is a solution based on a TaskScheduler, not related to Winforms nor WPF. It allows you to use all Task-related functions and tooling:

static void Main(string[] args)
{
    // one instance only is needed
    var scheduler = new SingleThreadTaskScheduler(thread =>
    {
        // configure it for STA
        thread.SetApartmentState(System.Threading.ApartmentState.STA);
    });

    using var fsw = new FileSystemWatcher(@"c:\temp");
    fsw.Created += onEvent;
    fsw.Changed += onEvent;
    fsw.Deleted += onEvent;
    fsw.Renamed += onRenamed;
    fsw.EnableRaisingEvents = true;

    // press any key to stop
    Console.ReadKey(true);

    void onEvent(object sender, FileSystemEventArgs e)
    {
        Task.Factory.StartNew(() => { Console.WriteLine(e.FullPath);  /* do something in STA */ }, CancellationToken.None, TaskCreationOptions.None, scheduler);
    }

    void onRenamed(object sender, RenamedEventArgs e)
    {
        Task.Factory.StartNew(() => { Console.WriteLine(e.FullPath);  /* do something in STA */ }, CancellationToken.None, TaskCreationOptions.None, scheduler);
    }
}

public sealed class SingleThreadTaskScheduler : TaskScheduler, IDisposable
{
    private readonly AutoResetEvent _stop = new AutoResetEvent(false);
    private readonly AutoResetEvent _dequeue = new AutoResetEvent(false);
    private readonly ConcurrentQueue<Task> _tasks = new ConcurrentQueue<Task>();
    private readonly Thread _thread;

    public event EventHandler Executing;

    public SingleThreadTaskScheduler(Action<Thread> threadConfigure = null)
    {
        _thread = new Thread(SafeThreadExecute) { IsBackground = true };
        threadConfigure?.Invoke(_thread);
        _thread.Start();
    }

    public DateTime LastDequeue { get; private set; }
    public bool DequeueOnDispose { get; set; }
    public int DisposeThreadJoinTimeout { get; set; } = 1000;
    public int WaitTimeout { get; set; } = 1000;
    public int DequeueTimeout { get; set; }
    public int QueueCount => _tasks.Count;

    public void ClearQueue() => Dequeue(false);
    public bool TriggerDequeue()
    {
        if (DequeueTimeout <= 0)
            return _dequeue != null && _dequeue.Set();

        var ts = DateTime.Now - LastDequeue;
        if (ts.TotalMilliseconds < DequeueTimeout)
            return false;

        LastDequeue = DateTime.Now;
        return _dequeue != null && _dequeue.Set();
    }

    public void Dispose()
    {
        _stop.Set();
        _stop.Dispose();
        _dequeue.Dispose();
        if (DequeueOnDispose)
        {
            Dequeue(true);
        }

        if (_thread != null && _thread.IsAlive)
        {
            _thread.Join(DisposeThreadJoinTimeout);
        }
    }

    private int Dequeue(bool execute)
    {
        var count = 0;
        do
        {
            if (!_tasks.TryDequeue(out var task))
                break;

            if (execute)
            {
                Executing?.Invoke(this, EventArgs.Empty);
                TryExecuteTask(task);
            }
            count++;
        }
        while (true);
        return count;
    }

    private void SafeThreadExecute()
    {
        try
        {
            ThreadExecute();
        }
        catch
        {
            // continue
        }
    }

    private void ThreadExecute()
    {
        do
        {
            if (_stop == null || _dequeue == null)
                return;

            _ = Dequeue(true);

            // note: Stop must be first in array (in case both events happen at the same exact time)
            var i = WaitHandle.WaitAny(new[] { _stop, _dequeue }, WaitTimeout);
            if (i == 0)
                break;

            // note: we can dequeue on _dequeue event, or on timeout
            _ = Dequeue(true);
        }
        while (true);
    }

    protected override void QueueTask(Task task)
    {
        if (task == null)
            throw new ArgumentNullException(nameof(task));

        _tasks.Enqueue(task);
        TriggerDequeue();
    }

    protected override IEnumerable<Task> GetScheduledTasks() => _tasks;
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
}
1
JonasH On

I have not done this, and I have not used COM much either, so take this with a grain of salt.

There is a Thread.SetApartment method. As far as I can tell this needs to be called before the thread is started, so you cannot use it to change the apartment state for the thread pool thread that FileSystemWatcher will raise the event on. You probably need to create a new thread explicitly and set the apartment state on it. You may also need to use this thread for all calls to this third party library.

But you can create a simple message queue. Have your newly created STA thread run a foreach loop on a BlockingCollection.GetConsumingEnumerable(), with whatever code you want to run inside the loop, and have your fileSystemWatcher add objects to this collection whenever there is any changes. You can use Action as the object type for the collection to have the thread run arbitrary code, or a FileSystemEventArgs if you only need to process file events.

0
Eugene Mayevski 'Callback On

No need to make things overcomplicated. Use a queue to send tasks to the main thread for processing. For this, create a list of task objects. In the secondary threads that FileSystemWatcher uses, put tasks to the list. Pick-and-proccess those tasks in your main thread. Remember to use lock() on the list in order to protect it from concurrency/multithreading issues.