这是我打算自己回答的问题,但请随意添加其他方法来完成此任务.
我正在打包应用程序以用于各种配置,我确定在MSI中执行自定义逻辑的最可靠方法是编写我自己的自定义操作DLL,它可以从PROPERTY表读取/写入,杀死进程,确定是否需要升级应用程序(然后在PROPERTY表中记录答案),并写入标准MSI日志.
我的解决方案是在Delphi中,需要您可以在此处下载的开源JEDI API翻译.我发现的一个问题是使用JwaMSI头文件的例子很少.希望有人会发现这是一个有用的例子.
这是主要单元,后面有第二个支持单元(可以包含在同一个DLL项目中).只需在Delphi中创建一个新的DLL(库),然后复制/粘贴此代码即可.该单元导出2个可从MSI调用的函数.他们是:
CheckIfUpgradeable
KillRunningApp
这两个函数都从属性表中读取一个PROPERTY值,并在完成时设置一个值.这个想法是,然后第二个自定义操作可以读取此属性并抛出错误,或将其用作安装条件.
此代码更多是一个示例,在下面的示例中,它检查是否需要升级'notepad.exe'的版本(这意味着存储在属性表值"NOTEPAD_VERSON"中的版本大于版本系统上的notepad.exe).如果不是,则将"UPGRADEABLE_VERSION"的属性设置为"NO"(默认情况下,此属性设置为"YES").
此代码还在PROPERTY表中查找"PROGRAM_TO_KILL",并在程序运行时终止该程序.它需要包含要杀死的程序的文件扩展名,例如"Notepad.exe"
library MsiHelper; uses Windows, SysUtils, Classes, StrUtils, jwaMSI, jwaMSIDefs, jwaMSIQuery, JclSysInfo, PsApi, MSILogging in 'MSILogging.pas'; {$R *.res} function CompareVersionNumbers(AVersion1, AVersion2: string): Integer; var N1, N2: Integer; //Returns 1 if AVersion1 < AVersion2 //Returns -1 if AVersion1 > AVersion2 //Returns 0 if values are equal function GetNextNumber(var Version: string): Integer; var P: Integer; S: string; begin P := Pos('.', Version); if P > 0 then begin S := Copy(Version, 1, P - 1); Version := Copy(Version, P + 1, Length(Version) - P); end else begin S := Version; Version := ''; end; if S = '' then Result := -1 else try Result := StrToInt(S); except Result := -1; end; end; begin Result := 0; repeat N1 := GetNextNumber(AVersion1); N2 := GetNextNumber(AVersion2); if N2 > N1 then begin Result := 1; Exit; end else if N2 < N1 then begin Result := -1; Exit; end until (AVersion1 = '') and (AVersion2 = ''); end; function GetFmtFileVersion(const FileName: String = ''; const Fmt: String = '%d.%d.%d.%d'): String; var sFileName: String; iBufferSize: DWORD; iDummy: DWORD; pBuffer: Pointer; pFileInfo: Pointer; iVer: array[1..4] of Word; begin // set default value Result := ''; // get filename of exe/dll if no filename is specified sFileName := FileName; if (sFileName = '') then begin // prepare buffer for path and terminating #0 SetLength(sFileName, MAX_PATH + 1); SetLength(sFileName, GetModuleFileName(hInstance, PChar(sFileName), MAX_PATH + 1)); end; // get size of version info (0 if no version info exists) iBufferSize := GetFileVersionInfoSize(PChar(sFileName), iDummy); if (iBufferSize > 0) then begin GetMem(pBuffer, iBufferSize); try // get fixed file info (language independent) GetFileVersionInfo(PChar(sFileName), 0, iBufferSize, pBuffer); VerQueryValue(pBuffer, '\', pFileInfo, iDummy); // read version blocks iVer[1] := HiWord(PVSFixedFileInfo(pFileInfo)^.dwFileVersionMS); iVer[2] := LoWord(PVSFixedFileInfo(pFileInfo)^.dwFileVersionMS); iVer[3] := HiWord(PVSFixedFileInfo(pFileInfo)^.dwFileVersionLS); iVer[4] := LoWord(PVSFixedFileInfo(pFileInfo)^.dwFileVersionLS); finally FreeMem(pBuffer); end; // format result string Result := Format(Fmt, [iVer[1], iVer[2], iVer[3], iVer[4]]); end; end; function KillRunningApp(hInstall: MSIHandle): Integer; stdcall; var aProcesses: array[0..1023] of DWORD; cbNeeded: DWORD; cProcesses: DWORD; i: integer; szProcessName: array[0..MAX_PATH - 1] of char; hProcess: THandle; hMod: HModule; sProcessName : PChar; iProcessNameLength : Cardinal; begin iProcessNameLength := MAX_PATH; sProcessName := StrAlloc(MAX_PATH); try //reads the value from "PROGRAM_TO_KILL" that is stored in the PROPERTY table MsiGetProperty(hInstall, 'PROGRAM_TO_KILL', sProcessName, iProcessNameLength); if not EnumProcesses(@aProcesses, sizeof(aProcesses), cbNeeded) then begin Exit; end; cProcesses := cbNeeded div sizeof(DWORD); for i := 0 to cProcesses - 1 do begin hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ or PROCESS_TERMINATE, False, aProcesses[i]); try if hProcess <> 0 then begin if EnumProcessModules(hProcess, @hMod, sizeof(hMod), cbNeeded) then begin GetModuleBaseName(hProcess, hMod, szProcessName, sizeof(szProcessName)); if UpperCase(szProcessName) = UpperCase(sProcessName) then begin TerminateProcess(hProcess, 0); end; end; end; finally CloseHandle(hProcess); end; end; finally StrDispose(sProcessName); end; Result:= ERROR_SUCCESS; //return success regardless of actual outcome end; function CheckIfUpgradeable(hInstall: MSIHandle): Integer; stdcall; var Current_Notepad_version : PChar; Current_Notepad_version_Length : Cardinal; sWinDir, sProgramFiles : string; bUpgradeableVersion : boolean; iNotepad_compare : integer; sNotepad_version : string; sNotepad_Location : string; iResult : Cardinal; begin bUpgradeableVersion := False; sWinDir := ExcludeTrailingBackslash(JclSysInfo.GetWindowsFolder); sProgramFiles := ExcludeTrailingBackslash(JclSysInfo.GetProgramFilesFolder); Current_Notepad_version_Length := MAX_PATH; Current_Notepad_version := StrAlloc(MAX_PATH); sNotepad_Location := sWinDir+'\system32\Notepad.exe'; iResult := ERROR_SUCCESS; try //reads the value from "NOTEPAD_VERSION" that is stored in the PROPERTY table MsiGetProperty(hInstall, 'NOTEPAD_VERSION', Current_Notepad_version, Current_Notepad_version_Length); if Not (FileExists(sNotepad_Location)) then begin bUpgradeableVersion := True; LogString(hInstall,'Notepad.exe was not found at: "'+sNotepad_Location+'"'); LogString(hInstall,'This version will be upgraded.'); iResult := ERROR_SUCCESS; Exit; end; sNotepad_version := GetFmtFileVersion(sNotepad_Location); LogString(hInstall,'Found Notepad version="'+sNotepad_version+'"'); iNotepad_compare := CompareVersionNumbers(sNotepad_version,StrPas(Current_Notepad_version)); if (iNotepad_compare < 0) then begin bUpgradeableVersion := False; end else begin bUpgradeableVersion := True; end; if bUpgradeableVersion then begin LogString(hInstall,'This version will be upgraded.'); iResult := ERROR_SUCCESS; end else begin MsiSetProperty(hInstall,'UPGRADEABLE_VERSION','NO'); //this indicates failure -- this value is read by another custom action executed after this action LogString(hInstall,'ERROR: A newer version of this software is already installed. Setup cannot continue!'); iResult := ERROR_SUCCESS; end; finally StrDispose(Current_Notepad_version); end; Result:= iResult; //this function always returns success, however it could return any of the values listed below // //Custom Action Return Values //================================ // //Return value Description // //ERROR_FUNCTION_NOT_CALLED Action not executed. //ERROR_SUCCESS Completed actions successfully. //ERROR_INSTALL_USEREXIT User terminated prematurely. //ERROR_INSTALL_FAILURE Unrecoverable error occurred. //ERROR_NO_MORE_ITEMS Skip remaining actions, not an error. // end; exports CheckIfUpgradeable; exports KillRunningApp; begin end.
这是支持单位"MSILogging.pas".此单元可以在其他MSI DLL项目中原样使用.
unit MSILogging; interface uses Windows, SysUtils, JwaMsi, JwaMsiQuery, JwaMSIDefs; procedure LogString(hInstall: MSIHandle; sMsgString : string); function MsiMessageBox(hInstall: MSIHandle; sMsgString : string; dwDlgFlags : integer): integer; implementation procedure LogString(hInstall: MSIHandle; sMsgString : string); var hNewMsiHandle : MSIHandle; begin try hNewMsiHandle := MsiCreateRecord(2); sMsgString := '-- MSI_LOGGING -- ' + sMsgString; MsiRecordSetString(hNewMsiHandle, 0, PChar(sMsgString) ); MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_INFO), hNewMsiHandle); finally MsiCloseHandle(hNewMsiHandle); end; end; function MsiMessageBox(hInstall: MSIHandle; sMsgString : string; dwDlgFlags : integer): integer; var hNewMsiHandle : MSIHandle; begin try hNewMsiHandle := MsiCreateRecord(2); MsiRecordSetString(hNewMsiHandle, 0, PChar(sMsgString) ); finally MsiCloseHandle(hNewMsiHandle); end; //Result := (MsiProcessMessage(hInstall, INSTALLMESSAGE(dwDlgFlags), hNewMsiHandle)); Result := (MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER + dwDlgFlags), hNewMsiHandle)); end; end.