How to cleanup resources in a DLL when Powershell ISE exits - like New-PSSession and New-PSJob

StackOverflow https://stackoverflow.com/questions/21454252

  •  04-10-2022
  •  | 
  •  

Question

I'm trying to implement a set of cmdlets in C# which create and manage external resources, with a similar pattern to New-PSSession, Get-PSSession, Remove-PSSession.

The objects created need to do work on their own threads, which is cancellable.

It works fine if I explicitly Dispose() each object. But if I create some objects, and then exit Powershell ISE without Disposing, the powershell_ise.exe process hangs around until I kill it in task manager.

I can fix this by calling Register-EngineEvent inside Powershell ISE, but that's a bit of a hack, as the DLL is failing to encapsulate its own cleanup.

I've tried two methods:

  1. Add a DomainUnload event handler
  2. Call Register-EngineEvent from inside the DLL using RunspaceInvoke

The code below reproduces the problem:

namespace Test
{
    /// <summary>
    /// An class which encapsulates some asynchronous work in a background thread.
    /// Implements IDisposable to cancel the work and cleanup.
    /// </summary>
    public class Thing : IDisposable
    {
        #region private fields

        private readonly Thread _thread;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private bool _disposed;

        #endregion

        #region static fields (for on-exit cleanup)

        private static List<Thing> _allThings = new List<Thing>();
        private static bool _registeredDisposeAll;

        #endregion

        #region public fields

        /// <summary>
        /// Simple counter to see asynchronous work happening
        /// </summary>
        public int Counter { set; get; }

        #endregion

        #region Constructor

        public Thing()
        {
            if (!_registeredDisposeAll)
            {
                // The first time this is called, register an on-exit handler
                // Neither of these work :-(

                // Attempt 1:
                // Register a DomainUnload handler
                AppDomain.CurrentDomain.DomainUnload += (o,a) => DisposeAll();

                // Attempt 2:
                // use RunspaceInvoke to call Register-EngineEvent
                using (var ri = new RunspaceInvoke())
                {
                    string script =
                        "Register-EngineEvent -sourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { [Test.Thing]::DisposeAll() }";

                    ri.Invoke(script);
                }

                _registeredDisposeAll = true;
            }

            // add this to a static registry of running Things
            _allThings.Add(this);

            // and start some asynchronous work in a thread
            _cancellationTokenSource = new CancellationTokenSource();
            var cancel = _cancellationTokenSource.Token;
            Counter = 0;
            _thread = new Thread(() =>
            {
                while (!cancel.IsCancellationRequested)
                {
                    Thread.Sleep(1000);
                    ++Counter;
                }
            });
            _thread.Start();
        }

        #endregion

        #region IDisposable

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            if (disposing)
            {
                _cancellationTokenSource.Cancel();
                _thread.Join();
                _cancellationTokenSource.Dispose();
            }
            _disposed = true;
        }

        /// <summary>
        /// Callback to destroy any remaining things        
        /// If this doesn't get called, powershell won't exit properly
        /// </summary>
        public static void DisposeAll()
        {
            foreach (var thing in _allThings)
            {
                thing.Dispose();
            }
            _allThings.Clear();
        }

        #endregion

    }

    /// <summary>
    /// A Cmdlet to create a new Thing, similar to New-PSSession or New-PSJob
    /// </summary>
    [Cmdlet(VerbsCommon.New, "Thing")]
    public class NewThing : Cmdlet
    {
        protected override void ProcessRecord()
        {
            WriteObject(new Thing());
        }
    }
}

To see this in action, compile as Test.dll, run powershell_ise.exe and then...

import-module Test.dll

$thing = New-Thing

exit

The Powershell ISE window will close, but the process hangs around because the Thing is still running. This only happens with ISE, not with regular powershell.exe (which is presumably more brutal when it exits?).

If before exiting you do the following, then it works fine.

Register-EngineEvent -sourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { [Test.Thing]::DisposeAll() }

How can i get the DLL to register its own cleanup?

I'm on Windows 7 x64 with .NET 4.5 and Powershell 3.0

Was it helpful?

Solution

You could set your thread to a background thread and that should allow ISE to exit although your thread will be stopped - not gracefully shutdown IIRC. Another option is to add a finalizer to your class. Note that you don't need the call to GC.SuppressFinalize(this); unless you have a finalizer so perhaps you do have one but it's not listed above?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top