1

There appears to be no information on this anywhere that I can find and I'd rather not wait until it happens again before I can start to piece things together. Unfortunately, therefore, this is a slightly broader question than I would normally like to post, but as I have yet to pin down the cause, reproduce it, collect complete details and document it fully, I can only wait until it next occurs to be able collect and provide further information. So, based on what I do know, I am hoping someone might be able to shed some light on this.

It seems that in some cases, Inno Setup will require a restart to complete that requires an Administrator to login following that restart. I believe it is related to file registrations, possibly in System32, that couldn't be done at the time of the install due to other changes already pending.

On the occasions this has happened there have been (I think) three randomly named (similar to the randomly generated {temp} constant) files in the Windows directory that I (retrospectively, after some thought) assume must be tied to a RunOnce Registry key (which I will look for the next time I see this happen), that I (again) assume run following login as an Administrator to complete the installation. It appears that they do not run until an Administrator logs in, leaving the installation in an incomplete state if a standard user logs in. Once an Administrator logs in these files disappear and the installed application works as expected, rather than giving a:

Class not Registered

error that appears when the application is run before an Administrator logs in and allows the installation to complete fully.

What I want to do is find a way to ensure the installation completes fully, regardless of the privileges of the user that logs in after reboot, as if the application is centrally deployed (e.g. via SCCM), there will be no Administrator available to login at the PC and the application won't run until one does (which defeats the point of using something like SCCM). I am actually quite surprised that Inno Setup does not take care of this automatically, by setting the files to run as the SYSTEM account on next logon or some similar method.

If someone can explain roughly what is happening here, how to find out the names of the files (I could possibly do this by reading the RunOnce Registry key, but I would need to know the name of the value I need to read) and what needs to be done to run them and complete the installation, I should be able to take care of this by, for example, using a Scheduled Task to run as SYSTEM at logon, or some other method.

Robert Wigley
  • 1,857
  • 16
  • 36

2 Answers2

2

You are correct in your assumptions.


When Inno Setup needs to register some files, but it also figures out that, it needs to restart the machine to complete some installations, it will defer the registration until after the restart. Even if the actual file, that needs to be registered, was successfully installed.


Inno Setup will create a registry key like:

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce]
"InnoSetupRegFile.0000000001"="\"C:\\WINDOWS\\is-NGP70.exe\" /REG /REGSVRMODE"

And files like:

C:\Windows\is-NGP70.exe
C:\Windows\is-NGP70.lst
C:\Windows\is-NGP70.msg

Where the .lst file contains list of files to be registered:

[s.]C:\Program Files (x86)\My Program\MyClass.dll

Note that the is-??? name is random, and is not same as the temporary folder of the installer.

In a log file, you will see:

2016-10-22 18:13:06.439   Delaying registration of all files until the next logon since a restart is needed.
2016-10-22 18:13:06.441   Registration executable created: C:\Users\martin\AppData\Local\Temp\is-NGP70.exe

Indeed, when the installer is run with Administrator privileges, the is-???.exe will silently do nothing, when a non-Administrator logs in.

But if you run the installer with non-Administrator privileges, the files will be written to %TEMP%; the key to HKCU\...\RunOnce; and a /REGU switch is used instead of the /REG switch; and the registration will proceed for any user.

[HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce]
"InnoSetupRegFile.0000000001"="\"C:\\Users\\user\\AppData\\Local\\Temp\\is-S5KU2.exe\" /REGU /REGSVRMODE"

To easily test the scenario, just create a simple installer that registers any .dll and set the AlwaysRestart=yes:

[Setup]
AlwaysRestart=yes

[Files]
Source: "MyClass.dll"; DestDir: "{app}"; Flags: regserver

If the restart is needed due to other files, not the file that needs registration, you may consider registering the .dll in Code using the RegisterServer function, to avoid the delayed registration.

RegisterServer(Is64Bit, ExpandConstant('{app}\MyClass.dll'), False);

For an example of using the above statement, see Inno Setup: How to remove Abort from regserver error?


Alternatively, as you already suggested yourself, you can use Window Scheduler to schedule a "onlogon" task with Administrator privileges to run the is-???.exe file.

See How to make the program run on startup with admin permission with Inno Setup?

While you can use the [Run] section entry to run the schtasks (as my answer to the above question shows), as the section is processed only after the RunOnce entry is created. But you will also need to remove the RunOnce entry. And that you cannot do in the [Registry] section, as that has been processed already. You will need to code this in a Pascal Script. Then it's probably better to do both in the code.

