从非托管进程中卸载 .NET DLL

Posted

技术标签:

【中文标题】从非托管进程中卸载 .NET DLL【英文标题】:Unload a .NET DLL from an unmanaged process 【发布时间】:2015-04-07 21:54:52 【问题描述】:

我正在扩展我的 Inno-Setup 脚本,其中包含在托管 DLL 中最好用 C# 实现的代码。我已经知道如何将托管 DLL 中的方法导出为用于非托管进程的函数。这可以通过 IL weaving 来完成,并且有一些工具可以自动执行此操作:

NetDllExport(我写的) UnmanagedExports

所以在导出后,我可以在 Inno-Setup 安装程序中从 Pascal 脚本调用我的函数。但是还有一个问题:DLL 似乎无法再卸载了。使用 Inno-Setup 的 UnloadDLL(...) 无效,并且文件保持锁定状态,直到安装程序退出。因此,安装程序会等待 2 秒,然后无法从临时目录(或安装目录)中删除我的 DLL 文件。事实上,它确实会一直留在那里,直到有人清理驱动器。

我知道托管程序集不能再从 AppDomain 中卸载,除非整个 AppDomain 被关闭(进程退出)。但这对非托管主机进程意味着什么?

有没有更好的方法让 Inno-Setup 在加载和使用后卸载或删除我的 DLL 文件?

【问题讨论】:

顺便说一句,.NET 是 Windows 操作系统组件已有一段时间了,无论如何只支持最新版本。所以 .NET 应该已经存在,不应该消失,甚至可能被认为在 Windows 上无处不在。 【参考方案1】:

正如其他答案中所建议的,您可以在安装结束后启动一个单独的进程,该进程将在安装过程完成后负责清理。

一个简单的解决方案是创建一个临时批处理文件,该文件循环直到可以删除 DLL 文件,然后还删除(现在为空的)临时文件夹及其本身。

procedure DeinitializeSetup();
var
  FilePath: string;
  BatchPath: string;
  S: TArrayOfString;
  ResultCode: Integer;
