Как лучше всего определить пользовательское действие в WiX?

У меня есть WiX установщик и одно настраиваемое действие (плюс отмена и откат) для него, которое использует свойство из установщика, Пользовательское действие должно произойти после того, как все файлы находятся на жестком диске. Кажется, для этого вам нужно 16 записей в файле WXS; восемь в корне, например:

<CustomAction Id="SetForRollbackDo" Execute="immediate" Property="RollbackDo" Value="[MYPROP]"/>
<CustomAction Id="RollbackDo" Execute="rollback" BinaryKey="MyDLL" DllEntry="UndoThing" Return="ignore"/>
<CustomAction Id="SetForDo" Execute="immediate" Property="Do" Value="[MYPROP]"/>
<CustomAction Id="Do" Execute="deferred" BinaryKey="MyDLL" DllEntry="DoThing" Return="check"/>
<CustomAction Id="SetForRollbackUndo" Execute="immediate" Property="RollbackUndo" Value="[MYPROP]"/>
<CustomAction Id="RollbackUndo" Execute="rollback" BinaryKey="MyDLL" DllEntry="DoThing" Return="ignore"/>
<CustomAction Id="SetForUndo" Execute="immediate" Property="Undo" Value="[MYPROP]"/>
<CustomAction Id="Undo" Execute="deferred" BinaryKey="MyDLL" DllEntry="UndoThing" Return="check"/>

И восемь внутри InstallExecuteSequence, например:

<Custom Action="SetForRollbackDo" After="InstallFiles">REMOVE&lt;>"ALL"</Custom>
<Custom Action="RollbackDo" After="SetForRollbackDo">REMOVE&lt;>"ALL"</Custom>
<Custom Action="SetForDo" After="RollbackDo">REMOVE&lt;>"ALL"</Custom>
<Custom Action="Do" After="SetForDo">REMOVE&lt;>"ALL"</Custom>
<Custom Action="SetForRollbackUndo" After="InstallInitialize">REMOVE="ALL"</Custom>
<Custom Action="RollbackUndo" After="SetForRollbackUndo">REMOVE="ALL"</Custom>
<Custom Action="SetForUndo" After="RollbackUndo">REMOVE="ALL"</Custom>
<Custom Action="Undo" After="SetForUndo">REMOVE="ALL"</Custom>

Есть ли лучший способ?

Ответы

Ответ 1

Я столкнулся с той же проблемой при написании установщиков WiX. Мой подход к проблеме в основном похож на то, что предложил Майк, и у меня есть сообщение в блоге Реализация пользовательских действий WiX часть 2: использование пользовательских таблиц.

Короче говоря, вы можете определить пользовательскую таблицу для своих данных:

<CustomTable Id="LocalGroupPermissionTable">
    <Column Id="GroupName" Category="Text" PrimaryKey="yes" Type="string"/>
    <Column Id="ACL" Category="Text" PrimaryKey="no" Type="string"/>
    <Row>
        <Data Column="GroupName">GroupToCreate</Data>
        <Data Column="ACL">SeIncreaseQuotaPrivilege</Data>
    </Row>
</CustomTable>

Затем напишите одно немедленное пользовательское действие, чтобы запланировать отложенные, откатные и совершать пользовательские действия:

extern "C" UINT __stdcall ScheduleLocalGroupCreation(MSIHANDLE hInstall)
{
    try {
        ScheduleAction(hInstall,L"SELECT * FROM CreateLocalGroupTable", L"CA.LocalGroupCustomAction.deferred", L"create");
        ScheduleAction(hInstall,L"SELECT * FROM CreateLocalGroupTable", L"CA.LocalGroupCustomAction.rollback", L"create");
    }
    catch( CMsiException & ) {
        return ERROR_INSTALL_FAILURE;
    }
    return ERROR_SUCCESS;
}

Следующий код показывает, как планировать одно настраиваемое действие. В основном вы просто открываете пользовательскую таблицу, читаете нужное свойство (вы можете получить схему любой настраиваемой таблицы, вызвав MsiViewGetColumnInfo()), а затем отформатируйте необходимые свойства в CustomActionData свойство (я использую форму /propname:value, хотя вы можете использовать все, что захотите).

void ScheduleAction(MSIHANDLE hInstall,
            const wchar_t *szQueryString,
            const wchar_t *szCustomActionName,
            const wchar_t *szAction)
{
    CTableView view(hInstall,szQueryString);
    PMSIHANDLE record;

    //For each record in the custom action table
    while( view.Fetch(record) ) {
        //get the "GroupName" property
        wchar_t recordBuf[2048] = {0};
        DWORD    dwBufSize(_countof(recordBuf));
        MsiRecordGetString(record, view.GetPropIdx(L"GroupName"), recordBuf, &dwBufSize);

        //Format two properties "GroupName" and "Operation" into
        //the custom action data string.
        CCustomActionDataUtil formatter;
        formatter.addProp(L"GroupName", recordBuf);
        formatter.addProp(L"Operation", szAction );

        //Set the "CustomActionData" property".
        MsiSetProperty(hInstall,szCustomActionName,formatter.GetCustomActionData());

        //Add the custom action into installation script. Each
        //MsiDoAction adds a distinct custom action into the
        //script, so if we have multiple entries in the custom
        //action table, the deferred custom action will be called
        //multiple times.
        nRet = MsiDoAction(hInstall,szCustomActionName);
    }
}

Что касается реализации отложенных, откатных и фиксированных пользовательских действий, я предпочитаю использовать только одну функцию и использовать MsiGetMode(), чтобы отличить, что нужно сделать:

