How to call DLL functions from InstallScript called by MSI custom action - dll

I have a DLL named FSSetup.dll that I want to call from InstallScript code. And I want to call the InstallScript code from an MSI custom action. But there seems to be a problem in that I don't know where to find FSSetup.dll. The InstallScript function is starting and the whole install aborts when it gets to the line where it calls the DLL function. The MSI log reports:
MSI (c) (1C:3C) [09:39:52:261]: Invoking remote custom action. DLL: C:\Users\bmarty\AppData\Local\Temp\MSIBEAE.tmp, Entrypoint: f1
Action ended 9:39:55: BrowseFSKeyFile. Return value 3.
Info 2896. Executing action BrowseFSKeyFile failed.
Action ended 9:39:55: InstallWelcome. Return value 3.
The InstallScript code looks like this:
export prototype BrowseFSKeyFile(HWND);
prototype FSSetup.FSBrowseFile( BYVAL STRING, BYVAL STRING, BYVAL STRING, BYREF STRING );
function BrowseFSKeyFile(hMSI)
NUMBER nKeyFile;
STRING svKeyFile, szKPath, szKFile, szTemp, szTemp1;
begin
MessageBox("Hi", INFORMATION);
svKeyFile = "";
FSBrowseFile( "*.key", "key", "Please select a KEY File...", svKeyFile );
MessageBox("Bye", INFORMATION);
...
end;
I see the "Hi" message, but the install aborts before "Bye". I suspect it's because FSSetup.dll doesn't exist in my %temp% directory where MSIBEAE.tmp resides, which presumably represents the extracted name of the ISSetup.dll that was generated by the InstallScript compile.
Edit:
The MSI debugger is showing a different value for SUPPORTDIR than the InstallScript is seeing. FSSetup.dll exists in the SUPPORTDIR provided by MSI, but not in the one seen from InstallScript.

You are missing a call to UseDll (and UnUseDll). In order to call UseDll, you will need to know the location of the DLL in question. If you have added it to the support files of the MSI project, you will need to retrieve the Windows Installer Property SUPPORTDIR to find this location - that's where Christopher Painter's answer comes into play - and then call e.g. UseDll(szSupportDir ^ "FSSetup.dll") before calling FSBrowseFile.

The SUPPORTDIR behavior was changed about 9 years ago when fundamental design flaws of InstallScript were fixed in InstallShield 12.0. See:
InstallShield 12 Beta2
and
InstallScript, meet CustomActionData
and
Upgrading Projects to InstallShield 12 (search for SUPPORTDIR)
Basically you need to do call MsiGetProperty() to get SUPPORTDIR. Also you need to pass this to your deferred custom actions as CustomActionData.

Related

Registering .net assembly for COM succeeds with regasm but fails using RegistrationServices.RegisterAssembly

