2008-12-15 14 views
6

Esta es una pregunta que tengo la intención de contestar a mí mismo, pero no dude en agregar otras formas de lograr esto.¿Cómo escribo una DLL de acción personalizada para usar en una MSI?

Estaba empaquetando una aplicación para usar en una amplia variedad de configuraciones, y determiné que la forma más confiable de realizar una lógica personalizada dentro de mi MSI sería escribir mi propia DLL de acción personalizada que podría leer/escribir de la tabla PROPERTY, finalice un proceso, determine si una aplicación debe actualizarse (y luego registre la respuesta en la tabla PROPERTY) y escriba en el registro estándar de MSI.

+0

Otra buena, pero más viejo, solución para las acciones personalizadas escritas en Delphi: http://community.flexerasoftware.com/showthread.php?124840-A-primer-on-custom-actions-written-in- Delphi – Mick

Respuesta

11

Mi solución está en Delphi, y requiere las traducciones de API de fuente abierta JEDI que puede download here. Un problema que he encontrado es que los ejemplos para usar los encabezados JwaMSI son pocos y distantes. Con suerte, alguien encontrará esto como un ejemplo útil.

Aquí está la unidad principal, con una segunda unidad de soporte a continuación (que puede incluir en el mismo proyecto DLL). Simplemente cree una nueva DLL (biblioteca) en Delphi y copie/pegue este código. Esta unidad exporta 2 funciones que se pueden llamar desde el MSI. Ellos son:

  1. CheckIfUpgradeable
  2. KillRunningApp

Ambas funciones leer un valor de propiedad de la tabla de propiedades y establezca un valor cuando la completa. La idea es que una segunda acción personalizada pueda leer esta propiedad y arrojar un error, o usarla como una condición de instalación.

Este código es más que un ejemplo, y en este ejemplo a continuación está comprobando si la versión de 'notepad.exe' debe actualizarse (esto significa que la versión almacenada en el valor de la tabla de propiedades "NOTEPAD_VERSON" es mayor que la versión de notepad.exe en el sistema). Si no es así, establece la propiedad de "UPGRADEABLE_VERSION" en "NO" (esta propiedad está establecida en "YES" de manera predeterminada).

Este código también se ve en la tabla PROPERTY para "PROGRAM_TO_KILL" y matará ese programa si se está ejecutando. Necesita incluir la extensión de archivo del programa para matar, p. "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. 

Y aquí está la unidad de soporte "MSILogging.pas". Esta unidad se puede utilizar tal cual en otros proyectos DLL de MSI.

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. 
+1

Tenga en cuenta que para cualquier función que desee que se pueda llamar desde su MSI, debe incluir "stdcall;" después de su prototipo Además, debe incluir una línea en algún lugar que diga "export function_name". – Mick

Cuestiones relacionadas