[wix-users] Custom Action to Create and Install a File?

Edwin Castro egcastr at gmail.com
Wed Feb 19 10:33:12 PST 2020


I think you should start with designing your deferred custom action. It
will need to read all data it needs from CustomActionData. Begin in the
deferred custom action so you can define all the bits of data you need and
how they will be marshalled through CustomActionData. I think the managed
API provides a mechanism where the CustomActionData is effectively
marshalled as key-value pairs in a dictionary. It's been nearly a decade
since I've worked on managed custom actions so bare with me.

In your WiX source, you will want to declare your deferred custom action
using the CustomAction element but you may not want to schedule it in
InstallExecuteSequence. You may want to let your immediate custom action do
the scheduling. Your immediate custom action would be responsible for
making all decisions and writing all the required data into
CustomActionData which is "passed" to the deferred custom action when the
immediate custom action schedules the deferred custom action to run. If you
find out you need a rollback custom action, then the immediate custom
action is the right place to make decisions about rollback data that may
need to be passed to the rollback custom action through CustomActionData.
Depending on your needs you make need to assemble separate CustomActionData
for your deferred custom action and your rollback custom action. Remember
rollback custom actions are scheduled before (first) deferred custom
actions.

If you do not have a lot of decision making the a "full blown" immediate
custom action might be overkill. In some cases, you can use simple
SetProperty custom actions to directly set the CustomActionData for your
rollback and deferred custom actions. In that case, you use something like
SetProperty to set a property with the same name as the target
deferred/rollback custom action. That property becomes the CustomActionData
for the deferred/rollback custom action. If you format the contents just
right, then the managed API can properly parse the CustomActionData and
provide it to you through the managed API. In a case like this you would
explicitly schedule your deferred/rollback custom actions in
InstallExecuteSequence since there is no immediate custom action
responsible for doing that work.

Let me try a concrete example for the SetProperty approach. NOTE: I have
not checked this code and make no guarantees. This is meant as a starting
point so you can do some research and fix it for your own purposes.

<Fragment>
    <Property Id="LuaVersion2" Value="5.3" />
    <Property Id="LuaRocksConfigFileName" Value="config-[LuaVersion2].lua"
/>

    <!-- Construct required data for deferred custom action. These are not
strictly necessary as they can be used inline in the SetProperty for
Configure.LuaRocks but I think this easier to understand. -->
    <SetProperty Id="LuaRocksConfigPath"
Value="[LUAROCKS_INSTALLLOCATION]\[LuaRocksConfigFileName]"
Before="SetConfigure.LuaRocks" />
    <SetProperty Id="LuaRocksRootPath" Value="[LUAROCKS_INSTALLLOCATION]"
Before="SetConfigure.LuaRocks" />
    <SetProperty Id="LuaRootPath" Value="[LUA_INSTALLLOCATION]"
Before="SetConfigure.LuaRocks" />
    <SetProperty Id="ToolsPath" Value="[LLVMMINGW_INSTALLLOCATION]"
Before="SetConfigure.LuaRocks" />

    <!-- Set "CustomActionData" for Configure.LuaRocks deferred custom
action. I think for managed custom actions the format is
key=value;key=value -->
    <SetProperty Id="Configure.LuaRocks"
Value="LuaRocksConfigPath=[LuaRocksConfigPath];LuaRocksRootPath=[LuaRocksRootPath];LuaRootPath=[LuaRootPath];ToolsPath=[ToolsPath]"
/>

    <!-- The deferred custom action -->
    <CustomAction Id="Configure.LuaRocks"
BinaryKey="WinLua.Installer.CustomAction" DllEntry="ConfigureLuaRocks"
Execute="deferred" Return="Check" />

    <!-- Schedule the deferred custom action between InstallInitialize and
InstallFinalize -->
    <InstallExecuteSequence>
        <!-- You may need a condition to make sure this does not run during
uninstall -->
        <Custom Action="Configure.LuaRocks" After="InstallFiles" />
    </InstallExecuteSequence>
</Fragment>