This is one of the strangest issue I have encountered.
There is a .net assembly, which is exposed to COM.
If you register it with regasm /codebase my.dll - it is sucessfully registered, and can be used.
However, if you register it from code using RegistrationServices.RegisterAssembly() :
[...]
RegistrationServices regSvcs = new RegistrationServices();
Assembly assembly = Assembly.LoadFrom(path);
// must call this before overriding registry hives to prevent binding failures on exported types during RegisterAssembly
assembly.GetExportedTypes();
using (RegistryHarvester registryHarvester = new RegistryHarvester(true))
{
// ******** this throws *********
regSvcs.RegisterAssembly(assembly, AssemblyRegistrationFlags.SetCodeBase);
}
Then it throws exception:
Could not load file or assembly 'Infragistics2.Win.UltraWinTree.v9.2, Version=9.2.20092.2083,
Culture=neutral, PublicKeyToken=7dd5c3163f2cd0cb' or one of its dependencies.
Provider type not defined. (Exception from HRESULT: 0x80090017)
This error has very little resource on the net, and looks like related to some security(?) cryptography(?) feature.
After long-long hours, I figured out what causes this (but don't know why):
If there is a public class with a public constructor in the assembly with a parameter UltraTree (from the referenced assembly 'Infragistics2.Win.UltraWinTree.v9.2'), then you cannot register from code, but with regasm only.
When I changed the have a public function Init(UltraTree tree), then it works, I can register from code. So:
// regasm: OK / RegistrationServices.RegisterAssembly(): exception
public class Foo
{
public Foo(UltraWinTree tree) { .. }
}
Foo foo = new Foo(_tree);
-------------- vs --------------
// regasm: OK / RegistrationServices.RegisterAssembly(): OK
public class Foo
{
public Foo() {}
public void Init(UltraWinTree tree) { .. }
}
Foo foo = new Foo();
foo.Init(_tree);
So I could workaround by passing UltraWinTree in a new Init() function instead of constructor, but this is not nice, and I want to know the reason, what the heck is going on?
Anyone has any idea? Thanks.
PS:
Okay, but why we want to register from code? As we use Wix to create installer, which uses heat.exe to harvest registry entries (which are added during asm registration), so heat.exe does assembly registration from code.
I've been dealing with this for years so this is the only answer you need to read:
Heat calls regasm /regfile. So does InstallShield when you tell it to. If you read this page:
https://learn.microsoft.com/en-us/dotnet/framework/tools/regasm-exe-assembly-registration-tool
There's a very important caveat in the remarks section.
You can use the /regfile option to generate a .reg file that contains
the registry entries instead of making the changes directly to the
registry. You can update the registry on a computer by importing the
.reg file with the Registry Editor tool (Regedit.exe). The .reg file
does not contain any registry updates that can be made by user-defined
register functions. The /regfile option only emits registry entries
for managed classes. This option does not emit entries for TypeLibIDs
or InterfaceIDs.
So what to do? Use Heat to generate most of the metadata. Then on a clean machine, (snapshot VM best) us a registry snapshot and compare tool such as InCntrl3 or InstallWatch Pro and sniff out what additional meta regasm writes to the registry. Finally massage that into your Wxs code.
Then on a cleam machine test the install. The result should work and not require any custom actions in the install.

BURN: Logging BURN_PACKAGE-> sczLogPathVariable to be used to create complete Log file

I am using WIX 3.7, and I am wanting to have my MSI and BURN log files, be created at my desired location. I tried verbatim both the approaches, mentioned in How to set or get all logs in a custom bootstrapper application newsgroup post. However, Log File gets created in the default location. Since I had time on hand, I decided to explore WIX 3.7 through WIX 3.9 Source Code, attempting to find where BURN_PACKAGE-> sczLogPathVariable data member is used.
However I found just 5 references to BURN_PACKAGE-> sczLogPathVariable
\wix38-debug\src\burn\engine\logging.cpp(191):
if ((!fRollback && pPackage->sczLogPathVariable && *pPackage->sczLogPathVariable) ||
\wix38-debug\src\burn\engine\logging.cpp(197): hr = VariableSetString(pVariables, fRollback ? pPackage->sczRollbackLogPathVariable : pPackage->sczLogPathVariable, sczLogPath, FALSE);
\wix38-debug\src\burn\engine\package.cpp(152):
hr = XmlGetAttributeEx(pixnNode, L"LogPathVariable", &pPackage->sczLogPathVariable);
\wix38-debug\src\burn\engine\package.cpp(303):
ReleaseStr(pPackage->sczLogPathVariable);
\wix38-debug\src\burn\engine\package.h(165):
LPWSTR sczLogPathVariable; // name of the variable that will be set to the log path.
I was expecting some code that would actually retrieve the value of MY VARIABLE and then CONCATENATE that value with the Log File Name that was synthesized, to create the complete file path. Maybe I am missing something obvious ? Do you guys have any suggestions ?
*Package/#LogPathVariable is used to specify a variable that gets the path to the log. To control the logging, use the Log element.

What should one return other than success for a custom Action

I am trying my hand at writing my first Custom Action for an installer (built with Advanced Installer) using the VB Custom Action Project template supplied with the WiX Toolset. Essentially I just want to test the amount of Ram on a machine to determine which version of a prerequisite ought to be installed if not already present.
What I have so far (in semi pseudo code) is the following;
Public Class CustomActions
<CustomAction()> _
Public Shared Function CustomAction1(ByVal session As Session) As ActionResult
session.Log("Begin CustomAction1")
If New Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory > [ram size here] Then
Return ActionResult.Success
Else
Return ActionResult.SkipRemainingActions
End If
End Function
End Class
What I would like to know is if returning ActionResult.SkipRemainingActions is the correct choice to use if the ram on the machine being tested is less than the result that prompts success.
Here's my 2 cents on this... If the condition isn't met, I would terminate the install. If you use what you have then the install will still continue/skip per say and it actually would skip all other conditions. When you use "ActionResult.Failure" this will roll back the installation.
I would use. . .
Return ActionResult.Failure
On a side note, I would wrap this up in a Try/Catch and throw a message . . .
This is the wrong question to answer (though I agree with the answer of ActionResult.Failure for it).
The right question is what is the best way to check for total available memory in Windows Installer. Per the Windows Installer Property Reference, the answer is to compare against PhysicalMemory, and the right place to do this is in a Condition element, not in a custom action.

Adding a new Custom Action to a project prevents an existing Custom Action from being run

I have a Custom Action project that has various CA's used by installers that my company creates, a few of those are used to manipulate the IIs7 through the Microsoft.Web.Administration API.
I added a new custom action called SetApplicationAutoStart the the class containing IIs related CA's. This custom action is used to set the autoStart attribute that forces the IIs to preload and start WCF services so that initial response time will be shorter.
After adding this action an existing CA called SetAppPoolLoadUserProfileTrue stopped working. This CA forces that setting on a site to true, even if the default site on the computer has been changed so that this setting is false, so we really need it to work.
The log files contains the following lines when the action fails.
MSI (s) (A0:18) [15:02:43:639]: Executing op: ActionStart(Name=SetAppPoolLoadUserProfileTrue,,)
Action 15:02:43: SetAppPoolLoadUserProfileTrue.
MSI (s) (A0:18) [15:02:43:641]: Executing op: CustomActionSchedule(Action=SetAppPoolLoadUserProfileTrue,ActionType=3073,Source=BinaryData,Target=SetAppPoolLoadUserProfileTrue,CustomActionData=AppPoolName=xxxxx)
MSI (s) (A0:18) [15:02:43:670]: Creating MSIHANDLE (377) of type 790536 for thread 50712
MSI (s) (A0:C8) [15:02:43:670]: Invoking remote custom action. DLL: C:\Windows\Installer\MSIBD82.tmp, Entrypoint: SetAppPoolLoadUserProfileTrue
CustomAction SetAppPoolLoadUserProfileTrue returned actual error code 1154 (note this may not be 100% accurate if translation happened inside sandbox)
MSI (s) (A0:C8) [15:02:43:673]: Closing MSIHANDLE (377) of type 790536 for thread 50712
MSI (s) (A0:18) [15:02:43:674]: Note: 1: 1723 2: SetAppPoolLoadUserProfileTrue 3: SetAppPoolLoadUserProfileTrue 4: C:\Windows\Installer\MSIBD82.tmp
Error 1723. There is a problem with this Windows Installer package. A DLL required for this install to complete could not be run. Contact your support personnel or package vendor. Action SetAppPoolLoadUserProfileTrue, entry: SetAppPoolLoadUserProfileTrue, library: C:\Windows\Installer\MSIBD82.tmp
MSI (s) (A0:18) [15:20:25:139]: Product: xxxxxxx -- Error 1723. There is a problem with this Windows Installer package. A DLL required for this install to complete could not be run. Contact your support personnel or package vendor. Action SetAppPoolLoadUserProfileTrue, entry: SetAppPoolLoadUserProfileTrue, library: C:\Windows\Installer\MSIBD82.tmp
Action ended 15:20:25: InstallFinalize. Return value 3.
This looks like a problem extracting the dotnet PE from the PE for this action. All other CA's in the binary work properly including the new one.
Same thing happened to me. "galets" was already pointing in the right direction, setting me on track (no rep to upvote, sorry).
Short version:
MakeSfxCA produces native dlls not following the PE / COFF spec leading to the observed behaviour.
Long version:
Construct a CA, e.g. "HavocAction", having three exported entry points (i.e. marked with "CustomAction" attribute), named "HavocEntryPointa", "HavocEntryPointB", "HavocZappEntryPoint" (mind the exact spelling). The methods may just return "ActionResult.Success".
Come up with simple setups, a) invoking just "HavocEntryPointa", b) invoking just "HavocEntryPointB"
==> Setup "a)" will work, Setup "b)" will fail
Uncomment the "CustomAction" attribute on "HavocZappEntryPoint" and the behaviour is inverted, i.e.
==> Setup "a)" will fail, Setup "b)" will work
Further analysis
When you dump the wrapped CA.dll-file with
dumpbin /Exports HavocAction.CA.dll
you get something like (excerpt)
125 7C 00003A36
126 7D 00003A4C HavocEntryPointa
127 7E 00003A62 HavocEntryPointB
128 7F 00003A78 HavocZappEntryPoint
129 80 000042FC zzzEmbeddedUIHandler
130 81 000043B8 zzzInitializeEmbeddedUI
131 82 0000467F zzzShutdownEmbeddedUI
132 83 00003AA5 zzzzInvokeManagedCustomActionOutOfProcW
This is wrong (search for "pecoff_v83.docx", cf. Exported DLL functions not ordered lexically?). The entries are supposed to be sorted (by ASCII) in order to do a binary search when loading methods from the dll (the entries "HavocEntryPointa" and "HavocEntryPointB" are interchanged).
My educated guess is, when loading code from the dll the binary search fails, resulting in the error. Due to the nature of a binary search, removing "HavocZappEntryPoint" inverts the effect.
Remark regarding OP
Kjartan first used "SetApplicationAutoStart" and "SetAppPoolLoadUserProfileTrue" which was not correctly exported to the CA.dll due to wrong ordering; the upper case letter "P" comes before the lower case "l" but this was interchanged by MakeSfxCA. His latter choice "ConfigureApplicationAutoStart" and "SetAppPoolLoadUserProfileTrue" is ordered conforming to the PE / COFF spec.
PS: This is http://wixtoolset.org/issues/4502 now.
Update
PPS: Starting with the WiX 3.9 RC3 release the bug fix for this issue is included; everything works as expected.
I experienced exactly the same symptom you are describing. There seems to be a problem with WiX toolset. My version of WiX tolset is 3.8, and I also had a custom action, which would not run, and changing its name fixed the problem. The SFX compiler simply compiles the broken DLL without any indication of a problem. What makes the matters worse is that in my case this was a function which was supposed to run on uninstall with Result="ignore", so I wouldn't even have any immediate indication that there is a problem after I run actual installer.
I tried experimenting to understand what exactly is the problem with the name, and did not find any satisfactory explanation. It seems to not matter where in the source code the offending function is, what is it alphabetically (e.g.: functions succeed which are before and after it alphabetically). Loading DLL into depends.exe shows that there is an export, but trying to rundll32 it fails to find that export. Changing its name fixes the problem. Also, sometimes you can add another function, and the failing one would succeed, but the one you just added fails instead.
Here's what I have done to fix the problem: I wrote a C++ program which loads the compiled custom action and verifies the export. Then I wrote a unit test, which verified all of the exports in custom action. This obviously does not fix the issue, but at least you will have a unit test failure and know that your installer is broken. Here's the c++ code if you are interested:
typedef int(__stdcall *CustomActionProc)(HANDLE);
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
_tprintf(_T("Parameters: DLL, EntryPoint\n"));
return 1;
}
LPCTSTR dllName = argv[1];
LPCTSTR entryPoint = argv[2];
HMODULE hLib = LoadLibrary(dllName);
if (hLib == NULL)
{
_tprintf(_T("Error loading %s\n"), dllName);
return 1;
}
CustomActionProc procAddress =
(CustomActionProc) GetProcAddress(hLib, CStringA(entryPoint));
if (procAddress == NULL)
{
_tprintf(_T("Error locating entrypoint %s\n"), entryPoint);
return 1;
}
return 0;
}
And the unit test is:
[TestMethod]
public void TestCustomActionCanBeInvoked()
{
var asm1 = typeof(MyCustomActionsClass).Assembly;
var methods = asm1.GetTypes().SelectMany(t =>
t.GetMethods().Where(m => m.GetCustomAttributes(false)
.Where(a => a.GetType().Name == "CustomActionAttribute").Any()));
var binFolder = (new FileInfo(this.GetType().Assembly.Location)).DirectoryName;
var customActionsSfx = Path.Combine(binFolder, "MyCustomAction.CA.dll");
var testMsiExport = Path.Combine(binFolder, "TestMsiExport.exe");
foreach (var m in methods)
{
Trace.WriteLine("Method Name: " + m.Name);
var p = Process.Start(new ProcessStartInfo()
{
FileName = testMsiExport,
Arguments = "\"" + customActionsSfx + "\" " + m.Name,
UseShellExecute = false,
RedirectStandardOutput = true,
});
p.OutputDataReceived += (s, d) => Trace.WriteLine(d.Data);
p.BeginOutputReadLine();
p.WaitForExit();
if (p.ExitCode != 0)
{
Assert.Fail("Bad Sfx export detected! Export name: " + m.Name);
}
}
}
Hope this helps someone in my situation. It was a very frustrating day trying to nail this down.
This is actually pretty strange but after a long time searching for answers and trying lot's of different things I tried changing the name of the new CA from SetApplicationAutoStart to ConfigureApplicationAutoStart and that resulted in SetAppPoolLoadUserProfileTrue to start working properly again

