How can I read custom action's executable output in WiX? - wix

I'm making MSI installer in WiX. During installation I want to run an executable from a custom action and get its standard output (not the return code) for later use during installation (with Property element, supposedly).
How can I achieve it in WiX (3.5)?

I used this code for a similar task (its a C# DTF custom action):
// your process data
ProcessStartInfo processInfo = new ProcessStartInfo() {
CreateNoWindow = true,
LoadUserProfile = true,
UseShellExecute = false,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.UTF8,
...
};
Process process = new Process();
process.StartInfo = processInfo;
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (!string.IsNullOrEmpty(e.Data) && session != null)
{
// HERE GOES THE TRICK!
Record record = new Record(1);
record.SetString(1, e.Data);
session.Message(InstallMessage.ActionData, record);
}
};
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
if (process.ExitCode != 0) {
throw new Exception("Execution failed (" + processInfo.FileName + " " + processInfo.Arguments + "). Code: " + process.ExitCode);
}
process.Close();

This is called "screen scraping" and while it's technically possible to create the infrastructure to run an EXE out of process, scrape it's output and then marshal the data back into the MSI context, it'll never be a robust solution.
A better solution would be to understand what the EXE does and how it does it. Then write a C# o C++ custom action that runs in process with access to the MSI handle so that you can do the work and set the properties you need to set.

Related

Is there a way to conect the input/output of a console application to the output/input of another program

I have an application i cannot edit that reads from the console and writes to it and i want to know how i can read what the program is saying and write back commands to the program.
This is for a minecraft server where i want to read what the players are saying and run commands acoording to what is said. (the server is the application i cannot edit)
I cannot create a modification for the server, because i am using a mod that checks if there are any other modifications done to the files and fails to load if that is the case.
I wrote a simple app in c# to get started with, to redirect the I/O streams you need to start the application (in this case the server) within your own application.
First we create a new instance of the System.Diagnostics.Process class
var process = new Process();
then we specify the start info
process.StartInfo = new ProcessStartInfo
{
FileName = Console.ReadLine(), //Reads executable path from console
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false
};
then we add an event handler, in this example it's just writes the lines with a "> " prefix
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => Console.WriteLine($"> {e.Data}");
and now we can start the process by calling Process#Start()
process.Start();
and finally we can call Process#BeginOutputReadLine() without this the OutputDataReceived event will never trigger
process.BeginOutputReadLine();
To send commands you can use the process' StandardInput stream
process.StandardInput.WriteLine("command");
The fully working code with example output (tested with cmd.exe, but it's must work with MC servers)
Code:
static void Main(string[] args)
{
Console.Write("Enter executable path: ");
var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Console.ReadLine(), //Reads executable path, for example cmd is the input
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false
};
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => Console.WriteLine($"> {e.Data}");
process.Start();
process.BeginOutputReadLine();
process.StandardInput.WriteLine("echo a");
//Prevent closing
Console.ReadKey();
}
Output:
Enter executable path: cmd
> Microsoft Windows [Version 10.0.18362.239]
> (c) 2019 Microsoft Corporation. Minden jog fenntartva.
>
> F:\VisualStudio\StackOverflow\StackOverflow\bin\Debug\netcoreapp2.1>echo a
> a
>

How to get local path for payload in WiX/Burn Managed Bootstrapper Application?

