[wix-users] Fwd: WiX Error Handling

Todd Hoatson todd.hoatson at gmail.com
Tue Jan 15 13:52:36 PST 2019


Hi Rob,
  in response...

> 7.  Not true (based on my understanding).

Could you please elaborate?  Do you feel Nick Ramirez' book is in error
about this?  Or has something changed since then?

> Seems like a totally reasonable feature request. You could go ask the
Windows Installer team to implement server
> side to client side property passing.

Where might one make such a request...?  Please understand, I'm fairly new
to installation, as this is only the 2nd installer I've worked on.  It was
never my desire / intention to learn anything at all about MSI - I just
wanted to learn how to make fairly-straightforward installers with WiX...

thanks,
Todd



On Tue, Jan 15, 2019 at 12:36 AM Rob Mensching <rob at firegiant.com> wrote:

> 1.  Yep, client side properties can be changed on the client side.
>
> 2.  Yep, server side properties can be changed on the server side.
>
> 3.  Yep, fatal error dialog lives on the client side.
>
> 4.  Yep, fatal error dialog lives on the client side.
>
> 5.  Yep. ::MsiProcessMessage() (what session.Message calls), can pump
> messages from the server side to the client side to be displayed in a
> message box.
>
> 6.  Normally the fatal error dialog doesn’t include specifics for an error
> message (much due to the Windows Installer design)
>
> 7.  Not true (based on my understanding).
>
> 8.  The Windows Installer team decided that sending properties “back” from
> the server side to the client side was not important (but you can send
> secure properties from the client side to the server side, thank goodness).
> Seems like a totally reasonable feature request. You could go ask the
> Windows Installer team to implement server side to client side property
> passing.
>
>
>
> _____________________________________________________________
>
> Short replies here. Complete answers over there: http://www.firegiant.com/
>
>
>
> *From:* Todd Hoatson <todd.hoatson at gmail.com>
> *Sent:* Monday, January 14, 2019 8:10 PM
> *To:* Rob Mensching <rob at firegiant.com>
> *Subject:* Re: [wix-users] Fwd: WiX Error Handling
>
>
>
> >  I think you've confirmed Properties (even secure ones) don't come back
> from server.
>
>
>
> Not exactly...  and I apologize in advance for a very long response
> here...  (if you like, you can skip the details and go straight to my
> summary observations below)
>
>
>
> It seems I have confirmed that properties don't come back from the server
> *when installing PER_USER*.
>
>
>
> And even this doesn't seem to fully explain my results...
>
>
>
> I have other properties which are set in C# custom actions, and those seem
> to be picked up by the rest of the installer.
>
>
>
> Example 1 - Font installation:
>
>                 <Property Id="SBLHEB_INSTALLED" Value='False'/>
>
>                 <Property Id="APPARATUS_INSTALLED" Value='False'/>
>
>
>
>                 <CustomAction Id="SetFontValues"
>
>                                 Return="check"
>
>                                 Execute="immediate"
>
>                                 BinaryKey="CustomActions.CA.dll"
>
>                                 DllEntry="LookForInstalledFonts"  />
>
>                 ...
>
>                 <InstallExecuteSequence>
>
>                   <Custom Action="CloseApplications"
> Before="AppSearch"></Custom>
>
>                   <Custom Action="SetFontValues"
> After="AppSearch"></Custom>
>
>                   ...
>
>                 </InstallUISequence>
>
>                 ...
>
>                 <DirectoryRef Id='FontsFolder'>
>
>                   <Component Id='Font1' Guid='*' Permanent='yes'>
>
>
> <Condition>SBLHEB_INSTALLED="False"</Condition>
>
>                                 <File Id='HebrewFont'
> Source='resources\SBL_Hbrw.ttf' TrueType='yes'/>
>
>                   </Component>
>
>                   <Component Id='Font2' Guid='*' Permanent='yes'>
>
>
> <Condition>APPARATUS_INSTALLED="False"</Condition>
>
>                                 <File Id='ApparatusFont'
> Source='resources\AppSILR.ttf' TrueType='yes'/>
>
>                   </Component>
>
>                   <Component Id='Font3' Guid='*' Permanent='yes'>
>
>
> <Condition>APPARATUS_INSTALLED="False"</Condition>
>
>                                 <File Id='ApparatusBoldFont'
> Source='resources\AppSILB.TTF' TrueType='yes'/>
>
>                   </Component>
>
>                   <Component Id='Font4' Guid='*' Permanent='yes'>
>
>
> <Condition>APPARATUS_INSTALLED="False"</Condition>
>
>                                 <File Id='ApparatusItalicFont'
> Source='resources\AppSILI.TTF' TrueType='yes'/>
>
>                   </Component>
>
>                   <Component Id='Font5' Guid='*' Permanent='yes'>
>
>
> <Condition>APPARATUS_INSTALLED="False"</Condition>
>
>                                 <File Id='ApparatusItalicBoldFont'
> Source='resources\AppSILBI.TTF' TrueType='yes'/>
>
>                   </Component>
>
>                   ...
>
>                 </DirectoryRef>
>
>
>
>                 <Feature Id='Fonts' Title='MyApp 9.0 - Fonts'
> Description='Installing fonts for MyApp 9' Level='1' >
>
>                   <ComponentRef Id='Font1'/>
>
>                   <ComponentRef Id='Font2'/>
>
>                   <ComponentRef Id='Font3'/>
>
>                   <ComponentRef Id='Font4'/>
>
>                   <ComponentRef Id='Font5'/>
>
>                   ...
>
>                 </Feature>
>
>
>
> And here is the C# code for the custom action:
>
>
>
>         [CustomAction]
>
>         public static ActionResult LookForInstalledFonts(Session session)
>
>         {
>
>             session.Log("--->CustomActions.LookForInstalledFonts<---");
>
>
>
>             //Check for each font that we want to install
>
>             session["SBLHEB_INSTALLED"] = DoesFontExist(session, "SBL
> Hebrew", FontStyle.Regular).ToString();
>
>             if (session["SBLHEB_INSTALLED"].Equals("False"))
>
>                 File.Delete(@"C:\\Windows\\Fonts\\SBL_Hbrw.ttf");
>
>
>
>             session["APPARATUS_INSTALLED"] = DoesFontExist(session,
> "Apparatus SIL", FontStyle.Regular).ToString();
>
>             if (session["APPARATUS_INSTALLED"].Equals("False"))
>
>             {
>
>                 session.Log("--->RemoveApparatus");
>
>                 File.Delete(@"C:\\Windows\\Fonts\\AppSILB.TTF");
>
>                 File.Delete(@"C:\\Windows\\Fonts\\AppSILBI.TTF");
>
>                 File.Delete(@"C:\\Windows\\Fonts\\AppSILI.TTF");
>
>                 File.Delete(@"C:\\Windows\\Fonts\\AppSILR.TTF");
>
>             }
>
>
>
>                     ...
>
>
>
>             return ActionResult.Success;
>
>         }
>
>
>
> As you can see from the code above, the properties SBLHEB_INSTALLED and
> APPARATUS_INSTALLED are updated in the C# code.  The updated values are
> referenced in the WiX code.
>
>
>
> When I run the installer, I see this in the log file:
>
>
>
> ...
>
> Return Value for SBL Hebrew : Regular is True
>
> MSI (s) (3C!E4) [14:32:09:690]: PROPERTY CHANGE: Modifying
> SBLHEB_INSTALLED property. Its current value is 'False'. Its new value:
> 'True'.
>
> Return Value for Apparatus SIL : Regular is True
>
> MSI (s) (3C!E4) [14:32:09:690]: PROPERTY CHANGE: Modifying
> APPARATUS_INSTALLED property. Its current value is 'False'. Its new value:
> 'True'.
>
> ...
>
> MSI (s) (3C:08) [14:32:09:721]: Component: Font1; Installed: Absent;
>  Request: Local;   Action: Null
>
> MSI (s) (3C:08) [14:32:09:721]: Component: Font2; Installed: Absent;
>  Request: Local;   Action: Null
>
> MSI (s) (3C:08) [14:32:09:721]: Component: Font3; Installed: Absent;
>  Request: Local;   Action: Null
>
> MSI (s) (3C:08) [14:32:09:721]: Component: Font4; Installed: Absent;
>  Request: Local;   Action: Null
>
> MSI (s) (3C:08) [14:32:09:721]: Component: Font5; Installed: Absent;
>  Request: Local;   Action: Null
>
> ...
>
> Property(S): SBLHEB_INSTALLED = True
>
> Property(S): CHARIS_INSTALLED = True
>
> Property(S): APPARATUS_INSTALLED = True
>
> ...
>
> Property(C): SBLHEB_INSTALLED = False
>
> Property(C): CHARIS_INSTALLED = False
>
> Property(C): APPARATUS_INSTALLED = False
>
>
>
> So I see that the fonts (for some reason) are installed on the server
> side, and for that reason they are able to receive the updated value
> indicating that the font has already been installed, and therefore the
> action is 'Null'.
>
>
>
>
>
> Example 2 - Project folder identification (use default folder location
> unless there is a registry entry indicating a location previously used by a
> prior version of the app):
>
>                 <CustomAction Id='SetDefProjFolder'
> Property='DEFPROJFOLDER' Value='[WindowsVolume]MyApp 9 Projects' />
>
>                 <Property Id="PROJFOLDERFOUND" Value="unset" />
>
>                 <CustomAction Id="VerifyProjectPath"
>
>                                 Return="check"
>
>                                 Execute="immediate"
>
>                                 BinaryKey="CustomActions.CA.dll"
>
>                                 DllEntry="VerifyProjectPath"  />
>
>
>
>                 <CustomAction Id='UseDefProjFolder' Property='PROJFOLDER'
> Value='[DEFPROJFOLDER]' />
>
>                 <CustomAction Id='UseRegProjFolder' Property='PROJFOLDER'
> Value='[REGPROJFOLDER]' />
>
>                 ...
>
>                 <InstallUISequence>
>
>                     <Show Dialog="FatalErrorDlg" OnExit="error" />
>
>                     <Custom Action="SetDefProjFolder"
> After="FindRelatedProducts"></Custom>
>
>                     <Custom Action="VerifyProjectPath"
> After="SetDefProjFolder"></Custom>
>
>                 </InstallUISequence>
>
>                 ...
>
>                 <Property Id='WIXUI_PROJECTSDIR'
> Value='PROJFOLDER'/>
>
>
>
> And here is the C# code for the custom action:
>
>
>
>         [CustomAction]
>
>         public static ActionResult VerifyProjectPath(Session session)
>
>         {
>
>             session.Log("Begin VerifyProjectPath in custom action dll");
>
>             string regProjPath = GetProjectDirFromRegistry();
>
>             if (string.IsNullOrEmpty(regProjPath))
>
>             {
>
>                 session["REGPROJFOLDER"] = null;
>
>                 session["PROJFOLDERFOUND"] = "NotFound";
>
>                 return ActionResult.Success;
>
>             }
>
>
>
>             session["REGPROJFOLDER"] = regProjPath;
>
>
>
>             if (Directory.Exists(regProjPath) &&
> Directory.GetFiles(regProjPath).Length > 0)
>
>                 session["PROJFOLDERFOUND"] = "AlreadyExisting";
>
>             else
>
>             {
>
>                 session["PROJFOLDERFOUND"] = "InvalidRegEntry";
>
>             }
>
>             return ActionResult.Success;
>
>         }
>
>
>
> As you can see from the code above, the properties REGPROJFOLDER and
> PROJFOLDERFOUND are updated in the C# code.  The updated values are
> referenced in the WiX code.
>
>
>
> When I run the installer, I see this in the log file:
>
>
>
> ...
>
> MSI (c) (20:7C) [14:32:02:210]: Doing action: SetDefProjFolder
>
> Action 14:32:02: SetDefProjFolder.
>
> Action start 14:32:02: SetDefProjFolder.
>
> MSI (c) (20:7C) [14:32:02:210]: PROPERTY CHANGE: Adding DEFPROJFOLDER
> property. Its value is 'C:\MyApp 9 Projects'.
>
> Action ended 14:32:02: SetDefProjFolder. Return value 1.
>
> MSI (c) (20:7C) [14:32:02:210]: Doing action: VerifyProjectPath
>
> Action 14:32:02: VerifyProjectPath.
>
> Action start 14:32:02: VerifyProjectPath.
>
> MSI (c) (20:44) [14:32:02:226]: Invoking remote custom action. DLL:
> C:\Users\<Me>\AppData\Local\Temp\MSIC8CA.tmp, Entrypoint: VerifyProjectPath
>
> MSI (c) (20:40) [14:32:02:226]: Cloaking enabled.
>
> MSI (c) (20:40) [14:32:02:226]: Attempting to enable all disabled
> privileges before calling Install on Server
>
> MSI (c) (20:40) [14:32:02:226]: Connected to service for CA interface.
>
> SFXCA: Extracting custom action to temporary directory:
> C:\Users\<Me>\AppData\Local\Temp\MSIC8CA.tmp-\
>
> SFXCA: Binding to CLR version v4.0.30319
>
> Calling custom action
> CustomActions!CustomActions.CustomActions.VerifyProjectPath
>
> Begin VerifyProjectPath in custom action dll
>
> MSI (c) (20!A0) [14:32:02:426]: PROPERTY CHANGE: Adding REGPROJFOLDER
> property. Its value is 'C:\MyApp 8 Projects\'.
>
> MSI (c) (20!A0) [14:32:02:426]: PROPERTY CHANGE: Modifying PROJFOLDERFOUND
> property. Its current value is 'unset'. Its new value: 'AlreadyExisting'.
>
> Action ended 14:32:02: VerifyProjectPath. Return value 1.
>
> ...
>
> MSI (c) (20:7C) [14:32:02:448]: PROPERTY CHANGE: Adding PROJFOLDER
> property. Its value is 'C:\'.
>
> ...
>
> MSI (c) (20:7C) [14:32:02:464]: Dir (target): Key: PROJFOLDER      ,
> Object: C:\
>
> ...
>
> MSI (c) (20:7C) [14:32:08:919]: Switching to server: APPFOLDER="C:\Program
> Files (x86)\MyApp 9\" PROJFOLDER="C:\" DEFPROJFOLDER="C:\ MyApp 9 Projects"
> EXPLANATIONTEXT="Since this is an upgrade of an existing installation, you
> can't change the location of the project folder." TARGETDIR="C:\"
> PROJFOLDERFOUND="AlreadyExisting" MSIFASTINSTALL="7"
> REBOOT="ReallySuppress" CURRENTDIRECTORY="C:\Projects\MyApp9\BuildDir"
> CLIENTUILEVEL="0" MSICLIENTUSESEXTERNALUI="1" CLIENTPROCESSID="13088"
> USERNAME="<me>" SOURCEDIR="C:\ProgramData\Package
> Cache\{24F072F4-5904-4200-87CA-BB068493FF79}v9.0.100.1\" ACTION="INSTALL"
> EXECUTEACTION="INSTALL" REGPROJFOLDER="C:\MyApp 8 Projects\"
> ROOTDRIVE="C:\" INSTALLLEVEL="1" SECONDSEQUENCE="1"
> WIXUI_INSTALLDIR_VALID="1"  ADDLOCAL=Application,Projects,Fonts
>
> ...
>
> MSI (s) (3C:08) [14:32:09:035]: Command Line: APPFOLDER=C:\Program Files
> (x86)\MyApp 9\ PROJFOLDER=C:\ DEFPROJFOLDER=C:\ MyApp 9 Projects
> EXPLANATIONTEXT=Since this is an upgrade of an existing installation, you
> can't change the location of the project folder. TARGETDIR=C:\
> PROJFOLDERFOUND=AlreadyExisting MSIFASTINSTALL=7 REBOOT=ReallySuppress
> CURRENTDIRECTORY=C:\Projects\MyApp9\BuildDir CLIENTUILEVEL=0
> MSICLIENTUSESEXTERNALUI=1 CLIENTPROCESSID=13088 USERNAME=<me>
> SOURCEDIR=C:\ProgramData\Package
> Cache\{24F072F4-5904-4200-87CA-BB068493FF79}v9.0.100.1\ ACTION=INSTALL
> EXECUTEACTION=INSTALL REGPROJFOLDER=C:\MyApp 8 Projects\ ROOTDRIVE=C:\
> INSTALLLEVEL=1 SECONDSEQUENCE=1 WIXUI_INSTALLDIR_VALID=1
> ADDLOCAL=Application,Projects,Fonts ACTION=INSTALL
>
> ...
>
> MSI (s) (3C:08) [14:32:09:035]: PROPERTY CHANGE: Adding PROJFOLDER
> property. Its value is 'C:\'.
>
> MSI (s) (3C:08) [14:32:09:035]: PROPERTY CHANGE: Adding DEFPROJFOLDER
> property. Its value is 'C:\MyApp 9 Projects'.
>
> MSI (s) (3C:08) [14:32:09:035]: PROPERTY CHANGE: Adding EXPLANATIONTEXT
> property. Its value is 'Since this is an upgrade of an existing
> installation, you can't change the location of the project folder.'.
>
> ...
>
> MSI (s) (3C:08) [14:32:09:035]: PROPERTY CHANGE: Modifying PROJFOLDERFOUND
> property. Its current value is 'unset'. Its new value: 'AlreadyExisting'.
>
> ...
>
> MSI (s) (3C:08) [14:32:09:035]: PROPERTY CHANGE: Adding REGPROJFOLDER
> property. Its value is 'C:\MyApp 8 Projects\'.
>
> ...
>
> MSI (s) (3C:08) [14:32:09:706]: Dir (target): Key: PROJFOLDER      ,
> Object: C:\
>
> ...
>
> Property(S): PROJFOLDER = C:\
>
> Property(S): WIXUI_PROJECTSDIR = PROJFOLDER
>
> Property(S): DEFPROJFOLDER = C:\MyApp 9 Projects
>
> Property(S): EXPLANATIONTEXT = Since this is an upgrade of an existing
> installation, you can't change the location of the project folder.
>
> Property(S): PROJFOLDERFOUND = AlreadyExisting
>
> Property(S): REGPROJFOLDER = C:\MyApp 8 Projects\
>
> ...
>
> Property(C): PROJFOLDER = C:\
>
> Property(C): WIXUI_PROJECTSDIR = PROJFOLDER
>
> Property(C): DEFPROJFOLDER = C:\MyApp 9 Projects
>
> Property(C): EXPLANATIONTEXT = Since this is an upgrade of an existing
> installation, you can't change the location of the project folder.
>
> Property(C): PROJFOLDERFOUND = AlreadyExisting
>
> Property(C): REGPROJFOLDER = C:\MyApp 8 Projects\
>
>
>
> So I see from this that VerifyProjectPath is executed (for some reason) on
> the client side, and for that reason it is able to pass the updated values
> indicating the appropriate project folder location.
>
>
>
> Observations:
>
> 1.  Custom code that is executed as part of InstallUISequence can set
> properties and have the updated values be visible to WiX code that is also
> executed (implicitly or explicitly) as part of InstallUISequence.
>
> 2.  Custom code that is executed as part of InstallExecuteSequence can set
> properties and have the updated values be visible to installation actions.
>
> 3.  It is possible to create a 'fatal error' dialog which is shown within
> the InstallUISequence (but it can only access values of properties that
> were also set within the InstallUISequence???)
>
> 4.  It is NOT possible to create a 'fatal error' dialog which is shown
> within the InstallExecuteSequence, which means it is not possible to use
> such a dialog to report errors which occur in the InstallExecuteSequence.
>
> 5.  As a result of #4, errors which occur in custom code that is executed
> as part of InstallExecuteSequence must be reported by a call
> to session.Message().
>
> 6.  However, the installer will still want to deal with the installation
> failure, and show the fatal error dialog (with the non-updated, i.e.
> default, value), which unfortunately produces 2 error dialogs for the same
> error, the second one having wrong information.
>
> 7.  This inability for custom actions in one sequence to communicate via
> properties to other parts of the installation in the other sequence only
> exists for PER_USER installations.
>
> 8.  This rather odd situation is, inexplicably, 'by design'.
>
>
>
> Can someone please help me understand why preventing free communication
> via properties among parts of the installation is somehow more secure and /
> or why PER_USER installations are inherently less secure and needing such
> restrictions?
>
>
>
> More importantly, can anyone suggest a way for our installer to avoid the
> problem in #6 (above)?
>
>
>
> thanks,
>
> Todd Hoatson
>
>
>
> On Fri, Jan 11, 2019 at 4:14 PM Rob Mensching <rob at firegiant.com> wrote:
>
> The "(s)" in the log line means server side and I think you've confirmed
> Properties (even secure ones) don't come back from server.
>
> _____________________________________________________________
>  Short replies here. Complete answers over there:
> http://www.firegiant.com/
>
> -----Original Message-----
> From: wix-users <wix-users-bounces at lists.wixtoolset.org> On Behalf Of
> Todd Hoatson via wix-users
> Sent: Friday, January 11, 2019 11:57 AM
> To: WiX Toolset Users Mailing List <wix-users at lists.wixtoolset.org>
> Cc: Todd Hoatson <todd.hoatson at gmail.com>
> Subject: Re: [wix-users] Fwd: WiX Error Handling
>
> Thanks, all, for the quick responses...
>
> > Your custom action is running on the server side and sets the Property
> there. When the Windows Installer returns to the client side, the Property
> is the client side value.
>
> That's interesting, because the log shows:
>
> MSI (s) (0C!4C) [10:55:58:092]: PROPERTY CHANGE: Modifying ERRMSG property.
> Its current value is 'Undetermined Error'. Its new value: 'Problem
> encountered while closing the application'.
> CustomAction CloseApplications returned actual error code 1603 (note this
> may not be 100% accurate if translation happened inside sandbox) …
> Property(S): ERRMSG = Problem encountered while closing the application
>
> This would seem to indicate that the value of ERRMSG had been retained
> after the custom action had ended...
> (Also, please note that the custom actions in our installer do not run
> asynchronously.)
>
> > Secure Properties can travel from the client to the server but IIRC
> Properties do not travel back.   You
> > could try marking the Property Secure and see if my memory in wrong
> > and
> see if Properties can travel back.
>
> > I expect there is some communication between the process running your
> CloseApplications custom action
> > and the server process but the data from the server process is not
> getting communicated back to the client
> > process which is responsible for your UI. My recollection matches
> > Rob's
> in that communicating a property
> > from the server process back to the client process is not allowed but
> > you
> could try making the property
> > secure just in case our recollection is wrong.
>
> This was not what I understood from reading WiX 3.6: A Developer's Guide
> to Windows Installer XML:
>
>      "Setting properties is just as easy. You'll set the value by
> referencing the key with the name of
>       your property.  Here's an example:
>
>       [CustomAction]
>       public static ActionResult CustomAction1(Session session)
>       {
>         session["MY_PROPERTY"] = "abc";
>         return ActionResult.Success;
>       }
>
>       If the property doesn't exist when you set it, it will be created.
> Similarly, you can clear a property
>       by settings its value to null. Creating or changing property values
> from a custom action doesn't
>       stop the installer from displaying those properties in the install
> log.  ..."
>
> Nick Ramirez doesn't say that properties set in custom action code written
> in C# can't be communicated back to the main process (what you have
> referred to as a "client" process).  There didn't seem to be anything there
> to indicate that something special needs to be done to get the change in
> value recognized by the main process.
>
> So it would seem that Nick Ramirez' book is lacking an important detail
> here.
>
> I searched to book to see how to mark a property as "secure".  There I
> read the following:
>
>      "Windows Installer may mark the installation as restricted. This
> means that the properties
>       you set could be ignored. The following scenarios may set this in
> motion:
>
>       The user performing the install is not an administrator
>       The install is marked as per-machine instead of per-user, meaning
> the ALLUSERS property
>       is set to 1 in your markup or the Package element's InstallScope
> attribute is set to perMachine
>       ..."
>
> Our installer (for whatever reason) does indeed have InstallScope set to
> perMachine.  So I changed the property declaration to:
>
>     <Property Id="ERRMSG" Value="Undetermined Error" Secure="yes" />
>
> However, there was *no change* in the installer behavior.
>
> In the book I read:
>
>      "You can tell when you need to use the Secure attribute if, in the
> install log, you see that the
>       RestrictedUserControl property has been set automatically.  You'll
> also see some of your
>       properties, if they're used in the execute sequence, being ignored.
> The following is a sample
>       log of that happening:
>
>      MSI (s) (C8:BC) [23:49:58:906]:
>         Machine policy value 'EnableUserControl' is 0
>      MSI (s) (C8:BC) [23:49:58:906]: PROPERTY CHANGE:
>         Adding RestrictedUserControl property. Its value is '1'.
>      MSI (s) (C8:BC) [23:49:58:906]:
>         Ignoring disallowed property MYPROPERTY
>
> I don't see anything like this in the log file...
>
> thanks,
> Todd
>
>
>
>
> --
>
> Todd Hoatson
>
> Mobile: 763-291-3312
>
> Email:   todd.hoatson at gmail.com
>
> www.linkedin.com/in/toddhoatson
>


-- 
Todd Hoatson
Mobile: 763-291-3312
Email:   todd.hoatson at gmail.com
www.linkedin.com/in/toddhoatson



More information about the wix-users mailing list