extern "C" UINT __stdcall LocalGroupCustomAction(MSIHANDLE hInstall)
{
    try {
        //Parse the properties from the "CustomActionData" property
        std::map<std::wstring,std::wstring> mapProps;
        {
            wchar_t szBuf[2048]={0};
            DWORD dwBufSize = _countof(szBuf); MsiGetProperty(hInstall,L"CustomActionData",szBuf,&dwBufSize);
            CCustomActionDataUtil::ParseCustomActionData(szBuf,mapProps);
        }

        //Find the "GroupName" and "Operation" property
        std::wstring sGroupName;
        bool bCreate = false;
        std::map<std::wstring,std::wstring>::const_iterator it;
        it = mapProps.find(L"GroupName");
        if( mapProps.end() != it ) sGroupName = it->second;
        it = mapProps.find(L"Operation");
        if( mapProps.end() != it )
            bCreate = wcscmp(it->second.c_str(),L"create") == 0 ? true : false ;

        //Since we know what opeartion to perform, and we know whether it is
        //running rollback, commit or deferred script by MsiGetMode, the
        //implementation is straight forward
        if( MsiGetMode(hInstall,MSIRUNMODE_SCHEDULED) ) {
            if( bCreate )
                CreateLocalGroup(sGroupName.c_str());
            else
                DeleteLocalGroup(sGroupName.c_str());
        }
        else if( MsiGetMode(hInstall,MSIRUNMODE_ROLLBACK) ) {
            if( bCreate )
                DeleteLocalGroup(sGroupName.c_str());
            else
                CreateLocalGroup(sGroupName.c_str());
        }
    }
    catch( CMsiException & ) {
        return ERROR_INSTALL_FAILURE;
    }
    return ERROR_SUCCESS;
}

Используя вышеуказанный метод, для типичного настраиваемого набора действий вы можете уменьшить таблицу настраиваемых действий до пяти записей:

<CustomAction Id="CA.ScheduleLocalGroupCreation"
              Return="check"
              Execute="immediate"
              BinaryKey="CustomActionDLL"
              DllEntry="ScheduleLocalGroupCreation"
              HideTarget="yes"/>
<CustomAction Id="CA.ScheduleLocalGroupDeletion"
              Return="check"
              Execute="immediate"
              BinaryKey="CustomActionDLL"
              DllEntry="ScheduleLocalGroupDeletion"
              HideTarget="yes"/>
<CustomAction Id="CA.LocalGroupCustomAction.deferred"
              Return="check"
              Execute="deferred"
              BinaryKey="CustomActionDLL"
              DllEntry="LocalGroupCustomAction"
              HideTarget="yes"/>
<CustomAction Id="CA.LocalGroupCustomAction.commit"
              Return="check"
              Execute="commit"
              BinaryKey="CustomActionDLL"
              DllEntry="LocalGroupCustomAction"
              HideTarget="yes"/>
<CustomAction Id="CA.LocalGroupCustomAction.rollback"
              Return="check"
              Execute="rollback"
              BinaryKey="CustomActionDLL"
              DllEntry="LocalGroupCustomAction"
              HideTarget="yes"/>

И таблица InstallSquence только для двух записей:

<InstallExecuteSequence>
    <Custom Action="CA.ScheduleLocalGroupCreation" 
            After="InstallFiles">
        Not Installed
    </Custom>
    <Custom Action="CA.ScheduleLocalGroupDeletion" 
            After="InstallFiles">
        Installed
    </Custom>
</InstallExecuteSequence>

Кроме того, с небольшим усилием большая часть кода может быть записана для повторного использования (например, чтение из пользовательской таблицы, получение свойств, форматирование необходимых свойств и установка свойств CustomActionData) и записей в пользовательском действии таблица не является специфичной для приложения (конкретные данные приложения записаны в пользовательской таблице), мы можем поместить таблицу пользовательских действий в собственный файл и просто включить ее в каждый проект WiX.

Для файла DLL пользовательского действия, поскольку данные приложения считываются из пользовательской таблицы, мы можем оставлять конкретные данные приложения из реализации DLL, поэтому таблица настраиваемых действий может стать библиотекой и, следовательно, ее легче использовать.

Вот как я сейчас пишу свои пользовательские действия WiX, если кто-то знает, как улучшить дальше, я бы очень это оценил.:)

(Вы также можете найти полный исходный код в своем сообщении в блоге, Реализация пользовательских действий WiX часть 2: использование пользовательских таблиц.).

Ответ 2

Пользовательские действия WiX - отличная модель для подражания. В этом случае вы объявляете только CustomAction, немедленное действие, отложенное действие и действие отката. Вы только планируете, с Custom, немедленное действие, где немедленное действие реализуется как код в родной DLL.

Затем в непосредственном действии code вы вызываете MsiDoAction, чтобы запланировать откат и отложенные действия: поскольку они отложены, они записываются в script в точке, которую вы вызываете MsiDoAction, а не выполнить немедленно. Вам нужно будет вызвать MsiSetProperty, чтобы установить данные пользовательских действий.

Загрузите исходный код WiX и изучите, как работает IISExtension. Действия WiX обычно анализируют пользовательскую таблицу и генерируют данные для свойства отложенного действия на основе этой таблицы.

Ответ 3

Если у вас есть сложные пользовательские действия, требующие поддержки отката, вы можете подумать о написании расширения Wix. Расширения обычно предоставляют поддержку авторинга (т.е. Новые теги XML, которые сопоставляются с записями таблицы MSI), а также автоматическое планирование пользовательских действий.

Это больше работает, чем просто писать пользовательское действие, но как только ваши ЦС достигают определенного уровня сложности, легкость создания этих расширений может стоить того.