PocketPC 的手动卸载程序
如果你不想使用 CAB,可以这样做。
引言
大多数 CE 应用程序使用 Microsoft 的 CABWIZ 工具来创建其安装程序和卸载程序。我认为这是一个丑陋的工具,用户界面笨拙。本文讨论了如何改写手动安装程序/卸载程序。
实际上,简短的回答是“你不能”。CABWIZ 仍然是必需的。但是,你可以做到的是,只使用 CABWIZ 作为骨架,其唯一的任务是执行你自己的手动卸载程序。因此,本文将讨论这个骨架。
我有一条非常重要的建议:始终检查错误,并在屏幕上显示精确的错误消息。使用 GetLastError()
和 FormatMessage()
来获取错误文本。当你在屏幕上显示它时,请准确说明错误来自何处。不要将多个错误归结为通用的“发生某种失败”消息。因为——我保证——你的安装程序会出错,最终用户会写信给你抱怨,除非你有完整的诊断信息,否则你永远不知道出了什么问题。(为了简单起见,本文省略了错误检查)。
背景
CABWIZ 是 Microsoft 的 eMbedded Visual Tools 3.0(免费)附带的工具。通常,你编写一个 .INF 文件并使用 CABWIZ 进行编译。这将生成一个 .CAB 文件。该 .CAB 文件可以在 PocketPC 上执行,然后它会安装应用程序并在 Settings > RemovePrograms 中添加一个条目。
手动编写安装程序很容易——你只需将可执行文件复制到 \Program 文件夹,并在 \Windows\Programs 中为其添加快捷方式。也许还要在开始菜单中添加快捷方式,也许还要注册一些注册表项。
问题是如何注册你的程序,以便它显示在 Start > Settings > System > RemovePrograms 下。好吧,系统会从注册表项中填充 RemovePrograms 对话框。
HKEY_LOCAL_MACHINE\SOFTWARE\Apps\<Provider> <AppName>
在这些键中,对话框会查找一个名为 Instl
的 DWORD
值;如果设置为 1,则表示应用程序已安装,并应在对话框中列出。如果为 0,则表示应用程序曾经安装过但现在已不再安装,因此不会被列出。
当用户选择卸载已安装的程序时,它会查找注册表项中的 CmdFile
和 IsvFile
值。第一个指向一个 .DAT 文件,通常是 \Windows\AppMgr\<Provider> <AppName>.DAT,该文件由安装程序放置在那里。它以专有的 .DAT 格式编写,例如 CABWIZ 生成的格式。第二个指向一个具有四个特定导出函数的 .DLL 文件,我们将对其进行讨论。
系统内置的卸载程序会执行 DLL 中的导出函数 Uninstall_Init
,然后解析并执行 .DAT 文件,最后执行 Uninstall_Exit
。因此,对于我们的手动卸载程序来说,我们的手动安装程序必须使用 CABWIZ 生成一个 .DAT 文件,并将其放置在该目录中,同时也将我们的 .DLL 文件也放置在那里。
但是有一个问题。系统在执行初始 CAB 文件(即首次安装软件时)时,还会创建几个其他相关的注册表项。这些额外的注册表项是所有已安装应用程序共享的。而且它们没有被文档化。因此,手动安装是不安全的,因为我们可能无法正确设置这些注册表项。相反,我们必须使用 .CAB 文件进行安装。
一切并未失去。我们讨厌基于 CAB 的安装程序,即使我们被迫使用它们,我们至少可以以最小的方式使用它们:这样 CAB 安装程序的唯一任务就是设置这些注册表项,仅此而已。然后,我们可以将安装程序的真正核心逻辑放在我们自己的应用程序中,将卸载程序的真正核心逻辑放在我们自定义编写的 .DLL 文件中。
顺便说一句,当用户单击“Remove”按钮时,系统会暂停一段时间,显示 CE-logo 沙漏(从零点五秒到二十秒,通常接近一秒),然后才执行 setup.dll。这完全没有必要。真是的!
简要说明:我们的安装程序将仅为最小目的调用 CAB,因此我们希望完全静默地调用它——没有任何对话框。以下是实现方法。
// How to install a CAB completely silently #include <windows.h> int WINAPI WinMain(HINSTANCE,HINSTANCE,LPTSTR,int) { // get the current application directory 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"); // First, if cabwiz thinks that the app was already // installed, then it will complain. Hence we lie to it. // MUST USE THE CORRECT REGKEY HERE! it is // Provider <space> AppName // as defined in ce_setup.inf and used by cabwiz. 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); } // The act of running cabwiz will also delete the cab. // I think that's horrible. So I make it readonly DWORD attr = GetFileAttributes(fn); if (attr==0xFFFFFFFF) { return MessageBox(NULL,fn,L"Not found",MB_OK); } SetFileAttributes(fn,attr|FILE_ATTRIBUTE_READONLY); // Now run the cab using wceload, telling it to be quiet: 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); } // Really, we should wait until the cabwiz has finished // before restoring the file attributes. But I put a // timeout of 50s in there just in case something went // wrong, since it's bad in a PocketPC to leave // blocked processes. SetFileAttributes(fn,attr); return 0; }
Setup.dll
如前所述,当用户单击“Remove”时,系统将调用一个自定义 DLL。这就是我们将卸载程序的逻辑放入的地方。这是该 DLL 的形式。两个导出安装函数如下:
#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) { // can either return "continue", or abort before we've // even started the install 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) { // can either return "okay", or "uninstall what we've just // installed". But we're going to return "okay" since it // succeeded, and then delete some dummy files. The // dummy files were just a workaround around a bug in // cabwiz, not a sign of failure. 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); // And continue with other installation work if we want: MessageBox(hpar,L"Installing...",L"My App",MB_OK); // ... return codeINSTALL_EXIT_DONE; }
临时文件的棘手问题将在下面讨论。
下面给出了两个导出的卸载函数。请注意,我们使用全局变量 install_dir
来记录安装目录。因为系统在调用 Uninstall_Init()
时会给我们这个信息,但在调用 Uninstall_Exit()
时(我们想要它的时候)就不会了。
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) { // we can either return "continue", or "abort before we // even start the uninstall". We will abort if our // application is already open. Note: but first, take // this opportunity to record the install_dir. // That's because we'll need to refer to it in // Uninstall_Exit, but the system doesn't to tell us it // again. Hence the need to remember. It's safe to // remember in a global variable, since Uninstall_Init // and _Exit are invoked in the same instance of the DLL. 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) { // Here we do the main work of our uninstalling: MessageBox(hpar,L"Uninstalling...",L"My App",MB_OK); // ... return codeUNINSTALL_EXIT_DONE; } BOOL WINAPI DllMain(HANDLE, DWORD, LPVOID) {return TRUE;}
CAB 文件
如前所述,我们的安装程序将(静默地)执行一个 CAB 文件来设置注册表项。这个 CAB 文件将包含我们在上一节构建的 setup.dll。要构建 CAB 文件,我们将使用 Microsoft 的 CABWIZ。这是控制文件 ce_setup.inf
[Version]
Signature = "$Windows NT$"
Provider = "Lu"
CESignature = "$Windows CE$"
[CEStrings]
AppName = "ce_setup"
InstallDir = %CE1%
系统会连接 Provider
和 AppName
(中间有一个空格),这个连接结果会显示在 RemovePrograms 对话框中。%CE1%
代表 \Program Files —— 其他 %CE#%
值在在线帮助文档的“Destination Directory Macro Reference”帮助主题中列出。你也可以使用例如 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%
这里 CESetupDLL
指的是我们之前构建的 setup.dll。[SourceDisksFiles]
将其与索引号关联起来,CABWIZ 会在 [SourceDisksNames]
下查找开发机上的文件位置。
[FileList]
是将要安装在 PocketPC 上的所有文件的列表。你可能期望它是空的。但是 CABWIZ 有一个 bug,如果你使用 setup.dll 但有少于四个项目,它就会崩溃。这就是为什么我们包含四个虚拟文件(每个文件大小为 1 字节!)。无论如何,我们的 Install_Exit
函数会在安装完成后立即将它们删除。真是的!
要在此 .inf 文件上调用 CABWIZ,从而构建我们的 .CAB,请使用此命令。(它全部在单行上。将其写入批处理文件!)
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