If you have a need for a rollback custom action then it must be scheduled
before Configure.LuaRocks and any data it needs will need to be set in a
similar fashion. If you create a back up file in the deferred custom action
that is meant to be used in the rollback custom action for rollback
purposes, then you will likely want to pass the back up file path to the
deferred custom action and rollback custom action through CustomActionData.
You would also want a commit custom action to clean up the back up file
(during rollback the rollback custom action would be responsible for
cleaning up the back up file.

The managed code for the deferred custom action just reads
LuaRocksConfigPath, LuaRocksRootPath, LuaRootPath and ToolsPath from
session.CustomActionData (?) or something similar and uses the values to
write the config file. Any other custom actions you need would work
similarly but do whatever work is appropriate. I'd start out with the
deferred custom action first but please do consider (and test) rollback
scenarios for install, repair, uninstall and upgrade!

Adding a row to the RemoveFile table is a little more involved. You'd want
an immediate custom action that runs during uninstall and is scheduled
before RemoveFiles. This immediate custom action needs to insert a row into
the RemoveFile table so it will use a SQL query to do so but since you need
to provide dynamic data you will use a Record to provide that data when the
query is executed. The details here are a little more involved and you must
understand the RemoveFile table format. All that said, you may not need to
do all that. Perhaps it is as simple as adding a RemoveFile element to a
component associated with the config file. Something like

<Directory Id="luarocksbin" Name="bin">
    <Component Id="..." Guid="...">
        <File Id="..." Source="...\luarocks.exe" KeyPath="yes" />
        <RemoveFile Id="RemoveConfigFile"
Property="LUAROCKS_INSTALLLOCATION" Name="[LuaRocksConfigFileName]"
On="uninstall" />
    </Component>
</Directory>

I'm less sure about this and it might have some odd interactions with your
use of merge modules. BUT if you can do something like that then you don't
need an immediate custom action to dynamically add a row to the RemoveFile
table which would be a win.

Lots to think about and lots to try. Let me know how it goes and I'll try
to help more.

--
Edwin G. Castro


On Tue, Feb 18, 2020 at 11:17 PM Russell Haley <russ.haley at gmail.com> wrote:

>
> On Tue, Feb 18, 2020 at 9:51 AM Edwin Castro <egcastr at gmail.com> wrote:
>
>> Am I reading this correctly? It appears the WriteConfigFile method
>> creates a file with static content. I am less sure about what the
>> RunConfigScript method actually does.
>>
>>
>> https://github.com/WinLua/WinLua-Source-Code/blob/master/WinLua-Release3/WinLua-Installer/Winlua.Installer.CustomAction/CustomAction.cs
>>
>
> Hi Edwin, thanks so much for responding! The current code is static
> because I'm testing, but the real file contains paths that need to come
> from the installer. WriteConfigFile was my attempt to do that.
> RunConfigScript is an existing hack to run an external script that creates
> the file I need (by elevating privilege). I don't like that; I want to
> internalize the process in the installer, with hopes to have a "modify"
> option to re-generate the file (not yet though, future feature).
>
>
>>
>>
>> If the file content is static then would it be possible to author the
>> config file with a static filename, something like config-version.lua, and
>> use an immediate custom action to update just the FileName column in the
>> appropriate row in the File table? Something like that would get you all
>> the content in the right places and then you just fix up the one bit of
>> data that varies by version at runtime.
>>
>> If the content for the config file really is dynamic, then could you just
>> generate the file at install time as you already do and use an immediate
>> custom action at uninstall time to add a row to the RemoveFile table to
>> remove the config file?
>>
> Yes, I'd be satisfied with the second suggestion (dynamic) if I can make
> it work. Thank you for the background information on Immediate/Deferred
> actions. If I understand, it seems that I need to write some properties
> during the immediate action and then in a deferred action, create the file
> based on the properties?
> I found something here:
> https://vadmyst.blogspot.com/2006/05/deferred-custom-actions-with-wix.html
> but I want to set those properties in code as well?
>
> I don't know anything about adding a row to the RemoveFiles table yet.
> Would that be done via SQL (or via a Record)?
>
> This is an innocuous question: Is this easier than writing a new file to
> the database and letting the installer write the file (assuming that's even
> going to work)? I suppose I would still need to manually remove the file?
>
> Your time is greatly appreciated. This has been hanging over my head for
> years!
>
> Regards,
> Russ
>
>>
>> --
>> Edwin G. Castro
>>
>>
>> On Tue, Feb 18, 2020 at 9:07 AM Edwin Castro <egcastr at gmail.com> wrote:
>>
>>> Hi Russell,
>>>
>>> I haven't quite followed exactly what you are trying to accomplish but I
>>> think I can give you a little context.
>>>
>>> The wcautil library is indeed a C++ library. It is a small wrapper
>>> library over most of the MSI API that you could read about on MSDN. I think
>>> the C# API you are using already provides all the goodies you need to do
>>> the transformations you want.
>>>
>>> The concept of a semi custom action is that you write an immediate mode
>>> custom action to change the msi database temporarily to add entries to msi
>>> tables. Those tables are then processed normally by the Windows Installer
>>> engine to do the "real" work. An example would be to dynamically add files
>>> to the RemoveFile table to delete files generated by the installed
>>> application at runtime. WiX has this custom action already baked in so you
>>> would not need to write it yourself but it is a good example.
>>>
>>> Immediate custom actions run in user context and cannot change protected
>>> system resources. Deferred custom actions can change protected system
>>> resources but they cannot change the msi database nor read/write most
>>> properties. Rollback custom actions are like deferred custom actions but
>>> they are used to "undo" actions taken by a deferred custom action. There
>>> are also commit custom actions but I have not used them yet. The main point
>>> is that custom actions run at different times and have different
>>> responsibilities. You'll need to understand that context first before you
>>> go writing custom actions. See the following for more information.
>>>
>>>
>>> http://lists.wixtoolset.org/pipermail/wix-users-wixtoolset.org/2018-May/006932.html
>>>
>>> The Saw Tooth diagram article is missing but it can be found at
>>>
>>>
>>> https://web.archive.org/web/20140412115309/http://flaming.com/images/SawTooth.PNG
>>>
>>> as mentioned in
>>>
>>>
>>> http://lists.wixtoolset.org/pipermail/wix-users-wixtoolset.org/2019-November/008511.html
>>>
>>> In any case, it sounds like you wrote an immediate custom action so you
>>> could modify the database but could not change the system by design.
>>>
>>> Let me re-read the thread more carefully and see if I can provide more
>>> tailored advice. I have a gut feeling that a semi custom action may not be
>>> appropriate if you feel you need to insert rows into the Component,
>>> FeatureComponent and File tables. Perhaps there is an easier way to handle
>>> all this.
>>>
>>> --
>>> Edwin G. Castro
>>>
>>>
>>> On Sun, Feb 16, 2020, 22:58 Russell Haley via wix-users <
>>> wix-users at lists.wixtoolset.org> wrote:
>>>
>>>> On Sun, Feb 16, 2020 at 8:44 PM Russell Haley <russ.haley at gmail.com>
>>>> wrote:
>>>>
>>>> > Thank you for the response, I didn't see this come through!l That's an
>>>> > interesting post from 13 years
>>>> >
>>>> Thanks again for this but it seems to be a long shot. I have not been
>>>> able
>>>> to find the wcautil library mentioned in the article:
>>>> https://www.joyofsetup.com/2007/07/01/semi-custom-actions/. This seems
>>>> to
>>>> be C++, is there a C# frontend? My attempts below seem to be a
>>>> failure...
>>>>
>>>> >
>>>> > I started drilling into the MSI database information here:
>>>> >
>>>> https://docs.microsoft.com/en-us/windows/win32/msi/about-the-installer-database
>>>> >
>>>> > From what I can tell I need my custom action to run after the feature
>>>> > selection and then create my file. If my desired feature is selected,
>>>> then
>>>> > I need to insert into Component, FeatureComponent and File. I started
>>>> going
>>>> > down the SQL route. Here is a test custom action that is supposed to
>>>> create
>>>> > a new config file and add it to "Feature2" (NOTE: This code doesn't
>>>> work
>>>> > yet...or at all?).
>>>> >
>>>>
>>>> > public class CustomActions
>>>> >     {
>>>> >         [CustomAction]
>>>> >         public static ActionResult CustomAction1(Session session)
>>>> >         {
>>>> >             session.Log("Begin CustomAction1");
>>>> >             if(session.Features.Contains("Feature2"))
>>>> >             {
>>>> >                 session.Log("TESTING: Found Feature2");
>>>> >                 File.WriteAllText(@"C:\temp\mytemp1.txt", "This is a
>>>> test
>>>> > of the public broadcast system.");
>>>> >                 var db = session.Database;
>>>> >
>>>> >                 Guid id = Guid.NewGuid();
>>>> >                 session.Log("TESTING: id = " + id);
>>>> > //Used a string builder just because I thought the queries would get
>>>> long
>>>> >                 StringBuilder sb = new StringBuilder();
>>>> >                 sb.Append("INSERT INTO `Component` (`ComponentId`,
>>>> > `Directory_`) VALUES(?, ?)");
>>>> >                 session.Log("TESTING: " + sb.ToString());
>>>> >                 db.Execute(sb.ToString(), id, "feature2folder2");
>>>> >
>>>> >                 sb.Clear();
>>>> >                 sb.Append("INSERT INTO `FeatureComponent` (`Feature_`,
>>>> > `Component_`) VALUES(?, ?)");
>>>> >                 session.Log("TESTING: FeatureComponent " +
>>>> sb.ToString());
>>>> >                 db.Execute(sb.ToString(), "Feature2", id);
>>>> >
>>>> >                 db.Execute("INSERT INTO `File` (`Component_`,
>>>> `FileName`,
>>>> > `FileSize`) VALUES (?, ?, ?)",
>>>> >                     id, "config-5.3.lua", 1024);
>>>> >                 session.Log("TESTING: File");
>>>> >                 db.Commit();
>>>> >             }
>>>> >             return ActionResult.Success;
>>>> >         }
>>>> > }
>>>> >
>>>>
>>>> My code doesn't seem to work. I cleaned up the SQL and made it one hard
>>>> coded string.   I also queried the directory table and realized the
>>>> folder
>>>> name has the package id appended:
>>>>
>>>>     public class CustomActions
>>>>     {
>>>>         [CustomAction]
>>>>         public static ActionResult CustomAction1(Session session)
>>>>         {
>>>>             session.Log("Begin CustomAction1");
>>>>             if(session.Features.Contains("Feature2"))
>>>>             {
>>>>                 session.Log("TESTING: Found Feature2");
>>>>                 File.WriteAllText(@"C:\temp\mytemp1.txt", "This is a
>>>> test
>>>> of the public broadcast system.");
>>>>                 var db = session.Database;
>>>>
>>>>                 PrintView(session);
>>>>                 Guid id = Guid.NewGuid();
>>>>                 session.Log("TESTING: id = " + id);
>>>>
>>>>                 //string InsertString = "INSERT INTO `Component`
>>>> (`ComponentId`, `Directory_`) VALUES(?, ?)";
>>>>                 //session.Log("TESTING: " + InsertString);
>>>>                 //var compRecord = session.Database.CreateRecord(2);
>>>>                 //compRecord.SetString(1, id.ToString());
>>>>                 //compRecord.SetString(2, "feature2folder2");
>>>>                 //db.Execute(InsertString, compRecord);
>>>>
>>>>                 string InsertString =
>>>>                     string.Format("INSERT INTO `Component`
>>>> (`ComponentId`,
>>>> `Directory_`) VALUES('{0}', '{1}')",
>>>>                     id.ToString(),
>>>> "feature2folder2.76597108_E928_4F11_BE09_3DC27AFA3AA3");
>>>>
>>>>                 session.Log("TESTING: " + InsertString);
>>>>
>>>>                 db.Execute(InsertString);
>>>>
>>>>                 InsertString = "INSERT INTO `FeatureComponent`
>>>> (`Feature_`,
>>>> `Component_`) VALUES(?, ?)";
>>>>                 session.Log("TESTING: FeatureComponent " +
>>>> InsertString);
>>>>                 var FCRecord = db.CreateRecord(2);
>>>>                 FCRecord.SetString(1, "Feature2");
>>>>                 FCRecord.SetString(2, id.ToString());
>>>>                 db.Execute(InsertString, FCRecord);
>>>>
>>>>                 db.Execute("INSERT INTO `File` (`Component_`,
>>>> `FileName`,
>>>> `FileSize`) VALUES (?, ?, ?)",
>>>>                     id, "config-5.3.lua", 1024);
>>>>                 session.Log("TESTING: File");
>>>>
>>>>                 db.Commit();
>>>>             }
>>>>             return ActionResult.Success;
>>>>         }
>>>>
>>>>         public static void PrintView(Session session)
>>>>         {
>>>>             //Database db2 = ins.OpenDatabase(strFileMsi,
>>>> WindowsInstaller.MsiOpenDatabaseMode.msiOpenDatabaseModeDirect);
>>>>             View vw2 = session.Database.OpenView(@"Select * FROM
>>>> Directory");
>>>>
>>>>             vw2.Execute(null);
>>>>             session.Log("Woot woot");
>>>>             Record rcrd2 = vw2.Fetch();
>>>>             while (rcrd2 != null)
>>>>             {
>>>>                 session.Log("Directory: " + rcrd2.GetString(1));
>>>>                 session.Log("Parent_Directory: " + rcrd2.GetString(2));
>>>>                 session.Log("DEFAULT: " + rcrd2.GetString(3));
>>>>                 //rcrd2.set_StringData(1, "No data");
>>>>
>>>> //vw2.Modify(WindowsInstaller.MsiViewModify.msiViewModifyUpdate, rcrd2);
>>>>
>>>>                 rcrd2 = vw2.Fetch();
>>>>
>>>>             }
>>>>         }
>>>>
>>>> Execution of the insert into component gives me "Function failed during
>>>> execution.". Here is my log files when I execute my custom query.
>>>>
>>>> TESTING: INSERT INTO `Component` (`ComponentId`, `Directory_`)
>>>> VALUES('8f26ef55-767b-4b27-aac5-b673694ddfb3',
>>>> 'feature2folder2.76597108_E928_4F11_BE09_3DC27AFA3AA3')
>>>> MSI (s) (88!F4) [21:35:14:523]: Note: 1: 2259 2:  3:  4:
>>>> Exception thrown by custom action:
>>>> System.Reflection.TargetInvocationException: Exception has been thrown
>>>> by
>>>> the target of an invocation. --->
>>>> Microsoft.Deployment.WindowsInstaller.InstallerException: Function
>>>> failed
>>>> during execution.
>>>>    at Microsoft.Deployment.WindowsInstaller.View.Execute(Record
>>>> executeParams)
>>>>    at Microsoft.Deployment.WindowsInstaller.Database.Execute(String sql,
>>>> Record record)
>>>>    at Microsoft.Deployment.WindowsInstaller.Database.Execute(String
>>>> sqlFormat, Object args)
>>>>    at CustomActionCreateFile.CustomActions.CustomAction1(Session
>>>> session)
>>>> in
>>>>
>>>> C:\Users\russh\source\repos\TestCreateActionInstaller\CustomActionCreateFile\CustomAction.cs:line
>>>> 40
>>>>    --- End of inner exception stack trace ---
>>>>    at System.RuntimeMethodHandle.InvokeMethod(Object target, Object
>>>> arguments, Signature sig, Boolean constructor)
>>>>    at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object
>>>> obj,
>>>> Object parameters, Object arguments)
>>>>    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj,
>>>> BindingFlags
>>>> invokeAttr, Binder binder, Object parameters, CultureInfo culture)
>>>>    at
>>>>
>>>> Microsoft.Deployment.WindowsInstaller.CustomActionProxy.InvokeCustomAction(Int32
>>>> sessionHandle, String entryPoint, IntPtr remotingDelegatePtr)
>>>> CustomAction CustomActionCreateFile returned actual error code 1603
>>>> (note
>>>> this may not be 100% accurate if translation happened inside sandbox)
>>>> Action ended 21:35:14: CustomActionCreateFile. Return value 3.
>>>>
>>>> I fear this problem is a black hole of my time. I have in front of me
>>>> three
>>>> possible options:
>>>>
>>>> - This option of adding to the database which seems fraught with
>>>> unknowns
>>>> and half answers. A article from 13 years ago that mentions an
>>>> undocumented
>>>> feature and solves a completely different problem than mine is a little
>>>> hard to put faith in.
>>>> - I also have a custom action to simply create a file in the
>>>> installation
>>>> folder but it doesn't work. The problem I am having is my custom action
>>>> doesn't seem to have administrative privileges and I get an accessed
>>>> denied
>>>> error.
>>>> - I have a third and final option which is to execute an external Lua
>>>> script with elevated privileges in the InstallFinalize step of my
>>>> installer. This has been tested and works, but it just feels like
>>>> cheating
>>>> and my uninstall leaves files and folders around. I can try and run a
>>>> custom action to remove the left over but... :(
>>>>
>>>> I suppose in the end I'll have to settle for a hard coded file name and
>>>> copy the contents into the file with elevated privileges after the fact.
>>>> How is it that we have this huge "feature rich" MSI system and my simple
>>>> use case evades all but the most convoluted solutions? I'd be relieved
>>>> to
>>>> know the answer is "You're doing it wrong" if someone could also point
>>>> me
>>>> in the correct direction...
>>>>
>>>> Regards,
>>>> Russ
>>>>
>>>>
>>>> > However, what I don't know is how to point to the source file in the
>>>> > database? Do I need to put it in the database as a blob or a varchar
>>>> or
>>>> > something? Or can I just add a reference from the local temp
>>>> directory?
>>>> >
>>>> > Thanks again!
>>>> > Russ
>>>> >
>>>> >
>>>> > On Thu, Feb 6, 2020 at 5:34 PM Blair Murri via wix-users <
>>>> > wix-users at lists.wixtoolset.org> wrote:
>>>> >
>>>> >> In a word: yes, for both questions. Look for Bob's blog entry about
>>>> >> "semi-custom" actions.
>>>> >>
>>>> >> Blair
>>>> >>
>>>> >> Get Outlook for Android<https://aka.ms/ghei36>
>>>> >>
>>>> >> ________________________________
>>>> >> From: wix-users <wix-users-bounces at lists.wixtoolset.org> on behalf
>>>> of
>>>> >> Russell Haley via wix-users <wix-users at lists.wixtoolset.org>
>>>> >> Sent: Sunday, February 2, 2020 11:42:46 AM
>>>> >> To: WiX Toolset Users Mailing List <wix-users at lists.wixtoolset.org>
>>>> >> Cc: Russell Haley <russ.haley at gmail.com>
>>>> >> Subject: [wix-users] Custom Action to Create and Install a File?
>>>> >>
>>>> >> Hi,
>>>> >>
>>>> >> I'm pretty chuffed. I have a new installer for my WinLua
>>>> distribution that
>>>> >> now includes the Lua package manager called LuaRocks and a custom
>>>> compiler
>>>> >> toolchain using llvm-mingw. My current installer has a CustomAction
>>>> that
>>>> >> creates a config file for the package manager named for the
>>>> appropriate
>>>> >> version of Lua. The current version of Lua is 5.3.5, so the config
>>>> file
>>>> >> needs to be named Config-5.3.lua. I currently save this file under
>>>> the
>>>> >> install directory of <program files>\WinLua\LuaRocks.  This custom
>>>> action
>>>> >> runs POST installation so I have the paths I need for creating the
>>>> >> contents
>>>> >> of the file. I do not want this file in a user accessible directory.
>>>> >>
>>>> >> My questions:
>>>> >>
>>>> >> 1) Can I use a custom action to create the file before the
>>>> installation
>>>> >> completes and ADD the config file to the list of files to be removed
>>>> (I
>>>> >> think that's termed the installer database)?
>>>> >>
>>>> >> 2) What about things like environment variables? Can I create them
>>>> based
>>>> >> on
>>>> >> installer information prior to the completion of the install process
>>>> and
>>>> >> have the installer remove them? Specifically I want to create an
>>>> >> environment variable called LUAROCKS_SYSCONFDIR and store the path
>>>> of the
>>>> >> config-5.3.lua mentioned above. However, the name and path won't be
>>>> known
>>>> >> until AFTER the file is created? I can already add to the PATH
>>>> variable
>>>> >> based on the installer paths, but this feels different.
>>>> >>
>>>> >> All my source code can be found here:
>>>> >>
>>>> >>
>>>> https://github.com/WinLua/WinLua-Source-Code/tree/master/WinLua-Release3/WinLua-Installer
>>>> >>
>>>> >> Regards,
>>>> >> Russell
>>>> >>
>>>> >> ____________________________________________________________________
>>>> >> WiX Toolset Users Mailing List provided by FireGiant
>>>> >> http://www.firegiant.com/
>>>> >>
>>>> >> ____________________________________________________________________
>>>> >> WiX Toolset Users Mailing List provided by FireGiant
>>>> >> http://www.firegiant.com/
>>>> >>
>>>> >
>>>>
>>>> ____________________________________________________________________
>>>> WiX Toolset Users Mailing List provided by FireGiant
>>>> http://www.firegiant.com/
>>>>
>>>



More information about the wix-users mailing list