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

22
我正在使用Inno-Setup脚本扩展我的代码,并希望在托管的DLL中以C#最佳实现某些代码。我已经知道如何将托管的DLL中的方法导出为函数,以便在不受管控的进程中使用。这可以通过IL编织来完成,并且有一些工具可以自动化此过程: 因此,在导出后,我可以从一个Inno-Setup安装程序的Pascal脚本中调用我的函数。但是有一个问题:该DLL似乎无法被卸载。使用Inno-Setup的UnloadDLL(...)没有效果,并且文件仍然被锁定,直到安装程序退出。由于这个原因,安装程序等待2秒钟,然后无法从临时目录(或安装目录)中删除我的DLL文件。 事实上,它会一直保留在那里,直到有人清理驱动器。
我知道托管的程序集不能再从AppDomain中卸载,除非整个AppDomain关闭(进程退出)。但对于不受管控的主机进程意味着什么呢?
有没有更好的方法允许Inno-Setup在加载和使用DLL文件后卸载或删除它?

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - ygoe
6个回答

4

如其他答案所建议的那样,您可以在安装结束时启动一个单独的进程来处理清理工作,以便在安装过程完成后进行。

一个简单的解决方案是创建一个临时批处理文件,循环执行直到 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;

1

如此Code Project文章所建议的: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退出。


为了保证可靠性,您必须等待安装程序完成。固定时间的等待并不可靠。 - Martin Prikryl

1
你可以添加一个批处理脚本(以运行cmd -c形式)来等待文件可删除并删除它,确保将inno选项设置为不等待cmd进程完成即可。
你也可以让你安装的程序在第一次执行时检测并删除它。

0

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


9
如果我没错的话,您应该将这个作为一个评论添加到问题本身。 - Matías Fidemraizer
虽然这是一个可能的解决方法,但并不理想,没有回答问题,并且没有解释为什么首先卸载DLL会失败。 - caesay

0

这是我所做的,改编自Martin的好答案。请注意'Sleep',这对我很有帮助。因为执行是在后台线程中调用的,不会阻塞,并留出足够的时间让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;    

要保证可靠性,您必须等待安装程序完成。固定时间的等待并不十分可靠。与@Mohammedparvez Shaikh的回答存在相同问题。 - Martin Prikryl

-1

实现您想要的简单方法是通过一个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;
      }
   }
}

1
这与 OP 无关。OP 想要在 .NET 中实现对现有非托管应用程序的扩展。由于他/她无法控制 DLL 加载过程,因此无法创建新的 AppDomain。 - Martin Prikryl
这确实相关。他可以访问托管的 DLL。该托管的 DLL 简单地创建一个应用程序域并加载另一个托管的 DLL,然后卸载它。所以我的回答中缺少的是需要2个托管的 DLL。我只是觉得这很明显。 - AQuirky
是的,这很明显。但是OP遇到的问题是:“DLL似乎无法再卸载了。使用Inno-Setup的UnloadDLL(...)没有效果,文件仍然被锁定,直到安装程序退出。因此,安装程序等待2秒钟,然后无法从临时目录(或安装目录)中删除我的DLL文件。实际上,它一直在那里,直到有人清理驱动器。” - 你的答案无法解决这个问题。 - Martin Prikryl

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