I have an installer (.msi) project that uses Wix Toolset v3.14. For some reason it is never up-to-date -- i.e. building it again always produces some activity (C:\Program Files (x86)\WiX Toolset v3.14\bin\Light.exe gets called, but not candle.exe). Is there any way to track down and fix the cause?
Here is what I observe when detailed output is ON:
Target "ReadPreviousBindInputsAndBuiltOutputs" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>" (target "Link" depends on it):
Task "ReadLinesFromFile"
Task Parameter:File=obj\x64\Debug\<my-project>.wixproj.BindContentsFileListen-us.txt
Output Item(s):
_BindInputs=
C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D
...
Done building target "ReadPreviousBindInputsAndBuiltOutputs" in project "<my-project>.wixproj".
Target "Link" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>.wixproj" (target "CompileAndLink" depends on it):
Building target "Link" completely.
Input file "C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D" does not exist.
...
<and here it executes Light.exe>
So, it looks like it reads BindContentsFileListen-us.txt and expects it to contain files that were inputs during last build run. But, unfortunately some of these files were generated in temporary folder and got wiped out (presumably during last build) and since they don't exist anymore -- Link step is re-executed. I observe this pattern every time I press F7, only number in MergeId.418703 gets changed every time (looks like process id to me).
UPDATE: this is a known (and pretty old) issue. As of now it is planned to be fixed in WiX v4.0.
I have hit the same issue, and the only information I found apart from this question was a pretty unhelpful mail thread from 2013 (1, 2) and an issue from the same era.
Troubleshooting
Reading through the logs and Wix's source code shows that the bugs occurs as follows:
light.exe, the linker, receives all of the object (.wixobj) files it should combine, some of them referencing the .msm merge module's file path.
light.exe uses a combination of mergemod.dll's IMsmMerge::ExtractCAB and cabinet.dll's ::FDICopy (through their own winterop.dll) to extract the contents of the merge module to a temporary path:
// Binder.cs:5612, ProcessMergeModules
// extract the module cabinet, then explode all of the files to a temp directory
string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab");
merge.ExtractCAB(moduleCabPath);
string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId);
Directory.CreateDirectory(mergeIdPath);
using (WixExtractCab extractCab = new WixExtractCab())
{
try
{
extractCab.Extract(moduleCabPath, mergeIdPath);
}
// [...]
}
At the same time, the contents of the merge module are inserted among the other input files in the fileRows collection:
// Binder.cs:5517, ProcessMergeModules
// NOTE: this is very tricky - the merge module file rows are not added to the
// file table because they should not be created via idt import. Instead, these
// rows are created by merging in the actual modules
FileRow fileRow = new FileRow(null, this.core.TableDefinitions["File"]);
// [...]
fileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat), Path.DirectorySeparatorChar, record[1]);
FileRow collidingFileRow = fileRows[fileRow.File];
FileRow collidingModuleFileRow = (FileRow)uniqueModuleFileIdentifiers[fileRow.File];
if (null == collidingFileRow && null == collidingModuleFileRow)
{
fileRows.Add(fileRow);
// keep track of file identifiers in this merge module
uniqueModuleFileIdentifiers.Add(fileRow.File, fileRow);
}
// [...]
fileRows ends up being written to the <project_name>BindContentsFileList<culture>.txt file inside the intermediate directory, including the temporary (and randomly named) files extracted from the merge module:
// Binder.cs:7346
private void CreateContentsFile(string path, FileRowCollection fileRows)
{
string directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using (StreamWriter contents = new StreamWriter(path, false))
{
foreach (FileRow fileRow in fileRows)
{
contents.WriteLine(fileRow.Source);
}
}
}
During the next build, the ReadPreviousBindInputsAndBuiltOutputs target from wix2010.targets reads the file into the #(_BindInputs) item group. This item group is then listed as input to the Link target. Since the temporary files have since disappeared, the target is always considered out-of-date and re-run, generating an new set of temporary files that get listed in BindContentsFileList, and so on.
Workaround
An actual fix would be to patch Wix so that merge modules discoverd in .wixobj files are listed in BindContentsFileList, and files extracted from them during linking aren't. Unfortunately I wasn't able to make Wix's source code compile, and can't be bothered to go through its distribution process. Hence, here is the workaround I have implemented.
Removing temporary files from the input list
This is done using a custom target which slots in-between ReadPreviousBindInputsAndBuiltOutputs and Link and filters #(_BindInputs) to remove whatever is under %temp%.
<Target
Name="RemoveTempFilesFromBindInputs"
DependsOnTargets="ReadPreviousBindInputsAndBuiltOutputs"
BeforeTargets="Link"
>
<PropertyGroup>
<!-- This includes a final backslash, so we can use StartsWith. -->
<TemporaryDirectory>$([System.IO.Path]::GetTempPath())</TemporaryDirectory>
</PropertyGroup>
<ItemGroup>
<_BindInputs
Remove="#(_BindInputs)"
Condition="$([System.String]::new('%(FullPath)').StartsWith('$(TemporaryDirectory)'))"
/>
</ItemGroup>
</Target>
At that point, Link triggers only when actual input files change. Success! However, changes to the .msm files are not detected. This might be good enough a solution anyway, since merge modules are generally static. Otherwise...
Detecting changes to merge modules
The main hurdle is that the only reference to the .msm file is within a .wxs source file, so we need to bridge the gap between that and MSBuild. There are a couple ways that can be used, such as parsing the .wixobj to fish out the WixMerge tables. However, I already had code in place to generate Wix code, so I went that way, lifting the merge modules into an MSBuild item group and using a custom task to generate a .wxs file referencing them in a feature. Full code below:
<Target
Name="GenerateMsmFragment"
BeforeTargets="GenerateCompileWithObjectPath"
Inputs="#(MsmFiles)"
Outputs="$(IntermediateOutputPath)MsmFiles.wxs"
>
<GenerateMsmFragment
MsmFiles="#(MsmFiles)"
FeatureName="MsmFiles"
MediaId="2"
OutputFile="$(IntermediateOutputPath)MsmFiles.wxs"
>
<Output TaskParameter="OutputFile" ItemName="Compile" />
</GenerateMsmFragment>
</Target>
// GenerateMsmFragment.cs
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace tasks
{
[ComVisible(false)]
public class GenerateMsmFragment : Task
{
[Required]
public ITaskItem[] MsmFiles { get; set; }
[Required]
public string FeatureName { get; set; }
[Required]
public string MediaId { get; set; }
[Output]
public ITaskItem OutputFile { get; set; }
public override bool Execute()
{
var xmlns = "http://schemas.microsoft.com/wix/2006/wi";
var outputXml = new XmlDocument();
outputXml.AppendChild(outputXml.CreateXmlDeclaration("1.0", "utf-8", null));
var fragmentElem = outputXml
.AppendElement("Wix", xmlns)
.AppendElement("Fragment", xmlns);
{
var mediaElem = fragmentElem.AppendElement("Media", xmlns);
mediaElem.SetAttribute("Id", MediaId);
mediaElem.SetAttribute("Cabinet", "MsmFiles.cab");
mediaElem.SetAttribute("EmbedCab", "yes");
}
{
var directoryRefElem = fragmentElem.AppendElement("DirectoryRef", xmlns);
directoryRefElem.SetAttribute("Id", "TARGETDIR");
var featureElem = fragmentElem.AppendElement("Feature", xmlns);
featureElem.SetAttribute("Id", FeatureName);
featureElem.SetAttribute("Title", "Imported MSM files");
featureElem.SetAttribute("AllowAdvertise", "no");
featureElem.SetAttribute("Display", "hidden");
featureElem.SetAttribute("Level", "1");
foreach (var msmFilePath in MsmFiles.Select(i => i.ItemSpec)) {
var mergeElem = directoryRefElem.AppendElement("Merge", xmlns);
mergeElem.SetAttribute("Id", msmFilePath);
mergeElem.SetAttribute("SourceFile", msmFilePath);
mergeElem.SetAttribute("DiskId", MediaId);
mergeElem.SetAttribute("Language", "0");
featureElem
.AppendElement("MergeRef", xmlns)
.SetAttribute("Id", msmFilePath);
}
}
Directory.CreateDirectory(Path.GetDirectoryName(OutputFile.GetMetadata("FullPath")));
outputXml.Save(OutputFile.GetMetadata("FullPath"));
return true;
}
}
}
// XmlExt.cs
using System.Xml;
namespace nrm
{
public static class XmlExt
{
public static XmlElement AppendElement(this XmlDocument element, string qualifiedName, string namespaceURI)
{
var newElement = element.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
public static XmlElement AppendElement(this XmlNode element, string qualifiedName, string namespaceURI)
{
var newElement = element.OwnerDocument.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
}
}
And voilĂ , working up-to-date detection for merge modules.
For the purpose of making a backup, I copy a directory with tests scripts to another location. For this I'm using Files.walkFileTree(fromDir, SimpleFileVisitor).
This works as expected and I can copy it multiple times without any problems.
But when I delete the target directory via Windows File explorer I get a java.nio.file.FileAlreadyExistsException on the target directory when I try to make a back up.
I'm pretty sure that the target directory is not there.
When I close the Java program and start it again, it works again. It is as if manually deleting the directory locks the 'non-existing' target directory and Java throws a FileAlreadyExistsException.
Here are the relevant parts of the SimpleFileVisitor
I'm using these options when copying the files
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES, LinkOption.NOFOLLOW_LINKS
#Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
Path copyDir = toDir.resolve(fromDir.relativize(dir));
try {
Files.createDirectories(copyDir);
}
catch(IOException ioe) {
LOG.error("Failed to create directory " + copyDir.toString());
throw ioe;
}
return FileVisitResult.CONTINUE;
}
#Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
Path toFile = toDir.resolve(fromDir.relativize(file));
try {
Files.copy(file, toFile, opts);
}
catch(IOException ioe) {
LOG.error("Failed to create file " + toFile.toString());
throw ioe;
}
return FileVisitResult.CONTINUE;
}
I was expecting that deleting the target directory via Windows File Explorer would not cause this problem, but apparently it does.
Is there anything I could do to make the copy action possible in case someone deletes the directory via Windows File Explorer.
In the Visual Studio Resources area, is there a way to set the default location in the file select dialog, when selecting "Add Existing File..."?
The default location, when clicking on that menu is:
C:\Program Files (x86)\Microsoft\Visual Studio\2017\Enterprise\Common7\IDE
True, it is a small thing, as selecting my Downloads folder or something else is easy, but I keep having to go somewhere else, usually my Downloads folder, and it would definitely save time to have the default location go somewhere else.
I know that the Open File common dialog, which Visual Studio uses, has the option in the initial structure to specify the initial (default) directory, so the only question is, if it is possible for the default directory to point somewhere else.
I am using Visual Studio 2017.
You can use my Visual Commander extension to monitor for the Resources.AddExistingFile command and set your preferred directory before its execution. See the following C# extension example:
public class E : VisualCommanderExt.IExtension
{
public void SetSite(EnvDTE80.DTE2 DTE, Microsoft.VisualStudio.Shell.Package package)
{
events = DTE.Events;
commandEvents = events.get_CommandEvents(null, 0);
commands = DTE.Commands as EnvDTE80.Commands2;
commandEvents.BeforeExecute += OnBeforeExecute;
}
public void Close()
{
commandEvents.BeforeExecute -= OnBeforeExecute;
}
private void OnBeforeExecute(string Guid, int ID, object CustomIn, object CustomOut, ref bool CancelDefault)
{
string name = GetCommandName(Guid, ID);
if (name == "Resources.AddExistingFile")
System.IO.Directory.SetCurrentDirectory(#"c:\downloads");
}
private string GetCommandName(string Guid, int ID)
{
if (Guid == null)
return "null";
try
{
return commands.Item(Guid, ID).Name;
}
catch (System.Exception)
{
}
return "";
}
private EnvDTE.Events events;
private EnvDTE.CommandEvents commandEvents;
private EnvDTE80.Commands2 commands;
}
Is it possible to exit msi installar from a custom action without errors?
I am using visual studio setup project to create the msi and I have added a custom action exe to install. If I return a value other than 0 from exe it terminate the installation. But it shows an error. I need to exit installation without showing errors.
Thanks.
Finally I found a way to do it using a vbscript custom action from http://chensuping.blogspot.com/2013/05/windows-setup-project-vbs-custom-action.html.
Try this code in your installer class. I hope it will resolve your problem.
protected override void OnBeforeInstall(IDictionary savedState)
{
if (LaunchOnBeforeInstall())
{
base.OnBeforeInstall(savedState);
}
else
{
throw new Exception("You cancelled installation");
}
}
public bool LaunchOnBeforeInstall()
{
Form2 frm2 = new Form2();
DialogResult result = frm2.ShowDialog();
if (result == DialogResult.Cancel)
{
return false;
}
else
{
return true;
}
}
And also put "NOTPREVIOUSVERSIONSINSTALLED"
I have searched numerous articles and tried several different ways of solving this issue.
Note that this process works fine in a Console app or a Windows app with the exact same code. So, there's nothing wrong with the logic. It's the way the Windows Service is processing these files that is somehow not letting subsequent files get through.
I tried reading a simple article to actually debug the service, but I don't have the "Attach To Process" selection under my Debug menu using Visual Studio 2010.
http://msdn.microsoft.com/en-us/library/7a50syb3(v=vs.100).aspx
I really need some help here, because my options have really run out.
Here is the simple scenario:
I have a FileSystemWatcher in the OnStart method that kicks off another method to process a file on my local machine. These are just simple txt files. When the first file is processed, everything is normal and I get the following in my Event log:
Data successfully inserted for file 20121212_AZM_Journey_MIS.txt
When I try and process subsequent files (exact same format, but a different name --- with the date), I get the following in my Event log:
The process cannot access the file 'C:\Projects\Data\VendingStats\20121213_AZM_Journey_MIS.txt' because it is being used by another process.
Note that the files are watched for in this same directory when created.
Here is my code in my Windows Service. Can someone please let me know how I can solve this issue? Note that e.FullPath evaluates to "C:\Projects\Data\VendingStats" on my local machine.
public partial class VendingStatsService : ServiceBase
{
public VendingStatsService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
FileSystemWatcher Watcher = new FileSystemWatcher(#"C:\Projects\Data\VendingStats");
Watcher.EnableRaisingEvents = true;
Watcher.Created += new FileSystemEventHandler(**Watcher_Created**);
Watcher.Filter = "*.txt";
Watcher.IncludeSubdirectories = false;
}
private void Watcher_Created(object sender, FileSystemEventArgs e)
{
try
{
using (TextReader tr = new StreamReader(e.FullPath))
{
// code here to insert data from file into a DB
}
Dispose();
}
catch (Exception ex)
{
EventLog.WriteEntry("VendorStats", "Error in the Main:" + "\r\n\r\n" + ex.Message + "\r\n\r\n" + ex.InnerException);
return;
}
}
public void Dispose()
{
GC.Collect();
}