Specify the registry uninstall key location/hive via [Code] - inno-setup

Inno Setup by default looks at the PrivilegesRequired setup variable, if this is set to admin or poweruser, the installer installs the uninstall registry key to HKLM. If this is set to lowest, then it will install the registry entries to HKCU.
I have a requirement to provide the user an option to install for "just me" or "everybody", and have done so by replacing the dir selection page with a radio selection of these two options. what I need to do now is also modify the registry install location based on this setting. If I install the app into the local user app data, it won't make sense to register the uninstall data at the HKLM level, because then other users will see it in the programs list and still be unable to uninstall or use it.
Edit: After looking through the documentation and the source of Install.pas, I found the CreateUninstallRegKey setup directive, which will disable Inno from installing registry keys at all, after which I can add my own registry entries, but is this really the only way?
Edit #2 (marked as duplicate): I've already taken a look at this Conditional Elevation question (and actually implemented it), and it's not the same as mine. The current elevation state does not alter where Inno Setup actually saves the uninstall registry info (in HKCU or HKLM). If you look at the Inno source code (Install.pas #507) you'll see that the PrivilegesRequired directive is the primary factor in where the registry is stored. If this is set to lowest, it doesnt matter if the installer is elevated or not - it will install the registry keys to HKCU, when the desired behavior is to select one or the other based on the users install preference, NOT the current elevation state. So all this being said, I'm looking for a solution to alter the registry root based on a code variable, regardless of current PrivilegesRequired or Elevation setting.

Inno Setup 6 has a built-in support for selecting between "Install for all users" and "Install for me only".
Basically, you can simply set PrivilegesRequiredOverridesAllowed:
[Setup]
PrivilegesRequiredOverridesAllowed=commandline dialog
For Inno Setup 5: As you found yourself, the logic is hard-coded. You cannot really control that.
The closest you can get is by using the undocumented (deprecated) PrivilegesRequired=none.
With this value (and with a help of installer-autodetection in Windows):
When you start the installer with an un-privileged account, it starts without prompting you for elevation. If you decide you need to elevate during the installation, you can restart the installer elevated.
When you start the installer with a privileged account, it always prompts you for elevation and won't start, if you reject that. So the installer always runs elevated. Again, you would have to restart the installer, if you decide you to proceed un-elevated. See How to Start a Process Unelevated or maybe Run un-elevated command from an elevated prompt?.
It's not exactly, what you want, but I do not think you can get any closer.
You can of course copy (move) the registry key between the HKCU and HKLM yourself by a code:
function MoveHKCUUninstallKeyToHKLM: Boolean;
var
UninstallKey: string;
AppId: string;
I: Integer;
ValueNames: TArrayOfString;
ValueName: string;
ValueStr: string;
ValueDWord: Cardinal;
begin
if '{#emit SetupSetting("AppId")}' <> '' then
begin
AppId := '{#emit SetupSetting("AppId")}';
end
else
begin
AppId := '{#emit SetupSetting("AppName")}';
end;
Result := False;
if AppId = '' then
begin
Log('Cannot identify AppId');
end
else
begin
UninstallKey :=
'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1';
Log(Format(
'AppId identified as "%s", using uninstall key "%s"', [AppId, UninstallKey]));
if not RegKeyExists(HKCU, UninstallKey) then
begin
Log('HKCU uninstall key not found');
end
else
if RegKeyExists(HKLM, UninstallKey) then
begin
Log('HKLM uninstall key exists already');
end
else
begin
Log('HKCU uninstall key found and HKLM key not exists yet');
if not RegGetValueNames(HKCU, UninstallKey, ValueNames) then
begin
Log('Cannot list uninstall key values');
end
else
begin
I := 0;
Result := True;
while (I < GetArrayLength(ValueNames)) and Result do
begin
ValueName := ValueNames[I];
if RegQueryStringValue(HKCU, UninstallKey, ValueName, ValueStr) then
begin
if not RegWriteStringValue(HKLM, UninstallKey, ValueName, ValueStr) then
begin
Log(Format('Error moving "%s" string value', [ValueName]));
Result := False;
end
else
begin
Log(Format('Moved "%s" string value', [ValueName]));
end;
end
else
if RegQueryDWordValue(HKCU, UninstallKey, ValueName, ValueDWord) then
begin
if not RegWriteDWordValue(HKLM, UninstallKey, ValueName, ValueDWord) then
begin
Log(Format('Error moving "%s" dword value', [ValueName]));
Result := False;
end
else
begin
Log(Format('Moved "%s" dword value', [ValueName]));
end;
end
else
begin
{ All uninstall values written by Inno Setup are either string or dword }
Log(Format('Value "%s" is neither string nor dword', [ValueName]));
Result := False;
end;
Inc(I);
end;
if Result then
begin
if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
begin
Log('Error removing HKCU uninstall key');
Result := False;
end
else
begin
Log('Removed HKCU uninstall key');
end;
end;
if not Result then
begin
if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
begin
Log('Failed to move uninstall key to HKLM, ' +
'and also failed to rollback the changes');
end
else
begin
Log('Failed to move uninstall key to HKLM, rolled back the changes');
end;
end;
end;
end;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
Log('Post install');
MoveHKCUUninstallKeyToHKLM;
end;
end;

