当Powershell ISE退出时如何清理DLL中的资源 - 比如New-PSSession和New-PSJob

时间:2014-01-30 10:47:16

标签: c# powershell powershell-ise

我正在尝试在C#中实现一组cmdlet,用于创建和管理外部资源,其模式与New-PSSession,Get-PSSession,Remove-PSSession类似。

创建的对象需要在自己的线程上工作,这是可以取消的。

如果我明确Dispose()每个对象,它工作正常。但是如果我创建了一些对象,然后在没有Disposing的情况下退出Powershell ISE,powershell_ise.exe进程会一直挂起,直到我在任务管理器中将其终止。

我可以通过在Powershell ISE中调用Register-EngineEvent来解决这个问题,但这有点像黑客,因为DLL无法封装自己的清理工作。

我尝试了两种方法:

  1. 添加DomainUnload事件处理程序
  2. 使用RunspaceInvoke
  3. 从DLL内部调用Register-EngineEvent

    下面的代码重现了这个问题:

    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());
            }
        }
    }
    

    要查看此操作,请编译为Test.dll,运行powershell_ise.exe,然后运行...

    import-module Test.dll
    
    $thing = New-Thing
    
    exit
    

    Powershell ISE窗口将关闭,但由于Thing仍在运行,因此进程仍然存在。这只发生在ISE上,而不是常规的powershell.exe(当它退出时可能更残酷?)。

    如果在退出之前执行以下操作,那么它可以正常工作。

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

    如何让DLL注册自己的清理?

    我使用的是带有.NET 4.5和Powershell 3.0的Windows 7 x64

1 个答案:

答案 0 :(得分:1)

您可以将线程设置为后台线程,并且应该允许ISE退出,尽管您的线程将被停止 - 而不是正常关闭IIRC。另一种选择是为你的班级添加终结者。请注意,除非您有终结器,否则您不需要拨打GC.SuppressFinalize(this);,所以也许你确实有一个,但它没有在上面列出?