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.
Solution 1:
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 toHKLM
, when elevated, not toHKCU
.
#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 '[email protected] stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external '[email protected] 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;
Solution 2:
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 '[email protected] 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;