WiX how to import/export data into C# Custom Action? - c#

I need to:
get data from users (from a UI), and put that data into Properties
import the Properties into some C# code using a Custom Action
do some stuff to the Properties (encrypt the values),
export the values back to WiX, where I will
create a registry key and put the encrypted values into them
I can accomplish everything on that list except for #4. That is, I can't seem to import and export values into the C# code. I think the problem is in the timing.
Here's an example of a Custom Action that is used to import some properties into some C# code:
<Property Id="VALUE" Value="value"/>
<SetProperty Id="CustomAction_PassProperty"
Value="VALUE=[VALUE]"
Sequence="execute"
Before="CustomAction_PassProperty"/>
<Binary Id="Binary_PassProps"
SourceFile="$(var.CreateRegistryKey.TargetDir)CreateRegistryKey.CA.dll"/>
<!-- Note that 'Impersonate="no"' elevates the privilege of the C# code, needed to create keys -->
<CustomAction Id="CustomAction_PassProperty"
BinaryKey="Binary_PassProps"
DllEntry="CreateKeys"
Execute="deferred"
Impersonate="no"
Return="check"
HideTarget="yes"/>
<InstallExecuteSequence>
<Custom Action="CustomAction_PassProperty"
After="InstallInitialize"/>
</InstallExecuteSequence>
Notice that the action is done after InstallInitialize.
Next, how to take the imported properties and convert them into variables in the C# code:
[CustomAction]
public static ActionResult CreateKeys(Session session)
{
string value = session.CustomActionData["VALUE"];
return ActionResult.Success;
}
Next, here's an example of how to export variables in C# code back into WiX, as properties:
<Binary Id="Binary_CustomActionTemplate"
SourceFile="$(var.CustomAction.TargetDir)CustomAction.CA.dll"/>
<CustomAction Id="CustomAction_CustomActionTemplate"
BinaryKey="Binary_CustomActionTemplate"
DllEntry="CustomActionTemplate"
Execute="immediate"
Return="check"/>
<InstallUISequence>
<Custom Action="CustomAction_CustomActionTemplate" After="LaunchConditions"/>
</InstallUISequence>
And this time the action is done after LaunchConditions.
Finally, how to create a Property, give it a value, and send it back to WiX in C#:
[CustomAction]
public static ActionResult CreateKeys(Session session)
{
session["VALUE"] = "Hello, world.";
return ActionResult.Success;
}
I think the problem lies in when -- that is, when during the installation sequence -- that I do both things (import and export), but I'm not sure. That is, I need to import the data into the C# code, do stuff in the C# code, then export data from C# code. But how?!? (waves hands dramatically at sky)
To sum up: how to import and export data into C# Custom Action in WiX (using the same C# code)?

AFAIK you cannot alter property values once in the execute sequence. Someone can correct me if I'm wrong and that would be an alternative answer to your question.
You should be able to encrypt the properties in the UI phase of the install. In your immediate custom action you can access the msi properties with session["PROPERTYNAME"] as opposed to session.CustomActionData["PROPERTYNAME"]. You can also set the properties: session["PROPERTYNAME"] = "new value string";.
Then on your Next/Install control on the page where the information is added you can put
<Control Id="Install" ...>
<Publish Event="DoAction" Value="EncodeAndSetValuesAction">1</Publish>
....
</Control>
If they are marked Secure='yes' and public (all caps name) then they should be usable in the execute phase of your installation with the encoded values.
Do note that if you launch your custom action with a DoAction control event (clicking next on the page where the user enters the values) you won't be able to get logging information in the msi log. See here for more info.
Small edit: I was writing this answer when you edited the question which shows you already know how to get the properties and set them in an immediate action.
Alternatively, you may be able to just schedule an immediate custom action to encrypt and set the values in the InstallExecuteSequence before InstallInitialize (or before WriteRegistryValues or anywhere?) and it will set the properties before the elevated Server portion of the install starts. I think the InstallExecuteSequence happens in two parts. One where it does a pass figuring out values and which files to install making an install script for the elevated Server portion of the install where it actually copies the files around and writes registry keys ect. This part is still the client context where you can run immediate custom actions and read/set properties. Secondly it will start the Server portion of the install with set in stone properties and follow the execution script that was just created.
I decided to put a short simplified explanation about the differences between immediate and deferred custom actions here as well.
There are basically two main differences between deferred and immediate custom action. Immediate custom actions can access the msi DB directly to read and/or modify properties but can't do anything that requires elevation unless the installer was run "as administrator" since it runs in the user's context.
Deferred actions can only run in the execute sequence and can only read property values set explicitly through CustomActionData with the same name as the action but they always have elevated privileges.