begin
  FilePath := ExpandConstant('tmp\MyAssembly.dll');
  if not FileExists(FilePath) then
  begin
    Log(Format('File %s does not exist', [FilePath]));
  end
    else
  begin
    BatchPath :=
      ExpandConstant('%TEMP\') +
      'delete_' + ExtractFileName(ExpandConstant('tmp')) + '.bat';
    SetArrayLength(S, 7);
    S[0] := ':loop';
    S[1] := 'del "' + FilePath + '"';
    S[2] := 'if not exist "' + FilePath + '" goto end';
    S[3] := 'goto loop';
    S[4] := ':end';
    S[5] := 'rd "' + ExpandConstant('tmp') + '"';
    S[6] := 'del "' + BatchPath + '"';
    if not SaveStringsToFile(BatchPath, S, False) then
    begin
      Log(Format('Error creating batch file %s to delete %s', [BatchPath, FilePath]));
    end
      else
    if not Exec(BatchPath, '', '', SW_HIDE, ewNoWait, ResultCode) then
    begin
      Log(Format('Error executing batch file %s to delete %s', [BatchPath, FilePath]));
    end
      else
    begin
      Log(Format('Executed batch file %s to delete %s', [BatchPath, FilePath]));
    end;
  end;
end;

【讨论】:

【参考方案2】:

您可以添加一个批处理脚本(以运行 cmd -c 的形式)在安装结束时执行,等待文件可删除并将其删除。 (只需确保将 inno 选项设置为不等待 cmd 进程完成)

您还可以让已安装的程序在首次执行时检测并删除它。

【讨论】:

【参考方案3】:

正如本代码项目文章中所建议的那样:https://www.codeproject.com/kb/threads/howtodeletecurrentprocess.aspx

使用如下所示的参数调用 cmd。

 Process.Start("cmd.exe", "/C ping 1.1.1.1 -n 1 -w 3000 > Nul & Del " +  Application.ExecutablePath);

但基本上正如@Sean 建议的那样,请确保您不要等待 cmd.exe 在脚本中退出。

【讨论】:

为了保证可靠性,您必须等待安装程序完成。等待固定时间并不可靠。【参考方案4】:

虽然不能完全回答您的问题,但您不能将 DLL 标记为在下次重新启动计算机时删除吗?

【讨论】:

如果我没记错的话,你应该把它作为评论添加到问题本身 虽然这是一种可能的解决方法,但它并不理想,没有回答问题,也没有解释首先卸载 DLL 失败的原因。【参考方案5】:

这就是我所做的,改编自 Martin 的出色回答。注意“睡眠”,这对我有用。因为执行是在后台线程中调用的,所以它不是阻塞器,并且为 InnoSetup 留出了足够的时间来释放资源。 之后,我就可以清理临时文件夹了。

// Gets invoked at the end of the installation
procedure DeinitializeSetup();
var
  BatchPath: String;
  S:         TArrayOfString;
  FilesPath: TStringList;
  ResultCode, I, ErrorCode: Integer;

begin
  I := 0
  FilesPath := TStringList.Create;

  FilesPath.Add(ExpandConstant('tmp\DLL1.dll'));
  FilesPath.Add(ExpandConstant('tmp\DLL2.dll'));
  FilesPath.Add(ExpandConstant('tmp\DLLX.dll'));

  while I < FilesPath.Count do
  begin
    if not FileExists(FilesPath[I]) then
    begin
      Log(Format('File %s does not exist', [FilesPath[I]]));
    end
    else
    begin
      UnloadDLL(FilesPath[I]);
      if Exec('powershell.exe',
        FmtMessage('-NoExit -ExecutionPolicy Bypass -Command "Start-Sleep -Second 5; Remove-Item -Recurse -Force -Path %1"', [FilesPath[I]]),
        '', SW_HIDE, ewNoWait, ErrorCode) then
      begin
        Log(Format('Temporary file %s successfully deleted', [ExpandConstant(FilesPath[I])]));
      end
      else
      begin
        Log(Format('Error while deleting temporary file: %s', [ErrorCode]));
      end;
    inc(I);
    end;
  end;

  Exec('powershell.exe',
    FmtMessage('-NoExit -ExecutionPolicy Bypass -Command "Start-Sleep -Second 5; Remove-Item -Recurse -Force -Path %1"', [ExpandConstant('tmp')]),
    '', SW_HIDE, ewNoWait, ErrorCode);
  Log(Format('Temporary folder %s successfully deleted', [ExpandConstant('tmp')]));
end;    

【讨论】:

【参考方案6】:

做你想做的事的简单方法是通过 AppDomain。您可以卸载 AppDomain,而不是最初的。所以解决方案是创建一个新的 AppDomain,在其中加载托管 DLL,然后卸载 AppDomain。

        AppDomain ad = AppDomain.CreateDomain("Isolate DLL");
        Assembly a = ad.Load(new AssemblyName("MyManagedDll"));
        object d = a.CreateInstance("MyManagedDll.MyManagedClass");
        Type t = d.GetType();
        double result = (double)t.InvokeMember("Calculate", BindingFlags.InvokeMethod, null, d, new object[]  1.0, 2.0 );
        AppDomain.Unload(ad);

这是 DLL 代码的样子...

namespace MyManagedDll

   public class MyManagedClass
   
      public double Calculate(double a, double b)
      
        return a + b;
      
   

【讨论】:

这与 OP 无关。 OP 希望在 .NET 中实现对现有非托管应用程序的扩展。 OP 无法创建新的 AppDomain,因为他/她不控制 DLL 加载过程。 确实是相关的。他可以访问托管 DLL。该托管 DLL 只是创建一个应用程序域并加载另一个托管 DLL,然后将其卸载。所以我的回答中缺少的是需要 2 个托管 DLL。我只是认为这很明显。 是的,这很明显。但是 OP 的问题是:“DLL 似乎无法再卸载了。使用 Inno-Setup 的 UnloadDLL(...) 没有任何效果,并且文件保持锁定状态,直到安装程序退出。因此,安装程序等待 2 秒,然后无法从临时目录(或安装目录)中删除我的 DLL 文件。事实上,它确实一直存在,直到有人清理驱动器。“ - 你的答案无法解决这个问题问题。

以上是关于从非托管进程中卸载 .NET DLL的主要内容,如果未能解决你的问题,请参考以下文章

gcServer 设置未从非托管 exe 传递到托管 dll

如何从非托管 COM dll 生成类型库

从非托管 C++ 配置 .NET 库

从非托管 DLL 导入函数时,0x8007007F 是啥意思?

从非托管 C++ mfc active x dll 启动 C# 对话框

如何将数据从非托管应用程序传递到 C# COM DLL