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;
Related
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.
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.
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
i need to know how to read a value from INF file [.inf], during the setup. I want the installer to check the version of the program that i am going to update, This program version is not stored in the registry or any other file, is only in the .inf file. Then is a must to get the version from it.
I got your answers, #Tlama and i cannot use a DLL to get version of the software.
This program only save the current version in the INF file.
What i want to do, is to make the installer to check the current versión of the software that i am working with, and display that version in a label text.
The inf information is this:
NetVersion=1.1.1.1
PatchVersion=2.0.1
ProductName=SoftwareX
I just need the PatchVersion to display after where it says version: #### :
this is the code i am trying to fix:
function GetInfsam: String;
var
sVersion : String;
Begin
sVersion := '';
GetIniString('', 'PatchVersion', 'sVersion', '{app}\Sam.inf');
Result := sVersion;
end;
Procedure InitializeWizard7();
var
L2Ver1 : Tlabel;
L2Ver2 : Tlabel;
Begin
L2Ver1:= TLabel.Create(WizardForm);
L2Ver1.Transparent:= True;
L2Ver1.AutoSize:= False;
L2Ver1.WordWrap:= True;
L2Ver1.Font.name:= 'Agency FB';
L2Ver1.Font.Size:= 12;
L2Ver1.Font.Color:= clwhite;
L2Ver1.Caption:= 'Version:';
L2Ver1.Parent:= WizardForm.SelectdirPage;
L2Ver1.Left := 5;
L2Ver1.top := 260;
L2Ver1.Width := 150;
L2Ver1.Height := 40;
L2Ver2:= TLabel.Create(WizardForm);
L2Ver2.Transparent:= True;
L2Ver2.AutoSize:= False;
L2Ver2.WordWrap:= True;
L2Ver2.Font.name:= 'Agency FB';
L2Ver2.Font.Size:= 12;
L2Ver2.Font.Color:= clwhite;
L2Ver2.Caption:= GetInfsam;
L2Ver2.Parent:= WizardForm.SelectdirPage;
L2Ver2.Left := L2Ver1.Width + L2Ver1.Left + 8;
L2Ver2.top := 260;
L2Ver2.Width := 100;
L2Ver2.Height := 40;
End;
Please, i need help to fix my code.
How to read INF file ?
INF files are just sort of INI files with the specified syntax. So to work with INF files you need to treat them as ordinary INI files. Assuming you have a INF file like this:
[Add.Code]
File.dll=File.dll
[File.dll]
File=http://www.code.com/file.dll
FileVersion=1,0,0,143
You can read the FileVersion key by using GetIniString this way:
procedure InitializeWizard;
var
Version: string;
begin
Version := GetIniString('File.dll', 'FileVersion', '', 'c:\File.inf');
if Version <> '' then
MsgBox('File version: ' + Version, mbInformation, MB_OK);
end;
Update:
1. Malformed INF file
According to your update, if the content of your INF file looks like this:
NetVersion=1.1.1.1
PatchVersion=2.0.1
ProductName=SoftwareX
then it's not a well formed INF file, but a name value pair text file saved with INF extension. Real INF files must have a valid [] section for each key value set, but this section is missing in your file.
2. GetIniString function cannot be called with empty Section parameter value
You must not call the GetIniString function with empty Section parameter value, because for the internally called GetPrivateProfileString function it means to return all section names for a given file, not value of a specified key. So for instance the following call is invalid, because the first parameter Section cannot be empty:
GetIniString('', 'KeyName', 'Default', 'c:\File.xxx');
3. How to work with a name value pair text file ?
You'll just need to work with that file as with a text file. For a key value text file handling would be ideal to use the TStringList class, or at least in Delphi. In InnoSetup unfortunately the TStringList class doesn't have published properties needed for a key value content manipulation, so you'll need to make your own key value text file parsing function. Here's the one for getting value for a given key. As the key value delimiter is supposed to be the = sign. This function returns a key value when succeed to find a AKeyName key in a given AFileName file or default ADefault value when fails:
function GetKeyValue(const AKeyName, AFileName, ADefault: string): string;
var
I: Integer;
KeyPos: Integer;
KeyFull: string;
FileLines: TArrayOfString;
begin
Result := ADefault;
if LoadStringsFromFile(AFileName, FileLines) then
begin
KeyFull := AKeyName + '=';
for I := 0 to GetArrayLength(FileLines) - 1 do
begin
FileLines[I] := TrimLeft(FileLines[I]);
KeyPos := Pos(KeyFull, FileLines[I]);
if KeyPos > 0 then
begin
Result := Copy(FileLines[I], KeyPos + Length(AKeyName) + 1, MaxInt);
Break;
end;
end;
end;
end;
To read a value of the PatchVersion key from the Sam.inf file expected in the currently selected install path you can use something like this. This script will update the label value whenever your user change the text in the directory edit box and when the directory selection page is going to be displayed:
var
// target version label must be declared globally
L2Ver2: TLabel;
procedure DirEditChange(Sender: TObject);
var
FilePath: string;
begin
// assign the expected INF file path
FilePath := AddBackslash(WizardForm.DirEdit.Text) + 'Sam.inf';
// read the PatchVersion key value, return N/A if not found
L2Ver2.Caption := GetKeyValue('PatchVersion', FilePath, 'N/A');
end;
procedure InitializeWizard;
begin
// create the target label as before
L2Ver2 := TLabel.Create(WizardForm);
...
// bind the DirEditChange method to the directory edit's OnChange event
WizardForm.DirEdit.OnChange := #DirEditChange;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
// if the page has been turned to the select directory page, update the
// label caption by firing the assigned OnChange event method manually
if (CurPageID = wpSelectDir) then
DirEditChange(nil);
end;
All my INF files have a structure similar to INI files.
If that's your case too I suggest you to open your INF as if it was INI, using (as suggested by TLama)
in setup functions:
function GetIniInt(const Section, Key: String; const Default, Min, Max: Longint; const Filename: String): Longint;
function GetIniString(const Section, Key, Default, Filename: String): String;
in preprocessor:
str ReadIni(str 1, str 2, str 3, str? 4)
Description (from Inno Setup help)
Reads the value from an INI file. Argument 1 must be the name of the INI file, argument 2 – the name of a section in the INI file, the third argument is the key in the section to read. Last optional argument can be used to provide the default value that will be returned on failure, if it is omitted, an empty string is returned.
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;