Related

Pass ConnectionString to Custom Action in WiX Installer (escape semicolon)

This is my first project with WiX.I'm creating an installer for Windows Service and during the installation, I need to collect some configuration data that the service will use. This includes the connection string to the database.
<Binary Id="CustomActionBinary" SourceFile="$(var.ServiceSetupActions.TargetDir)$(var.ServiceSetupActions.TargetName).CA.dll"/>
<CustomAction
Id="ServiceSetupActions"
BinaryKey="CustomActionBinary"
DllEntry="SaveCompanySettings"
Execute="deferred"
Return="check"
Impersonate="no" />
<CustomAction
Id="SetupCustomProperties"
Property="ServiceSetupActions"
Value="DBTYPE=[DBTYPE];CONNECTIONSTRING=[CONNECTIONSTRING];INSTALLFOLDER=[INSTALLFOLDER]"/>
<InstallExecuteSequence>
<Custom Action="SetupCustomProperties" Before="ServiceSetupActions" />
<Custom Action="ServiceSetupActions" After="InstallFiles">NOT Installed</Custom>
</InstallExecuteSequence>
Problem is that WiX is using a semicolon as a data separator (https://github.com/wixtoolset/Dtf/blob/09dc7b4e4494182c0906bf5492f12e09c918444f/src/WixToolset.Dtf.WindowsInstaller/customactiondata.cs#L32), so the connection string that is entered during the setup is incorrectly deserialized in my custom action.
The question is: How can I correctly pass a string that contains a semicolon to custom action (using a session.CustomActionData) so it will not be misformed.
The alternative is a full SQL connection dialog that will ask for server, database, user name, and password, but the service can handle PostgresSQL and MS SQL and sometimes the connection strings may contain some modifications.
I see you tagged this C# so I'm assuming your using DTF custom actions.
In DTF you can create another custom action and run it in immediate execution. In that action you can create a CustomActionData collection, populate the key value pairs and then call session.DoAction() and pass it the name of the deferred custom action to schedule and the CustomActionData collection. The serialization magic will happen magically and the key value pairs will be available in the deferred custom action simply by saying session.CustomActionData(key);
Please check this sample and see if it works for you:
https://github.com/glytzhkof/WiXDeferredModeSample
Here is another version of the sample, this one uses the DTF CustomActionData class for more "auto-magic": https://github.com/glytzhkof/WiXDeferredModeSampleDTF - it will schedule the custom actions for you and you can just access the properties in deferred mode. Limited testing done.
Set an upgrade GUID in this source line where it says "PUT-GUID-HERE" - you can create a GUID here.
Change the property MYPROPERTY to contain semicolons ;. Do so at this WiX source line. You can use the below sample text if you like (anything else will do of course):
<Property Id="MYPROPERTY" Hidden="yes" Secure="yes">Test;Test1;Test2</Property>
Compile and do a test run. There will be a message box showing and it should contain the full string you specified in the source file.
If you want to combine several property values inside the string you send to deferred mode you have a few options. The simplest is to set several properties with the values you need and then combine them in a string sent to deferred mode. You can set the properties in several ways: dialog, command line, input boxes, etc...:
MYCOMPANY = "Some Company"
MYDATABASE = "TheDatabaseName"
Etc...
Then you call Session.Format in an immediate mode custom action to resolve values in your string. Something like this:
COMPANYID=[COMPANYID];DBTYPE=[DBTYPE];CONNECTIONSTRING=[CONNECTIONSTRING];INSTALLFOLDER=[INSTALLFOLDER]
var CAdata = Session.Format("MYCOMPANY=[MYCOMPANY];MYDB=[MYDB];MYDBTYPE=[MYDBTYPE]");
session["MYPROPERTYSENTTODEFERREDMODE"] = CAdata;
You also need a Type 51 CA to actually send that string to the deferred mode custom action. You see how that is done in the sample above.
Then you retrieve the string value of MYPROPERTYSENTTODEFERREDMODE in a deferred mode custom action and you should be able to use it directly?
Links:
An existing answer on deferred mode and WiX markup.

WIX Custom Action modify file in INSTALLFOLDER after InstallFinalize

I have written a C# custom action for my WIX V3 Installer which is supposed to modify my appsettings.json in the INSTALLFOLDER. The action´s Execute-attribute is set to immediate and Impersonate="no",it is called after InstallFinalize but it encounters a problem within this action which is the missing admin permission.
The action modifies appsettings.json in the INSTALLFOLDER which is Program File (x86).
The custom action reads, deserializes, modifies, and serializes the data normally with no error.
The error happens during writing to appsettings.json in InstallFolder.
Although the error appears the rest of the application is installed and is working fine.
I have tried combining Execute and Custom actions in ALL possible combinations, and while I get privileges to write to InstallFolder if I change the Custom Action to run before the installation is finished I can't find the appsettings.json file because all the files at that point are temporary files (.tmp), and with non-relevant names.
The error that appears:
Error message
Part of my Product.wsx code:
<Property Id="Password" Value="Error password" />
<Property Id="FilePath" Value="C:\Program Files (x86)\Company\Product\" />
<CustomAction Id="SetUserName" Property="Username" Value="[ACCOUNT]"/>
<CustomAction Id="SetPassword" Property="Password" Value="[PASSWORD]"/>
<CustomAction Id="SetFilePath" Property="FilePath" Value="[INSTALLFOLDER]"/>
<Binary Id="GetData" SourceFile="$(var.SetupExtensions.TargetDir)\$(var.SetupExtensions.TargetName).CA.dll" />
<CustomAction Id="ChangeJSON" BinaryKey="GetData" DllEntry="CustomAction1" Execute="immediate" Impersonate="no" Return="check"/>
<InstallExecuteSequence>
<Custom Action="SetUserName" After="CostFinalize" />
<Custom Action="SetPassword" After="CostFinalize" />
<Custom Action="SetFilePath" After="CostFinalize"/>
<Custom Action='ChangeJSON' After='InstallFinalize'></Custom>
</InstallExecuteSequence>
My Custom Action code:
public static ActionResult CustomAction1(Session session)
{
try
{
session.Log( "Begin CustomAction1" );
string user = session["Username"];
string password = session["Password"];
string loc = session["FilePath"];
var json = System.IO.File.ReadAllText( loc +"appsettings.json" );
var root = JsonConvert.DeserializeObject<Root>(json);
root.Default.UserName = user;
root.Default.Password = password;
json = JsonConvert.SerializeObject( root, Formatting.Indented );
System.IO.File.WriteAllText( loc + "appsettings.json", json );
//The MessageBox bellow shows(and is with correct info) when I remove System.IO.File.WriteAllText above ^^
MessageBox.Show("Username: "+ user +"\nPassword: "+password +"\nFilePath: " + loc);
return ActionResult.Success;
}
catch(Exception ex )
{
session.Log( "Error: " + ex.Message );
MessageBox.Show(ex.Message);
return ActionResult.Failure;
}
How do I modify appsettings.json through my Custom Action?
Custom actions that change the system state should run between InstallIntialize and InstallFinalize. This means you should schedule it Before InstallFinalize not after.
It should also run in deferred execution with no impersonation. You will need another custom action scheduled prior to this one that creates custom action data and passes the data to the deferred custom action.
Ideally you should also have rollback and commit actions to support rollbacks and test using the WIXFAILWHENDEFERRED custom action.
Read:
http://www.installsite.org/pages/en/isnews/200108/index.htm
http://blog.iswix.com/2011/10/beam-me-up-using-json-to-serialize.html
Application Launch: I would consider updating the JSON via the application launch sequence if you can. 1) Single source solution, 2) familiar territory for developers, 3) easier and better debugging features and 4) no impersonation-, sequencing- and conditioning-complexities like you get for custom actions: Why is it a good idea to limit the use of custom actions in my WiX / MSI setups?
Deferred CA Sample: You can find a sample of a deferred mode custom action WiX solution here:
https://github.com/glytzhkof/WiXDeferredModeSample
Inline Sample: This older answer shows the key constructs inline in the answer:
Wix Custom Action - session empty and error on deferred action.
CustomActionData: Deferred mode custom actions do not have access to the session objects property values like immediate mode custom actions do. As Painter has written you need to "send" text or settings to deferred mode by writing the data into the execution script. The data is then available in deferred mode by reading the special property CustomActionData. The above sample should have the required constructs to see how this works. See the MSI documentation as well and also Robert Dickau's MSI Properties and Deferred Execution.
Transaction: Deferred mode custom actions can only exist between InstallInitialize and InstallFinalize. Actions between these actions run with elevated rights and can write to per-machine locations (not writeable for normal users). You can schedule yourself right before InstallFinalize for starters to test your current delete mechanism (I would try other approaches).
Rollback CAs: Rollback custom actions are intended to undo what was done with your JSON custom action and then revert everything to the previous state (cleanup after failed install). Writing these is quite involved and takes a lot of testing - many just skip them. It is best to try to find a library or a framework that does the job for you. I am not aware of any except the one linked to below, and I don't know its state. Rollback custom actions must precede the actual CA in the InstallExecuteSequence (when an MSI is rolling back it executes the sequence in reverse).
With all this complexity, custom actions become the leading source of deployment errors: https://robmensching.com/blog/posts/2007/8/17/zataoca-custom-actions-are-generally-an-admission-of-failure/
Links:
How to pass CustomActionData to a CustomAction using WiX?
https://github.com/NerdyDuck/WixJsonExtension (not sure of status of this)

