Introduction
Most CE apps use Microsoft's CABWIZ tool to make their installers and uninstallers. I think it's an ugly tool, with a clunky user-interface. This article discusses how to write a manual installer/uninstaller instead.
Actually, the short answer is "You can't." CABWIZ is still needed. However, what you can do is to use CABWIZ only as a skeleton, whose only task is to execute your own manual uninstaller. This article is therefore about that skeleton.
I have one very important piece of advice: always check errors, and display the precise error message on-screen. Use GetLastError()
and FormatMessage()
to get the error text. And when you display it on-screen, note precisely where the error came from. Do not lump several errors into a generic "some failure occurred" message. This is because -- I guarantee -- there will be errors in your setup program, and end-users will write to you to complain, and unless you have a complete diagnostic then you'll never know what went wrong. (I have omitted error-checking in this article for simplicity).
Background
CABWIZ is a tool that comes bundled with eMbedded Visual Tools 3.0 (free) from Microsoft. Normally, you write an .INF file and compile it with CABWIZ. This generates a .CAB file. The .CAB file can be executed on the PocketPC, whereupon it installs the application and adds an entry into the Settings > RemovePrograms.
It's easy to write an installer manually - all you have to do is copy your executable into the folder \Program, and add a shortcut to it in \Windows\Programs. And maybe a shortcut in the start menu, and maybe register some registry keys.
The problem is how to register your program so it shows up under Start > Settings > System > RemovePrograms. Well, the system populates the RemovePrograms dialog with entries from the registry keys.
HKEY_LOCAL_MACHINE\SOFTWARE\Apps\<Provider> <AppName>
Within these keys, the dialog looks for a DWORD
value Instl
; if it is set to 1, then the app is currently installed, and should be listed in the dialog. If it is 0, then the app used to be installed but is no longer, and so won't be listed.
When the user elects to remove an installed program, it looks in the registry key for the values CmdFile
and IsvFile
. The first points to a .DAT file, usually \Windows\AppMgr\<Provider> <AppName>.DAT, which was put there by the installer. It is written in a proprietary .DAT format, such as is generated by CABWIZ. The second points to a .DLL file with four particular exports, which we will discuss.
The system's built-in uninstaller executes the exported function Uninstall_Init
from the DLL, then parses and executes the .DAT file, then executes Uninstall_Exit
. Therefore, for our manual-uninstaller purposes, our manual-installer would have to use CABWIZ to generate a .DAT file, and to place it in that directory, and to place our .DLL there as well.
But there's a problem. There are several other related registry keys which the system placed there when it executed the initial CAB file (i.e. when it installed the software in the first place). These extra registry keys are shared between all installed applications. And they're not documented. Therefore, it is not safe to do the install manually, since we mightn't set up these registry keys properly. Instead, we must use a .CAB file to do the install.
All is not lost. We hate CAB-based installers, and even though we're forced to use them, we can at least use them in a minimal way: so that the ONLY task of the CAB-installer is to set up these registry keys, and nothing more. Then we can put the real brains of our installer inside our own application, and the real brains of our uninstaller inside our custom-written .DLL file.
Incidentally, when the user clicks the 'Remove' button, the system pauses for some time with its CE-logo hourglass (between half a second and twenty seconds, usually closer to one second) before executing the setup.dll. There's no earthly reason for this. Doh!
A brief note: Our installer will invoke the CAB purely for minimal purposes, and so we want to invoke it completely silent -- without any dialogs. Here's how:
#include <windows.h>
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPTSTR,int)
{
wchar_t fn[MAX_PATH]; GetModuleFileName(NULL,fn,MAX_PATH);
wchar_t *c=fn, *lastslash=c;
while (*c!=0) {if (*c=='\\') lastslash=c+1; c++;}
wcscpy(lastslash,L"ce_setup.cab");
HKEY hkey; LONG lres;
lres=RegOpenKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Apps\\Lu ce_setup",0,0,&hkey);
if (lres==ERROR_SUCCESS)
{ DWORD val=0;
RegSetValueEx(hkey,L"Instl",0,REG_DWORD,(LPBYTE)&val,
sizeof(val));
RegCloseKey(hkey);
}
DWORD attr = GetFileAttributes(fn);
if (attr==0xFFFFFFFF)
{ return MessageBox(NULL,fn,L"Not found",MB_OK);
}
SetFileAttributes(fn,attr|FILE_ATTRIBUTE_READONLY);
const wchar_t *cmd = L"\\windows\\wceload.exe";
wchar_t arg[MAX_PATH+40];
wsprintf(arg,L"/noaskdest /noui \"%s\"",fn);
PROCESS_INFORMATION pi;
BOOL res=CreateProcess(cmd,arg,0,0,0,0,0,0,0,&pi);
if (!res) MessageBox(NULL,L"Couldn't",L"ce_setup",MB_OK);
else
{ WaitForSingleObject(pi.hProcess,50000);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
SetFileAttributes(fn,attr);
return 0;
}
Setup.dll
As discussed, when the user clicks on 'Remove', the system will invoke a custom DLL. And this is where we put the brains of our uninstaller. Here is the form of this DLL. The two exported installer-functions are as follows:
#include <windows.h>
enum codeINSTALL_INIT
{ codeINSTALL_INIT_CONTINUE=0,
codeINSTALL_INIT_CANCEL
};
enum codeINSTALL_EXIT
{ codeINSTALL_EXIT_DONE=0,
codeINSTALL_EXIT_UNINSTALL
};
extern "C" __declspec(dllexport) codeINSTALL_INIT
Install_Init(HWND hpar, BOOL fFirstCall,
BOOL fPreviouslyInstalled,
LPCTSTR pszSuggestedInstallDir)
{
return codeINSTALL_INIT_CONTINUE;
}
extern "C" __declspec(dllexport) codeINSTALL_EXIT
Install_Exit(HWND hpar, LPCTSTR pszChosenInstallDir,
WORD cFailedDirs, WORD cFailedFiles,
WORD cFailedRegKeys, WORD cFailedRegVals,
WORD cFailedShortcuts)
{
wchar_t buf[MAX_PATH]; wcscpy(buf,pszChosenInstallDir);
wcscat(buf,L"\\ce_setup_dummyN.txt");
int len=wcslen(buf)-5;
buf[len]='1'; DeleteFile(buf);
buf[len]='2'; DeleteFile(buf);
buf[len]='3'; DeleteFile(buf);
buf[len]='4'; DeleteFile(buf);
MessageBox(hpar,L"Installing...",L"My App",MB_OK);
return codeINSTALL_EXIT_DONE;
}
The ugly issue with the temporary files will be discussed below.
The two exported uninstaller-functions are given below. Note that we make use of a global variable install_dir
to record the install directory. That's because this information is given to us when the system calls Uninstall_Init()
, but not when it calls Uninstall_Exit()
(which is when we want it).
wchar_t install_dir[MAX_PATH];
enum codeUNINSTALL_INIT
{ codeUNINSTALL_INIT_CONTINUE=0,
codeUNINSTALL_INIT_CANCEL
};
enum codeUNINSTALL_EXIT
{ codeUNINSTALL_EXIT_DONE=0
};
extern "C" __declspec(dllexport) codeUNINSTALL_INIT
Uninstall_Init(HWND hpar, LPCTSTR pszInstallDir)
{
wcscpy(install_dir,pszInstallDir);
HWND halready = FindWindow(L"MyMainClass",L"My App");
if (!halready) return codeUNINSTALL_INIT_CONTINUE;
MessageBox(hpar,L"Quit program before uninstalling it",
L"My App",MB_OK);
return codeUNINSTALL_INIT_CANCEL;
}
extern "C" __declspec(dllexport) codeUNINSTALL_EXIT
Uninstall_Exit(HWND hpar)
{
MessageBox(hpar,L"Uninstalling...",L"My App",MB_OK);
return codeUNINSTALL_EXIT_DONE;
}
BOOL WINAPI DllMain(HANDLE, DWORD, LPVOID) {return TRUE;}
The CAB file
As discussed, our installer will execute a CAB file (silently) to set up the registry keys. This CAB file will include the setup.dll that we built in the previous section. To build the CAB file, we will use Microsoft's CABWIZ. This is the control file ce_setup.inf:
[Version]
Signature = "$Windows NT$"
Provider = "Lu"
CESignature = "$Windows CE$"
[CEStrings]
AppName = "ce_setup"
InstallDir = %CE1%
The system concatenates Provider
and AppName
(with a space in between), and this concatenation is what appears in the RemovePrograms dialog. The %CE1%
stands for \Program Files -- other %CE#%
values are listed in the online help documentation under the help topic "Destination Directory Macro Reference". You can also use e.g. InstallDir = %CE1%\Subdirectory
.
[DefaultInstall]
CopyFiles = FileList
AddReg =
CESetupDLL = ce_setup.dll
[FileList]
ce_setup_dummy1.txt,ce_setup_dummy.txt,
ce_setup_dummy2.txt,ce_setup_dummy.txt,
ce_setup_dummy3.txt,ce_setup_dummy.txt,
ce_setup_dummy4.txt,ce_setup_dummy.txt,
; Bug! If using a dll, then less than four files
; generate an error when running the cab. ???
[SourceDisksNames]
1 = ,"source directory",,
2 = ,"bin directory",,ARMRel
[SourceDisksFiles]
ce_setup_dummy.txt=1
ce_setup.dll=2
[DestinationDirs]
FileList = 0,%InstallDir%
Here CESetupDLL
refers to the setup.dll that we built previously. [SourceDisksFiles]
associates it with an index number, and CABWIZ looks under [SourceDisksNames]
to find where this file is located in the development machine.
[FileList]
is a list of all the files that will be installed in the PocketPC. You might expect this to be empty. But there's a bug in CABWIZ whereby, if you use a setup.dll but have less than four items, it crashes. That's why we include four dummy files (each with size 1 byte!). Anyway, our Install_Exit
function deletes them again immediately after installation has finished. Doh!
To invoke CABWIZ on this .inf file, and so build our .CAB, use this command. (it's all just a single line. Write it in a batch file!)
c:\progra~1\windows ce tools\wce300\pocket pc
2002\support\activesync\windows ce
application installation\cabwiz\cabwiz.exe" ce_setup.inf /err cabwiz.log