2
votes

I will try to be as brief as possible without attaching all the related source files. I have tracked down the issue as much as my Pascal knowledge allows me...

I thing I found a disk caching problem that occurs, for my case, at step ssInstall. I have an installer for an app that, if it finds an older app version installed, it will invoke an uninstallation like this:

procedure CurStepChanged(CurStep: TSetupStep);
var
  uninstallStr: String;
  ResultCode: Integer;
begin
  if (CurStep = ssInstall) and IsUpdatableApplicationInstalled() then
  begin
  uninstallStr := GetUninstallString();
  uninstallStr := RemoveQuotes(uninstallStr);
  Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
  if Result and (ResultCode = 0) then
    Log('CurStepChanged = ssInstall; uninstall OK');
  //-------------
  //Sleep(30000);
  //-------------
end;

Also the folders/files are defined like this:

[Dirs]
Name: "{app}\db"; Flags: uninsalwaysuninstall

[Files]
Source: "..\bin\*"; DestDir: "{app}\bin"; Flags: ignoreversion createallsubdirs recursesubdirs
Source: "..\java\jre\*"; DestDir: "{app}\jre"; Flags: ignoreversion recursesubdirs createallsubdirs
blah...

Test case1; Normal installation: Everything goes smoothly. Log file part:

Starting the installation process.
Creating directory: C:\Program Files                               <---
Creating directory: C:\Program Files\MyApp                         <---
Creating directory: C:\Program Files\MyApp\db                      <---
Creating directory: C:\Program Files\MyApp\jre                     <---
Creating directory: C:\Program Files\MyApp\jre\lib
Creating directory: C:\Program Files\MyApp\jre\lib\applet
Directory for uninstall files: C:\Program Files\MyApp
Creating new uninstall log: C:\Program Files\MyApp\unins000.dat    <--- !!!
-- File entry --
Dest filename: C:\Program Files\MyApp\unins000.exe                 <--- !!!
blah...

Test case2; Update old version: When getting to step ssInstall, the uninstaller launches, it finishes then the installation begins. Log file part:

CurStepChanged = ssInstall; uninstall OK
Starting the installation process.
Creating directory: C:\Program Files\MyApp\jre\lib
Creating directory: C:\Program Files\MyApp\jre\lib\applet
Directory for uninstall files: C:\Program Files\MyApp
Creating new uninstall log: C:\Program Files\MyApp\unins001.dat    <--- !!!
-- File entry --
Dest filename: C:\Program Files\MyApp\unins001.exe                 <--- !!!
blah...

As you can see some folders do not get created and my app fails later on when it tries to write to 'db' folder.

If I uncomment the Sleep() command, everything runs smoothly and both the log files are identical.

It seems the disk has enough time to flush the changes! Somehow there must be a flush() command missing in inno-setup.

Can any of the inno-gurus comment or help somehow? Is there a flush() i could call, instead of the sleep()? Any help is appreciated. I just want to be sure before I file a bug request.

3
How are those directories created ? From a [Dirs] section ? If not, then when and how do you create them ? Could you include this information with a code related to the directory creation into your question, please ? - TLama
Thanks. Yet one more thing. That Exec you're calling with ewWaitUntilTerminated wait type ? - TLama
I completed Exec() params; pls see above... - fubar
I believe you're correct in your analysis. Inno waits until the OS signals for the handle of the process that's executed with Exec, then takes one last round of message processing. This is in HandleProcessWait in 'InstFunc.pas' of Inno sources. It's at OS' dicretion, however, when to actually remove the deleted files etc.. I think there could be a function that would flush disk(s) cache. Until then you might try your own. Import and call FlushFileBuffers passing a handle for the volume(s) involved. Naturally this would require admin access. Can't guess if it would be trivial right now.. - Sertac Akyuz
If you are running the uninstaller from {app} then these files/folders cannot be deleted until the uninstaller actually exits. Inno's uninstaller manages this in such a way that it can delete these folders but it will still result in your Exec call terminating slightly before the uninstaller has actually completed its job. As such waiting for it to "really" finish (which is basically what your Sleep call is doing) will be required before you proceed to start the installation. Which is another reason why uninstalling first is discouraged. - Miral

3 Answers

4
votes

Just to sum up the comment trail on the question:

Don't Uninstall

The best solution is to not run the uninstaller at all. You can remove redundant files via the [InstallDelete] section; eg. to remove a "jre" subfolder completely (to be replaced as part of your installation) then do this:

[InstallDelete]
Type: filesandordirs; Name: "{app}\jre"

(Only use this for subfolders like this and only sparingly; you can get yourself in trouble if you delete too much stuff.)

For normal individual application files installed by a previous version that are now redundant, you can remove them like so:

[InstallDelete]
Type: files; Name: "{app}\redundant.dll"

