How can I author a WiX custom action that
Is always called at the end of an installation, at least if there's an install error
Copies the current MSI log file from its current local to the user's APPDATA folder
I have this managed custom action code. Not sure how to author its invocation in my Wix script. Should the custom action be scheduled for after InstallFinalize? Can it be scheduled OnExit="error"?
[CustomAction]
public static void CopyLogFile(Session session)
{
const string company = "MyCompany";
const string product = "MyProduct";
try
{
session.Log("CustomAction.CopyLogFile entry");
var msiLogFilePath = session.CustomActionData["LOGFILEPATH"];
if (msiLogFilePath != null)
{
session.Log("CustomAction.CopyLogFile MSI log filename: {0}", msiLogFilePath);
var localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var destDirPath = Path.Combine(localAppDataPath, company, product);
var destDir = Directory.CreateDirectory(destDirPath);
session.Log("CustomAction.CopyLogFile Destination directory: {0}", destDir.FullName);
var destFilePath = Path.Combine(destDir.FullName, Path.GetFileName(msiLogFilePath));
File.Copy(msiLogFilePath, destFilePath, true);
session.Log("CustomAction.CopyLogFile Log file copied to: {0}", destFilePath);
}
else
{
session.Log("CustomAction.CopyLogFile File path not found");
}
}
catch (Exception exception)
{
session.Log("CustomAction.CopyLogFile exception {0}", exception);
}
finally
{
if (session != null)
{
session.Log("CustomAction.CopyLogFile exit");
session.Close();
}
}
}
Yes, you can schedule it after InstallFinalize (so did I, but I copy it every time if it is not a complete removal of the package):
<InstallExecuteSequence>
<Custom Action="CopyLogfile" After="InstallFinalize">NOT (REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE)</Custom>
</InstallExecuteSequence>
Remember to also add it to the UI sequence if you have any. I added it as event to the PushButton in the SetupCompleteSuccess- and SetupCompleteError-dialogs (maybe you need to add it only to the latter one?) like in the following:
<Dialog Id="SetupCompleteSuccess" X="50" Y="50" Width="374" Height="266" Title="[ProductName]" NoMinimize="yes">
<Control Id="OK" Type="PushButton" X="230" Y="243" Width="66" Height="17" Text="&Finish" TabSkip="no" Default="yes" Cancel="yes">
<Publish Event="EndDialog" Value="Exit">1</Publish>
<!-- ### Invoking copying the logfile if the Finish-button is pressed -->
<Publish Event="DoAction" Value="CopyLogfile">MsiLogFileLocation AND NOT (REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE)</Publish>
</Control>
<!-- ... rest of the dialog ... -->
</Dialog>
Regarding showing it only in case of an error: Maybe checking for the ProductState-properrty? Searched the web for this but didn't find anything useful.
Edit: Maybe a proper way to execute it only in case of an error is the usage of Custom Action flags that execute it only during rollback. In WiX it would be the Execute="rollback"-attribute for the CustomAction-tag.
Don't mess with custom actions after InstallFinalize if you can help it. They are often skipped entirely when deploying via tools such as SCCM, Unicenter, etc...
A custom dialog with a button to open the log file at the end of the install could work ok though.
Related
I am setting up a scheduled task during deployment using TaskScheduler. At the same time I am using a settings file to store user configuration. Now when the task scheduler runs it doesn't access the settings file because the users don't match.
During installation the setup project runs as system to get writing Permission in the application directory. I am running a custom action to write both, the settings and the scheduled Task yet the users don't match.
How can I ensure that the users match. do I have to create a separate application to make the configuration so that the settings can be in the application scope? This makes no sense to me because these settings will be different per deployment and set by the user and not me.
Edit: Location of the user settings file
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ApplicationName");
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
Directory.SetCurrentDirectory(path);
Script Snippet creating the scheduled task
using Microsoft.Win32.TaskScheduler;
...
if (Interval == null) Interval = TimeSpan.FromHours(1);
if (Duration == null) Duration = TimeSpan.FromDays(1);
using(TaskService ts = new TaskService())
{
var td = ts.NewTask();
// attempt to get the correct user, gets system user as if ommitted
td.Principal.UserId = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
td.Principal.LogonType = TaskLogonType.InteractiveToken;
td.Settings.ExecutionTimeLimit = TimeSpan.FromMinutes(5);
var trigger = new DailyTrigger((short)DaysInterval)
{
StartBoundary = DateTime.Today,
Repetition = new RepetitionPattern(Interval, Duration)
};
td.Triggers.Add(trigger);
string exePath = System.Reflection.Assembly.GetEntryAssembly().Location;
td.Actions.Add("\"" + exePath + "\"", "", Path.GetDirectoryName(exePath));
ts.RootFolder.RegisterTaskDefinition("myTask", td);
}
The custom action, so to speak is too big and split on different files accros the project to share here
Edit: Example for setting fields of the settings file
Properties.Settings _s = Properties.Settings.Default;
_s.ClientIp = Tx_Ip.Text;
_s.ShopID = Tx_ShopId.Text;
_s.Save();
Edit: I have now logged the User, UserPath and AppPath both during Setup and when running through scheduled Task.
Setup
User: NT-AUTORITÄT\SYSTEM
UserPath: C:\Users\user\AppData\Local\appname
AppPath: C:\Program Files (x86)\businessname\appname
Scheduled Task
User: NT-AUTORITÄT\SYSTEM
UserPath: C:\Windows\system32\config\systemprofile\AppData\Local\appname
AppPath: C:\Program Files (x86)\businessname\appname
So actually it is the same User but the UserPath is different. As I am Writing the settings during Setup they get saved in the wrong location and later can't get accessed during the scheduled task.
The solution to this problem is using a separate project to perform the custom actions and just use the Application scope for the Settings. This way The configuration can be created during Installation. The settings can only be changed with administrator privileges if the application is deployed to the setups default folder.
Proof of concept Code:
Main Application Project
using System;
namespace MyApplication
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Properties.MyApplication.Default.Setting);
Console.ReadLine();
}
}
}
Custom Action Project
using System;
using System.Configuration;
using System.IO;
using System.Reflection;
namespace WriteAppSettingsTest
{
class Program
{
static void Main(string[] args)
{
string exePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "MyApplication.exe");
Console.WriteLine(exePath);
Console.ReadLine();
Configuration cfg = ConfigurationManager.OpenExeConfiguration(exePath);
ClientSettingsSection section = (ClientSettingsSection)cfg.GetSectionGroup("applicationSettings").Sections["MyApplication.Properties.MyApplication"];
section.Settings.Get("Setting").Value.ValueXml.InnerText = "Edited Setting";
section.SectionInformation.ForceSave = true;
cfg.Save();
}
}
}
The main application requires a settings file. For this example I called it "MyApplication.settings". It needs a setting called "Setting". In this example I set the value to "default value".
In the setup project execute the custom action project under the commit custom action. the Installer class property has to be set as false to work like this.
This solution also assumes that both project outputs are located in the same folder
Attempts to use Configuration.Save() fail when the WPF program is installed under "Program Files". If installed elsewhere, the Save() works as expected. Isn't the program itself authorized to edit its own .config file?
I get two exceptions:
System.Configuration: An error occurred loading a configuration file: Access to the path 'C:\Program Files\Advanced Applications\ConfigurationTest\ejoqasrr.tmp' is denied. (C:\Program Files\Advanced Applications\ConfigurationTest\WPF.exe.Config)
mscorlib: Access to the path 'C:\Program Files\Advanced Applications\ConfigurationTest\ejoqasrr.tmp' is denied.
I tried opening with ConfigurationUserLevel.PerUserRoaming) instead of ConfigurationUserLevel.None but it still failed, reporting different exceptions:
System.Configuration: An error occurred executing the configuration section handler for appSettings.
System.Configuration: ConfigurationSection properties cannot be edited when locked.
[NOTE: Searching found an article that appears related but didn't provide a resolution for this issue:
Access to path denied for App.Config on Windows 8. How can I update App.Config?
The first two methods are called from MainVM and invoke the underlying base methods which expect to find a correlated property on the passed in object and then synchronize the appropriate values in AppSettings.
private void SettingsLoad()
{
SettingsLoad(this);
}
private void SettingsSave()
{
try
{
SettingsSave(this);
}
catch (Exception ex)
{
currentMessageAction = MessageAction.Acknowledge;
window.MessagePanel.Show("EXCEPTION!", Utility.ParseException(ex));
}
}
....
public static void SettingsLoad(object main)
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
foreach (KeyValueConfigurationElement setting in config.AppSettings.Settings)
{
// Attempt to get a reference to the property and, if found, set its value
var prop = main.GetType().GetProperty(setting.Key);
if (prop != null)
{
prop.SetValue(main, setting.Value);
}
}
}
public static void SettingsSave(object main)
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
foreach (KeyValueConfigurationElement setting in config.AppSettings.Settings)
{
// Attempt to get a reference to the property and, if found, update its value
var prop = main.GetType().GetProperty(setting.Key);
if (prop != null)
{
setting.Value = prop.GetValue(main).ToString();
}
}
// This line produces an exception
config.Save(ConfigurationSaveMode.Modified);
}
Isn't the program itself authorized to edit its own .config file?
No. The user account under which the application is running must have permissons to write to the folder/file.
Apparently it doesn't have the appropriate permissions to write to the "Program Files" folder in your case.
How to Set File and Folder Permissions in Windows
Simply, how do I change the default path which IIS Express uses to write files too, etc.
It's currently set to C:\Program Files (x86)\IIS Express.
Note: I'm using Visual Studio 2019.
Code snippet:
System.IO.File.WriteAllText(#"file1.txt", "Test!");
Please try https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.setcurrentdirectory?view=netcore-2.2
public static void Main()
{
// Create string for a directory. This value should be an existing directory
// or the sample will throw a DirectoryNotFoundException.
string dir = #"C:\test";
try
{
//Set the current directory.
Directory.SetCurrentDirectory(dir);
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine("The specified directory does not exist. {0}", e);
}
// Print to console the results.
Console.WriteLine("Root directory: {0}", Directory.GetDirectoryRoot(dir));
Console.WriteLine("Current directory: {0}", Directory.GetCurrentDirectory());
}
I'm looking for a way to run code by executing the following steps:
Receiving a list of NuGet packages (a list of tuples ("package name", "package version", "path to main class").
Retrieving them in a local directory (cf code sample #1)
Loading them in my program at run-time
Running the main classes by introspection (cf code sample #2)
By now I am struggling with the third step. I can't find out how to load my package at run-time.
My main question are:
How can I find out in which folders were stored the retrieved packages?
How can I load the content of those directories into my program?
Code Sample #1:
private static void getPackageByNameAndVersion(string packageID, string version)
{
IPackageRepository repo =
PackageRepositoryFactory.Default
.CreateRepository("https://packages.nuget.org/api/v2");
string path = "C:/tmp_repo";
PackageManager packageManager = new PackageManager(repo, path);
Console.WriteLine("before dl pkg");
packageManager.InstallPackage(packageID, SemanticVersion.Parse(version));
}
Code sample #2:
private static void loadByAssemblyNameAndTypeName(string assemblyName, string typeName)
{
AppDomain isolationAppDomain = AppDomain.CreateDomain("tmp");
object a = isolationAppDomain.CreateInstanceAndUnwrap(assemblyName, typeName);
Type x = a.GetType();
MethodInfo m = x.GetMethod("Main");
m.Invoke(a, new object[] { });
}
Grab a cup of coffee :)
Downloading the nuget package?
Nuget.Core (nuget package) is a good choice, and here is a snippet of code that I have that should be able to download a nuget package by id and version
var repo = PackageRepositoryFactory.Default
.CreateRepository("https://packages.nuget.org/api/v2");
string path = "c:\\temp";
var packageManager = new PackageManager(repo, path);
packageManager.PackageInstalled += PackageManager_PackageInstalled;
var package = repo.FindPackage("packageName", SemanticVersion.Parse("1.0.0"));
if (package != null)
{
packageManager.InstallPackage(package, false, true);
}
Notice that I plugged an event handler to the PackageInstalled event of the PackageManager class.
How do we load an assembly in an isolated app domain?
Since reflection API does not provide a way to load an assembly in a specific domain, We will create a proxy class that act as a loader in our isolated domain:
public class TypeProxy : MarshalByRefObject
{
public Type LoadFromAssembly(string assemblyPath, string typeName)
{
try
{
var asm = Assembly.LoadFile(assemblyPath);
return asm.GetType(typeName);
}
catch (Exception) { return null; }
}
}
And now, is how to put it all together?
Here comes the complex part:
private static void PackageManager_PackageInstalled(object sender,
PackageOperationEventArgs e)
{
var files = e.FileSystem.GetFiles(e.InstallPath, "*.dll", true);
foreach (var file in files)
{
try
{
AppDomain domain = AppDomain.CreateDomain("tmp");
Type typeProxyType = typeof(TypeProxy);
var typeProxyInstance = (TypeProxy)domain.CreateInstanceAndUnwrap(
typeProxyType.Assembly.FullName,
typeProxyType.FullName);
var type = typeProxyInstance.LoadFromAssembly(file, "<KnownTypeName>");
object instance =
domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
}
catch (Exception ex)
{
Console.WriteLine("failed to load {0}", file);
Console.WriteLine(ex.ToString());
}
}
}
Notice that this method is the event handler that gets executed after downloading the nuget package
Also
Note that you will need to replace <KnownTypeName> with the expected type name coming from the assembly (or maybe run a discovery of all public types in the assembly)
Worth noting that I haven't executed this code myself and cannot guarantee that it will work out of the box, and still might need some tweaking. but Hopefully it is the concept that allows you to solve the problem.
Don't do that! You are probably trying to load NuGet content at a customers computer to save some space on distribution of your software. Isn't it that?
The common recommended approach is to download the NuGet content as the second step of an automated build (after downloading the source code), build the software and run the automated tests with the NuGet content you have downloaded. And then distribute the build with the NuGet content you have tested as the complex whole unit.
I have console application which launches some tests during build.
<exec>
<executable>Tests.exe</executable>
<baseDirectory>Q:\Software\Platform\</baseDirectory>
<buildTimeoutSeconds>100</buildTimeoutSeconds>
</exec>
And i want to redirect it's output to text file and then include it in my report file.
I tried the following approach to redirect output to NativeUtilsTestReport.txt (like in command line somefile.exe > file.txt):
<exec>
<executable>Tests.exe</executable>
<baseDirectory>Q:\Software\Platform\</baseDirectory>
<buildArgs> > NativeUtilsTestReport.txt</buildArgs>
<buildTimeoutSeconds>100</buildTimeoutSeconds>
</exec>
but it doesn't seem to work. Tests works fine, but there is no text file with report.
How can i get output from this "exec" section?
You cannot use < or > in xml. I am able to redirect output like this.
<exec>
<executable>Tests.exe</executable>
<baseDirectory>Q:\Software\Platform\</baseDirectory>
<buildArgs> > NativeUtilsTestReport.txt</buildArgs>
<buildTimeoutSeconds>100</buildTimeoutSeconds>
</exec>
Then you can include the output into your log using the merge task.
<merge>
<files>
<file>NativeUtilsTestReport.txt</file>
</files>
</merge>
I would also recommend that you save your output to the artifact directory.
$(ArtifactDirectory)\NativeUtilsTestReport.txt
I'll have to use a metaphor.
NUnit it a testing program.
So when you run
nunit-console.exe SomeDll.dll "c:\somefolder\NUnitTest_RESULTS.xml"
The console app itself will outsome some XML....with a specific name.
So even if get an output xml file, this doesn't do anything for CC.NET.
You use a CC.NET file-merge task to "suck up" the NUnitTest_RESULTS.xml into the "ThisBuildCCNet_SuperDuperXmlFile.xml". (That's not the name, that is the concept).
And that doesn't do anything really.
CC.NET has "NUnit" installed, which is a link to an xsl file.
So after NUnitTest_RESULTS.xml is created AND NUnitTest_RESULTS.xml is "sucked up" into the super-duper.xml file AND you have the NUnit-Xsl enabled, when you click on the NUnit menu item (on the webpage of your project), CC.NET will take the nunit.xsl and apply it to the xml that now exists in the super-duper.xml file.
Aka, a basic xsl-xml transformation.
So to your question, if you want your console app to run tests, it should be able to output XML to a file. If it can't do that, then you're d3ad in the water.
If you get xml, then you have to use the file-merge task to "suck it up", and you have to author or find an xsl file to transform it.
.......
EDIT
If you cannot generate xml or hmtl (via htmlReportPlugin )
then I suggest this article:
http://testquest.docs.bsquare.com/Integration_Samples_CCNet/Configuring_CCNet_dashboard_to_Display_Test_Execution_Logs.htm
I ended up with writing my own ExecutableTask for cruise control, which reads output from CCnet IIntegrationResult.TaskResults and writes it to file.
Application writes to output plain text, but CCnet transforms it to following XML:
<buildresults>
<task><buildArgs />
<buildTimeoutSeconds>100</buildTimeoutSeconds>
<baseDirectory>Q:\software\Platform\</baseDirectory>
<dynamicValues />
<environment />
<executable>NativeUtils.Tests.exe</executable>
<OutputFilePath>Q:\software\NativeUtilsTestReport.txt</OutputFilePath>
<priority>Normal</priority>
<successExitCodes />
</task>
<message>Running 50 test cases...</message>
</buildresults>
<buildresults>
<task><buildArgs />
<buildTimeoutSeconds>100</buildTimeoutSeconds>
<baseDirectory>Q:\software\Platform\</baseDirectory>
<dynamicValues />
<environment />
<executable>NativeUtils.Tests.exe</executable>
<OutputFilePath>Q:\software\NativeUtilsTestReport.txt</OutputFilePath>
<priority>Normal</priority>
<successExitCodes />
</task>
<message level="Error">*** No errors detected</message>
</buildresults>
Here is code:
[ReflectorType("execWithOutput")]
class ExecutableTestTask:ExecutableTask
{
[ReflectorProperty("OutputFilePath", Required = false)]
public string OutputFilePath { get; set; }
protected override bool Execute(ThoughtWorks.CruiseControl.Core.IIntegrationResult result)
{
bool rez = base.Execute(result);
if (!string.IsNullOrEmpty(OutputFilePath))
{
using (FileStream fs = new FileStream(OutputFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
using (StreamWriter sw = new StreamWriter(fs))
{
var lastIndex = result.TaskResults.Count-1;
// read output from last executed task
var output = ((ProcessTaskResult)result.TaskResults[lastIndex]).Data;
string parsedOutput = readMessagesFromXml(output);
sw.Write(parsedOutput);
}
}
}
return rez;
}
// parse xml
private string readMessagesFromXml(string xml)
{
StringBuilder sb = new StringBuilder();
try
{
// wrap output to into single root node
var xmlWithRootNode = string.Format("<report>{0}</report>", xml);
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlWithRootNode);
var nodes = doc.SelectNodes("//report/buildresults/message");
if (nodes.Count > 0)
{
foreach (XmlNode node in nodes)
{
sb.AppendLine("<div>" + node.InnerText + "</div>");
}
}
else
{
sb.AppendLine("Xml output does not contain valid data or there are no messages");
}
}
catch (Exception ex)
{
sb.AppendLine("Exception during parsing XML task output. "+ex.ToString());
}
return sb.ToString();
}
}
And then i call it during build:
<execWithOutput>
<executable>NativeUtils.Tests.exe</executable>
<baseDirectory>Q:\software\Platform\</baseDirectory>
<OutputFilePath>Q:\software\NativeUtilsTestReport.txt</OutputFilePath>
<buildTimeoutSeconds>100</buildTimeoutSeconds>
</execWithOutput>