PowerShell如何完全卸载模块?

48

我正在调试一个 Powershell 项目。我使用 Import-Module 从我的 C# dll 中加载 PS 模块,一切正常。但是调用Remove-Module并不能完全卸载模块,因为 DLL 仍然被锁定,无法删除。

有没有办法让 PSH 完全卸载模块,释放 DLL,以便我可以复制它并再次使用Import-Module重新加载它,而不必重新启动 PSH 控制台?

更新:
如果将模块加载到单独的 AppDomain 中,它是否仍像普通模块一样工作?有人能提供示例吗?


1
我也一直在探索这个问题——我想将一个NuGet包作为PowerShell插件分发,为了允许包的更新,它需要能够删除旧的包版本(其中包含支持的dll文件)。我认为将函数封装在命令行exe中可能是最好的选择。:( - David Faivre
10个回答

35

有一个解决方法。打开另一个PowerShell实例:

PS > powershell
PS > [load DLL]
PS > [do work]
PS > exit

退出后,您将返回到调用此命令的 PowerShell 实例(假设您在 PowerShell 实例内部调用了 powershell 命令)。您可以传递任何正常的参数给 powershell 命令,因此您可以使用 -Command 或 -File。例如:

PS > powershell -Command '[load DLL]; [do work]' # Executes a command and exits
PS > powershell -Command '.\myscript.ps1 param1 param2' # Executes the script and exits
PS > powershell -File .\myscript.ps1 param1 param2 # Executes a script and exits.
当PowerShell退出时,它将释放对DLL的锁定,允许您继续工作。所有这些都是从PowerShell命令行界面完成的。我没有测试如果在脚本中间抛出powershell会发生什么或者这是否适用于ISE。(我认为在ISE中可以使用。)即使在脚本内部不起作用,这在开发过程中仍然很有用。
编辑:进行了一些检查。所以似乎从脚本和ISE中都可以正常工作,但是在ISE中有一���注意点。从ISE中,当您位于单独的PowerShell进程中时,您无法读取来自用户的任何输入。如果尝试这样做,则脚本或命令停止等待,但不像平常那样显示输入框,当然,您也无法直接在ISE的输出窗口中键入。因此,如果需要在[执行工作]之间提示输入,请在启动新的PowerShell实例之前提示,并将其作为参数传递到工作中。如果您正在使用常规的PowerShell命令行,则这些问题根本就不存在。

1
晚了几年,但类似的解决方法是在后台作业中完成工作。 Powershell后台作业作为单独的进程运行。您仍然无法读取用户输入,但比生成新的Powershell实例更灵活一些。 - JamesQMurphy
@JamesQMurphy 后台作业看起来像是异步操作的设计。如果那正是你需要的,那听起来是个不错的选择,但如果不是的话,在可以直接运行该进程的情况下,会增加很多复杂度。我有没有看到其他好处? - jpmc26
3
我能想到两个好处:首先,你可以将脚本块传递给作业,这样你就可以将脚本保存在一个源文件中。第二,你可以将(可序列化的)对象作为参数传递给作业,而不仅仅是文本参数。这类似于你的解决方法,所以我没有将它发布为单独的答案。 - JamesQMurphy

18

不可以。由于PowerShell在底层使用.NET,因此具有相同的要求。如果没有卸载应用程序域,就无法从.NET AppDomain中卸载dll。由于PowerShell UI位于同一应用程序域中,因此这是不可能的。


12

我在这里看到了一些可行的答案,但这是我的答案,以防这对某些人仍然是个问题(而且这非常简单,很好)。

Enter-PSSession -localcomputername
[load dlls]
[execute script(s)]
Exit-PSSession

简而言之,为本地计算机创建PSSession会创建一个不同的PowerShell会话,包括被视为“已加载”的内容,当您退出时,它会为您清理这些内容。


FYI,除非您首先配置winrm,否则此操作似乎无法在PowerShell Core(在Windows 10上测试了7.0.3)中运行。这可以使用命令“winrm quickconfig”或“Enable-PSRemoting -Force”来完成(强制跳过确认)。 - rbleattler

8
在Cmdlet开发的背景下,如果遇到无法卸载DLL的问题,我会采用两种方法。
首先,我在Visual Studio中进行开发,并设置一个外部程序(PowerShell)来加载我的Cmdlet。这样,在开始调试时加载我的模块,在停止调试时卸载我的模块。
其次,对于那些我知道要加载模块、做一些工作并确保模块在之后被卸载的情况,我使用第二个PowerShell实例。这已经在其他答案中讨论过了,下面的答案展示了我如何通过在我的Profile中使用一个带有别名的函数来启用此工作流程。我更改提示,以便我可以在“递归PowerShell窗口”中有一个视觉提醒。 在您的配置文件中创建一个脚本来启动PowerShell
function Start-DebugPowerShell
{
    PowerShell -NoProfile -NoExit -Command {
        function prompt {
            $newPrompt = "$pwd.Path [DEBUG]"
            Write-Host -NoNewline -ForegroundColor Yellow $newPrompt
            return '> '
        }
    }
}
Set-Alias -Name sdp -Value Start-DebugPowerShell

编辑Cmdlet项目的调试设置

启动外部程序:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

命令行参数:

-NoProfile -NoExit -Command "Import-Module .\MyCoolCmdlet.dll"

调试您的模块

现在从Visual Studio开始调试器,使用F5,然后您就有了一个新的PowerShell窗口,其中加载了您的Cmdlet,并且您可以按照自己的喜好进行调试。

从任何PowerShell窗口使用“sdp”别名

由于Start-DebugPowerShell函数在我们的配置文件中并且我们给它起了一个别名sdp,因此您可以随时使用它来启动第二个PowerShell实例。


1
我可以确认,这个答案中的Visual Studio调试部分是解决这个问题的非常好的解决方案。 - Andrew Keeton

5
我认为这也适用于PowerShell:在.NET世界中,卸载程序集的唯一方法是将其加载到不同的AppDomain中;一旦程序集加载到AppDomain中,它将保持加载状态,直到该AppDomain的生命周期结束。
这里有一个示例,来自一个询问几乎相同问题并展示了几种创建和将模块加载到新的AppDomain中的方法的线程:http://www.eggheadcafe.com/conversation.aspx?messageid=30789124&threadid=30766269

我本来就预料到可能会是这种情况,但我认为你不能以这种方式加载并与 PSH 模块进行交互。因此,真正的答案很可能是我想要实现的不可能。 - Eric Schoonover
可以使用AppDomain影子复制功能加载和卸载DLL,并替换DLL。http://www.codeproject.com/Articles/633140/MEF-and-AppDomain-Remove-Assemblies-On-The-Fly - John Baughman
答案中的链接已失效。 - Slogmeister Extraordinaire

3

我曾经遇到过同样的问题,最终我将想要加载的DLL包装在一个命令行exe文件中,并从脚本中调用它。这样,我完全避免了在我的应用程序中加载DLL。


遗憾的是,我认为我最终也可能会这样做。 - David Faivre

3
我使用一个简单的脚本来重命名目标DLL并将其作为模块加载。这里有两个技巧:
  1. 当从.NET程序集对象中加载模块时,我们得到了名为“dynamic_code_module_FirstPowershellModule”的已加载模块
  2. 因此,在导入之前,我们卸载此模块并从重命名后的文件创建新模块
以前的程序集在域中未被使用。
每次项目重建后都必须运行脚本。
Get-Module -Name "*FirstPowershellModule*" | Remove-Module
$ii++
$destPath = "D:\Dev\FirstPowershellModule\FirstPowershellModule\bin\Debug\FirstPowershellModule" + $ii+ ".dll"
Copy-Item D:\Dev\FirstPowershellModule\FirstPowershellModule\bin\Debug\FirstPowershellModule.dll -Destination $destPath
$ass = [System.Reflection.Assembly]::LoadFile($destPath)
import-module -Assembly $ass

1

复制 DLL 并加载该副本。您可以重新加载 DLL。


0

PS模块是.net程序集,当你使用Import-Module命令时,会将它们加载到PowerShell主机(应用程序)的AppDomain中。而Remove-Module命令则只会从当前会话中删除模块。

根据MSDN, http://msdn.microsoft.com/en-us/library/ms173101(v=vs.80).aspx

没有办法卸载单个程序集而不卸载包含它的所有应用程序域。请使用AppDomain的Unload方法来卸载应用程序域。有关详细信息,请参阅卸载应用程序域。

你可以在新的AppDomain中启动一个新的PowerShell主机,将你的模块导入到主机中并执行PowerShell任务。该模块与之前运行的主机一样正常。唯一的区别是它在运行于不同AppDomain的主机中。


0

我有一个外部模块(ImportExcel),我想像这样更新它,但是没有成功。对于这种情况,Install-Module -Scope CurrentUser <MyExternalModuleName>(如果它在您的用户模块文件夹中,则省略-Scope ...参数)可以解决问题。

(他们如何在内部管理它-我不知道,但您可以保持当前的PS会话。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接