The PrivilegesRequired=none solution was not what I wanted. In some cases, it still prompts for elevation on administrator accounts and also the registry destination was still not reflective of the users selection.
Since I was already using a native helper DLL in my Inno Setup project, I coded this in C++ as I'm more comfortable there. I'm calling this method is called in CurStepChanged where CurPage=ssDoneInstall. Just call this method with the [Setup] AppId and whether or not the registry keys should be installed locally or not.
#include <shlwapi.h>
extern "C" __declspec(dllexport)
bool DetectAndMoveRegKeyW(LPCWSTR app_id, bool install_local)
{
std::wstring s_app = app_id;
std::wstring path =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + s_app + L"_is1";
LPCWSTR c_path = path.c_str();
LRESULT res;
HKEY source = nullptr, subKey = nullptr;
// try to find source in HKLM
source = HKEY_LOCAL_MACHINE;
res = RegOpenKeyExW(source, c_path, 0, KEY_READ, &subKey);
if (subKey != nullptr)
RegCloseKey(subKey);
// try to find source in HKCU
if (res != ERROR_SUCCESS)
{
subKey = nullptr;
source = HKEY_CURRENT_USER;
res = RegOpenKeyExW(source, c_path, 0, KEY_READ, &subKey);
if (subKey != nullptr)
RegCloseKey(subKey);
}
if (res != ERROR_SUCCESS)
return false; // cant find the registry key
HKEY dest = install_local ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
if (source == dest)
return true; // registry already in the right place
// copy registry key to correct destination
HKEY hOldKey;
HKEY hNewKey;
bool bResult = false;
if (RegOpenKeyW(source, c_path, &hOldKey) == 0)
{
if (RegCreateKeyW(dest, c_path, &hNewKey) == 0)
{
bResult = (SHCopyKeyW(hOldKey, nullptr, hNewKey, 0) == 0);
RegCloseKey(hNewKey);
}
RegCloseKey(hOldKey);
if (bResult)
{
RegDeleteKeyW(source, c_path);
}
}
return bResult;
}
I'm exporting this method as cdecl instead of stdcall, this is because VC++ ignores the C extern and mangles method names anyways when using stdcall. You'll need to import this as cdecl in inno (see inno docs for this). Also, of course this is the Unicode-only implementation, if you require an Ansi version it should be simple enough.
IMPORTANT NOTICE:
This code is incomplete, it doesn't account for 64bit registry redirection. Inno-Setup completely ignores windows registry redirection and this code doesn't search the 64 bit registry at all since Inno and itself are running in 32bit.

Related

Silent Setup exit codes required by Microsoft Store: Disk Space full

Microsoft Store seems to require nowadays a quite few EXE RETURN (exit) codes during Silent setup that are not available by default. How to e.g. return an exit code when the DISK is FULL? I can see its possible to return error code when reboot is required with /RESTARTEXITCODE=exit but it would be required to have exit codes also in these cases:
Application already exists
Installation already in progress
Disk space is full
More exe return codes listed here:
https://learn.microsoft.com/en-us/windows/uwp/publish/msiexe/provide-package-details
I can see the current Inno Setup exit codes here:
https://jrsoftware.org/ishelp/index.php?topic=setupexitcodes
Imo, you cannot really implement this cleanly in Inno Setup. You would have to re-implement the disk space check and forcefully abort the installation with the required custom exit code.
I recommend you add a "Store installation" command-line switch to your installer to enable all Store-related hacks you will likely need to implement.
Something like this:
[Code]
function IsStoreInstallation: Boolean;
begin
Result := WizardSilent() and CmdLineParamExists('/StoreInstallation');
end;
procedure StoreInstallationExit(Code: Integer);
begin
Log(Format('Aborting store installation with code %d', [Code]));
ExitProcess(Code);
end;
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
Free, Total: Int64;
AbortCode: Integer;
begin
Log('prepare');
if IsStoreInstallation() then
begin
Log('Store installation, checking for available disk space');
AbortCode := 0;
if not GetSpaceOnDisk64(WizardDirValue, Free, Total) then
begin
Log('Failed to check for available disk space, aborting');
AbortCode := 15;
end
else
if Free < Int64(10) * 1024 * 1024 then
begin
Log(Format('Too low available disk space (%s), aborting', [
IntToStr(Free)]));
AbortCode := 15;
end
else
begin
Log(Format('Enough available disk space (%s)', [IntToStr(Free)]));
end;
if AbortCode <> 0 then
begin
StoreInstallationExit(AbortCode);
end;
end;
end;
For CmdLineParamExists, see Is it possible to accept custom command line parameters with Inno Setup
For ExitProcess, see Exit from Inno Setup installation from [Code]

