22
votes

I'm extending my Inno-Setup script with code that I can best implement in C# in a managed DLL. I already know how to export methods from a managed DLL as functions for use in an unmanaged process. It can be done by IL weaving, and there are tools to automate this:

So after exporting, I can call my functions from Pascal script in an Inno-Setup installer. But then there's one issue: The DLL can't seem to be unloaded anymore. Using Inno-Setup's UnloadDLL(...) has no effect and the file remains locked until the installer exits. Because of this, the setup waits for 2 seconds and then fails to delete my DLL file from the temp directory (or install directory). In fact, it really stays there until somebody cleans up the drive.

I know that managed assemblies cannot be unloaded from an AppDomain anymore, unless the entire AppDomain is shut down (the process exits). But what does it mean to the unmanaged host process?

Is there a better way to allow Inno-Setup to unload or delete my DLL file after loading and using it?

6
BTW, .NET is a Windows operating system component for some time now, and only the latest version is supported anyway. So .NET should already be there, should not go away, and might even be considered ubiquitous on Windows. - ygoe

6 Answers

4
votes

As suggested in other answers, you can launch a separate process at the end of the installation that will take care of the cleanup, after the installation processes finishes.

A simple solution is creating an ad-hoc batch file that loops until the DLL file can be deleted and then also deletes the (now empty) temporary folder and itself.

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
votes

You could add a batch script (in the form of running cmd -c) to be executed at the end of setup that waits for the file to be deletable and deletes it. (just make sure to set the inno option to not wait for the cmd process to complete)

You could also make your installed program detect and delete it on first execution.

1
votes

As suggested in this Code Project Article : https://www.codeproject.com/kb/threads/howtodeletecurrentprocess.aspx

call a cmd with arguments as shown below.

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

But basically as @Sean suggested, make sure you dont wait for the cmd.exe to exit in your script.

0
votes

While not exactly an answer to your question, can't you just mark the DLL to be deleted next time the computer is restarted?

0
votes

Here's what I did, adapted from Martin's great answer. Notice the 'Sleep', this did the trick for me. Because the execution is called in a background thread, that is not a blocker, and leaves sufficient time for InnoSetup to free up the resources. After doing that, I was able to clean the temporary folder.

// 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;    
-1
votes

The easy way to do what you want is through an AppDomain. You can unload an AppDomain, just not the initial one. So the solution is to create a new AppDomain, load your managed DLL in that and then unload the 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);

Here is what the DLL code looks like...

namespace MyManagedDll
{
   public class MyManagedClass
   {
      public double Calculate(double a, double b)
      {
        return a + b;
      }
   }
}