Index
Introduction
By reading the article title, we think that I will present the great MSI technology (Windows Installer). In fact, that's not the subject of the article at all. This article presents an easy way to manage any MSI package before, during and after MSI processing. It has no relation with MSI custom actions and different steps in product installation process as we can see in different market products as Visual Studio setup and deployment, InstallShield Studio, Wise Installer etc.
It may happen that we cannot do all things we want in MSI packages with respect to the actions taking place before and after installation. The only thing you need to use immediately in the program presented in this article is a configuration file which I will explain in the next section.
In the same time, the article presents many useful and ready to use hints on:
- how to create threads in UI programming and disable our UI to be closed (by Atl+F4, for example),
- how to launch silently Win32 or console programs, and DOS commands with/without parameters,
- how to destroy a whole directory or to delete only files with some pattern in one or many directories,
- how to design easily a non deterministic progress bar (see the image above) when we cannot evaluate exactly the operation length in terms of time or count,
- how to realize a gradient color brush on any rectangle of any control (see the image above) by simply calling one function.
Before I go into details, I have to give credit to some people who have contributed free source in CodeProject, and from them I took many good hints and nice ideas to construct some of the article functions:
- Patrik Svensson for his article entitled A Gradient Static control from which I took the idea of Gradient fill.
- Robert Edward Caldecott for his article WTL Button menu Class from which I took the idea of using Marlett font in designing non deterministic progress bar.
The idea of testing if Windows Installer is present by looking into the registry is not mine, and I don't have the author's name. In spite that this method works for all Windows OS (except Windows 95), it is more appropriate, at least for Windows 2000/XP/2003, to test if Windows Installer service is installed on the target system. The fact that the file msiexec.exe is present in System32 directory does not prove at all that Windows Installer is alive.
How to use it
MsiMgr.exe Configuration_File
Where Configuration_File
is the configuration file containing the package version and description, log file path, and actions that should be processed by MsiMgr.
Examples:
MsiMgr C:\MyFile.ini
MsiMgr C:\My directory\MyFile.ini
Even if the configuration file path contains spaces, don't add quotes, otherwise it doesn't work. I will explain in details in the next section the way the configuration file should be organized, and the rules should be respected in each of the included lines.
How the program is organized
Configuration file has the .INI file format. The following diagram shows the different sections and the general rule of included lines in each section.
Each line in the configuration file has the following scheme except the SETTINGS
section, in which we indicate the package version, description and logfile path as we will see below:
FILE = [PARAMTERS], VERB, [CONDITION, CONDITION_DATA], [MESSGE_TO_DISPLAY]
Parameters between [] are optional. Of course, the VERB
and the FILE
are obligatory. If FILE
or CONDITION_DATA
is a path containing spaces, make sure to enclose the string between two quotes".
This line means:
Apply the verb VERB
to FILE
+PARAMTERS
only if CONDITION
applied on CONDITION_DATA
is true or if there is no condition, and display MESSGE_TO_DISPLAY
during the line process.
Where:
Table 1: Typical line elements table
FILE |
is the file to process depending on the verb value |
PARAMTERS |
Parameters to pass to File during the verb application |
VERB |
is the verb to process |
CONDITION |
is the condition |
CONDITION_DATA |
is the condition data on which condition should be applied |
MESSGE_TO_DISPLAY |
is the message to display on the UI during the verb execution |
The tables below show the values the parameters VERB and CONDITION can have. Note that the VERB
applies to FILE
, and the CONDITION
applies to CONDITION_DATA
. Note also that neither the verb values nor the condition values are case sensitive.
Table 2: VERB
values table
DelFile |
Delete file |
DelDir |
Delete directory |
DelKey |
Delete registry key |
DelKeyValue |
Delete registry key value |
ExecFile |
Execute file by passing parameters PARAMTERS if any |
UnIns |
Uninstall MSI package |
Ins |
Install MSI package |
Table 3: CONDITION
values table
IF_EXIST |
If file/directory exists |
IF_NOT_EXIST |
If file/directory not exists |
IF_KEY_EXIST |
If key exists. In this case, the CONDITIONS_DATA should end with backslash sign to differ from a key value. |
IF_KEY_NOT_EXIST |
If key not exists. In this case, the CONDITIONS_DATA should end with backslash. |
IF_KEY_VALUE_EXIST |
If key value exists. In this case, the CONDITIONS_DATA should not end with backslash. |
IF_KEY_VALUE_NOT_EXIST |
If key value not exists. |
Here is an example of a configuration file to make the entire concept clearer. It depends on your requirements and imagination to fill the configuration file.
[SETTINGS]
;Package version
Version=1.0.0.1
;Package description
Description=this is install test
;Logs will be generated in C:\Mylog.log
logfile=C:\Mylog.log
;Wait at most 5 minutes for processes execution
ExecTimeout=300000
[PREINSTALL]
;This line will delete directory C:\Test !
c:\Test=,deldir,,,Deleting directory C:\test...
;This line will delete directory C:\Temp only if directory C:\Test1 exist!
C:\Temp\=,delfile,IF_EXIST,C:\Test1, deleting all *.dll in C:\Temp directory...
;This line will delete directory C:\Temp1
only if file C:\Test2\MyFlagFile.001 exist!
C:\Temp1=,delfile,IF_EXIST,C:\Test2\MyFlagFile.001,
deleting all *.dll in C:\Temp directory...
;This line will unregister the service whose path
C:\Windows\system32\MyService.exe if this one exist
C:\Windows\system32\MyService.exe=/Unregister,ExecFile,IF_EXIST,
C:\Windows\system32\MyService.exe,Unregistring the service Myservice...
;This line will delete the service binary
C:\Windows\system32\MyService.exe if this one exist
C:\Windows\system32\MyService.exe=/Unregister,DelFile,IF_EXIST,
C:\Windows\system32\MyService.exe,deleting the service Myservice...
;This line will launch NotePad (works only on WinXP/2000/2003),
for Win9X replace cmd by Command
cmd /C %windir%\notepad.exe=,ExecFile,,,Launching NotePad...
[UNINSTALL]
;This section and the next one apply only to MSI packages
;(that's why we don't need to specify msi engine/service msiexec.exe)
;This line will uninstall the product with code indicated on the left
;by hiding the cancel button and generating the msi log in C:\MSIlog.log
{125F0499-6759-11D5-A54F-0090278A1BB8}=/QB+! /LC:\MSIlog.log,
UnIns,,,Uninsall my product in progress...
[INSTALL]
;Install c:\Packages\MyPackage\MyMsi.msi with the MSI paramter
"ALLUSERS=1 /QB" without any condition
c:\Packages\MyPackage\MyMsi.msi=ALLUSERS=1 /QB,INS,,,
Installing MyMsi in prgress. Please wait...
[POSTINSTALL]
;The same rules apply to this section as those of PREINSTALL section
Note: When you specify as a VERB
ExecFile, the execution waits, by default, for the process termination at most the value of ExecTimeout
specified in SETTINGS
section in milliseconds. This timeout may be adjusted for each package by evaluating, in pessimistic case, the most long operation even if the target machines can have less or more CPU performance.
Using the code
The project is a Win32 MFC application. The main window contains only one child dialog. The remaining parts of the UI around the dialog are just decoration that have been accomplished using some GDI functions that I will explain later.
By making a correspondence between the tables above (Tables 1, 2, and 3 above), and the program source code, they correspond respectively to:
enum VerbFlag {
DelFile,
DelDir,
DelKey,
DelKeyValue,
ExecFile,
UnIns,
Ins,
};
enum ConditionFlag{
IF_EXIST,
IF_NOT_EXIST,
IF_KEY_EXIST,
IF_KEY_NOT_EXIST,
IF_KEY_VALUE_EXIST,
IF_KEY_VALUE_NOT_EXIST,
};
struct InstallStruct {
CString Data;
CString Parameter;
int Verb;
int Flag;
CString FlagFile;
CString Message;
};
The main program thread MainThread
responsible for processing all sections is as follows:
DWORD WINAPI MainThread(LPVOID lp)
{
DWORD ExitCode=0;
BOOL ret[4];
for (int i= 0;i<4; i++) ret[i]=TRUE;
ret[0]=PreInstall(hdlg,ExitCode);
if (ret[0]==TRUE) {
ret[1]=UnInstallPackage(hdlg,ExitCode);
if (ret[1]==TRUE) {
ret[2]=InstallPackage(hdlg,ExitCode);
if (ret[2]==TRUE)
ret[3]=PostInstall(hdlg,ExitCode);
}
}
CString Msg;
if ( (ret[0]&ret[1]&ret[2]&ret[3])!=0) {
Msg=_T("\nMSI package processing has finished with success.");
UpdateMessage(hdlg,Msg);
}
else {
Msg=_T("\nThe MSI package processing has failed!");
UpdateMessage(hdlg,Msg);
MessageBox(hdlg, Msg, TITLE, MB_OK|MB_ICONEXCLAMATION|MB_SYSTEMMODAL);
}
WriteToLogFile(LOG_FILE,Msg.GetBuffer(0));Msg.ReleaseBuffer();
Sleep(2000);
SendMessage(hdlg,WM_DESTROY,0,0);
DONE=1;
return ExitCode;
}
The decoration around the dialog is made by a call to the function below:
void MakeGradient(HWND hWnd, COLORREF FirstColor, COLORREF SecondColor,
CString &TextOut, COLORREF TextColor, UINT TextFormat,
CString &FontName, int FontSize,
CRect &Rect, DWORD Direction, BOOL CleanUp)
{
static HINSTANCE h_msimg32;
if (h_msimg32==NULL) h_msimg32= LoadLibrary( "msimg32.dll" );
CDC dc;
HDC hDC=GetWindowDC(hWnd);
dc.Attach(hDC);
CFont Font;
int nHeight = -1-MulDiv(FontSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
int nWidth = -1-MulDiv(FontSize, GetDeviceCaps(hDC, LOGPIXELSX), 72);
Font.CreateFont(nHeight, nWidth,
0, 0, FW_DONTCARE, FALSE, FALSE, 0,
DEFAULT_CHARSET, OUT_CHARACTER_PRECIS,
CLIP_CHARACTER_PRECIS, ANTIALIASED_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
FontName );
CFont *pOldFont = dc.SelectObject( &Font );
if (h_msimg32) {
typedef UINT (CALLBACK* LPFNDLLFUNC1)
(HDC,CONST PTRIVERTEX,DWORD,
CONST PVOID,DWORD,DWORD);
LPFNDLLFUNC1 dllfunc_GradientFill =
((LPFNDLLFUNC1) GetProcAddress( h_msimg32,
"GradientFill" ));
if (Rect.IsRectNull()) {
GetClientRect(hWnd, &Rect);
Rect.top-=1;
Rect.bottom+=2;
Rect.right+=2;
Rect.left-=1;
}
TRIVERTEX rcVertex[2];
rcVertex[0].x=Rect.left;
rcVertex[0].y=Rect.top;
rcVertex[0].Red=GetRValue(FirstColor)<<8;
rcVertex[0].Green=GetGValue(FirstColor)<<8;
rcVertex[0].Blue=GetBValue(FirstColor)<<8;
rcVertex[0].Alpha=0x0000;
rcVertex[1].x=Rect.right;
rcVertex[1].y=Rect.bottom;
rcVertex[1].Red=GetRValue(SecondColor)<<8;
rcVertex[1].Green=GetGValue(SecondColor)<<8;
rcVertex[1].Blue=GetBValue(SecondColor)<<8;
rcVertex[1].Alpha=0;
GRADIENT_RECT rect;
rect.UpperLeft=0;
rect.LowerRight=1;
dllfunc_GradientFill(dc, rcVertex, 2, &rect, 1, Direction);
if ( (h_msimg32!=NULL) && (CleanUp==TRUE) ) {
h_msimg32=NULL;
FreeLibrary(h_msimg32);
}
}
SetTextColor(dc, TextColor);
SetBkMode(dc, TRANSPARENT);
DrawText(dc, TextOut, -1, &Rect, TextFormat);
Rect.DeflateRect(-1,-1, -1, -1 );
dc.DrawEdge(&Rect,EDGE_BUMP,BF_RECT);
dc.SelectObject( pOldFont );
dc.Detach();
}
To make the progress bar, a timer is set in the InitInstance
function. Then the function above is called in the main frame window procedure as a response to WM_TIMER
message by using a string table and Marlett font as follows:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
..............
static int i;
.................
static CString str[22]={
{"gggg11111111111111111111111111111111111111111111"},
{"111gggg11111111111111111111111111111111111111111"},
{"11111gggg111111111111111111111111111111111111111"},
{"1111111gggg1111111111111111111111111111111111111"},
{"111111111gggg11111111111111111111111111111111111"},
{"11111111111gggg111111111111111111111111111111111"},
{"1111111111111gggg1111111111111111111111111111111"},
{"111111111111111gggg11111111111111111111111111111"},
{"11111111111111111gggg111111111111111111111111111"},
{"1111111111111111111gggg1111111111111111111111111"},
{"111111111111111111111gggg11111111111111111111111"},
{"11111111111111111111111gggg111111111111111111111"},
{"1111111111111111111111111gggg1111111111111111111"},
{"111111111111111111111111111gggg11111111111111111"},
{"11111111111111111111111111111gggg111111111111111"},
{"1111111111111111111111111111111gggg1111111111111"},
{"111111111111111111111111111111111gggg11111111111"},
{"11111111111111111111111111111111111gggg111111111"},
{"1111111111111111111111111111111111111gggg1111111"},
{"111111111111111111111111111111111111111gggg11111"},
{"11111111111111111111111111111111111111111gggg111"},
{"1111111111111111111111111111111111111111111gggg1"}
};
CRect rect;
switch (message)
{
..................
case WM_TIMER:
if (DONE==1) {
KillTimer(hWnd, 1);
DestroyWindow(hWnd);
return 0;
}
GetClientRect(GetDlgItem(hdlg,IDC_STATIC_PROGRESS),&rect);
MakeGradient(GetDlgItem(hdlg,IDC_STATIC_PROGRESS),
RGB(128, 128, 128), RGB(255, 255, 255),
str[i], RGB(119, 60, 0),
DT_CENTER|DT_VCENTER, CString("Marlett"),
10, rect, GRADIENT_FILL_RECT_H,FALSE);
i+=1;
i=i%18;
if (h==NULL)
h=CreateThread(NULL,0,MainThread, &hWnd, NULL,&ID);
break;
.............
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
How the program processes each line in each section?
The lines are processed in the same order as they are in the configuration file. Each line at one time is represented by a global variable.
InstallStruct Install;
and it is processed by following the steps:
- Get the
FILE
which will be stored in Install.Data
.
- Get the
Data
value (right-hand of =
), say strArr
, of type CStringArray
.
- Split
strArr
into Parameter
, Verb
, Flag
(CONDITION
), FlagFile
(CONDITION_DATA
), and Message
by calling the function:
void SplitString(CString &str, char Sep, CStringArray &SplitArray)
- Get
VERB
, CONDITION
, CONDITION_DATA
, and MESSGE_TO_DISPLAY
values. Since the VERB
and CONDITION
can have different values, two functions are used to get their values: int GetVerb(CStringArray strArr)
int GetCondition(CStringArray strArr)
- Call the function corresponding to the current section as indicated in the table:
SECTION |
Corresponding function |
PREINSTALL |
BOOL PreInstall(HWND hWnd, DWORD &ExitCode) |
UNINSTALL |
BOOL UnInstallPackage(HWND hWnd, DWORD &ExitCode) |
INSTALL |
BOOL InstallPackage(HWND hWnd, DWORD &ExitCode) |
POSTINSTALL |
BOOL PostInstall(HWND hWnd, DWORD &ExitCode) |
I will not explain how these functions work, but as I mentioned in the Introduction, I will give some intermediate functions used indirectly, in the next section. These functions may be used in any program to achieve common tasks.
How to's
- How to create threads in UI programming and disable our UI to be closed normally (by Atl+F4 for example):
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_NOCLOSE;
}
and see the thread creation MainThread above.
- How to launch silently Windows or console programs and DOS commands with/without parameters:
BOOL CreateProc(char *ModuleName, char *CommandLine, BOOL WaitOrNot,
DWORD TimeOut, BOOL Show, DWORD &ExitCode)
{
STARTUPINFO startup_info;
PROCESS_INFORMATION process_info;
BOOL status;
DWORD dwexitcode = 0;
if ( strcmp(CommandLine, "")==0 && strcmp(ModuleName, "")==0 )
return TRUE;
ZeroMemory(&startup_info, sizeof(STARTUPINFO));
startup_info.cb = sizeof ( startup_info );
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_SHOWNORMAL?(Show==TRUE):SW_HIDE;
status = CreateProcess((LPTSTR)
(strcmp(ModuleName,"")==0)?NULL:ModuleName,
(LPTSTR)CommandLine,
NULL, NULL, FALSE,
0 , NULL, NULL,
&startup_info, &process_info );
if ( status ) {
if ( WaitOrNot )
if (TimeOut>0)
WaitForSingleObject ( process_info.hProcess, TimeOut );
else
WaitForSingleObject ( process_info.hProcess, INFINITE );
}
if (GetExitCodeProcess(process_info.hProcess, &dwexitcode) != 0) {
ExitCode = dwexitcode;
CloseHandle ( process_info.hThread );
CloseHandle ( process_info.hProcess );
}
if (status == 0 ) return FALSE;
else return TRUE;
}
Example: If you want to execute a DOS command or a console program silently, do the following:
char *Cmd=getenv("COMSPEC");
char cmdline[255];
sprintf(cmdline,"%s /C Dir *.*/s>C:\\Out.txt",Cmd);
DWORD ExitCode=0;
BOOL ret=CreateProc("", cmdline, TRUE ,
10000 , FALSE , ExitCode);
char
Windir[MAX_PATH];
if (GetWindowsDirectory(Windir,MAX_PATH)>0){
char modName[MAX_PATH];
sprintf(modName,"%s\\Notepad.exe",Windir);
sprintf(cmdline,"%s C:\\Out.txt",modName);
ret=CreateProc("", cmdline, FALSE ,
0 ,
TRUE , ExitCode);
}
- How to destroy a whole directory or to delete only files with some pattern, in one or many directories:
CAUTION: Beware! Using the functions below without paying attention can be dangerous for your data. If you want to test them, specify a directory with non pertinent data.
Deleting a directory and optionally sub-directories and root directory can be done by simply calling the function:
BOOL ClearDirEx( CString &TheDir, BOOL DelSubDirs, BOOL DelRootDir)
{
CString Pattern="*.*";
BOOL ret= ClearDir(TheDir.GetBuffer(MAX_PATH),
DelSubDirs, DelRootDir, Pattern.GetBuffer(3));
TheDir.ReleaseBuffer();
Pattern.ReleaseBuffer();
return ret;
}
Deleting files having some extension in a directory and optionally in all sub-directories can be done by calling the function:
BOOL DeleteFilesEx( CString &TheDir,
CString &Pattern, BOOL IncludeSubDirs)
{
BOOL ret= ClearDir(TheDir.GetBuffer(MAX_PATH),
IncludeSubDirs, FALSE, Pattern.GetBuffer(3));
TheDir.ReleaseBuffer();
Pattern.ReleaseBuffer();
return ret;
}
The common function called in the two functions above in different ways is:
BOOL ClearDir(char *TheDir, BOOL DelSubDirs, BOOL DelRootDir, char *Pattern)
{
BOOL bFound=TRUE;
WIN32_FIND_DATA FileData;
HANDLE hSearch;
static char *CurDir=TheDir;
char TempDir[MAX_PATH];
if (TheDir==NULL) return TRUE;
if (DelRootDir==TRUE) DelSubDirs=TRUE;
sprintf(TempDir, TheDir);
strcat(TempDir,"\\");
strcat(TempDir, "*.*");
static char *revPat=_strrev(Pattern);
hSearch = FindFirstFile((LPCTSTR)TempDir, &FileData);
while ( bFound ) {
if (hSearch != INVALID_HANDLE_VALUE) {
GetFileAttributes( (LPCTSTR)FileData.cFileName );
if ( FileData.dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY ) { //Directory data
if ( strcmp((char*)FileData.cFileName, "." )!=0 &&
strcmp((char*)FileData.cFileName,"..")!=0 )
{ //There is some more files
sprintf(TempDir, TheDir);
strcat(TempDir, "\\");
strcat(TempDir,(char*)FileData.cFileName);
if ( ClearDir(TempDir, DelSubDirs,
DelRootDir, revPat)==FALSE)
return FALSE;
}
} else { //file data
if ( SetCurrentDirectory(TheDir)!=0 ) {
if (SetFileAttributes((LPCTSTR)FileData.cFileName,
FILE_ATTRIBUTE_ARCHIVE)!=0 &&
SetFileAttributes((LPCTSTR)FileData.cFileName,
FILE_ATTRIBUTE_NORMAL)!=0 ) {
if (strcmp(Pattern,"*.*")==0) {
if ( DeleteFile((LPCTSTR)FileData.cFileName)==0)
// cannot delete file, return FALSE
return FALSE;
}
else {
char *revFile=_strrev(FileData.cFileName);
if (_strnicmp(revFile,revPat,strlen(Pattern))==0){
revFile=_strrev(FileData.cFileName);
if ( DeleteFile((LPCTSTR)revFile)==0)
return FALSE;
}
}
}
}
}
bFound=FindNextFile(hSearch, &FileData);
} else return TRUE;
}
FindClose(hSearch);
SetCurrentDirectory("\\");
if ( (DelRootDir==TRUE) && (stricmp(TheDir,CurDir)==0) ) {
// We cannot remove local or network drive !!!
if ( strlen(TheDir)>2 && IsDriveLetter(TheDir)==FALSE ) {
if ( RemoveDirectory(TheDir)==0)
return FALSE;
} else return TRUE;
}
else if ( (stricmp(Pattern,"*.*")==0) && (DelSubDirs==TRUE)
&& (stricmp(TheDir,CurDir)!=0) ) {
if ( RemoveDirectory(TheDir)==0)
return FALSE;
}
return TRUE;
}
- How to design easily a non deterministic progress bar (see the image above) when we cannot evaluate exactly the operation length in terms of time or count:
The answer to this question consists of the following steps:
- Put on your dialog a Static control, for example. (It can be, in fact, another kind of control like Edit Box or Button, but the Static control is more appropriate.) This will be our progress bar.
- At the beginning of the long task (long copy, search etc.), set a timer by specifying a timer procedure if you don't already have a global window procedure related to your UI.
- In the timer procedure or the global one (in the
WM_TIMER
message handling), make a call to the function responsible for the progress bar update such as calling the function: MakeGradient(...). In this article, the call is made in the WM_TIMER message handling. The trick to display something like a progress bar is the Marlett font for this control. I have used the digit 1 (1) for the empty squares, and the g (g) character for the filled squares, moving towards in a cyclic time. In fact, you can display anything you want in other existing fonts on the target machine too; the basic idea remains the same. The common length of our strings in the table (str in the above code) can be fixed dynamically with respect to the control width and in order to almost hold the whole string.
- Once the operation has reached its end, kill the timer.
- How to realize a gradient color brush (Alpha-blend) on any rectangle of any control (see the image above) by simply calling one function:
As indicated before, all the decoration around the main frame window and the progress bar painting is done by a call to one function, namely MakeGradient(...) with many parameters to personalize the appearance of the control. This function can be called on any window (Static, button, Edit etc.) by providing a good handle to the window to get a device context handle, a rectangle on which the gradient can be done, and other information as colors, text, font name, font size etc. It's up to your imagination to use this function in your projects. Note that this function works only on Windows 98/2000 and up, since the Microsoft library msimg32.dll (Extension component for Windows GDI) is not available on Windows 95 and NT 4.0. I am at least sure that it works on Win 98/2000/XP. To see details about functions that can be called from this library, you can examine the header file WINGDI.H, they are prototyped there.
Conclusion
This article has shown how we can drive install/uninstall one or more MSI packages with all operations that can be done before, during, and after, by just providing a configuration file. Feel free to make any suggestion, criticism, or improvement.
History
Initial version 1.0.0.1 - August 8, 2004.