I am currently working in a WiX/Burn Managed Bootstrapper Application and cannot figure out how to get the local path for a payload (MSI).
I let the user select which applications they want to install in my custom UI, and I want to not show applications for which the MSI is missing. I also need to see information in the MSI's database.
I know I can determine missing payloads by handling "ResolveSource" but that doesn't happen until right before the application in installed.
I deserialize the BootstrapperApplicationData.xml file first thing so I have information about which MSIs MIGHT be installed, but it still doesn't help me determine the source of the MSIs.
Does anyone know how to determine the local path to a payload?
EDIT: Here is an example for how I reference all the installers:
<MsiPackage Id="AppName"
SourceFile="$(var.ProjectName.TargetDir)ProjectName.msi"
Name="MSI\ProjectName.msi"
Compressed="no"/>
In the GetLastUsedSourceFolder function in cache.cpp, you can see that the engine gets the source folder from the WixBundleLastUsedSource variable, and the parent directory of the WixBundleOriginalSource variable if WixBundleLastUsedSource isn't set.
You can use this along with the Name attribute of the WixPayloadProperties element in the BootstrapperApplicationData.xml file to predetermine where the engine will look for a payload. Note that the engine will actually look in the cache first.
The MSI files are embedded into the bundle .exe and aren't extracted from the bundle until right before the application is installed, which corresponds to when the ResolveSource event fires. However, if you really want to get this information, you can programatically extract the MSI files yourself and inspect them using the WiX DTF library (wix.dll in the /bin folder of your WiX install).
using Microsoft.Tools.WindowsInstallerXml;
private void ExtractEmbeddedMsiInstallers()
{
var tmpFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var bundlePath = Engine.StringVariables["WixBundleOriginalSource"];
Unbinder unbinder = null;
try
{
unbinder = new Unbinder();
//The next line will extract the MSIs into the tmpFolder in a subfolder named "AttachedContainer"
unbinder.Unbind(bundlePath, OutputType.Bundle, tmpFolder);
}
finally
{
if (null != unbinder)
unbinder.DeleteTempFiles();
}
}
You also mentioned needing to inspect data in the MSI database. Here's a sample of how to do that:
using (var database = new InstallPackage(msiFilePath, DatabaseOpenMode.Transact) { WorkingDirectory = _someTempFolder })
{
if (database.Tables.Contains("CustomAction"))
{
using (View view = database.OpenView("SELECT `Action`, `Type`, `Source`, `Target` FROM `CustomAction`"))
{
view.Execute();
foreach (Record rowRecord in view)
using (rowRecord)
{
var actionName = rowRecord.GetString(1);
var actionType = rowRecord.GetInteger(2);
var binaryName = rowRecord.GetString(3);
var methodName = rowRecord.GetString(4);
//Do something with the values
}
}
}
}

Custom Bootstrapper to show only MSI dialogs

I'm using Wix 3.6 and I would like to implement a bootstrapper for my prerequisites and for certain exe's that I need to call during my install.
I know Burn exists and have played around with it, although I don't like the dialog it keeps up even if you show the MSI dialog set.
I currently have my prerequisites and exe files installing with C++ custom actions but I would like my user to have a better installation process and also follow the Windows Installer Best Practices guidelines.
Does anyone know to to make a bootstrapper that shows the MSI dialogs and have any examples?
At first, if you want to follow installer best practices, I wouldn't recommend installing prerequisites using custom actions.
Speaking about your question, here is my implementation of custom bootstrapper in C#:
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace SetupBootstrapper
{
class Program
{
[STAThread]
static void Main(string[] args)
{
var currentDir = AppDomain.CurrentDomain.BaseDirectory;
var parameters = string.Empty;
if (args.Length > 0)
{
var sb = new StringBuilder();
foreach (var arg in args)
{
if (arg != "/i" && arg != "/x" && arg != "/u")
{
sb.Append(" ");
sb.Append(arg);
}
}
parameters = sb.ToString();
}
bool isUninstall = args.Contains("/x") || args.Contains("/u");
string msiPath = Path.Combine(currentDir, "MyMsiName.msi");
if(!File.Exists(msiPath))
{
MessageBox.Show(string.Format("File '{0}' doesn't exist", msiPath), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string installerParameters = (isUninstall ? "/x" : "/i ") + "\"" + msiPath + "\"" + parameters;
var installerProcess = new Process { StartInfo = new ProcessStartInfo("msiexec", installerParameters) { Verb = "runas" } };
installerProcess.Start();
installerProcess.WaitForExit();
}
}
}
To make it ask for elevated permissions I add application manifest file into project with desired access level.
The drawback of implementing bootstrapper in .NET is of course that you can't adequately process situation when .NET is a prerequisite itself and may be absent on target machine.

Check if a file exists in the project in WinRT

I have a WinRT Metro project which displays images based on a selected item. However, some of the images selected will not exist. What I want to be able to do is trap the case where they don't exist and display an alternative.
Here is my code so far:
internal string GetMyImage(string imageDescription)
{
string myImage = string.Format("Assets/MyImages/{0}.jpg", imageDescription.Replace(" ", ""));
// Need to check here if the above asset actually exists
return myImage;
}
Example calls:
GetMyImage("First Picture");
GetMyImage("Second Picture");
So Assets/MyImages/SecondPicture.jpg exists, but Assets/MyImages/FirstPicture.jpg does not.
At first I thought of using the WinRT equivalent of File.Exists(), but there doesn't appear to be one. Without having to go to the extent of trying to open the file and catching an error, can I simply check if either the file exists, or the file exists in the project?
You could use GetFilesAsync from here to enumerate the existing files. This seems to make sense considering you have multiple files which might not exist.
Gets a list of all files in the current folder and its sub-folders. Files are filtered and sorted based on the specified CommonFileQuery.
var folder = await StorageFolder.GetFolderFromPathAsync("Assets/MyImages/");
var files = await folder.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == "fileName");
if (file != null)
{
//do stuff
}
Edit:
As #Filip Skakun pointed out, the resource manager has a resource mapping on which you can call ContainsKey which has the benefit of checking for qualified resources as well (i.e. localized, scaled etc).
Edit 2:
Windows 8.1 introduced a new method for getting files and folders:
var result = await ApplicationData.Current.LocalFolder.TryGetItemAsync("fileName") as IStorageFile;
if (result != null)
//file exists
else
//file doesn't exist
There's two ways you can handle it.
1) Catch the FileNotFoundException when trying to get the file:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
try
{
// Don't forget to decorate your method or event with async when using await
var file = await installedLocation.GetFileAsync(fileName);
// Exception wasn't raised, therefore the file exists
System.Diagnostics.Debug.WriteLine("We have the file!");
}
catch (System.IO.FileNotFoundException fileNotFoundEx)
{
System.Diagnostics.Debug.WriteLine("File doesn't exist. Use default.");
}
catch (Exception ex)
{
// Handle unknown error
}
2) as mydogisbox recommends, using LINQ. Although the method I tested is slightly different:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
var files = await installedLocation.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == fileName);
if (file != null)
{
System.Diagnostics.Debug.WriteLine("We have the file!");
}
else
{
System.Diagnostics.Debug.WriteLine("No File. Use default.");
}
BitmapImage has an ImageFailed event that fires if the image can't be loaded. This would let you try to load the original image, and then react if it's not there.
Of course, this requires that you instantiate the BitmapImage yourself, rather than just build the Uri.
Sample checking for resource availability for c++ /cx (tested with Windows Phone 8.1):
std::wstring resPath = L"Img/my.bmp";
std::wstring resKey = L"Files/" + resPath;
bool exists = Windows::ApplicationModel::Resources::Core::ResourceManager::Current->MainResourceMap->HasKey(ref new Platform::String(resKey.c_str()));

