2

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.

fubar
  • 338
  • 1
  • 6
  • 20
  • 1
    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 Sep 19 '13 at 18:23
  • sorry, I have added the [Dir] and [Files] part. It also seems that even with a smaller Sleep(3000) the installer works fine. – fubar Sep 19 '13 at 18:30
  • 2
    Thanks. Yet one more thing. That `Exec` you're calling with `ewWaitUntilTerminated` wait type ? – TLama Sep 19 '13 at 18:39
  • 1
    I completed Exec() params; pls see above... – fubar Sep 19 '13 at 18:52
  • Have you tried running the uninstaller from the `PrepareToInstall` step instead? That's the best place to do it. (Also note that *normally* you don't need to -- and shouldn't -- uninstall to do updates.) – Miral Sep 19 '13 at 21:29
  • 2
    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 Sep 19 '13 at 22:01
  • @Aktuz I am not sure how this could be done inside an inno script. Perhaps you mean to patch the inno src? – fubar Sep 20 '13 at 06:03
  • @Miral I know inno supports updates via it's log file but this is no use for me since i also create a bunch of other files, db files, etc. I want to be sure; I need a clean installation. So i (1) backup all my required files, db, etc. (2) uninstall, (3) install, (4) restore my files/db, (5) upgrade my files/db. Also, I am using ssInstall step because of this **[link](http://www.mirality.co.nz/inno/tips.php#hooks)** and specific this comment **'The user must always be allowed to go back and change their mind, or even cancel the entire installation, without any consequences.'** – fubar Sep 20 '13 at 06:17
  • 1
    @fubar - No, I meant the script. Look for 'Pascal Scripting: Using DLLs' in documentation, there should be an example script too. – Sertac Akyuz Sep 20 '13 at 09:00
  • it seems from this [link](http://msdn.microsoft.com/en-us/library/windows/desktop/aa364439%28v=vs.85%29.aspx) that I need a file handle first! But I have none! The unistaller deletes the files. Am i missing something else here? – fubar Sep 20 '13 at 11:27
  • 1
    @fubar - Yes, my comment wasn't detailed but you could have paid attention reading nevertheless ;-). From the comment: *".. passing a handle for the volume(s) .. "* (not files). What I had in mind was getting a handle to the installation volume with `CreateFile`, and then passing it to `FlushFileBuffers`. Again, I don't know if it would be trivial, or even possible... Sorry for I don't have time to test this. PS: prepend the '@' sign attached to the name of the person you're commenting, so that he gets notified. – Sertac Akyuz Sep 20 '13 at 12:24
  • @Aktuz This seems beyond my abilities... I have also notified the project owner. Let's hope he has some time to evaluate this. – fubar Sep 20 '13 at 12:41
  • 1
    @fubar: the `PrepareToInstall` step is executed immediately before `CurStepChanged(ssInstall)`, and after the point when the user has given final confirmation that they want the installation to continue. So you wouldn't be stopping the user from changing their minds. And I don't see why "a bunch of other files" would prevent an in-place upgrade without uninstall from working; it's quite the opposite, usually. – Miral Sep 23 '13 at 09:18
  • @Miral I guess I like better using Abort (in step ssInstall I am allowed to) than returning some string on error. Still I will try out your recommendation mainly because my log files shows me a 4sec delay between the start of an empty PrepareToInstall() and the start of CurStepChanged(ssInstall). What is inno doing in that period of time? Is it checking files and flushing buffers? I will test and report my findings. Still an upgrade is no case for me. I just do not trust inno to just 'replace where needed' the hundred java files that are required in each java new version. – fubar Sep 23 '13 at 11:29
  • @Miral I moved my code inside `PrepareToInstall` but no luck! The exact same error occurs unless I `Sleep(3000)`! – fubar Sep 23 '13 at 13:20
  • 2
    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 Sep 24 '13 at 03:56
  • 1
    Other tasks carried out during PrepareToInstall include checking for in-use files, to request application shutdown via Restart Manager. And if you want to unconditionally delete and replace the java support files, all you need to do is to put in an `[InstallDelete]` for your jre subfolder. (Don't do it for the main app folder though, that way lies gremlins.) – Miral Sep 24 '13 at 04:02

3 Answers3

4

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.)

Miral
  • 12,637
  • 4
  • 53
  • 93
  • At least now I understand why the deletion is late! I always guessed that the uninstaller _"moved"_ the actual uninstaller staff in `{tmp}` and run from there. That way, it would have no problem deleting itself within `{app]` folder. Now I understand the problem. Still I much prefer the uninstaller vs `[InstallDelete]` and all other deletes I have to do e.g. registries etc. Also in each new app version I would have to do serious installer changes. The uninstaller is a much easier and better way to go with; my 50 cents! Still I just **hate** the Sleep() command since it's system related. – fubar Sep 24 '13 at 09:10
  • I will try some other idea to work around this issue and report back. @Miral **big thanks** for your effort! – fubar Sep 24 '13 at 09:11
  • There's no point in trying to check if the process is running or not. It is not. Inno waits until its handle is signaled. – Sertac Akyuz Sep 24 '13 at 15:15
  • @SertacAkyuz: Yes and no. In this particular case, while the process that you actually executed is no longer running, another process with identical name and bits (but different load location) is still running. It's this second process that you actually have to wait for. While Inno tries to minimise the delay between the first and second processes exiting, it is not zero. – Miral Sep 25 '13 at 06:00
4

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.

fubar
  • 338
  • 1
  • 6
  • 20
0

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;
Francois
  • 41
  • 5