You can run the schtasks (using the Exec function) and remove the entry (using the RegDeleteValue function) from the CurStepChanged(ssPostInstall) event function.

You will also need to delete the task. Maybe you can use the /Z switch to "mark the task for deletion after its final run". But I'm not sure this can be combined with the /SC onlogon. If not, you need to run schtasks /Delete as part of the task.

Martin Prikryl
  • 167,268
  • 50
  • 405
  • 846
  • Thank you Martin. This is exactly the information I needed and great that I should be able to test this now. Knowing this, I think I know what might be causing it. It is likely occurring when a .NET Framework prerequisite install (which only runs when required) returns an exit code that indicates it needs a restart, after which I then manually set the `NeedRestart` function to `True`. I assume this would cause the same behaviour as setting `AlwaysRestart=yes`? – Robert Wigley Oct 22 '16 at 16:15
  • That's it. As I have suggested, use the [`RegisterServer` function](http://www.jrsoftware.org/ishelp/index.php?topic=isxfunc_registerserver) in `Code`, instead of using the `regserver` flag and you will be ok. – Martin Prikryl Oct 22 '16 at 16:19
  • 1
    I've added few more details to my answer. – Martin Prikryl Oct 22 '16 at 17:16
  • Thanks again Martin. A very comprehensive answer, as usual. I kind of knew you would know what was happening here. You are a constant fountain of knowledge regarding Inno Setup. I will look at both options to resolve this, but am siding towards the Scheduled Task at present as that was my initial thought on how to deal with this and there are quite a few files that need to be registered. – Robert Wigley Oct 22 '16 at 19:05
  • 1
    I can confirm this was being caused by setting the `NeedRestart` function to `True` after a .NET Framework prerequisite install ran as part of the installation in `PrepareToInstall`. The best way to resolve this, in this situation at least, was to move setting the `NeedRestart` function to `True` to the end of the installation, rather than at the beginning. Doing so prevents this from happening in the first place and prevents the need for a more complex solution. – Robert Wigley Oct 24 '16 at 12:41
  • 1
    But `NeedRestart` is called only once, before file registration. So this does not really make sense. – Martin Prikryl Oct 24 '16 at 12:51
  • In that case, unfortunately this is not a full solution. I did not realise it was only called once. I have just double-checked and I am no longer getting the prompt to restart at the end when setting it later, which makes sense if it is only called the one time. It does seem the best solution to not set this manually, to prevent it happening at all, but I still need to be able to indicate the need for a restart for .NET. Maybe I will look at displaying a custom page or message. – Robert Wigley Oct 24 '16 at 13:03
  • I assume, in theory, even if I do not manually set `NeedRestart` to `True`, the installer could for another reason (e.g. needing to replace some files that are in use), which would still result in the same issue arising? Therefore, the only complete solution to this seems to be to set the is-????.exe to run after restart with elevated privileges. – Robert Wigley Oct 24 '16 at 14:58
  • That's true. But what DLL is that? Is it used by your application only or by others too? – Martin Prikryl Oct 24 '16 at 15:29
  • There are some shared DLLs as well as private ones. However, my thought is that regardless of DLL registration requirements, I assume there may be other reasons the installer would determine it needed a restart itself and set `NeedRestart` to `True`, which would result in this issue. Therefore it seems to me that the only way to be sure of preventing this is to code up running the is-????.exe or EXEs (I guess there could be more than one since there is provisioning for more than one in the Registry) with elevated privileges after reboot. I will post my code when complete as it may help others. – Robert Wigley Oct 24 '16 at 15:35
  • 1
    The installer will restart when: 1) It cannot replace some files, because they are in use. 2) `NeedRestart` returns true. 3) You use `AlwaysRestart` directive; or `restart` flag with some of the components or tasks 4) Some of the `Run` programs schedule a post-restart file rename. - So you pretty much have everything, except for the file replacement, under control. – Martin Prikryl Oct 24 '16 at 15:42
  • 1
    There can be more `is-????.exe`, if other Inno Setup-based installers, executed before yours, have pending registrations. I think you can pretty much safely assume that the highest `InnoSetupRegFile.???` is yours. – Martin Prikryl Oct 24 '16 at 15:43
  • If my installer is run more than once, I get multiple `is-????.exe` files and multiple `RunOnce` Registry entries. At least when `NeedsRestart` is permanently set to `True`. I was planning on just running everything it finds. Maybe a bit belt and braces, but should do the job. – Robert Wigley Oct 24 '16 at 15:46
  • That's what I include in *"other Inno Setup-based installers, executed before yours"*. Each run will create one entry (or none). So if each your installer run converts its entry (if any) to the `ONLOGON` task, you will never have more pending `RunOnce` entries. You should probably read list of entries before an installation starts and again after the installation to reliably find the new key (again, there should be one or none). – Martin Prikryl Oct 24 '16 at 16:17
  • An additional thought on this is that using the other method `RegisterServer` to manually register the files in `[Code]` would prevent the uninstaller from automatically unregistering them when uninstalling and would require additional code to be added to e.g. `CurUninstallStepChanged` to do so. – Robert Wigley Oct 25 '16 at 10:12
  • Ad `RegisterServer`: That's true, you would have to unregister them using the `UnregisterServer`. – Martin Prikryl Oct 25 '16 at 10:16