What does it mean that message boxes are being suppressed in Inno Setup?

Here's a page from documentation: SuppressibleMsgBox.
What does it mean If message boxes are being suppressed...?
There's a link to the explanation right after the part you have quoted:
If message boxes are being suppressed (see Setup Command Line Parameters), Default is returned.
In the link, there's /SUPPRESSMSGBOXES commandline parameter documented:
Instructs Setup to suppress message boxes. Only has an effect when combined with '/SILENT' or '/VERYSILENT'.
So normally, the SuppressibleMsgBox behaves as the MsgBox. But if you run the installer with the /SUPPRESSMSGBOXES parameter, the SuppressibleMsgBox does nothing, only silently returns the value of the Default parameter.
A practical example of use of the function:
function NextButtonClick(CurPageID: Integer): Boolean;
var
Dir: string;
Msg: string;
begin
Result := True;
if CurPageID = wpSelectDir then
begin
Dir := WizardForm.DirEdit.Text;
if Pos(' ', Dir) > 0 then
begin
Msg :=
'It is not recommended to install the application to a path with spaces. '
+ 'Do you want to continue anyway?';
if SuppressibleMsgBox(Msg, mbInformation, MB_YESNO, IDYES) = IDNO then
begin
Result := False;
end;
end;
end;
end;
In an interactive installation, the installer will warn, if the user tries to install to a path with spaces. But if you are automating a silent installation with /SILENT /SUPPRESSMSGBOXES, the installer will proceed.
It's good idea to use SuppressibleMsgBox whenever you do not want that particular message to break silent installation. So for most cases.

WizardForm.DirEdit.Text not updating properly. Inno setup

I am trying to set the path in the 'choose install directory' form using INNO setup. Here is my code
procedure CurPageChanged(pageID: Integer);
var
sInstallDir: String;
begin
// Default install dir is the IIS install path
if (pageID = wpSelectDir) then begin
sInstallDir := GetIISInstallPath + '\MyFolder';
Log('GetIISInstallPath: '+ GetIISInstallPath);
Log('sInstallDir: ' + sInstallDir);
WizardForm.DirEdit.Text := sInstallDir;
end;
end;
The problem I am having is that 'GetIISInstallPath' returns me 'c:\inetpub\wwwroot and that is what I see in the WizardForm. It seems to not add the MyFolder bit.
I printed out the involved variables and they all have the correct value.
sInstallDir shows up as 'C:\inetpub\wwwroot\MyFolder' but it does not show in the text field. It shows (as mentioned) only 'C:\inetpub\wwwroot'.
Please advise.
Thank You
Your code works fine for me but, can I suggest you to use
[Setup]
...
DefaultDirName={code:GetDefaultDirName}
[code]
...
function GetDefaultDirName(): String;
begin
Result := GetIISInstallPath + '\MyFolder';
end;
Doing this the "GetIISInstallPath + \MyFolder" will be your default directory

InnoSetup - Set DefaultDirName at runtime

