如何编写自定义动作DLL以在MSI中使用?

时间:2008-12-15 02:47:56

标签: delphi windows-installer custom-action

这是我打算回答的问题,但请随意添加其他方法来完成此任务。

我正在打包一个应用程序,用于各种配置,我确定在MSI中执行自定义逻辑的最可靠方法是编写我自己的自定义操作DLL,它可以从中读取/写入PROPERTY表,终止进程,确定是否需要升级应用程序(然后在PROPERTY表中记录答案),并写入标准MSI日志。

1 个答案:

答案 0 :(得分:11)

我的解决方案是在Delphi中,并且需要您可以download here的开源JEDI API翻译。我发现的一个问题是使用JwaMSI头文件的例子很少。希望有人会发现这是一个有用的例子。

这是主要单元,后面有第二个支持单元(可以包含在同一个DLL项目中)。只需在Delphi中创建一个新的DLL(库),然后复制/粘贴此代码即可。该单元导出2个可从MSI调用的函数。他们是:

  1. CheckIfUpgradeable
  2. KillRunningApp
  3. 这两个函数都从属性表中读取一个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.