Debugging Inno Setup installer that respawns itself - inno-setup

As it can be seen from this question we start a new instance of Inno Setup:
Instance := ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
where
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
All the code from this question's answer I moved to the VCL_Styles.iss file and included it into my main script.
The problem is that after I've passed the ShellExecute call and terminate by the debugger afterwards one instance of Inno Setup keeps running (so I have to kill the process using Windows Task Manager) and I get the following messages in the Debug Output:
*** Terminating process
*** Removing left-over temporary directory: C:\Users\JCONST~1\AppData\Local\Temp\is-PV9OS.tmp
*** Setup is still running; can't get exit code
instead of exit code 6 which according to the documentation is returned when:
The Setup process was forcefully terminated by the debugger (Run |
Terminate was used in the Compiler IDE).
I'm not sure which instance of Inno Setup is still running and how can I stop it?
Here's the contents of the VCL.Styles that I include into my main script so I get the aforementioned error:
[Setup]
ShowLanguageDialog=no
[Code]
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
<event('InitializeSetup')>
function MyInitializeSetup2: Boolean;
var
Instance: THandle;
I: Integer;
S, Params, Language: String;
begin
Result := True;
for I := 1 to ParamCount do
begin
S := ParamStr(I);
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
Params := Params + '/LANG=en';
Language := ExpandConstant('{param:LANG}');
if Language = '' then
begin
Instance := ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
if Instance <= 32 then
begin
S := 'Running installer with the selected language failed. Code: %d';
MsgBox(Format(S, [Instance]), mbError, MB_OK);
end;
Result := False;
Exit;
end;
end;

When the debugger steps over the ShellExecute and the new instance of the installer process is started, the IDE debugger seems to pick that process and restarts the debugging. I assume this is not intended behaviour, or at least not a well-tested one. The Terminate function then probably tries to close/communicate with to the old process (which has terminated on its own meanwhile – due to its InitializeSetup returning False after the ShellExecute).
Martijn Laan (the current maintainer of Inno Setup) stated that Inno Setup is not designed to respawn itself. Actually Inno Setup own Exec API explicitly prevents respawning the installer. Bypassing this restriction by using WinAPI ShellExecute instead introduces the problem described in the question. It's not a surprise that the debugger cannot handle this situation.

Looks like an Inno Setup's IDE bug may have caused that problem.
Here's the report link:
https://groups.google.com/g/innosetup/c/pDSbgD8nbxI/m/0lvTsslOAwAJ

Related

error Type mismatch after upgrading to 6.0.2 in inno