2

Thanks to Martin's answer, here is the code I came up with (tested and working) to fix this in case it helps anyone else. Note that this has the added benefit (for me at least) of running any and all pending file registrations (that require an Administrator to login) needed by other products using Inno Setup for installation, regardless of the installer that created them.

procedure CurStepChanged(CurStep: TSetupStep);  
var
  intRegFileNumber, intIndex, intCmdFileRegNumber: Integer;
  strRegFileCmd, strRegFileNumber, strCmdFileRegNumber: String;
  arrRegFileLines: TArrayOfString;
  intResultCode: Integer;
begin
//Run additional tasks after the installation finishes i.e. after the [Run] section completes
  if CurStep = ssPostInstall then
    begin
      //File registrations after restart require Administrator login fix
      if RegValueExists(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InnoSetupRegFile.0000000001') then
        begin
          intRegFileNumber := 1;
          strRegFileNumber := Format('%.10d', [intRegFileNumber]);
          intIndex := 1;
          intCmdFileRegNumber := 0;
          strCmdFileRegNumber := Format('%.3d', [intCmdFileRegNumber]);
          SetArrayLength(arrRegFileLines, 100);
          arrRegFileLines[0] := '@echo off';
          while RegValueExists(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InnoSetupRegFile.' + strRegFileNumber) do
            begin
              RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InnoSetupRegFile.' + strRegFileNumber, strRegFileCmd);
              RegDeleteValue(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InnoSetupRegFile.' + strRegFileNumber);
              arrRegFileLines[intIndex] := strRegFileCmd;
              intRegFileNumber := intRegFileNumber + 1;
              strRegFileNumber := Format('%.10d', [intRegFileNumber]);
              intIndex := intIndex + 1;
            end;
          while FileExists(ExpandConstant('{win}\is-filereg' + strCmdFileRegNumber + '.cmd')) do
            begin
              intCmdFileRegNumber := intCmdFileRegNumber + 1;
              strCmdFileRegNumber := Format('%.3d', [intCmdFileRegNumber]);
            end;
          arrRegFileLines[intIndex] := 'start cmd.exe /c "timeout.exe /t 2 /nobreak & schtasks.exe /delete /f /tn "Inno Setup File Registrations ' + strCmdFileRegNumber + '" && del /f /q "%windir%\is-filereg' + strCmdFileRegNumber + '.cmd""';
          arrRegFileLines[intIndex + 1] := 'cls';
          arrRegFileLines[intIndex + 2] := 'exit';
          SetArrayLength(arrRegFileLines, intIndex + 3);
          SaveStringsToFile(ExpandConstant('{win}\is-filereg' + strCmdFileRegNumber + '.cmd'), arrRegFileLines, False);
          Exec(ExpandConstant('{sys}\schtasks.exe'), '/create /ru "SYSTEM" /sc onstart /rl highest /f /tn "Inno Setup File Registrations ' + strCmdFileRegNumber + '" /tr "''' + ExpandConstant('{win}\is-filereg' + strCmdFileRegNumber + '.cmd') + '''', '', SW_HIDE,
            ewWaitUntilTerminated, intResultCode);
        end;
    end;
end;
Robert Wigley
  • 1,857
  • 16
  • 36
  • `start cmd.exe` spawns a separate process, allowing the one started by the Scheduled Task to finish so that it can delete both the Scheduled Task and the batch file (itself). Slight force of habit, I add `cls` and `exit` to every batch file to ensure it closes. I will look at the other changes you suggest. Thanks. – Robert Wigley Oct 25 '16 at 09:11
  • 1
    Code updated to remove the redundant loop and change `if` to `while`, as this would not have worked beyond checking one file. I will look at using `TStringList` in the future, as I am not overly familiar with using this function and it works as it stands (albeit with a hardcoded upper limit of 100, but that should be plenty). – Robert Wigley Oct 25 '16 at 10:01