Custom Action - Error 1001: Could not find file myApp.InstallState

I have tried to create a custom action for a Visual Studio Installer project to modify the permissions for a config file.
The Installer.cs is as follows:
public override void Commit(IDictionary savedState)
{
base.Commit(savedState);
// Get path of our installation (e.g. TARGETDIR)
//string configPath = System.IO.Path.GetDirectoryName(Context.Parameters["AssemblyPath"]) + #"\config.xml";
string configPath = #"C:\Program Files\Blueberry\Serial Number Reservation\config.xml";
// Get a FileSecurity object that represents the current security settings.
FileSecurity fSecurity = File.GetAccessControl(configPath);
//Get SID for 'Everyone' - WellKnownSidType works in non-english systems
SecurityIdentifier everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
// Add the FileSystemAccessRule to the security settings.
fSecurity.AddAccessRule(new FileSystemAccessRule(everyone, FileSystemRights.Modify | FileSystemRights.Synchronize, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
// Set the new access settings.
File.SetAccessControl(configPath, fSecurity);
}
public override void Install(IDictionary stateSaver)
{
base.Install(stateSaver);
}
public override void Rollback(IDictionary savedState)
{
base.Rollback(savedState);
}
public override void Uninstall(IDictionary savedState)
{
base.Uninstall(savedState);
}
Then I add the Primary Output (Installer class = true) into the Commit section of the setup project's Custom Actions.
When I run the installer, I get the following error:
Error 1001: Could not find file 'c:\mypath\myapp.InstallState'
Scouring the web I've found a few examples of similar experiences, but none of the solutions offered have worked for me.
Any ideas?
You can find a solution here
To quote:
The problem is that the MSI infrastructure is looking for the installation state file which is usually created during the Install
phase. If the custom action does not participate in the Install phase,
no file is created.
The solution is to add the custom action to both the Install and the
Commit phases, although it does nothing during the install phase.
I had this problem when I didn't specify a custom action in my installer project for all four overrides (Install, Uninstall, Commit, and Rollback). As soon as I specified my project output as the custom action for all four, the issue went away.
The only overrides in my installer class that did anything were Commit and Uninstall; I think that Install was in charge of creating the InstallState file in the first place, and since it was never called the InstallState file was never created.
Sometimes this happens when the installer class is not created correctly. Here is a tutorial which may help you: http://devcity.net/Articles/339/1/article.aspx
Make sure that your custom action follows the tutorial recommendations.
Sometimes, "Debugger.Launch();" is put at those overwritten functions for debugging. If you build the installer with the statement there, and during your installation, a dialog will popup to ask you whether debug is needed, if you press 'cancel debugging', you'll get this error dialog. Because you added the 'Debugger.Launch()' at your function, then that function will be considered as 'missed' by installer. So, don't forget to remove it.
Try installing this as in an administrator command prompt. This worked for me.!
For me, the issue was as simple as just adding a closing quote around one of the textbox names in my CustomActionData string.
I was using the "Textboxes (A)" and "Textboxes (B)" windows in the User Interface section. A has 1 box, EDITA1, where I get the path to a file, and B has 2 boxes, EDITB1 and EDITB2, for some database parameters. My CustomActionData string looked like this:
/filepath="[EDITA1]" /host="[EDITB1] /port="[EDITB2]"
It should have been:
/filepath="[EDITA1]" /host="[EDITB1]" /port="[EDITB2]"
(closing quote on [EDITB1])
I used the Install override in my Installer class to get the values (i.e. string filepath = Context.Parameters["filepath"];) and used it to write them to an INI file for my app to use once installed. I put the "Primary output" under all of the phases in the Custom Actions GUI, but did nothing with the InstallerClass property (default: True) and only set the CustomActionData string on the Install one. I didn't even include override functions in my Installer class, since I was doing nothing that was custom in the other phases.
create your post install executable as you would a console app ex: "mypostinstallapp.exe".
install the "mypostinstallapp.exe" with your msi product. maybe put it with the Application Folder or a shared folder if you want to use it with multiple installs.
in the custom actions (rightclick the msi project and view custom actions) and add an action in the Commit section. assuming you want it to run towards the end.
then select your "mypostinstallapp.exe" from the actions view and in its properties set InstallerClass False.
what I do in "mypostinstallapp.exe" is look for a cmd file and run it as a process.
so i can add stuff to the cmd file and try to forget about the custom action process.
Make sure in the output properties you check installer class to false
You can set a register key to the installation path. Click on the Setup Project, View Register, and set the value as: [ProgramFiles64Folder][Manufacturer][ProductName].
Then you can create a Console App to get in to the register and take this information and run your program. Just need to add as a custom action on Commit the Console App you created.
Try setting Installer class = false instead of true in the properties for your custom action. That fixed this problem for me.

How to read in text from the visual studio debug output window

I've read several articles that tell you how to add text to the output window in visual studio from within an Add-On (specifically, a visual studio 2008 integration package, via the visual studio 2008 SDK 1.1), but no examples of how to read text from the output window. My goal is to parse text from the debug output window while debugging a certain application (TRACE output and possibly stdin/stdout). The IVsOutputWindowPane interface has no methods for reading in text from the output window. The documentation seems to imply that it is possible, but it doesn't provide an example:
http://msdn.microsoft.com/en-us/library/bb166236(VS.80).aspx
Quote: In addition, the OutputWindow and OutputWindowPane objects add some higher-level functionality to make it easier to enumerate the Output window panes and to retrieve text from the panes.
Preferably I'd like to be able to subscribe to an event that fires when a new line of text arrives, similar to a StreamReader's asynchronous reads.
It is possible, it is just a long winding path to get to it:
ServiceProvider -> IVsOutputWindow -> GetPane( debugwindow ) -> IVsUserData -> GetData( wpftextviewhost ) -> IWpfTextViewHost -> IWpfTextView -> TextBuffer -> Changed event.
Presuming you have a VS IServiceProvider from somewhere else (vsix extension/whatever, global service provider), and without any error checking, it looks like this:
IVsOutputWindow outWindow = ServiceProvider.GetService(typeof(SVsOutputWindow)) as IVsOutputWindow;
Guid debugPaneGuid = VSConstants.GUID_OutWindowDebugPane;
IVsOutputWindowPane pane;
outWindow.GetPane(ref debugPaneGuid, out pane);
// from here up you'll find in lots of other stackoverflow answers,
// the stuff from here down is interesting to this question
IVsUserData userData = (IVsUserData)pane;
object o;
Guid guidViewHost = DefGuidList.guidIWpfTextViewHost;
userData.GetData(ref guidViewHost, out o);
IWpfTextViewHost viewHost = (IWpfTextViewHost)o;
IWpfTextView textView = viewHost.TextView;
textView.TextBuffer.Changed += YourTextChangedHandlerHere;
Your text changed handler will then get called every time the output window gets more data. you won't necessarily get it line by line, but you'll probably more likely than not get big chunks you'll need to deal with on your own.
It is highly likely that some of the above did not even exist in VS in 2010. But it exists now!
The default behavior (when you don’t set the listener explicitly) of VS is to display trace massages in the debugger output window, which you appreciate if you want a simple solution and do no other actions with the massages.
Unfortunately this is not your case. So you have to define a trace listener to send (and store) your trace massages where you then will be able to read them. The trace listener could be a file (for example XML) or you can create a custom listener by deriving a class from the base class TraceListener if you don't want to bother yourself with an additional file.
I don't know that what you ask is possible. But, you can register your add-in as a debugger for your application so that you get the output the trace messages. These are typically routed to OutputDebugString, and can be captured as described in this article: http://www.drdobbs.com/showArticle.jhtml?articleID=184410719. It does not give you the normal output, only debug, but it does not depend on the technology of the debugged application.
The solution on this page selects the text in order to read it. I'm hoping there's a better way.
Automatically stop Visual C++ 2008 build at first compile error?
Private Sub OutputWindowEvents_OnPaneUpdated(ByVal pPane As OutputWindowPane) Handles OutputWindowEvents.PaneUpdated
pPane.TextDocument.Selection.SelectAll()
Dim Context As String = pPane.TextDocument.Selection.Text
pPane.TextDocument.Selection.EndOfDocument()
End Sub

My App.Config file isn't saving anything I modify

Here's my code:
public void VerifyIfFirstTimeRun()
{
if (System.Configuration.ConfigurationSettings.AppSettings["FirstTimeRunning"] == "true")
{
//Do bla bla bla
//Then do bla bla bla
System.Configuration.ConfigurationSettings.AppSettings["FirstTimeRunning"] = "false";
}
}
Problem is, I'm testing this with the F5 key, it boots up and sure enough using a breakpoint shows that it is indeed going inside the If Condition, but when I "Stop" the application and press F5 again it goes inside the If Condition AGAIN. Is this standard operational procedures?
If so, how can I test if its working?
This is going against the spirit of what the App.config file is used for ... but to answer your question, you need to do System.Configuration.Configuration.Save().
Edit:
App.config is typically used to configure database connections, providers, etc. And is usually set once at installation. It's suggested that user settings go into a separate config file user.config. See this for the explanation.
Edit:
System.Configuration.Configuration class.
Note - now that I read why you're using these settings, may I suggest another way? You should probably just check if the file is there:
if (!File.Exists("thefilepath"))
{
AlertUserOfMissingFile();
ChooseNewFile();
}
It's safer this way anyhow because having a setting set to true doesn't necessarily mean the file is where you think it is.
I'm not sure you should expect this to save; you can, however, have a settings file that has a setting (a bool in this case) in the user's context, which saves (when you ask it to) via Settings.Default.Save().
I don't advice you to use App.settings for this purpose.
Take a look a this article
Settings in C#
In Solution Explorer, expand the Properties node of your project.
In Solution Explorer, double-click the .settings file in which you want to add a new setting. The default name for this file is Settings.settings.
In the Settings designer, set the Name, Type, Scope, and Value for your setting. Each row represents a single setting. Figure 1 shows an example of the Settings designer.
If you want to persist changes to user settings between application sessions, call the Save method, as shown in the following code:
Properties.Settings.Default.Save();

Categories

Resources