I have been focusing quite a bit on silent installs lately. In fact I have recently rewritten two installs that were giving problems to the customer base simply because they could not be installed silently – or unattended.
In my experience, quite a few corporations require that software be able to be installed in an unattended/silent method. For example, when a large corporation with 20,000 customer support personnel wants to distribute new software to their desktops, they use a high-end software distribution program (SMS, Trivoli) that will push the application install to the desktops during the night, launch the install package and reboot the machine. No user intervention is required, and typically the user may not have administrative privileges so the application install package needs to be able to run both unattended and with elevated administrative privileges.
Smaller organizations may simply stage the application install program on a network share and provide every customer support personnel a link to a batch file (.BAT) that will run the application, supplying all of the options and data input without requiring user intervention. All the user would do would be to click on the link before departing the office.
I know the vast majority of installs are very simple and when you create a batch file, about the only item you will need is to pass the INSTALLDIR parameter. But what happens when you have 6 to 8 dialogs, each with multiple data fields. Here is an example of the MSI Properties I needed to pass to an application install to run silently with the correct configuration:
INSTALLDIR=\”C:\Program Files\ISI Systems\”
ISI_PLATFORM_GROUP=1
ISI_CS_COMPONENT_AS=1
ISI_CS_COMPONENT_CM=1
ISI_CS_COMPONENT_SM=1
ISI_CS_COMPONENT_AD=1
ISI_CS_TOPOLOGY_GROUP=1
ISI_AUDITDB_SERVER=DEVSERVER
ISI_AUDITDB_NAME=MSSQLSERVER
ISI_HTTP_SERVERNAME=ISI_SERVER
ISI_HTTP_SERVERPORT=80
ISI_HTTP_SERVERPATH=haven/services/content.asp
It looks nice lined up and very easy to read, but unfortunately the MSI Command line parameters must be on one contiguous input line – you cannot break these input parameters across two or more lines. Now image how difficult it will be to explain the information clearly to a customer who is attempting to create a silent install BATCH file.
I remember years back how very much in vogue the INI type files were for reading in program data. Now everything is XML format, which is not quite as easy for the customer to manipulate. Started me thinking…
I ended up playing around with a silent parameter file – using a INI file format. I wanted to keep the MSI Command line as simple as possible. The goal is to create a BATCH file, such as “SilentInstall.bat”, but just specify a MSI Property, such as: “SILENT_PARAMETER_FILE” which provides the path and file name of the special INI file that would contain all of the application configuration items.
Examples of the contents of a BATCH file “SilentInstall.BAT”, would be:
“c:\Install\Setup.exe” /s /v”/qb ß Line break for clarity only
SILENT_PARAMETER_FILE=C:\Install\SilentParms.INI REBOOT=ReallySuppress
Msiexec /I c:\Install\AppSetup.msi /qb ß Line break for clarity only
SILENT_PARAMETER_FILE=C:\Install\SilentParms.INI REBOOT=ReallySuppress
Now the contents of the SilentParms.INI file would contain the application information that would normally be filled in the dialog data element fields. Use comments to ensure clarity, especially if you are informing the customer of the relationships between selection values.
Here is a sample SilentParms.INI file:
REM ****************************************************************
REM ** Silent Parm File for running application Packrat
REM ****************************************************************
[GLOBAL]
InstallDirectory=”c:\Program Files\ISI Systems”
[PLATFORM_GROUP]
REM ****************************************************************
REM ** Activate only one Platform – choices are:
REM ** Platform=Rotunda
REM ** Platform=Balance
REM ** Platform=Beam
REM ****************************************************************
Platform=Rotunda
[ROTUNDA]
REM ****************************************************************
REM ** Choose as many Components for the Rotunda selection
REM ** required
REM ** Note: values are not used if Platform selected was not
REM ** ROTUNDA
REM ****************************************************************
AudioServer=Yes
CacheMonitor=Yes
SearchMonitor=No
Auditing=NO
REM ****************************************************************
REM ** Choose Web or Viewer Services for the Rotunda selection.
REM ** Choices are:
REM ** CS_Services=Web
REM ** CS_Services=Viewer
REM ** Note: values are not used if Platform selected was not
REM ** ROTUNDA
REM ****************************************************************
CS_Services=Viewer
REM ****************************************************************
REM ** Provide data for the Rotunda HTTP Services selection.
REM **
REM ** Choices for Viewer Services:
REM ** HTTP_SERVERNAME=CHARLES1
REM ** HTTP_SERVERPORT=80
REM ** HTTP_SERVERPATH=witness/services/content.asp
REM **
REM ** Choices for WEB Services:
REM ** HTTP_SERVERNAME=CHARLES1
REM ** HTTP_SERVERPORT=8080
REM ** HTTP_SERVERPATH=adapter/locator
REM ** Note: values are not used if Platform selected was not
REM ** ROTUNDA
REM ****************************************************************
HTTP_SERVERNAME=CHARLES1
HTTP_SERVERPORT=80
HTTP_SERVERPATH=witness/services/content.asp
REM ****************************************************************
REM ** Provide data for the Rotunda Auditing selection.
REM ** Choices are:
REM ** AUDITDB_SERVER=CHARLES1
REM ** AUDITDB_NAME=CHARLES2
REM ** Note: values are not used if Platform selected was not
REM ** ROTUNDA
REM ****************************************************************
AUDITDB_SERVER=CHARLES1
AUDITDB_NAME=CHARLES2
[BALANCE_SERVICES]
REM ****************************************************************
REM ** Choose Web or COM Services for the Balance selection.
REM ** Choices are:
REM ** Balance_Services=WEB
REM ** Balance_Services=COM
REM ** Note: value is not used if Platform selected was not BALANCE
REM ****************************************************************
Bal_Services=WEB
Personally the INI style format is just cleaner to look at, and should be much easier to document – especially when you can put comments throughout the document.
InstallScript Custom Action
Now here is a sample InstallScript that will read the INI file and create the selected values to MSI Properties. Couple of things to note:
- Since you typically will run silently, the Custom Action must be run both in the UI Sequence as well as the Execute Sequence. Obviously the majority of time the appliation will be running silently and only in the Execute Sequence. But also think of the INI file used as a method to prefill data elements if QA decides to run with dialogs for evaluation requirements. That is why you need to enable it to run in both sequences – but only allow it to run once!
- The conversion of the INI values into MSI Properties should precisely deliver the same installation configuration. This is a good method to test – run the install silently with the MSI Properties, and then run with the INI file. The results should match up correctly.
- If a file and path are entered via the MSI Property “SILENT_PARAMETER_FILE”, and the file cannot be found, then trigger an abort.
- When the Script contains a INSTALLDIR parm, you need to create a Type 35 Custom Action and use the property value “ISI_INSTALLPATH” (Set in the script) to set the INSTALLDIR directory property.
- Be sure to allow for the path entered to be in UNC form – could be a remote location!
- To shorten the Script – I have excised the redundant script code.
/////////////////////////////////////////////////////////////////////////
// Function: ISI_ReadSilentParms
//
// Purpose: This function will be called by the script engine when
// Windows(TM) Installer executes your custom action
// and will read and validate the Silent run parms
//
///////////////////////////////////////////////////////////////////////function ISI_ReadSilentParms(hMSI)
STRING szProgramFolder, szCommandLine, szItemName, szMigration;
STRING szPath, szRegKey, szRegSubKey, szSupportDir;
STRING szProgramFiles, szBuffer, szLogMessage, szParmFile;
STRING szINI_SectionName, szINI_ValueName;
STRING szPlatform, szAudioServer, szCacheMonitor;
STRING szSearchMonitor;
STRING szAuditing, szCS_Services, szBalance_Services;
STRING szAuditDB, szAuditDB_Server;
STRING szAuditDB_Name, szCS_ServerName, szCS_ServerPort;
STRING szCS_ServerPath;
NUMBER nBuffer, nResult, nvType, nvSize;
begin
//WriteToLogFiles
ISI_WriteLogFile (“ISI–> Entering ISI_ReadSilentParms Function”);
//***********************************************
//** Retrieve MSI Property that tells where
//** the silent parm file is located
//***********************************************
nBuffer = 256;
MsiGetProperty (ISMSI_HANDLE, “SILENT_PARAMETER_FILE”, szParmFile, nBuffer);
if (nBuffer = 0) then
szBuffer = “ISI–> INFO: Retrieved no value from the MSI Property ‘SILENT_PARAMETER_FILE’!”;
Sprintf (szLogMessage, szBuffer, szParmFile);
ISI_WriteLogFile (szLogMessage);
goto BypassSilentParmFileRead;
endif;
szBuffer = “ISI–> INFO: Retrieved ‘%s’ value from the MSI Property ‘SILENT_PARAMETER_FILE’ “;
Sprintf (szLogMessage, szBuffer, szParmFile);
ISI_WriteLogFile (szLogMessage);
LongPathToShortPath (szParmFile);
LongPathToQuote ( szParmFile, TRUE );
if ( Is (FILE_EXISTS, szParmFile) = FALSE) then
szBuffer = “ISI–> ERROR: File ‘%s’ was not found –
cancelling install! “;
Sprintf (szLogMessage, szBuffer, szParmFile);
ISI_WriteLogFile (szLogMessage);
return ERROR_INSTALL_FAILURE; //Breaks out of install and aborts
endif;
szINI_SectionName = “GLOBAL”;
szINI_ValueName = “InstallDirectory”;
nResult = GetProfString ( szParmFile, szINI_SectionName, szINI_ValueName, g_szInstallDir );
if (nResult = 0) then
szBuffer = “ISI–> INFO: Value retrieved from Section ‘%s’ with name ‘%s’! was ‘%s’ “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName, g_szInstallDir);
ISI_WriteLogFile (szLogMessage);
else
szBuffer = “ISI–> ERROR: Value could not be retrieved from Section ‘%s’ with name ‘%s’! “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName);
ISI_WriteLogFile (szLogMessage);
endif;
// Set MSI Property which will reset INSTALLDIR Property for new files being installed
MsiSetProperty (ISMSI_HANDLE, “ISI_INSTALLPATH”, g_szInstallDir);
szBuffer = “ISI–> INFO: Set ISI_INSTALLPATH Directory to be: ‘%s’ “;
Sprintf (szLogMessage, szBuffer, g_szInstallDir);
ISI_WriteLogFile (szLogMessage);
// ********************************************************
// ** Retrieve the Platform value
// ********************************************************
szINI_SectionName = “PLATFORM_GROUP”;
szINI_ValueName = “Platform”;
nResult = GetProfString ( szParmFile, szINI_SectionName, szINI_ValueName, szPlatform );
if (nResult = 0) then
szBuffer = “ISI–> INFO: Value retrieved from Section ‘%s’ with name ‘%s’! was ‘%s’ “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName, szPlatform);
ISI_WriteLogFile (szLogMessage);
nResult = StrCompare ( “Rotunda”, szPlatform ); //Case insensitive comparison
if (nResult = 0) then
MsiSetProperty (ISMSI_HANDLE, “ISI_PLATFORM_GROUP”, “1″);
szBuffer = “ISI–> INFO: Set MSI Property ‘%s’ with value of 1 – set as Rotunda”;
Sprintf (szLogMessage, szBuffer, “ISI_PLATFORM_GROUP”);
ISI_WriteLogFile (szLogMessage);
goto BypassPlatformRead;
endif;
nResult = StrCompare ( “Balance”, szPlatform );
if (nResult = 0) then
MsiSetProperty (ISMSI_HANDLE, “ISI_PLATFORM_GROUP”, “2″);
szBuffer = “ISI–> INFO: Set MSI Property ‘%s’ with value of 2 – set as Balance”;
Sprintf (szLogMessage, szBuffer, “ISI_PLATFORM_GROUP”);
ISI_WriteLogFile (szLogMessage);
goto BypassPlatformRead;
endif;
nResult = StrCompare ( “Ultra”, szPlatform );
if (nResult = 0) then
MsiSetProperty (ISMSI_HANDLE, “ISI_PLATFORM_GROUP”, “3″);
szBuffer = “ISI–> INFO: Set MSI Property ‘%s’ with value of 3 – set as Ultra”;
Sprintf (szLogMessage, szBuffer, “ISI_PLATFORM_GROUP”);
ISI_WriteLogFile (szLogMessage);
goto BypassPlatformRead;
endif;
//PROBLEM DID NOT FIND MATCHING VALUE!!!
szBuffer = “ISI–> ERROR: Value could not be retrieved from Section ‘%s’ with name ‘%s’! “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName);
ISI_WriteLogFile (szLogMessage);
MsiSetProperty (ISMSI_HANDLE, “ISI_PLATFORM_GROUP”, “1″); //Setting Default!
else
szBuffer = “ISI–> ERROR: Value could not be retrieved from Section ‘%s’ with name ‘%s’! “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName);
ISI_WriteLogFile (szLogMessage);
MsiSetProperty (ISMSI_HANDLE, “ISI_PLATFORM_GROUP”, “1″); //Setting Default!
endif;
BypassPlatformRead:
// ********************************************************
// ** Retrieve the ROTUNDA Components value
// ********************************************************
szINI_SectionName = “ROTUNDA”;
szINI_ValueName = “AudioServer”;
nResult = GetProfString ( szParmFile, szINI_SectionName, szINI_ValueName, szAudioServer );
if (nResult = 0) then
szBuffer = “ISI–> INFO: Value retrieved from Section ‘%s’ with name ‘%s’! was ‘%s’ “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName, szAudioServer);
ISI_WriteLogFile (szLogMessage);
else
szBuffer = “ISI–> ERROR: Value could not be retrieved from Section ‘%s’ with name ‘%s’! “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName);
ISI_WriteLogFile (szLogMessage);
endif;
nResult = StrCompare ( “Yes”, szAudioServer ); //Case insensitive comparison
if (nResult = 0) then
MsiSetProperty (ISMSI_HANDLE, “ISI_CS_COMPONENT_AS”, “1″);
szBuffer = “ISI–> INFO: Set MSI Property ‘ISI_CS_COMPONENT_AS’ with value of 1″;
Sprintf (szLogMessage, szBuffer, “ISI_CS_COMPONENT_AS”);
ISI_WriteLogFile (szLogMessage);
else
MsiSetProperty (ISMSI_HANDLE, “ISI_CS_COMPONENT_AS”, “”);
szBuffer = “ISI–> INFO: Set MSI Property ‘ISI_CS_COMPONENT_AS’ with value of 0″;
Sprintf (szLogMessage, szBuffer, “ISI_CS_COMPONENT_AS”);
ISI_WriteLogFile (szLogMessage);
endif;
// ********************************************************
// ** Retrieve the ROTUNDA Services values
// ********************************************************
szINI_ValueName = “CS_Services”;
nResult = GetProfString ( szParmFile, szINI_SectionName, szINI_ValueName, szCS_Services );
if (nResult = 0) then
szBuffer = “ISI–> INFO: Value retrieved from Section ‘%s’ with name ‘%s’! was ‘%s’ “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName, szCS_Services);
ISI_WriteLogFile (szLogMessage);
nResult = StrCompare ( “Viewer”, szCS_Services ); //Case insensitive comparison
if (nResult = 0) then
MsiSetProperty (ISMSI_HANDLE, “ISI_CS_TOPOLOGY_GROUP”, “1″);
szBuffer = “ISI–> INFO: Set MSI Property ‘ISI_CS_TOPOLOGY_GROUP’ with value of 1 – Viewer”;
Sprintf (szLogMessage, szBuffer, “ISI_CS_TOPOLOGY_GROUP”);
ISI_WriteLogFile (szLogMessage);
else
MsiSetProperty (ISMSI_HANDLE, “ISI_CS_TOPOLOGY_GROUP”, “2″);
szBuffer = “ISI–> INFO: Set MSI Property ‘ISI_CS_TOPOLOGY_GROUP’ with value of 2 – WEB”;
Sprintf (szLogMessage, szBuffer, “ISI_CS_TOPOLOGY_GROUP”);
ISI_WriteLogFile (szLogMessage);
endif;
else
szBuffer = “ISI–> ERROR: Value could not be retrieved from Section ‘%s’ with name ‘%s’! “;
Sprintf (szLogMessage, szBuffer, szINI_SectionName, szINI_ValueName);
ISI_WriteLogFile (szLogMessage);
endif;
//WriteToLogFiles
BypassSilentParmFileRead:
ISI_WriteLogFile (“ISI–> Exiting ISI_ReadSilentParms Function”);
end;
Hope this helps! Happy Coding…
Charles