I have an application that needs to allow for up to three simultaneous installations on the same machine.
For some reason, the following code behaves as if UsePreviousAppDir is set to yes. The second time I install the app, the path ends up mangled.
The value I want to see is
C:\Our App\install_x where x corresponds to the user's selection.
It works the first time, but the second run results in something like this:
C:\Our App\install_x\install_y, where x corresponds to the value selected with the first installation and y corresponds to the value selected during this installation.
The install version is a radio button selection grabbed from the first screen in the installer. How do I eliminate this issue?
Setup section:
[Setup]
AppName=Our App
AppId=Our App
AppVerName=Our App Version(CM)
DefaultDirName=C:\Our App
DefaultGroupName=Our Group Name
OutputDir=..\
OutputBaseFilename=mm_setup
DisableStartupPrompt=yes
Compression=zip
UsePreviousAppDir=no
VersionInfoDescription=Our App Setup
CreateUninstallRegKey=no
DirExistsWarning=no
And the method where I set the install version:
procedure gSetVersion;
begin
if gVersionPage.SelectedValueIndex = 0 then
begin
gInstallArea := 'install_a';
end
else if gVersionPage.SelectedValueIndex = 1 then
begin
gInstallArea := 'install_b';
end
else if gVersionPage.SelectedValueIndex = 2 then
begin
gInstallArea := 'install_c';
end
WizardForm.DirEdit.Text := WizardDirValue + '\' + gInstallArea;
end;
Solved the problem via the following hack. Not sure why it was necessary to manually edit the string when the `UsePreviousAppDir=no' was set, but this works
procedure gSetVersion;
var
installVersionIndex: Integer;
installDir: String;
begin
case gVersionPage.SelectedValueIndex of
0: gInstallArea := 'install_a';
1: gInstallArea := 'install_b';
2: gInstallArea := 'install_c';
end
//Set the default installation folder.
//This is necessary because InnoSetup intermittently
//ignores the 'UsePreviousAppDir=no' [Setup] directive
//and because the 'DefaultDirName' directive gets populated
//prior to the user selecting the install version
installVersionIndex := Pos('install_', WizardDirValue);
installDir := WizardDirValue;
if installVersionIndex > 0 then
begin
Delete(installDir, installVersionIndex, 20);
end
WizardForm.DirEdit.Text := installDir + '\' + gInstallArea;
end;

How to set automaticly DefaultDirName for previous Inno Setup installation?

my previous installation (A) in Inno Setup has AppID={{8ADA0E54-F327-4717-85A9-9DE3F8A6D100}.
I have another installation (B) with different AppID and I want to install it into the same directory as installation (A).
How do I get automaticly DefaultDirName? I don't want to use the same AppID, because when I uninstall the installation (B) and installation (A) stays installed, it will delete AppID string from registry (installation (A) string).
Can you help me, please?
You'll probably need some code to do what you want. You'll also need a way to find the installation directory of Application A. Here's some code that I've used
[Setup]
DefaultDirName={code:GetDefaultDir}
[Code]
function GetDefaultDir(def: string): string;
var
sTemp : string;
begin
//Set a defualt value so that the install doesn't fail.
sTemp := ExpandConstant('{pf}') + '\MyCompany\MyAppA';
//We need to get the current install directory.
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\MyCompany\Products\MyAppNameA',
'InstallDir', sTemp) then
begin
//We found the value in the registry so we'll use that. Otherwise we use the default
end;
Result := sTemp;
end;
I developed the following code to find the installation directory based on AppID. It accommodates per-user registry entries as well as those for the entire machine. It has been tested on Windows 7 Enterprise on a domain and in a Virtual PC XP Professional machine:
[code]
const
PreviousAppID = '8ADA0E54-F327-4717-85A9-9DE3F8A6D100';
AppFolder = 'SomeFolder';
UninstallPath = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{'
+ PreviousAppID + '}_is1';
// Some posts have 'InstallDir', but I have never observed that
InstallKey = 'InstallLocation';
function GetDefaultDir( Param: String ) : String;
var
UserSIDs: TArrayOfString;
I: Integer;
begin
// Check if the current user installed it
if RegQueryStringValue( HKEY_CURRENT_USER, UninstallPath,
InstallKey, Result ) then
// Current user didn't install it. Did someone else?
else if RegGetSubkeyNames( HKEY_USERS, '', UserSIDs ) then begin
for I := 0 to GetArrayLength( UserSIDs ) - 1 do begin
if RegQueryStringValue( HKEY_USERS, UserSIDs[I] + '\' + UninstallPath,
InstallKey, Result ) then break;
end;
end;
// Not installed per-user
if Result = '' then begin
// What about installed for the machine?
if RegQueryStringValue( HKEY_LOCAL_MACHINE, UninstallPath,
InstallKey, Result ) then
// Doesn't appear to be installed, as admin default to Program Files
else if IsAdminLoggedOn() then begin
Result := ExpandConstant('{pf}\') + AppFolder;
// As non-admin, default to Local Application Data
end else begin
Result := ExpandConstant('{localappdata}\') + AppFolder;
end;
end;
end;

Resources