Code was working normally, but after upgrading to inno 6.0.2, i got an error when compile. Error:
Type mismatch
in line if LoadStringFromFile(TmpFile, ExecStdout) then code as below:
function NextButtonClick(CurPageID: Integer): Boolean;
var
TmpFile, ExecStdout: string;
ResultCode: integer;
begin
Result := True;
if CurPageID = HostingPage.ID then
begin
Domain := HostingPage.values[0];
DomainPort := HostingPage.values[1];
TmpFile := ExpandConstant('{tmp}') + '\~pid.txt';
Exec('cmd.exe',
'/C FOR /F "usebackq tokens=5 delims= " %i IN (`netstat -ano ^|find "0.0:'+DomainPort+'"`) DO '
+ '#tasklist /fi "pid eq %i" | find "%i" > "' + TmpFile + '"', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
if LoadStringFromFile(TmpFile, ExecStdout) then
begin
MsgBox('The Port ('+DomainPort+') ' #13 + ExecStdout, mbError, MB_OK);
Result := False;
end;
DeleteFile(TmpFile);
end;
end;
The code you posted in not complete, always post MCVE.
The problem resides in using wrong type of parameter, see the documentation:
function LoadStringFromFile(const FileName: String; var S:
AnsiString): Boolean;
Change it like this:
function NextButtonClick(CurPageID: Integer): Boolean;
var
TmpFile: String;
ExecStdout: AnsiString; // << Was String in your script
ResultCode: Integer;
This is because Inno Setup 6 is Unicode only:
Change in default behavior: Starting with Inno Setup 6 there's only one version available: Unicode Inno Setup. Unicode Inno Setup has been available for 9 years but in case you have not yet updated to it: please see the Unicode Inno Setup topic in the help file for more information. Basically, unless you're using [Code] to make DLL calls with string parameters you shouldn't have to make any changes to your script.

Conditional reboot in innosetup installer [duplicate]

My Inno Setup script is used to install a driver. It runs my InstallDriver.exe after this file was copied during step ssInstall.
I need to ask the user to restart in some cases according to the value returned by InstallDriver.exe.
This means that I cannot put InstallDriver.exe in section [Run] because there's no way to monitor it's return value.
So I put it in function CurStepChanged() as follows:
procedure CurStepChanged(CurStep: TSetupStep);
var
TmpFileName, ExecStdout, msg: string;
ResultCode: Integer;
begin
if (CurStep=ssPostInstall) then
begin
Log('CurStepChanged(ssPostInstall)');
TmpFileName := ExpandConstant('{app}') + '\InstallDriver.exe';
if Exec(TmpFileName, 'I', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then .......
However, I can't find a way to make my script restart at this stage.
I thought of using function NeedRestart() to monitor the output of the driver installer, but it is called earlier in the process.
Does it make sense to call the driver installer from within NeedRestart()?
NeedRestart does not look like the right place to install anything. But it would work, as it's fortunately called only once. You will probably want to present a progress somehow though, as the wizard form is almost empty during a call to NeedRestart.
An alternative is to use AfterInstall parameter of the InstallDriver.exe or the driver binary itself (whichever is installed later).
#define InstallDriverName "InstallDriver.exe"
[Files]
Source: "driver.sys"; DestDir: ".."
Source: "{#InstallDriverName}"; DestDir: "{app}"; AfterInstall: InstallDriver
[Code]
var
NeedRestartFlag: Boolean;
const
NeedRestartResultCode = 1;
procedure InstallDriver();
var
InstallDriverPath: string;
ResultCode: Integer;
begin
Log('Installing driver');
InstallDriverPath := ExpandConstant('{app}') + '\{#InstallDriverName}';
if not Exec(InstallDriverPath, 'I', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Log('Failed to execute driver installation');
end
else
begin
Log(Format('Driver installation finished with code %d', [ResultCode]))
if ResultCode = NeedRestartResultCode then
begin
Log('Need to restart to finish driver installation');
NeedRestartFlag := True;
end;
end;
end;
function NeedRestart(): Boolean;
begin
if NeedRestartFlag then
begin
Log('Need restart');
Result := True;
end
else
begin
Log('Do not need restart');
Result := False;
end;
end;

Inno Setup specify log name within the installer

Setting SetupLogging=yes creates the a file:
%TEMP%\Setup Log YYYY-MM-DD #NNN.txt
Is there any way to specify the name of the file? Note that I know I can rename it using FileCopy at the end of the installation (How can I log Inno Setup installations?), but I simply want to specify the name of the file at the outset, much like can be done with the switch /log=%TEMP%\ProductInstall.log. Is this possible?
No. It's not possible. The log file name format for the SetupLogging is hard-coded.
All you can do it to check in InitializeSetup, if /LOG= was specified on command-line and if not, re-spawn the installer with the /LOG=.
Though it's somewhat an overkill.
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
function InitializeSetup(): Boolean;
var
HasLog: Boolean;
Params: string;
I: Integer;
S: string;
RetVal: Integer;
begin
HasLog := False;
Params := '';
for I := 1 to ParamCount do
begin
S := ParamStr(I);
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
HasLog := True;
break;
end;
// Do not pass our /SL5 switch
// This should not be needed since Inno Setup 6.2,
// see https://groups.google.com/g/innosetup/c/pDSbgD8nbxI
if CompareText(Copy(S, 1, 5), '/SL5=') = 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
Result := True;
if HasLog then
begin
Log('Log specified, continuing.');
end
else
begin
// add selected language, so that user is not prompted again
Params := Params + ' /LANG=' + ActiveLanguage;
// force logging
Params :=
Params + ' /LOG="' + ExpandConstant('{%TEMP}\ProductInstall.log') + '"';
Log(Format('Log file not specified, restarting setup with [%s]', [Params]));
RetVal :=
ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
Log(Format('Restarting setup returned [%d]', [RetVal]));
if RetVal > 32 then
begin
Log('Restart with logging succeeded, aborting this instance');
Result := False;
end
else
begin
Log(Format('Restarting with logging failed [%s], keeping this instance', [
SysErrorMessage(RetVal)]));
end;
end;
end;
Here's what I came up with to rename the log after setup completes. This is a bit tricky because you can't rename it while Inno Setup's still using it, but using start and timeout I was able to spin off a separate cmd process that waits a second, then does the rename.
[Run]
; Rename the log file to My_setup_log.txt.
Filename: "{cmd}"; WorkingDir: "{%TEMP}"; Parameters: "/d /c start "" "" /b cmd /d /c ""timeout 1 >NUL & del My_setup_log.txt 2>NUL & ren """"""{log}"""""" My_setup_log.txt"""; Flags: runhidden
As you can see, getting the proper number of quotes to surround the filenames in the quoted cmd argument requires six quotes in the Inno Setup command. If you want spaces in your renamed log file, put the six quotes on either side of it too. Yes, this would mean the string will end with nine consecutive quote marks!

How to set StatusMsg from PrepareToInstall event function

My application requires .NET Framework to be installed so I run .NET installation in PrepareToIntall event function. While the installation is running I would like to display some simple message on Wizard.
I found How to set the status message in [Code] Section of Inno install script? but the solution there doesn't work for me.
I tried
WizardForm.StatusLabel.Caption := CustomMessage('InstallingDotNetMsg');
and also
WizardForm.PreparingLabel.Caption := CustomMessage('InstallingDotNetMsg');
EDIT
I have to do this in PrepareToInstall function, because I need to stop the setup when .net installation fails.
Code looks like this right now:
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
isDotNetInstalled : Boolean;
errorCode : Integer;
errorDesc : String;
begin
isDotNetInstalled := IsDotNetIntalledCheck();
if not isDotNetInstalled then
begin
//WizardForm.PreparingLabel.Caption := CustomMessage('InstallingDotNetMsg');
WizardForm.StatusLabel.Caption := CustomMessage('InstallingDotNetMsg');
ExtractTemporaryFile('dotNetFx40_Full_x86_x64.exe');
if not ShellExec('',ExpandConstant('{tmp}\dotNetFx40_Full_x86_x64.exe'),'/passive /norestart', '', SW_HIDE, ewWaitUntilTerminated, errorCode) then
begin
errorDesc := SysErrorMessage(errorCode);
MsgBox(errorDesc, mbError, MB_OK);
end;
isDotNetInstalled := WasDotNetInstallationSuccessful();
if not isDotNetInstalled then
begin
Result := CustomMessage('FailedToInstalldotNetMsg');
end;
end;
end;
Any Ideas how to achieve this?
The StatusLabel is hosted by the InstallingPage wizard page while you're on PreparingPage page in the PrepareToInstall event method. So that's a wrong label. Your attempt to set the text to the PreparingLabel was correct, but failed because that label is hidden by default (it is shown when you return non empty string as a result to the event method).
But you can show it for a while (you are using ewWaitUntilTerminated flag, so your installation is synchronous, thus it won't hurt anything):
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
WasVisible: Boolean;
begin
// store the original visibility state
WasVisible := WizardForm.PreparingLabel.Visible;
try
// show the PreparingLabel
WizardForm.PreparingLabel.Visible := True;
// set a label caption
WizardForm.PreparingLabel.Caption := CustomMessage('InstallingDotNetMsg');
// do your installation here
finally
// restore the original visibility state
WizardForm.PreparingLabel.Visible := WasVisible;
end;
end;
Another solution is to use CreateOutputProgressPage to display a progress page over the top of the Preparing to Install page. See the CodeDlg.iss example script included with Inno for an example of the usage; it's fairly straightforward.

Make Inno Setup installer request privileges elevation only when needed

Inno Setup installer has the PrivilegesRequired directive that can be used to control, if privileges elevation is required, when installer is starting. I want my installer to work even for non-admin users (no problem about installing my app to user folder, instead of the Program Files). So I set the PrivilegesRequired to none (undocumented value). This makes UAC prompt popup for admin users only, so they can install even to the Program Files. No UAC prompt for non-admin users, so even them can install the application (to user folder).
This has some drawbacks though:
Some people use distinct admin and non-admin accounts on their machines, working with non-admin account normally. In general, when launching installation using non-admin account, when they get UAC prompt, they enter credentials for the admin account to proceed. But this won't work with my installer, because there's no UAC prompt.
(Overly suspicious) people with admin account, who want to install to user folder, cannot launch my installer without (not-needed) admin privileges.
Is there some way to make Inno Setup request privileges elevation only when needed (when user selects installation folder writable by admin account only)?
I assume there's no setting for this in Inno Setup. But possibly, there's a programmatic solution (Inno Setup Pascal scripting) or some kind of plugin/DLL.
Note that Inno Setup 6 has a built-in support for non-administrative install mode.
Inno Setup 6 has a built-in support for non-administrative install mode.
Basically, you can simply set PrivilegesRequiredOverridesAllowed:
[Setup]
PrivilegesRequiredOverridesAllowed=commandline dialog
Additionally, you will likely want to use the auto* variants of the constants. Notably the {autopf} for the DefaultDirName.
[Setup]
DefaultDirName={pf}\My Program
The following is my (now obsolete) solution for Inno Setup 5, based on #TLama's answer.
When the setup is started non-elevated, it will request elevation, with some exceptions:
Only on Windows Vista and newer (though it should work on Windows XP too)
When upgrading, the setup will check if the current user has a write access to the previous installation location. If the user has the write access, the setup won't request the elevation. So if the user has previously installed the application to user folder, the elevation won't be requested on upgrade.
If the user rejects the elevation on a new install, the installer will automatically fall back to "local application data" folder. I.e. C:\Users\standard\AppData\Local\AppName.
Other improvements:
the elevated instance won't ask for language again
by using PrivilegesRequired=none, the installer will write uninstall information to HKLM, when elevated, not to HKCU.
#define AppId "myapp"
#define AppName "MyApp"
#define InnoSetupReg \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + AppId + "_is1"
#define InnoSetupAppPathReg "Inno Setup: App Path"
[Setup]
AppId={#AppId}
PrivilegesRequired=none
...
[Code]
function IsWinVista: Boolean;
begin
Result := (GetWindowsVersion >= $06000000);
end;
function HaveWriteAccessToApp: Boolean;
var
FileName: string;
begin
FileName := AddBackslash(WizardDirValue) + 'writetest.tmp';
Result := SaveStringToFile(FileName, 'test', False);
if Result then
begin
Log(Format(
'Have write access to the last installation path [%s]', [WizardDirValue]));
DeleteFile(FileName);
end
else
begin
Log(Format('Does not have write access to the last installation path [%s]', [
WizardDirValue]));
end;
end;
procedure ExitProcess(uExitCode: UINT);
external 'ExitProcess#kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
function Elevate: Boolean;
var
I: Integer;
RetVal: Integer;
Params: string;
S: string;
begin
{ Collect current instance parameters }
for I := 1 to ParamCount do
begin
S := ParamStr(I);
{ Unique log file name for the elevated instance }
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
S := S + '-elevated';
end;
{ Do not pass our /SL5 switch }
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
{ ... and add selected language }
Params := Params + '/LANG=' + ActiveLanguage;
Log(Format('Elevating setup with parameters [%s]', [Params]));
RetVal :=
ShellExecute(0, 'runas', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
Log(Format('Running elevated setup returned [%d]', [RetVal]));
Result := (RetVal > 32);
{ if elevated executing of this setup succeeded, then... }
if Result then
begin
Log('Elevation succeeded');
{ exit this non-elevated setup instance }
ExitProcess(0);
end
else
begin
Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
end;
end;
procedure InitializeWizard;
var
S: string;
Upgrade: Boolean;
begin
Upgrade :=
RegQueryStringValue(HKLM, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S) or
RegQueryStringValue(HKCU, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S);
{ elevate }
if not IsWinVista then
begin
Log(Format('This version of Windows [%x] does not support elevation', [
GetWindowsVersion]));
end
else
if IsAdminLoggedOn then
begin
Log('Running elevated');
end
else
begin
Log('Running non-elevated');
if Upgrade then
begin
if not HaveWriteAccessToApp then
begin
Elevate;
end;
end
else
begin
if not Elevate then
begin
WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#AppName}');
Log(Format('Falling back to local application user folder [%s]', [
WizardForm.DirEdit.Text]));
end;
end;
end;
end;
There is no built-in way for conditional elevation of the setup process during its lifetime in Inno Setup. However, you can execute the setup process by using runas verb and kill the non-elevated one. The script that I wrote is a bit tricky, but shows a possible way how to do it.
Warning:
The code used here attempts to execute the elevated setup instance always; there is no check whether the elevation is actually required or not (how to decide whether the elevation is needed optionally ask in a separate question, please). Also, I can't tell at this time, if it's safe to do such manual elevation. I'm not sure if Inno Setup doesn't (or will not) rely on the value of the PrivilegesRequired directive in some way. And finally, this elevation stuff should be executed only on related Windows versions. No check for this is done in this script:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
PrivilegesRequired=lowest
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
HINSTANCE = THandle;
procedure ExitProcess(uExitCode: UINT);
external 'ExitProcess#kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): HINSTANCE;
external 'ShellExecute{#AW}#shell32.dll stdcall';
var
Elevated: Boolean;
PagesSkipped: Boolean;
function CmdLineParamExists(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to ParamCount do
if CompareText(ParamStr(I), Value) = 0 then
begin
Result := True;
Exit;
end;
end;
procedure InitializeWizard;
begin
{ initialize our helper variables }
Elevated := CmdLineParamExists('/ELEVATE');
PagesSkipped := False;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
{ if we've executed this instance as elevated, skip pages unless we're }
{ on the directory selection page }
Result := not PagesSkipped and Elevated and (PageID <> wpSelectDir);
{ if we've reached the directory selection page, set our flag variable }
if not Result then
PagesSkipped := True;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
Params: string;
RetVal: HINSTANCE;
begin
Result := True;
{ if we are on the directory selection page and we are not running the }
{ instance we've manually elevated, then... }
if not Elevated and (CurPageID = wpSelectDir) then
begin
{ pass the already selected directory to the executing parameters and }
{ include our own custom /ELEVATE parameter which is used to tell the }
{ setup to skip all the pages and get to the directory selection page }
Params := ExpandConstant('/DIR="{app}" /ELEVATE');
{ because executing of the setup loader is not possible with ShellExec }
{ function, we need to use a WinAPI workaround }
RetVal := ShellExecute(WizardForm.Handle, 'runas',
ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
{ if elevated executing of this setup succeeded, then... }
if RetVal > 32 then
begin
{ exit this non-elevated setup instance }
ExitProcess(0);
end
else
{ executing of this setup failed for some reason; one common reason may }
{ be simply closing the UAC dialog }
begin
{ handling of this situation is upon you, this line forces the wizard }
{ stay on the current page }
Result := False;
{ and possibly show some error message to the user }
MsgBox(Format('Elevating of this setup failed. Code: %d', [RetVal]),
mbError, MB_OK);
end;
end;
end;

Resources