I am developing an installation msi file using Wix toolset (in VS 2012) and faced with the problem that I can't add a confirmation dialog when user press button 'Cancel'. I tried to add also custom action dll with the message box, but if the function returns ActionResult.Failure, then my setup shows error dialog.
Original control:
<Control Type="PushButton" Id="progressCancelBtn" Width="56" Height="17" X="296" Y="244">
<Text>Cancel</Text>
<Publish Event="EndDialog" Value="Exit" />
</Control>
Try number 1:
public class CustomActions
{
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
DialogResult result = MessageBox.Show("Exit installation?", "Exit", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
{
return ActionResult.Failure;
}
else
{
return ActionResult.Success;
}
}
}
Custom action:
<CustomAction Id='ShowExitMsgBox' BinaryKey='CustomActions' DllEntry='ShowExitMessageBox' Execute='immediate' Return='ignore'/>
<Binary Id='CustomActions' SourceFile='$(var.CASourcesPath)\CustomActions.CA.dll'/>
And tried to use:
<Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
<Text>Cancel</Text>
<Publish Event="DoAction" Value="ShowExitMsgBox">1</Publish>
<!--Publish Event="EndDialog" Value="Exit" /-->
</Control>
But it shows like failure, instead of cancellation.
Thank you for help in advance.
Update:
I changed my custom action C# code to the next:
public class CustomActions
{
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
DialogResult result = MessageBox.Show("Exit installation?", "Exit", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
{
session["CANCELEDPROP"] = "1";
}
else
{
session["CANCELEDPROP"] = "0";
}
return ActionResult.Success;
}
}
and WIX part:
// in Product.wxs
<Property Id="CANCELEDPROP" Secure="yes">0</Property>
// in UIFile.wxs
<Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
<Text>Cancel</Text>
<Publish Event="DoAction" Value="ShowExitMsgBox">1</Publish>
<Publish Event="EndDialog" Value="Exit">CANCELEDPROP=1</Publish>
</Control>
It works on the first page, but I get an error on progress page: "Windows installer has stopped working" after I pressed some button on my message box. :(
I have found out if I don't use message box, then everything works. It is strange.
Update 2:
I tried to use session.Message instead of Windows.Forms, but message box just doesn't appear. Strange.
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
Record record = new Record();
record.FormatString = "Exit installation?";
MessageResult result = session.Message(InstallMessage.Warning
| (InstallMessage)System.Windows.Forms.MessageBoxIcon.Warning
| (InstallMessage)System.Windows.Forms.MessageBoxButtons.YesNo,
record);
if (result == MessageResult.Yes)
{
session["CANCELEDPROP"] = "1";
}
else
{
session["CANCELEDPROP"] = "0";
}
return ActionResult.Success;
}
Finally I have found a solution by using CancelDlg from WixUIExtension.dll
(http://wixtoolset.org/documentation/manual/v3/wixui/dialog_reference/wixui_dialogs.html)
Add reference to WixUIExtension.dll (path on my computer is the next C:\Program Files (x86)\WiX Toolset v3.11\bin)
Add reference to icon that would be on the message box (Product.wxs):
< Binary Id="WixUI_Ico_Info" SourceFile="Images/MyIcon.ico" />
Control itself:
< Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
< Text>Cancel
< Publish Event="SpawnDialog" Value="CancelDlg">1
< /Control>
Related
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="MyApp" Language="1033" Version="1.0.0.0" Manufacturer="MyAppDev" UpgradeCode="067ac37f-0d36-4173-a24a-5037927bd6da">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<Feature Id="ProductFeature" Title="MyApp" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="MyApp" />
</Directory>
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
<!-- <Component Id="ProductComponent"> -->
<!-- TODO: Insert files, registry keys, and other resources here. -->
<!-- </Component> -->
<Component>
<File Source="$(var.MyApp.TargetPath)" />
</Component>
<Component>
<File Id="Postcodes.txt" Source="C:\Project\MyApp\MyApp\Files\Postcodes.txt" KeyPath="yes" />
</Component>
</ComponentGroup>
<Feature Id="MainApplication" Title="Main Application" Level="1">
<ComponentRef Id="Postcodes.txt" />
</Feature>
</Fragment>
</Wix>
My Windows form application using a text file which is stored in application directory. And with the help of WIX toolset. I have created an installer But the problem is content text file not exist. That's why i am getting File not found exception.
Please help me, How can i add this content text file to WIX installer? Or what would i need to add "Product.wxs" file?
You need to add a custom action. This is a direct copy of my work minus the company name that it was produced for.
<CustomAction Id="RestAction" BinaryKey="myCompanyAgentSetup.WixExtension.Package.dll" DllEntry="Execute" Execute="immediate" Return="check" />
This is a class file \ dll that is created in the Wix project. This is my actual dll \ class file
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Deployment.WindowsInstaller;
namespace myCompanyAgentSetup.WixExtension
{
public static class myCompanyAgentSetupWixExtension
{
[CustomAction]
public static ActionResult Execute(Session session)
{
var errorMsg = string.Empty;
var record = new Record();
var token = Environment.GetEnvironmentVariable("RX_JOB_NO");
var restUser = session["RESTUSER"];
var restPass = session["RESTPASS"];
var restUrl = string.Format(session["RESTURL"], token);
var request = (HttpWebRequest)WebRequest.Create(restUrl);
var encoded = Convert.ToBase64String(Encoding.Default.GetBytes(restUser + ":" + restPass));
request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + encoded);
request.Credentials = new NetworkCredential(restUser, restPass);
Console.WriteLine("attempting to get API Key");
try
{
var response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode.ToString() != "OK")
{
record = new Record
{
FormatString = string.Format(response.StatusDescription)
};
session.Message(InstallMessage.Error, record);
Console.WriteLine("Unable to get API Key");
Console.WriteLine("Adding RX_CLOUDBACKUP_API Environment Variable with no value");
UpdateConfigFiles("");
}
else
{
var apiKey = new StreamReader(response.GetResponseStream()).ReadToEnd();
if (apiKey.Contains("Error"))
{
record = new Record
{
FormatString = string.Format(apiKey)
};
session.Message(InstallMessage.Error, record);
session.Message(InstallMessage.Terminate, record);
}
Console.WriteLine("Adding RX_CLOUDBACKUP_API with value - " + apiKey);
UpdateConfigFiles(apiKey);
return ActionResult.Success;
}
}
catch (Exception e)
{
record = new Record
{
FormatString = string.Format(e.Message)
};
session.Message(InstallMessage.Error, record);
session.Message(InstallMessage.Terminate, record);
}
//An error has occurred, set the exception property and return failure.
session.Log(errorMsg);
session["CA_ERRORMESSAGE"] = errorMsg;
record = new Record
{
FormatString = string.Format("Something has gone wrong!")
};
session.Message(InstallMessage.Error, record);
session.Message(InstallMessage.Terminate, record);
return ActionResult.Failure;
}
private static void UpdateConfigFiles(string apiKey)
{
if (!string.IsNullOrEmpty(apiKey))
{
Environment.SetEnvironmentVariable("RX_CLOUDBACKUP_API", null, EnvironmentVariableTarget.Machine);
Environment.SetEnvironmentVariable("RX_CLOUDBACKUP_API", apiKey, EnvironmentVariableTarget.Machine);
}
else
{
Environment.SetEnvironmentVariable("RX_CLOUDBACKUP_API", "", EnvironmentVariableTarget.Machine);
}
}
}
}
Hopefully this will get you going. If you need anything else let me know
I have written a simple Installer that gets the value of property when I gave using following command:
msiexec /i file.msi /l*v output.txt IPADDRESS="192.168.2.1"
I extracted the value of IPADDRESS in C# Custom action and created two folder output and config. In config, I write the content of IPADDRESS and output is for logging. Here's my C# code:
namespace SetupCA
{
public class CustomActions
{
[CustomAction]
public static ActionResult WriteFileToDisk(Session session)
{
session.Log("Begin WriteFileToDisk");
string ipAddress = session["IPADDRESS"];
string path = session["LocalAppDataFolder"]; //With trailing slash
path = path.Replace(#"\", #"\\").ToString();
string log_path = path + #"lpa\\output\\";
string config_path = path + #"lpa\\config\\";
session.Log("Local App Data Modified Path is: " + path.ToString());
session.Log("Logging Folder Path is: " + log_path.ToString());
string temp = #"
{{
""logpoint_ip"" : ""{0}""
}}";
string config = string.Format(temp, ipAddress);
session.Log("Config Generated from property is: " + config);
System.IO.Directory.CreateDirectory(config_path);
try
{
System.IO.File.Delete(path + "lpa.config");
}
catch (Exception e)
{
session.Log(e.ToString());
}
System.IO.File.WriteAllText(config_path + "lpa.config", config);
session.Log("Confile file is written");
System.IO.Directory.CreateDirectory(log_path);
session.Log("Logging Folder is Created");
return ActionResult.Success;
}
}
}
Now I have created a Visual C++ application that checks if the program has been registered during startup or not. If not, it adds the exe file in Registry and enters the Infinite loop. If I run the installed exe, it appears in command window. What I want is to run the exe in background and user could view the exe inside Process in Task Manager. I don't want to disturb user by showing a blank terminal window that seems to do nothing. Can this be done in Wix or should I change my code?
I have attached my C++ code and Wix File.
CODE
#include <iostream>
#include <Windows.h>
#include <ShlObj.h>
#include <log4cplus/logger.h>
#include <log4cplus/fileappender.h>
#include <log4cplus/layout.h>
#include <log4cplus/ndc.h>
#include <log4cplus/helpers/loglog.h>
#include <log4cplus/loggingmacros.h>
#include <boost\lexical_cast.hpp>
#include <boost\algorithm\string\replace.hpp>
using namespace log4cplus;
Logger root;
std::string GetLocalAppDataPath();
void LoggingInit();
void LoggingInit()
{
log4cplus::initialize ();
helpers::LogLog::getLogLog()->setInternalDebugging(false);
std::string local_path = GetLocalAppDataPath();
local_path = local_path + "\\lpa\\output\\";
SharedAppenderPtr append_1(new RollingFileAppender(LOG4CPLUS_TEXT( local_path + "outputgen.log"), 10*1024*1024, 5));
append_1->setName(LOG4CPLUS_TEXT("LogpointAgentLog"));
PatternLayout *p = new PatternLayout(LOG4CPLUS_TEXT("[%D] <%-5p> [%F : %L] %m%n"));
append_1->setLayout(std::auto_ptr<Layout>(p));
Logger::getRoot().addAppender(append_1);
root = Logger::getRoot();
}
std::string GetLocalAppDataPath()
{
HANDLE hfile;
TCHAR szPath[MAX_PATH];
if(SUCCEEDED(SHGetFolderPath(NULL,CSIDL_LOCAL_APPDATA,NULL,0, szPath)))
{
std::string path = boost::lexical_cast<std::string>(szPath);
boost::replace_all(path, "\\", "\\\\");
return path;
}
}
BOOL IsMyProgramRegisteredForStartup(PCWSTR pszAppName)
{
HKEY hKey = NULL;
LONG lResult = 0;
BOOL fSuccess = TRUE;
DWORD dwRegType = REG_SZ;
wchar_t szPathToExe[MAX_PATH] = {};
DWORD dwSize = sizeof(szPathToExe);
lResult = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_READ, &hKey);
fSuccess = (lResult == 0);
if (fSuccess)
{
lResult = RegGetValueW(hKey, NULL, pszAppName, RRF_RT_REG_SZ, &dwRegType, szPathToExe, &dwSize);
fSuccess = (lResult == 0);
}
if (fSuccess)
{
fSuccess = (wcslen(szPathToExe) > 0) ? TRUE : FALSE;
}
if (hKey != NULL)
{
RegCloseKey(hKey);
hKey = NULL;
}
return fSuccess;
}
BOOL RegisterMyProgramForStartup(PCWSTR pszAppName, PCWSTR pathToExe)
{
HKEY hKey = NULL;
LONG lResult = 0;
BOOL fSuccess = TRUE;
DWORD dwSize;
const size_t count = MAX_PATH*2;
wchar_t szValue[count] = {};
wcscpy_s(szValue, count, L"\"");
wcscat_s(szValue, count, pathToExe);
wcscat_s(szValue, count, L"\" ");
lResult = RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, NULL, 0, (KEY_WRITE | KEY_READ), NULL, &hKey, NULL);
fSuccess = (lResult == 0);
if (fSuccess)
{
dwSize = (wcslen(szValue)+1)*2;
lResult = RegSetValueExW(hKey, pszAppName, 0, REG_SZ, (BYTE*)szValue, dwSize);
fSuccess = (lResult == 0);
}
if (hKey != NULL)
{
RegCloseKey(hKey);
hKey = NULL;
}
return fSuccess;
}
int main()
{
//std::string loc = GetLocalAppDataPath(); //Without trailing slashes
LoggingInit();
if(IsMyProgramRegisteredForStartup(L"My_Program"))
{
//do nothing
}
else
{
LOG4CPLUS_INFO(root, "Starting Starup App");
wchar_t szPathToExe[MAX_PATH];
GetModuleFileNameW(NULL, szPathToExe, MAX_PATH);
RegisterMyProgramForStartup(L"My_Program", szPathToExe);
LOG4CPLUS_INFO(root, "Ending Starup App");
}
LOG4CPLUS_INFO(root, "BEFORE INFINITE LOOP #######################");
while(1)
{
LOG4CPLUS_INFO(root, "INSIDE THE WHILE LOOOOOOPPPP");
Sleep(5000);
}
return 0;
}
WIX FILE
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="InstallerForStartupCPP" Language="1033" Version="1.0.0.0" Manufacturer="LPAA" UpgradeCode="70510e56-b6ab-4e6f-beb6-40bb2e30c568">
<Package InstallerVersion="200" Compressed="no" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<Feature Id="ProductFeature" Title="InstallerForStartupCPP" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="Startup CPP" />
</Directory>
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<Component Id="ProductComponent">
<File
Id="STARTUPCPPINSTALLER"
Name="StartupCPPInstaller.exe"
DiskId="1"
Source="$(var.StartupCPPInstaller.TargetPath)"
Vital="yes"
KeyPath="yes" />
</Component>
<Component Id="log4cplus">
<File Source ="G:\SarVaGYa\myworkspace\LatestLpa\lpa\lpa_c\ext_library\log4cplus\bin\Release\log4cplus.dll" />
</Component>
</ComponentGroup>
<Binary Id="SetupCA" SourceFile="..\SetupCA\bin\Release\SetupCA.CA.dll"/>
<CustomAction Id="WRITEFILETODISK" Execute="immediate" BinaryKey="SetupCA" DllEntry="WriteFileToDisk" />
<InstallExecuteSequence>
<Custom Action="WRITEFILETODISK" Sequence="2"></Custom>
</InstallExecuteSequence>
</Fragment>
</Wix>
How am I supposed to install the MSI file and run the program in background.? Please help.
PS: I have tried making a service. I do not get GetLocalAppDataPath true value if I run the program as service.
The straightforward way would be to change C++ application subsystem type to windows in project settings: Linker->System->SubSystem and replace main function with WinMain - it will not create any windows itself, you can ignore its parameters and do what you currently do in main.
All "RegisterMyProgramForStartup" is doing is writing a registry value. This is basic Windows Installer 101 functionality exposed by the RegistryValue element. All of this custom action code is an antipattern.
I have a custom action that shows list of application pools in the system and want to bind these values into combobox which is inside customdlg.wxs and while executing MSI setup I should get a combobox that show list of app pools.
Can anyone guide me how to bind values to combobox?
custom action
//List all App pools
public static ActionResult GetAppPools(Session session)
{
using (ServerManager iisMgr=new ServerManager())
{
System.Collections.IEnumerator ie = iisMgr.ApplicationPools.GetEnumerator();
List<string> apppools = new List<string>();
while (ie.MoveNext())
{
apppools.Add(((Microsoft.Web.Administration.ApplicationPool)(ie.Current)).Name);
}
}
return ActionResult.Success;
}
customdlg.wxs
<Control Id="ApplicationPoolLabel" Type="Text" Y="94" X="11" Width="349" Height="16" TabSkip="no" Text="Application Pool: " />
<Control Id="ApplicationPoolCombo" Type="ComboBox" Y="94" X="100" Width="150" Height="16" ToolTip="Application Pool" Property="APP_POOL" Text="{80}">
<ComboBox Property="APP_POOL">
<ListItem Text="[APP_POOL]" Value="[APP_POOL]" />
</ComboBox>
</Control>
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.
I am creating a MSI based Installer using Wix.
My Custom Action declaration goes like this...
<Binary Id="CustomActions" SourceFile="DLLs\CustomActions.CA.dll" />
<CustomAction Id="CheckPath" Return="check" Execute="immediate" BinaryKey="CustomActions" DllEntry="CheckPath" />
And under WixUI_InstallDir Dialog UI,
<UI Id="WixUI_InstallDir">
.....
<Publish Dialog="SelectDirDlg" Control="Next" Event="DoAction" Value="CheckPath" Order="2">1</Publish>
.....
</UI>
And in C# file,
[CustomAction]
public static ActionResult CheckPath(Session session)
{
Record record2 = new Record();
record.FormatString = "The path that you have selected is invalid!";
session.Message(InstallMessage.Error | (InstallMessage)MessageButtons.OK, record);
return ActionResult.Success;
}
I am expecting a Message box via the above Custom Action when the user selects an invalid path. But the message box is not shown.
What am I doing wrong?
Custom actions triggered via a DoAction control event cannot show message boxes. See http://msdn.microsoft.com/en-us/library/windows/desktop/aa368322%28v=vs.85%29.aspx.