ClickOnce application won't accept command-line arguments

I have a VB.NET application that takes command-line arguments.
It works fine when debugging provided I turn off Visual Studio's ClickOnce security setting.
The problem occurs when I try to install the application on a computer via ClickOnce and try to run it with arguments. I get a crash when that happens (oh noes!).
There is a workaround for this issue: move the files from the latest version's publish folder to a computer's C: drive and remove the ".deploy" from the .exe. Run the application from the C: drive and it will handle arguments just fine.
Is there a better way to get this to work than the workaround I have above?
Thanks!
"Command-line arguments" only work with a ClickOnce app when it is run from a URL.
For example, this is how you should launch your application in order to attach some run-time arguments:
http://myserver/install/MyApplication.application?argument1=value1&argument2=value2
I have the following C# code that I use to parse ClickOnce activation URL's and command-line arguments alike:
public static string[] GetArguments()
{
var commandLineArgs = new List<string>();
string startupUrl = String.Empty;
if (ApplicationDeployment.IsNetworkDeployed &&
ApplicationDeployment.CurrentDeployment.ActivationUri != null)
{
// Add the EXE name at the front
commandLineArgs.Add(Environment.GetCommandLineArgs()[0]);
// Get the query portion of the URI, also decode out any escaped sequences
startupUrl = ApplicationDeployment.CurrentDeployment.ActivationUri.ToString();
var query = ApplicationDeployment.CurrentDeployment.ActivationUri.Query;
if (!string.IsNullOrEmpty(query) && query.StartsWith("?"))
{
// Split by the ampersands, a append a "-" for use with splitting functions
string[] arguments = query.Substring(1).Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(a => String.Format("-{0}", HttpUtility.UrlDecode(a))).ToArray();
// Now add the parsed argument components
commandLineArgs.AddRange(arguments);
}
}
else
{
commandLineArgs = Environment.GetCommandLineArgs().ToList();
}
// Also tack on any activation args at the back
var activationArgs = AppDomain.CurrentDomain.SetupInformation.ActivationArguments;
if (activationArgs != null && activationArgs.ActivationData.EmptyIfNull().Any())
{
commandLineArgs.AddRange(activationArgs.ActivationData.Where(d => d != startupUrl).Select((s, i) => String.Format("-in{1}:\"{0}\"", s, i == 0 ? String.Empty : i.ToString())));
}
return commandLineArgs.ToArray();
}
Such that my main function looks like:
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
var commandLine = GetArguments();
var args = commandLine.ParseArgs();
// Run app
}