Wix: Cannot list out properties created using C# custom action

I am creating a wix installer. In some of the wxs file i have defined some properties and i am also creating some session properties inside C# custom action.
Now my requirement is to list out all session properties.
So for this i have queried Property table and got all properties that where defined in .wxs file.
For this i have used below custom action:
Microsoft.Deployment.WindowsInstaller.View listBoxView = session.Database.OpenView(string.Format("select * from Property"));
listBoxView.Execute();
while (true)
{
using (Record r = listBoxView.Fetch())
{
if (r == null)
{
break;
}
else
{
Console.WriteLine(r[1].ToString(), r[2].ToString());
}
}
}
}
}
But it does not list out any session property that i have created using c# custom action.
Can someone help me how to list out properties that i created using c# custom action or where these properties get stored?
Thanks much
When you query the property table the way you do in your sample, you get only those properties defined statically in your WiX authoring, and this is expected. At install-time there's a concept called in-memory property collection - this consists out of all the properties defined in various ways: statically in Property table, provided via the command-line, system, defined in custom actions, etc.
You can access all those properties via the Session object. Just call session[name], where name is the name of the property you're going to get. I doubt there's an enumerator defined for properties, but in real life you rarely need to iterate the properties - you rather try to get a certain one.
Workaround if you REALLY need to enumerate it (requires MSI 4.0 or newer):
Add full logging in the project
<Property Id="MsiLogging" Value="Iwearucmopvx" />
This will log the entire installation process to your %TEMP% folder.
Then get the logfile location with:
var logFile = session["MsiLogFileLocation"];
Since the logfile is locked by the MSI logging you will have to access it in shared mode:
new FileStream(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
Read it and get the latest in-memory PROPERTY values by searching out the lines beginning with "PROPERTY CHANGE:".
MSI (c) (90:78) [21:07:16:108]: PROPERTY CHANGE: Adding MsiRunningElevated property. Its value is '1'.
MSI (c) (90:78) [21:07:16:108]: PROPERTY CHANGE: Adding Privileged property. Its value is '1'.
MSI (c) (90:78) [21:07:16:108]: Note: 1: 1402 2: HKEY_CURRENT_USER\Software\Microsoft\MS Setup (ACME)\User Info 3: 2
MSI (c) (90:78) [21:07:16:108]: PROPERTY CHANGE: Adding USERNAME property. Its value is 'Employee'.
MSI (c) (90:78) [21:07:16:108]: Note: 1: 1402 2: HKEY_CURRENT_USER\Software\Microsoft\MS Setup (ACME)\User Info 3: 2
MSI (c) (90!A8) [21:07:30:900]: PROPERTY CHANGE: Modifying WEB_APP_NAME property. Its current value is '$projectname$/v1.0.0'. Its new value: '$projectname$\v1.0.0'.
MSI (c) (90!A8) [21:07:30:901]: PROPERTY CHANGE: Adding WEB_APP_NAME_NORMAL property. Its value is '$projectname$/v1.0.0'.
MSI (c) (90!A8) [21:07:30:901]: PROPERTY CHANGE: Adding WEB_APP_NAME_LAST property. Its value is 'v1.0.0'.
The drawback is that there will always be a log in the %TEMP% folder left over. Maybe add some cleaning up or something.