(You'll need to do something slightly fancier if they had "regserver" or "sharedfile" when installed.)

If you Uninstall regardless, wait longer

The uninstaller cannot delete itself or the folder where it is located while it is still running. While Inno does take care of this in such a way that it is able to delete the uninstaller and folder, it does mean that your Exec call to the uninstaller will return before this deletion has occurred and before the uninstall process actually finishes.

You will need to wait longer after the Exec for the uninstall to actually finish before you let it continue with the installation. Using a Sleep is simple enough and will work in most cases but if you want the best results you'll need to call into the WinAPI to check the running processes list.

Additionally, you should use the PrepareToInstall event function to perform the actual uninstallation. This will better allow you to handle cases such as uninstall errors or when a reboot is required between uninstall and reinstall. (And because it executes at the "right" time in the installation process.)

4
votes

This is how I actually worked around this issue. I am posting my solution in hope to help others with the same problem.

Big thanks to all that helped out and especially to Miral for pointing me in the right direction!

The solution is rather simple; wait until the uninstaller exe is deleted!

const
  DELAY_MILLIS = 250;
  MAX_DELAY_MILLIS = 30000;


function GetUninstallString(): String;
var
  uninstallPath: String;
  uninstallStr: String;
begin
  uninstallPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
  uninstallStr := '';
  if not RegQueryStringValue(HKLM, uninstallPath, 'UninstallString', uninstallStr) then
    RegQueryStringValue(HKCU, uninstallPath, 'UninstallString', uninstallStr);
  Result := RemoveQuotes(uninstallStr);
end;


function ForceUninstallApplication(): Boolean;
var
  ResultCode: Integer;
  uninstallStr: String;
  delayCounter: Integer;
begin
  // 1) Uninstall the application
  Log('forcing uninstall of application);
  uninstallStr := GetUninstallString();
  Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES /LOG', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
  if not Result then
  begin
    Log('application uninstall failed!');
    Exit
  end;
  Log('application uninstalled!');

  // 2) Be sure to wait a while, until the actual uninstaller is deleted!
  Log('waiting a while until uninstaller changes are flushed in the filesystem...');
  delayCounter := 0;
  repeat
    Sleep(DELAY_MILLIS);
    delayCounter := delayCounter + DELAY_MILLIS;
  until not FileExists(uninstallStr) or (delayCounter >= MAX_DELAY_MILLIS);
  if (delayCounter >= MAX_DELAY_MILLIS) then
    RaiseException('Timeout exceeded trying to delete uninstaller: ' + uninstallStr);
  Log('waited ' + IntToStr(delayCounter) + ' milliseconds');
end;

Function ForceUninstallApplication() can be successfuly called from either PrepareToInstall or CurStepChanged(ssInstall). For my case it takes about 500 millis when I use my SSD hard disk and maybe a couple of seconds when I use my external usb hard disk.

0
votes

Thank you to fubar for this answer ! I just made a little modification in order to avoid the same problem with the install folder which is delete during uninstall process. It could be deleted after the call to ForceUninstallApplication() and avoid the creation of the install folder.

To avoid this problem, I just create a temp file in the install folder and remove when installation in finish. When a unknow file is present folder, unins000.exe don't delete the folder.

Here the fubar code updated:

const
  DELAY_MILLIS = 250;
  MAX_DELAY_MILLIS = 30000;

var
  tempUninstallFilename: String;


function GetUninstallString(): String;
var
  uninstallPath: String;
  uninstallStr: String;
begin
  uninstallPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
  uninstallStr := '';
  if not RegQueryStringValue(HKLM, uninstallPath, 'UninstallString', uninstallStr) then
    RegQueryStringValue(HKCU, uninstallPath, 'UninstallString', uninstallStr);
  Result := RemoveQuotes(uninstallStr);
end;


function ForceUninstallApplication(): Boolean;
var
  ResultCode: Integer;
  uninstallStr: String;
  delayCounter: Integer;
begin
  // 1) Create a temporary file to avoid destruction of install folder during uninstall.
  uninstallStr := RemoveQuotes(GetUninstallString());
  tempUninstallFilename := ExtractFileDir(uninstallStr) + '\uninstall.log';
  SaveStringToFile(tempUninstallFilename, '...', False);
  Log('Create temp file: ' + tempUninstallFilename);

  // 2) Uninstall the application
  Log('forcing uninstall of application);
  uninstallStr := GetUninstallString();
  Result := Exec(uninstallStr, '/SILENT /NORESTART /SUPPRESSMSGBOXES /LOG', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
  if not Result then
  begin
    Log('application uninstall failed!');
    Exit
  end;
  Log('application uninstalled!');

  // 2) Be sure to wait a while, until the actual uninstaller is deleted!
  Log('waiting a while until uninstaller changes are flushed in the filesystem...');
  delayCounter := 0;
  repeat
    Sleep(DELAY_MILLIS);
    delayCounter := delayCounter + DELAY_MILLIS;
  until not FileExists(uninstallStr) or (delayCounter >= MAX_DELAY_MILLIS);
  if (delayCounter >= MAX_DELAY_MILLIS) then
    RaiseException('Timeout exceeded trying to delete uninstaller: ' + uninstallStr);
  Log('waited ' + IntToStr(delayCounter) + ' milliseconds');
end;


//============================================================================================================
// SUMMARY
// You can use this event function to perform your own pre-install and post-install tasks.
procedure CurStepChanged(CurStep: TSetupStep);
begin
  if (CurStep = ssInstall) then
  begin
    ForceUninstallApplication();
  end ;

  if (CurStep = ssPostInstall) then
  begin
    DeleteFile(tempUninstallFilename);
  end;
end;