From 3f583916719eeef598d10a5d4e14ef14f008243b Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 11 May 2021 07:36:37 -0700 Subject: Merge Dtf --- src/samples/Dtf/DDiff/CabDiffEngine.cs | 131 +++ src/samples/Dtf/DDiff/DDiff.cs | 72 ++ src/samples/Dtf/DDiff/DDiff.csproj | 39 + src/samples/Dtf/DDiff/DirectoryDiffEngine.cs | 154 +++ src/samples/Dtf/DDiff/FileDiffEngine.cs | 83 ++ src/samples/Dtf/DDiff/IDiffEngine.cs | 68 ++ src/samples/Dtf/DDiff/MsiDiffEngine.cs | 276 +++++ src/samples/Dtf/DDiff/MspDiffEngine.cs | 127 ++ src/samples/Dtf/DDiff/TextFileDiffEngine.cs | 83 ++ src/samples/Dtf/DDiff/VersionedFileDiffEngine.cs | 90 ++ src/samples/Dtf/Documents/Guide/Content/about.htm | 59 + .../Dtf/Documents/Guide/Content/buildingcas.htm | 94 ++ .../Dtf/Documents/Guide/Content/cabpack.htm | 63 + src/samples/Dtf/Documents/Guide/Content/cabs.htm | 101 ++ .../Dtf/Documents/Guide/Content/cabwrapper.htm | 63 + .../Dtf/Documents/Guide/Content/caconfig.htm | 83 ++ .../Dtf/Documents/Guide/Content/caproxy.htm | 74 ++ .../Dtf/Documents/Guide/Content/databases.htm | 120 ++ .../Dtf/Documents/Guide/Content/debuggingcas.htm | 66 ++ .../Dtf/Documents/Guide/Content/dependencies.htm | 88 ++ .../Documents/Guide/Content/filepatchwrapper.htm | 34 + .../Dtf/Documents/Guide/Content/history.htm | 437 +++++++ .../Dtf/Documents/Guide/Content/installutil.htm | 94 ++ .../Dtf/Documents/Guide/Content/inventory.htm | 78 ++ .../Dtf/Documents/Guide/Content/managedcas.htm | 53 + .../Dtf/Documents/Guide/Content/msihelper.htm | 59 + .../Dtf/Documents/Guide/Content/msiwrapper.htm | 80 ++ .../Dtf/Documents/Guide/Content/packages.htm | 86 ++ .../Dtf/Documents/Guide/Content/powerdiff.htm | 71 ++ .../Dtf/Documents/Guide/Content/samplecas.htm | 84 ++ .../Dtf/Documents/Guide/Content/samples.htm | 59 + .../Dtf/Documents/Guide/Content/support.htm | 52 + src/samples/Dtf/Documents/Guide/Content/using.htm | 50 + .../Dtf/Documents/Guide/Content/whatsnew.htm | 257 ++++ src/samples/Dtf/Documents/Guide/Content/wifile.htm | 73 ++ .../Dtf/Documents/Guide/Content/writingcas.htm | 114 ++ src/samples/Dtf/Documents/Guide/DTF.hhc | 132 +++ src/samples/Dtf/Documents/Guide/DTF.hhk | 126 ++ src/samples/Dtf/Documents/Guide/DTF.hhp | 49 + src/samples/Dtf/Documents/Guide/dtfguide.helpproj | 29 + .../Dtf/Documents/Guide/styles/presentation.css | 394 +++++++ .../Dtf/Documents/Reference/Compression.htm | 13 + .../Dtf/Documents/Reference/Compression1.png | Bin 0 -> 79032 bytes .../Dtf/Documents/Reference/Compression2.png | Bin 0 -> 68312 bytes .../Dtf/Documents/Reference/WindowsInstaller.htm | 14 + .../Dtf/Documents/Reference/WindowsInstaller1.png | Bin 0 -> 207803 bytes .../Dtf/Documents/Reference/WindowsInstaller2.png | Bin 0 -> 180714 bytes .../Dtf/Documents/Reference/WindowsInstaller3.png | Bin 0 -> 120423 bytes .../Dtf/Documents/Reference/dtfref.shfbproj | 75 ++ src/samples/Dtf/Documents/Reference/helplink.js | 184 +++ src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs | 5 + src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj | 56 + .../Dtf/EmbeddedUI/InstallProgressCounter.cs | 176 +++ src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs | 132 +++ src/samples/Dtf/EmbeddedUI/SetupWizard.xaml | 17 + src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs | 111 ++ src/samples/Dtf/Inventory/Columns.resx | 252 ++++ src/samples/Dtf/Inventory/Features.cs | 107 ++ .../Dtf/Inventory/IInventoryDataProvider.cs | 67 ++ src/samples/Dtf/Inventory/Inventory.cs | 1231 ++++++++++++++++++++ src/samples/Dtf/Inventory/Inventory.csproj | 42 + src/samples/Dtf/Inventory/Inventory.ico | Bin 0 -> 4710 bytes src/samples/Dtf/Inventory/Inventory.resx | 265 +++++ src/samples/Dtf/Inventory/components.cs | 626 ++++++++++ src/samples/Dtf/Inventory/msiutils.cs | 46 + src/samples/Dtf/Inventory/patches.cs | 227 ++++ src/samples/Dtf/Inventory/products.cs | 145 +++ src/samples/Dtf/Inventory/xp.manifest | 15 + src/samples/Dtf/ManagedCA/AssemblyInfo.cs | 5 + src/samples/Dtf/ManagedCA/ManagedCA.csproj | 33 + src/samples/Dtf/ManagedCA/SampleCAs.cs | 127 ++ src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.cs | 711 +++++++++++ src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.csproj | 33 + .../Dtf/Tools/MakeSfxCA/MakeSfxCA.exe.manifest | 20 + src/samples/Dtf/Tools/MakeSfxCA/app.config | 10 + src/samples/Dtf/Tools/SfxCA/ClrHost.cpp | 262 +++++ src/samples/Dtf/Tools/SfxCA/EmbeddedUI.cpp | 281 +++++ src/samples/Dtf/Tools/SfxCA/EntryPoints.def | 140 +++ src/samples/Dtf/Tools/SfxCA/EntryPoints.h | 162 +++ src/samples/Dtf/Tools/SfxCA/Extract.cpp | 282 +++++ src/samples/Dtf/Tools/SfxCA/RemoteMsi.cpp | 629 ++++++++++ src/samples/Dtf/Tools/SfxCA/RemoteMsiSession.h | 898 ++++++++++++++ src/samples/Dtf/Tools/SfxCA/SfxCA.cpp | 363 ++++++ src/samples/Dtf/Tools/SfxCA/SfxCA.rc | 10 + src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj | 68 ++ src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj.filters | 62 + src/samples/Dtf/Tools/SfxCA/SfxUtil.cpp | 209 ++++ src/samples/Dtf/Tools/SfxCA/SfxUtil.h | 31 + src/samples/Dtf/Tools/SfxCA/packages.config | 4 + src/samples/Dtf/Tools/SfxCA/precomp.cpp | 3 + src/samples/Dtf/Tools/SfxCA/precomp.h | 18 + src/samples/Dtf/Tools/Tools.proj | 15 + src/samples/Dtf/WiFile/WiFile.cs | 147 +++ src/samples/Dtf/WiFile/WiFile.csproj | 27 + src/samples/Dtf/XPack/AssemblyInfo.cs | 5 + src/samples/Dtf/XPack/XPack.cs | 80 ++ src/samples/Dtf/XPack/XPack.csproj | 27 + 97 files changed, 12813 insertions(+) create mode 100644 src/samples/Dtf/DDiff/CabDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/DDiff.cs create mode 100644 src/samples/Dtf/DDiff/DDiff.csproj create mode 100644 src/samples/Dtf/DDiff/DirectoryDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/FileDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/IDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/MsiDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/MspDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/TextFileDiffEngine.cs create mode 100644 src/samples/Dtf/DDiff/VersionedFileDiffEngine.cs create mode 100644 src/samples/Dtf/Documents/Guide/Content/about.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/buildingcas.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/cabpack.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/cabs.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/cabwrapper.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/caconfig.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/caproxy.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/databases.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/debuggingcas.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/dependencies.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/filepatchwrapper.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/history.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/installutil.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/inventory.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/managedcas.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/msihelper.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/msiwrapper.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/packages.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/powerdiff.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/samplecas.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/samples.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/support.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/using.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/whatsnew.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/wifile.htm create mode 100644 src/samples/Dtf/Documents/Guide/Content/writingcas.htm create mode 100644 src/samples/Dtf/Documents/Guide/DTF.hhc create mode 100644 src/samples/Dtf/Documents/Guide/DTF.hhk create mode 100644 src/samples/Dtf/Documents/Guide/DTF.hhp create mode 100644 src/samples/Dtf/Documents/Guide/dtfguide.helpproj create mode 100644 src/samples/Dtf/Documents/Guide/styles/presentation.css create mode 100644 src/samples/Dtf/Documents/Reference/Compression.htm create mode 100644 src/samples/Dtf/Documents/Reference/Compression1.png create mode 100644 src/samples/Dtf/Documents/Reference/Compression2.png create mode 100644 src/samples/Dtf/Documents/Reference/WindowsInstaller.htm create mode 100644 src/samples/Dtf/Documents/Reference/WindowsInstaller1.png create mode 100644 src/samples/Dtf/Documents/Reference/WindowsInstaller2.png create mode 100644 src/samples/Dtf/Documents/Reference/WindowsInstaller3.png create mode 100644 src/samples/Dtf/Documents/Reference/dtfref.shfbproj create mode 100644 src/samples/Dtf/Documents/Reference/helplink.js create mode 100644 src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs create mode 100644 src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj create mode 100644 src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs create mode 100644 src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs create mode 100644 src/samples/Dtf/EmbeddedUI/SetupWizard.xaml create mode 100644 src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs create mode 100644 src/samples/Dtf/Inventory/Columns.resx create mode 100644 src/samples/Dtf/Inventory/Features.cs create mode 100644 src/samples/Dtf/Inventory/IInventoryDataProvider.cs create mode 100644 src/samples/Dtf/Inventory/Inventory.cs create mode 100644 src/samples/Dtf/Inventory/Inventory.csproj create mode 100644 src/samples/Dtf/Inventory/Inventory.ico create mode 100644 src/samples/Dtf/Inventory/Inventory.resx create mode 100644 src/samples/Dtf/Inventory/components.cs create mode 100644 src/samples/Dtf/Inventory/msiutils.cs create mode 100644 src/samples/Dtf/Inventory/patches.cs create mode 100644 src/samples/Dtf/Inventory/products.cs create mode 100644 src/samples/Dtf/Inventory/xp.manifest create mode 100644 src/samples/Dtf/ManagedCA/AssemblyInfo.cs create mode 100644 src/samples/Dtf/ManagedCA/ManagedCA.csproj create mode 100644 src/samples/Dtf/ManagedCA/SampleCAs.cs create mode 100644 src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.cs create mode 100644 src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.csproj create mode 100644 src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.exe.manifest create mode 100644 src/samples/Dtf/Tools/MakeSfxCA/app.config create mode 100644 src/samples/Dtf/Tools/SfxCA/ClrHost.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/EmbeddedUI.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/EntryPoints.def create mode 100644 src/samples/Dtf/Tools/SfxCA/EntryPoints.h create mode 100644 src/samples/Dtf/Tools/SfxCA/Extract.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/RemoteMsi.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/RemoteMsiSession.h create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxCA.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxCA.rc create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj.filters create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxUtil.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/SfxUtil.h create mode 100644 src/samples/Dtf/Tools/SfxCA/packages.config create mode 100644 src/samples/Dtf/Tools/SfxCA/precomp.cpp create mode 100644 src/samples/Dtf/Tools/SfxCA/precomp.h create mode 100644 src/samples/Dtf/Tools/Tools.proj create mode 100644 src/samples/Dtf/WiFile/WiFile.cs create mode 100644 src/samples/Dtf/WiFile/WiFile.csproj create mode 100644 src/samples/Dtf/XPack/AssemblyInfo.cs create mode 100644 src/samples/Dtf/XPack/XPack.cs create mode 100644 src/samples/Dtf/XPack/XPack.csproj (limited to 'src/samples') diff --git a/src/samples/Dtf/DDiff/CabDiffEngine.cs b/src/samples/Dtf/DDiff/CabDiffEngine.cs new file mode 100644 index 00000000..6100ced8 --- /dev/null +++ b/src/samples/Dtf/DDiff/CabDiffEngine.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using WixToolset.Dtf.Compression.Cab; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class CabDiffEngine : IDiffEngine + { + public CabDiffEngine() + { + } + + private bool IsCabinetFile(string file) + { + using(FileStream fileStream = File.OpenRead(file)) + { + return new CabEngine().IsArchive(fileStream); + } + } + + public virtual float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + (IsCabinetFile(diffInput1) || IsCabinetFile(diffInput2))) + { + return .80f; + } + else + { + return 0; + } + } + + public bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + IComparer caseInsComp = CaseInsensitiveComparer.Default; + + // TODO: Make this faster by extracting the whole cab at once. + // TODO: Optimize for the match case by first comparing the whole cab files. + + CabInfo cab1 = new CabInfo(diffInput1); + CabInfo cab2 = new CabInfo(diffInput2); + IList cabFilesList1 = cab1.GetFiles(); + IList cabFilesList2 = cab2.GetFiles(); + CabFileInfo[] cabFiles1 = new CabFileInfo[cabFilesList1.Count]; + CabFileInfo[] cabFiles2 = new CabFileInfo[cabFilesList2.Count]; + cabFilesList1.CopyTo(cabFiles1, 0); + cabFilesList2.CopyTo(cabFiles2, 0); + string[] files1 = new string[cabFiles1.Length]; + string[] files2 = new string[cabFiles2.Length]; + for(int i1 = 0; i1 < cabFiles1.Length; i1++) files1[i1] = cabFiles1[i1].Name; + for(int i2 = 0; i2 < cabFiles2.Length; i2++) files2[i2] = cabFiles2[i2].Name; + Array.Sort(files1, cabFiles1, caseInsComp); + Array.Sort(files2, cabFiles2, caseInsComp); + + + for(int i1 = 0, i2 = 0; i1 < files1.Length || i2 < files2.Length; ) + { + int comp; + if(i1 == files1.Length) + { + comp = 1; + } + else if(i2 == files2.Length) + { + comp = -1; + } + else + { + comp = caseInsComp.Compare(files1[i1], files2[i2]); + } + if(comp < 0) + { + diffOutput.WriteLine("{0}< {1}", linePrefix, files1[i1]); + i1++; + difference = true; + } + else if(comp > 0) + { + diffOutput.WriteLine("{0}> {1}", linePrefix, files2[i2]); + i2++; + difference = true; + } + else + { + string tempFile1 = Path.GetTempFileName(); + string tempFile2 = Path.GetTempFileName(); + cabFiles1[i1].CopyTo(tempFile1, true); + cabFiles2[i2].CopyTo(tempFile2, true); + IDiffEngine diffEngine = diffFactory.GetDiffEngine(tempFile1, tempFile2, options); + StringWriter sw = new StringWriter(); + if(diffEngine.GetDiff(tempFile1, tempFile2, options, sw, linePrefix + " ", diffFactory)) + { + diffOutput.WriteLine("{0}{1}", linePrefix, files1[i1]); + diffOutput.Write(sw.ToString()); + difference = true; + } + + File.SetAttributes(tempFile1, File.GetAttributes(tempFile1) & ~FileAttributes.ReadOnly); + File.SetAttributes(tempFile2, File.GetAttributes(tempFile2) & ~FileAttributes.ReadOnly); + try + { + File.Delete(tempFile1); + File.Delete(tempFile2); + } + catch(IOException) + { +#if DEBUG + Console.WriteLine("Could not delete temporary files {0} and {1}", tempFile1, tempFile2); +#endif + } + i1++; + i2++; + } + } + + return difference; + } + + public virtual IDiffEngine Clone() + { + return new CabDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/DDiff.cs b/src/samples/Dtf/DDiff/DDiff.cs new file mode 100644 index 00000000..27a5a782 --- /dev/null +++ b/src/samples/Dtf/DDiff/DDiff.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Text; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class DDiff + { + public static void Usage(TextWriter w) + { + w.WriteLine("Usage: DDiff target1 target2 [options]"); + w.WriteLine("Example: DDiff d:\\dir1 d:\\dir2"); + w.WriteLine("Example: DDiff patch1.msp patch2.msp /patchtarget target.msi"); + w.WriteLine(); + w.WriteLine("Options:"); + w.WriteLine(" /o [filename] Output results to text file (UTF8)"); + w.WriteLine(" /p [package.msi] Diff patches relative to target MSI"); + } + + public static int Main(string[] args) + { + if(args.Length < 2) + { + Usage(Console.Out); + return -1; + } + + string input1 = args[0]; + string input2 = args[1]; + string[] options = new string[args.Length - 2]; + for(int i = 0; i < options.Length; i++) options[i] = args[i+2]; + + TextWriter output = Console.Out; + + for(int i = 0; i < options.Length - 1; i++) + { + switch(options[i].ToLower()) + { + case "/o": goto case "-output"; + case "-o": goto case "-output"; + case "/output": goto case "-output"; + case "-output": output = new StreamWriter(options[i+1], false, Encoding.UTF8); break; + } + } + + IDiffEngineFactory diffFactory = new BestQualityDiffEngineFactory(new IDiffEngine[] + { + new DirectoryDiffEngine(), + new FileDiffEngine(), + new VersionedFileDiffEngine(), + new TextFileDiffEngine(), + new MsiDiffEngine(), + new CabDiffEngine(), + new MspDiffEngine(), + }); + + IDiffEngine diffEngine = diffFactory.GetDiffEngine(input1, input2, options); + if(diffEngine != null) + { + bool different = diffEngine.GetDiff(input1, input2, options, output, "", diffFactory); + return different ? 1 : 0; + } + else + { + Console.Error.WriteLine("Dont know how to diff those inputs."); + return -1; + } + } + } +} diff --git a/src/samples/Dtf/DDiff/DDiff.csproj b/src/samples/Dtf/DDiff/DDiff.csproj new file mode 100644 index 00000000..332ad4d0 --- /dev/null +++ b/src/samples/Dtf/DDiff/DDiff.csproj @@ -0,0 +1,39 @@ + + + + + + + {1CDF4242-4C00-4744-BBCD-085128978FF3} + Exe + WixToolset.Dtf.Samples.DDiff + DDiff + v2.0 + OnOutputUpdated + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/DDiff/DirectoryDiffEngine.cs b/src/samples/Dtf/DDiff/DirectoryDiffEngine.cs new file mode 100644 index 00000000..89e8b47e --- /dev/null +++ b/src/samples/Dtf/DDiff/DirectoryDiffEngine.cs @@ -0,0 +1,154 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class DirectoryDiffEngine : IDiffEngine + { + public DirectoryDiffEngine() + { + } + + public virtual float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && Directory.Exists(diffInput1) && + diffInput2 != null && Directory.Exists(diffInput2)) + { + return .70f; + } + else + { + return 0; + } + } + + public bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + IComparer caseInsComp = CaseInsensitiveComparer.Default; + + string[] files1 = Directory.GetFiles(diffInput1); + string[] files2 = Directory.GetFiles(diffInput2); + for(int i1 = 0; i1 < files1.Length; i1++) + { + files1[i1] = Path.GetFileName(files1[i1]); + } + for(int i2 = 0; i2 < files2.Length; i2++) + { + files2[i2] = Path.GetFileName(files2[i2]); + } + Array.Sort(files1, caseInsComp); + Array.Sort(files2, caseInsComp); + + for(int i1 = 0, i2 = 0; i1 < files1.Length || i2 < files2.Length; ) + { + int comp; + if(i1 == files1.Length) + { + comp = 1; + } + else if(i2 == files2.Length) + { + comp = -1; + } + else + { + comp = caseInsComp.Compare(files1[i1], files2[i2]); + } + if(comp < 0) + { + diffOutput.WriteLine("{0}< {1}", linePrefix, files1[i1]); + i1++; + difference = true; + } + else if(comp > 0) + { + diffOutput.WriteLine("{0}> {1}", linePrefix, files2[i2]); + i2++; + difference = true; + } + else + { + string file1 = Path.Combine(diffInput1, files1[i1]); + string file2 = Path.Combine(diffInput2, files2[i2]); + IDiffEngine diffEngine = diffFactory.GetDiffEngine(file1, file2, options); + StringWriter sw = new StringWriter(); + if(diffEngine.GetDiff(file1, file2, options, sw, linePrefix + " ", diffFactory)) + { + diffOutput.WriteLine("{0}{1}", linePrefix, files1[i1]); + diffOutput.Write(sw.ToString()); + difference = true; + } + i1++; + i2++; + } + } + + string[] dirs1 = Directory.GetDirectories(diffInput1); + string[] dirs2 = Directory.GetDirectories(diffInput2); + for(int i1 = 0; i1 < dirs1.Length; i1++) + { + dirs1[i1] = Path.GetFileName(dirs1[i1]); + } + for(int i2 = 0; i2 < dirs2.Length; i2++) + { + dirs2[i2] = Path.GetFileName(dirs2[i2]); + } + Array.Sort(dirs1, caseInsComp); + Array.Sort(dirs2, caseInsComp); + + for(int i1 = 0, i2 = 0; i1 < dirs1.Length || i2 < dirs2.Length; ) + { + int comp; + if(i1 == dirs1.Length) + { + comp = 1; + } + else if(i2 == dirs2.Length) + { + comp = -1; + } + else + { + comp = caseInsComp.Compare(dirs1[i1], dirs2[i2]); + } + if(comp < 0) + { + diffOutput.WriteLine("{0}< {1}", linePrefix, dirs1[i1]); + i1++; + difference = true; + } + else if(comp > 0) + { + diffOutput.WriteLine("{0}> {1}", linePrefix, dirs2[i2]); + i2++; + difference = true; + } + else + { + string dir1 = Path.Combine(diffInput1, dirs1[i1]); + string dir2 = Path.Combine(diffInput2, dirs2[i2]); + IDiffEngine diffEngine = diffFactory.GetDiffEngine(dir1, dir2, options); + StringWriter sw = new StringWriter(); + if(diffEngine.GetDiff(dir1, dir2, options, sw, linePrefix + " ", diffFactory)) + { + diffOutput.WriteLine("{0}{1}\\", linePrefix, dirs1[i1]); + diffOutput.Write(sw.ToString()); + difference = true; + } + i1++; + i2++; + } + } + return difference; + } + + public virtual IDiffEngine Clone() + { + return new DirectoryDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/FileDiffEngine.cs b/src/samples/Dtf/DDiff/FileDiffEngine.cs new file mode 100644 index 00000000..20ecd857 --- /dev/null +++ b/src/samples/Dtf/DDiff/FileDiffEngine.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class FileDiffEngine : IDiffEngine + { + public FileDiffEngine() + { + } + + public virtual float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2)) + { + return .10f; + } + else + { + return 0; + } + } + + public bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + FileInfo file1 = new FileInfo(diffInput1); + FileInfo file2 = new FileInfo(diffInput2); + + if(file1.Length != file2.Length) + { + diffOutput.WriteLine("{0}File size: {1} -> {2}", linePrefix, file1.Length, file2.Length); + difference = true; + } + else + { + FileStream stream1 = file1.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + FileStream stream2 = file2.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + + byte[] buf1 = new byte[512]; + byte[] buf2 = new byte[512]; + + while(!difference) + { + int count1 = stream1.Read(buf1, 0, buf1.Length); + int count2 = stream2.Read(buf2, 0, buf2.Length); + + for(int i = 0; i < count1; i++) + { + if(i == count2 || buf1[i] != buf2[i]) + { + difference = true; + break; + } + } + if(count1 < buf1.Length) // EOF + { + break; + } + } + + stream1.Close(); + stream2.Close(); + + if(difference) + { + diffOutput.WriteLine("{0}Files differ.", linePrefix); + } + } + + return difference; + } + + public virtual IDiffEngine Clone() + { + return new FileDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/IDiffEngine.cs b/src/samples/Dtf/DDiff/IDiffEngine.cs new file mode 100644 index 00000000..9895d6ff --- /dev/null +++ b/src/samples/Dtf/DDiff/IDiffEngine.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public interface IDiffEngine + { + float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory); + + bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory); + + IDiffEngine Clone(); + } + + public interface IDiffEngineFactory + { + IDiffEngine GetDiffEngine(string diffInput1, string diffInput2, string[] options); + } + + public class BestQualityDiffEngineFactory : IDiffEngineFactory + { + public virtual IDiffEngine GetDiffEngine(string diffInput1, string diffInput2, string[] options) + { + float bestDiffQuality = 0; + IDiffEngine bestDiffEngine = null; + + foreach(IDiffEngine diffEngine in diffEngines) + { + float diffQuality = diffEngine.GetDiffQuality(diffInput1, diffInput2, options, this); + if(diffQuality > bestDiffQuality) + { + bestDiffQuality = diffQuality; + bestDiffEngine = diffEngine; + } + } + return (bestDiffEngine != null ? bestDiffEngine.Clone() : null); + } + + public BestQualityDiffEngineFactory() : this(null) { } + public BestQualityDiffEngineFactory(IDiffEngine[] diffEngines) + { + this.diffEngines = (diffEngines != null ? new ArrayList(diffEngines) : new ArrayList()); + } + + protected IList diffEngines; + + public virtual void Add(IDiffEngine diffEngine) + { + diffEngines.Add(diffEngine); + } + + public virtual void Remove(IDiffEngine diffEngine) + { + diffEngines.Remove(diffEngine); + } + + public IList DiffEngines + { + get + { + return ArrayList.ReadOnly(diffEngines); + } + } + } +} diff --git a/src/samples/Dtf/DDiff/MsiDiffEngine.cs b/src/samples/Dtf/DDiff/MsiDiffEngine.cs new file mode 100644 index 00000000..91bc2969 --- /dev/null +++ b/src/samples/Dtf/DDiff/MsiDiffEngine.cs @@ -0,0 +1,276 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using WixToolset.Dtf.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller.Package; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class MsiDiffEngine : IDiffEngine + { + public MsiDiffEngine() + { + } + + protected bool IsMsiDatabase(string file) + { + // TODO: use something smarter? + switch(Path.GetExtension(file).ToLower()) + { + case ".msi": return true; + case ".msm": return true; + case ".pcp": return true; + default : return false; + } + } + + protected bool IsMspPatch(string file) + { + // TODO: use something smarter? + switch(Path.GetExtension(file).ToLower()) + { + case ".msp": return true; + default : return false; + } + } + + public virtual float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + (IsMsiDatabase(diffInput1) || IsMsiDatabase(diffInput2))) + { + return .70f; + } + else if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + (IsMspPatch(diffInput1) || IsMspPatch(diffInput2))) + { + return .60f; + } + else + { + return 0; + } + } + + public virtual bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + Database db1 = new Database(diffInput1, DatabaseOpenMode.ReadOnly); + Database db2 = new Database(diffInput2, DatabaseOpenMode.ReadOnly); + + if(GetSummaryInfoDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + if(GetDatabaseDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + if(GetStreamsDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + + db1.Close(); + db2.Close(); + return difference; + } + + protected bool GetSummaryInfoDiff(Database db1, Database db2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + SummaryInfo summInfo1 = db1.SummaryInfo; + SummaryInfo summInfo2 = db2.SummaryInfo; + if(summInfo1.Title != summInfo2.Title ) { diffOutput.WriteLine("{0}SummaryInformation.Title {{{1}}}->{{{2}}}", linePrefix, summInfo1.Title, summInfo2.Title); difference = true; } + if(summInfo1.Subject != summInfo2.Subject ) { diffOutput.WriteLine("{0}SummaryInformation.Subject {{{1}}}->{{{2}}}", linePrefix, summInfo1.Subject, summInfo2.Subject); difference = true; } + if(summInfo1.Author != summInfo2.Author ) { diffOutput.WriteLine("{0}SummaryInformation.Author {{{1}}}->{{{2}}}", linePrefix, summInfo1.Author, summInfo2.Author); difference = true; } + if(summInfo1.Keywords != summInfo2.Keywords ) { diffOutput.WriteLine("{0}SummaryInformation.Keywords {{{1}}}->{{{2}}}", linePrefix, summInfo1.Keywords, summInfo2.Keywords); difference = true; } + if(summInfo1.Comments != summInfo2.Comments ) { diffOutput.WriteLine("{0}SummaryInformation.Comments {{{1}}}->{{{2}}}", linePrefix, summInfo1.Comments, summInfo2.Comments); difference = true; } + if(summInfo1.Template != summInfo2.Template ) { diffOutput.WriteLine("{0}SummaryInformation.Template {{{1}}}->{{{2}}}", linePrefix, summInfo1.Template, summInfo2.Template); difference = true; } + if(summInfo1.LastSavedBy != summInfo2.LastSavedBy ) { diffOutput.WriteLine("{0}SummaryInformation.LastSavedBy {{{1}}}->{{{2}}}", linePrefix, summInfo1.LastSavedBy, summInfo2.LastSavedBy); difference = true; } + if(summInfo1.RevisionNumber != summInfo2.RevisionNumber) { diffOutput.WriteLine("{0}SummaryInformation.RevisionNumber {{{1}}}->{{{2}}}", linePrefix, summInfo1.RevisionNumber, summInfo2.RevisionNumber); difference = true; } + if(summInfo1.CreatingApp != summInfo2.CreatingApp ) { diffOutput.WriteLine("{0}SummaryInformation.CreatingApp {{{1}}}->{{{2}}}", linePrefix, summInfo1.CreatingApp, summInfo2.CreatingApp); difference = true; } + if(summInfo1.LastPrintTime != summInfo2.LastPrintTime ) { diffOutput.WriteLine("{0}SummaryInformation.LastPrintTime {{{1}}}->{{{2}}}", linePrefix, summInfo1.LastPrintTime, summInfo2.LastPrintTime); difference = true; } + if(summInfo1.CreateTime != summInfo2.CreateTime ) { diffOutput.WriteLine("{0}SummaryInformation.CreateTime {{{1}}}->{{{2}}}", linePrefix, summInfo1.CreateTime, summInfo2.CreateTime); difference = true; } + if(summInfo1.LastSaveTime != summInfo2.LastSaveTime ) { diffOutput.WriteLine("{0}SummaryInformation.LastSaveTime {{{1}}}->{{{2}}}", linePrefix, summInfo1.LastSaveTime, summInfo2.LastSaveTime); difference = true; } + if(summInfo1.CodePage != summInfo2.CodePage ) { diffOutput.WriteLine("{0}SummaryInformation.Codepage {{{1}}}->{{{2}}}", linePrefix, summInfo1.CodePage, summInfo2.CodePage); difference = true; } + if(summInfo1.PageCount != summInfo2.PageCount ) { diffOutput.WriteLine("{0}SummaryInformation.PageCount {{{1}}}->{{{2}}}", linePrefix, summInfo1.PageCount, summInfo2.PageCount); difference = true; } + if(summInfo1.WordCount != summInfo2.WordCount ) { diffOutput.WriteLine("{0}SummaryInformation.WordCount {{{1}}}->{{{2}}}", linePrefix, summInfo1.WordCount, summInfo2.WordCount); difference = true; } + if(summInfo1.CharacterCount != summInfo2.CharacterCount) { diffOutput.WriteLine("{0}SummaryInformation.CharacterCount {{{1}}}->{{{2}}}", linePrefix, summInfo1.CharacterCount, summInfo2.CharacterCount); difference = true; } + if(summInfo1.Security != summInfo2.Security ) { diffOutput.WriteLine("{0}SummaryInformation.Security {{{1}}}->{{{2}}}", linePrefix, summInfo1.Security, summInfo2.Security); difference = true; } + summInfo1.Close(); + summInfo2.Close(); + + return difference; + } + + protected bool GetDatabaseDiff(Database db1, Database db2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + string tempFile = Path.GetTempFileName(); + if(db2.GenerateTransform(db1, tempFile)) + { + difference = true; + + Database db = db1; + db.ViewTransform(tempFile); + + string row, column, change; + using (View view = db.OpenView("SELECT `Table`, `Column`, `Row`, `Data`, `Current` " + + "FROM `_TransformView` ORDER BY `Table`, `Row`")) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + column = String.Format("{0} {1}", rec[1], rec[2]); + change = ""; + if (rec.IsNull(3)) + { + row = ""; + if (!rec.IsNull(4)) + { + change = "[" + rec[5] + "]: " + DecodeColDef(rec.GetInteger(4)); + } + } + else + { + row = "[" + String.Join(",", rec.GetString(3).Split('\t')) + "]"; + if (rec.GetString(2) != "INSERT" && rec.GetString(2) != "DELETE") + { + column = String.Format("{0}.{1}", rec[1], rec[2]); + change = "{" + rec[5] + "}->{" + rec[4] + "}"; + } + } + + diffOutput.WriteLine("{0}{1,-25} {2} {3}", linePrefix, column, row, change); + } + } + } + File.Delete(tempFile); + + return difference; + } + + private string DecodeColDef(int colDef) + { + const int icdLong = 0x0000; + const int icdShort = 0x0400; + const int icdObject = 0x0800; + const int icdString = 0x0C00; + const int icdTypeMask = 0x0F00; + const int icdNullable = 0x1000; + const int icdPrimaryKey = 0x2000; + + string def = ""; + switch(colDef & (icdTypeMask)) + { + case icdLong : def = "LONG"; break; + case icdShort : def = "SHORT"; break; + case icdObject: def = "OBJECT"; break; + case icdString: def = "CHAR[" + (colDef & 0xFF) + "]"; break; + } + if((colDef & icdNullable) != 0) + { + def = def + " NOT NULL"; + } + if((colDef & icdPrimaryKey) != 0) + { + def = def + " PRIMARY KEY"; + } + return def; + } + + protected bool GetStreamsDiff(Database db1, Database db2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + IList streams1List = db1.ExecuteStringQuery("SELECT `Name` FROM `_Streams`"); + IList streams2List = db2.ExecuteStringQuery("SELECT `Name` FROM `_Streams`"); + string[] streams1 = new string[streams1List.Count]; + string[] streams2 = new string[streams2List.Count]; + streams1List.CopyTo(streams1, 0); + streams2List.CopyTo(streams2, 0); + + IComparer caseInsComp = CaseInsensitiveComparer.Default; + Array.Sort(streams1, caseInsComp); + Array.Sort(streams2, caseInsComp); + + for (int i1 = 0, i2 = 0; i1 < streams1.Length || i2 < streams2.Length; ) + { + int comp; + if (i1 == streams1.Length) + { + comp = 1; + } + else if (i2 == streams2.Length) + { + comp = -1; + } + else + { + comp = caseInsComp.Compare(streams1[i1], streams2[i2]); + } + if(comp < 0) + { + diffOutput.WriteLine("{0}< {1}", linePrefix, streams1[i1]); + i1++; + difference = true; + } + else if(comp > 0) + { + diffOutput.WriteLine("{0}> {1}", linePrefix, streams2[i2]); + i2++; + difference = true; + } + else + { + if(streams1[i1] != ("" + ((char)5) + "SummaryInformation")) + { + string tempFile1 = Path.GetTempFileName(); + string tempFile2 = Path.GetTempFileName(); + + using (View view = db1.OpenView(String.Format("SELECT `Data` FROM `_Streams` WHERE `Name` = '{0}'", streams1[i1]))) + { + view.Execute(); + + using (Record rec = view.Fetch()) + { + rec.GetStream(1, tempFile1); + } + } + + using (View view = db2.OpenView(String.Format("SELECT `Data` FROM `_Streams` WHERE `Name` = '{0}'", streams2[i2]))) + { + view.Execute(); + + using (Record rec = view.Fetch()) + { + rec.GetStream(1, tempFile2); + } + } + + IDiffEngine diffEngine = diffFactory.GetDiffEngine(tempFile1, tempFile2, options); + StringWriter sw = new StringWriter(); + if(diffEngine.GetDiff(tempFile1, tempFile2, options, sw, linePrefix + " ", diffFactory)) + { + diffOutput.WriteLine("{0}{1}", linePrefix, streams1[i1]); + diffOutput.Write(sw.ToString()); + difference = true; + } + + File.Delete(tempFile1); + File.Delete(tempFile2); + } + i1++; + i2++; + } + } + + return difference; + } + + public virtual IDiffEngine Clone() + { + return new MsiDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/MspDiffEngine.cs b/src/samples/Dtf/DDiff/MspDiffEngine.cs new file mode 100644 index 00000000..285bc83d --- /dev/null +++ b/src/samples/Dtf/DDiff/MspDiffEngine.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using WixToolset.Dtf.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller.Package; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class MspDiffEngine : MsiDiffEngine + { + public MspDiffEngine() + { + } + + private string GetPatchTargetOption(string[] options) + { + for(int i = 0; i < options.Length - 1; i++) + { + switch(options[i].ToLower()) + { + case "/p": goto case "-patchtarget"; + case "-p": goto case "-patchtarget"; + case "/patchtarget": goto case "-patchtarget"; + case "-patchtarget": return options[i+1]; + } + } + return null; + } + + public override float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + GetPatchTargetOption(options) != null && + (IsMspPatch(diffInput1) && IsMspPatch(diffInput2))) + { + return .80f; + } + else if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + GetPatchTargetOption(options) == null && + (IsMspPatch(diffInput1) && IsMsiDatabase(diffInput2)) || + (IsMsiDatabase(diffInput1) && IsMspPatch(diffInput2))) + { + return .75f; + } + else + { + return 0; + } + } + + public override bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + InstallPackage db1, db2; + if(IsMspPatch(diffInput1)) + { + string patchTargetDbFile = GetPatchTargetOption(options); + if(patchTargetDbFile == null) patchTargetDbFile = diffInput2; + string tempPatchedDbFile = Path.GetTempFileName(); + File.Copy(patchTargetDbFile, tempPatchedDbFile, true); + File.SetAttributes(tempPatchedDbFile, File.GetAttributes(tempPatchedDbFile) & ~System.IO.FileAttributes.ReadOnly); + db1 = new InstallPackage(tempPatchedDbFile, DatabaseOpenMode.Direct); + db1.ApplyPatch(new PatchPackage(diffInput1), null); + db1.Commit(); + } + else + { + db1 = new InstallPackage(diffInput1, DatabaseOpenMode.ReadOnly); + } + if(IsMspPatch(diffInput2)) + { + string patchTargetDbFile = GetPatchTargetOption(options); + if(patchTargetDbFile == null) patchTargetDbFile = diffInput1; + string tempPatchedDbFile = Path.GetTempFileName(); + File.Copy(patchTargetDbFile, tempPatchedDbFile, true); + File.SetAttributes(tempPatchedDbFile, File.GetAttributes(tempPatchedDbFile) & ~System.IO.FileAttributes.ReadOnly); + db2 = new InstallPackage(tempPatchedDbFile, DatabaseOpenMode.Direct); + db2.ApplyPatch(new PatchPackage(diffInput2), null); + db2.Commit(); + } + else + { + db2 = new InstallPackage(diffInput2, DatabaseOpenMode.ReadOnly); + } + + if(GetSummaryInfoDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + if(GetDatabaseDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + if(GetStreamsDiff(db1, db2, options, diffOutput, linePrefix, diffFactory)) difference = true; + + db1.Close(); + db2.Close(); + + try + { + if(IsMspPatch(diffInput1)) File.Delete(db1.FilePath); + if(IsMspPatch(diffInput1)) File.Delete(db2.FilePath); + } + catch(IOException) + { + #if DEBUG + Console.WriteLine("Could not delete temporary files {0} and {1}", db1.FilePath, db2.FilePath); + #endif + } + + if(IsMspPatch(diffInput1) && IsMspPatch(diffInput2)) + { + Database dbp1 = new Database(diffInput1, DatabaseOpenMode.ReadOnly); + Database dbp2 = new Database(diffInput2, DatabaseOpenMode.ReadOnly); + + if(GetStreamsDiff(dbp1, dbp2, options, diffOutput, linePrefix, diffFactory)) difference = true; + dbp1.Close(); + dbp2.Close(); + } + + return difference; + } + + public override IDiffEngine Clone() + { + return new MspDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/TextFileDiffEngine.cs b/src/samples/Dtf/DDiff/TextFileDiffEngine.cs new file mode 100644 index 00000000..22567023 --- /dev/null +++ b/src/samples/Dtf/DDiff/TextFileDiffEngine.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class TextFileDiffEngine : IDiffEngine + { + public TextFileDiffEngine() + { + } + + private bool IsTextFile(string file) + { + // Guess whether this is a text file by reading the first few bytes and checking for non-ascii chars. + + bool isText = true; + FileStream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + byte[] buf = new byte[256]; + int count = stream.Read(buf, 0, buf.Length); + for(int i = 0; i < count; i++) + { + if((buf[i] & 0x80) != 0) + { + isText = false; + break; + } + } + stream.Close(); + return isText; + } + + public float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + (IsTextFile(diffInput1) && IsTextFile(diffInput2))) + { + return .70f; + } + else + { + return 0; + } + } + + public bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + try + { + bool difference = false; + ProcessStartInfo psi = new ProcessStartInfo("diff.exe"); + psi.Arguments = String.Format("\"{0}\" \"{1}\"", diffInput1, diffInput2); + psi.WorkingDirectory = null; + psi.UseShellExecute = false; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.RedirectStandardOutput = true; + Process proc = Process.Start(psi); + + string line; + while((line = proc.StandardOutput.ReadLine()) != null) + { + diffOutput.WriteLine("{0}{1}", linePrefix, line); + difference = true; + } + + proc.WaitForExit(); + return difference; + } + catch(System.ComponentModel.Win32Exception) // If diff.exe is not found, just compare the bytes + { + return new FileDiffEngine().GetDiff(diffInput1, diffInput2, options, diffOutput, linePrefix, diffFactory); + } + } + + public IDiffEngine Clone() + { + return new TextFileDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/DDiff/VersionedFileDiffEngine.cs b/src/samples/Dtf/DDiff/VersionedFileDiffEngine.cs new file mode 100644 index 00000000..ad4014f3 --- /dev/null +++ b/src/samples/Dtf/DDiff/VersionedFileDiffEngine.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using WixToolset.Dtf.WindowsInstaller; + +namespace WixToolset.Dtf.Samples.DDiff +{ + public class VersionedFileDiffEngine : IDiffEngine + { + public VersionedFileDiffEngine() + { + } + + private bool IsVersionedFile(string file) + { + return Installer.GetFileVersion(file) != ""; + } + + public float GetDiffQuality(string diffInput1, string diffInput2, string[] options, IDiffEngineFactory diffFactory) + { + if(diffInput1 != null && File.Exists(diffInput1) && + diffInput2 != null && File.Exists(diffInput2) && + (IsVersionedFile(diffInput1) || IsVersionedFile(diffInput2))) + { + return .20f; + } + else + { + return 0; + } + } + + public bool GetDiff(string diffInput1, string diffInput2, string[] options, TextWriter diffOutput, string linePrefix, IDiffEngineFactory diffFactory) + { + bool difference = false; + + string ver1 = Installer.GetFileVersion(diffInput1); + string ver2 = Installer.GetFileVersion(diffInput2); + + if(ver1 != ver2) + { + diffOutput.WriteLine("{0}File version: {1} -> {2}", linePrefix, ver1, ver2); + difference = true; + } + else + { + FileStream stream1 = new FileStream(diffInput1, FileMode.Open, FileAccess.Read, FileShare.Read); + FileStream stream2 = new FileStream(diffInput2, FileMode.Open, FileAccess.Read, FileShare.Read); + + byte[] buf1 = new byte[512]; + byte[] buf2 = new byte[512]; + + while(!difference) + { + int count1 = stream1.Read(buf1, 0, buf1.Length); + int count2 = stream2.Read(buf2, 0, buf2.Length); + + for(int i = 0; i < count1; i++) + { + if(i == count2 || buf1[i] != buf2[i]) + { + difference = true; + break; + } + } + if(count1 < buf1.Length) // EOF + { + break; + } + } + + stream1.Close(); + stream2.Close(); + + if(difference) + { + diffOutput.WriteLine("{0}File versions match but bits differ.", linePrefix); + } + } + + return difference; + } + + public IDiffEngine Clone() + { + return new VersionedFileDiffEngine(); + } + } +} diff --git a/src/samples/Dtf/Documents/Guide/Content/about.htm b/src/samples/Dtf/Documents/Guide/Content/about.htm new file mode 100644 index 00000000..393b5a81 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/about.htm @@ -0,0 +1,59 @@ + + + Deployment Tools Foundation Overview + + + + + + +
+ Deployment Tools Foundation
+ Deployment Tools Foundation
+
+ Overview + v4.0 +
+
+
+ +
+

Deployment Tools Foundation is a rich set of .NET class libraries and + related resources that together bring the Windows deployment platform + technologies into the .NET world. It is designed to greatly simplify + deployment-related development tasks while still exposing the complete + functionality of the underlying technology.

+ +

The primary focus of DTF is to provide a foundation for development of + various kinds of tools to support deployment throughout the product + lifecycle, including setup authoring, building, analysis, debugging, and + testing tools. In addition to tools, DTF can also be useful for install-time + activities such as setup bootstrappers, external UI, and custom actions, + and for application run-time activities that need to access the deployment + platform.

+ +

For a description of the the latest changes, see What's New.

+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/buildingcas.htm b/src/samples/Dtf/Documents/Guide/Content/buildingcas.htm new file mode 100644 index 00000000..e88ad552 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/buildingcas.htm @@ -0,0 +1,94 @@ + + + Building Managed Custom Actions + + + + + + +
+ Deployment Tools Foundation
+ Building Managed Custom Actions
+
+ + Development Guide > + Managed CAs > + Building + +
+
+
+ +
+ +

The build process for managed CA DLLs is a little complicated becuase of the + proxy-wrapper and dll-export requirements. Here's an overview:

+
    +
  1. +

    Compile your CA assembly, which references WixToolset.Dtf.WindowsInstaller.dll and + marks exported custom actions with a CustomActionAttribute.

    +
  2. +

    Package the CA assembly, CustomAction.config, WixToolset.Dtf.WindowsInstaller.dll, + and any other dependencies using MakeSfxCA.exe. The filenames of CustomAction.config + and WixToolset.Dtf.WindowsInstaller.dll must not be changed, since + the custom action proxy specifically looks for those files.

    +
+


+

+

Compiling

+

+    csc.exe
+        /target:library
+        /r:$(DTFbin)\WixToolset.Dtf.WindowsInstaller.dll
+        /out:SampleCAs.dll
+        *.cs
+            
+

Wrapping


+    MakeSfxCA.exe
+        $(OutDir)\SampleCAsPackage.dll
+        $(DTFbin)\SfxCA.dll
+        SampleCAs.dll
+        CustomAction.config
+        $(DTFbin)\WixToolset.Dtf.WindowsInstaller.dll
+            
+

+

Now the resulting package, SampleCAsPackage.dll, is ready to be inserted + into the Binary table of the MSI.

+


+

+

For a working example of building a managed custom action package + you can look at included sample ManagedCAs project.

+


+

+ +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/cabpack.htm b/src/samples/Dtf/Documents/Guide/Content/cabpack.htm new file mode 100644 index 00000000..2d9f725e --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/cabpack.htm @@ -0,0 +1,63 @@ + + + Archive Pack/Unpack Tool + + + + + + +
+ Deployment Tools Foundation
+ Archive Pack/Unpack Tool
+
+ + Development Guide > + Samples > + XPack + +
+
+
+ +
+

Usage: CabPack.exe <directory> <package.cab>
+Usage: XPack /P <archive.cab> <directory>
+Usage: XPack /P <archive.zip> <directory>
+
+Packs all files in a directory tree into an archive,
+using either the cab or zip format. Any existing archive
+with the same name will be overwritten.
+
+
+Usage: XPack /U <archive.cab> <directory>
+Usage: XPack /U <archive.zip> <directory>
+
+Unpacks all files from a cab or zip archive to the
+specified directory. Any existing files with the same
+names will be overwritten.
+

+


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/cabs.htm b/src/samples/Dtf/Documents/Guide/Content/cabs.htm new file mode 100644 index 00000000..e88d1e15 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/cabs.htm @@ -0,0 +1,101 @@ + + + Working with Cabinet Files + + + + + + +
+ Deployment Tools Foundation
+ Working with Cabinet Files
+
+ + Development Guide > + Cabinet Files + +
+
+
+ +
+ +

Creating a cabinet

+
    CabInfo cabInfo = new CabInfo("package.cab");
+    cabInfo.Pack("D:\\FilesToCompress");

+

1.  Create a new CabInfo instance referring to the (future) location of the .cab file.

+

2.  Compress files:

    +
  • Easily compress an entire directory with the Pack method.
  • +
  • Compress a specific list of exernal and internal filenames with the PackFiles method.
  • +
  • Compress a dictionary mapping of internal to external filenames with the PackFileSet method.
  • +
+ +


+

Listing a cabinet

+
    CabInfo cabInfo = new CabInfo("package.cab");
+    foreach (CabFileInfo fileInfo in cabInfo.GetFiles())
+        Console.WriteLine(fileInfo.Name + "\t" + fileInfo.Length);

+

1.  Create a new CabInfo instance referring to the location of the .cab file.

+

2.  Enumerate files returned by the GetFiles method.

    +
  • Each CabFileInfo instance contains metadata about one file.
  • +
+ +


+

Extracting a cabinet

+
    CabInfo cabInfo = new CabInfo("package.cab");
+    cabInfo.Unpack("D:\\ExtractedFiles");

+

1.  Create a new CabInfo instance referring to the location of the .cab file.

+

2.  Extract files:

    +
  • Easily extract all files to a directory with the Unpack method.
  • +
  • Easily extract a single file with the UnpackFile method.
  • +
  • Extract a specific list of filenames with the UnpackFiles method.
  • +
  • Extract a dictionary mapping of internal to external filenames with the UnpackFileSet method.
  • +
+ +


+

Getting progress

+ Most cabinet operation methods have an overload that allows you to specify a event handler + for receiving archive + progress events. The XPack sample + demonstrates use of the callback to report detailed progress to the console. + +


+

Stream-based compression

+ The CabEngine class contains static methods for performing compression/decompression operations directly + on any kind of Stream. However these methods are more difficult to use, since the caller must implement a + stream context + that provides the file metadata which would otherwise have been provided by the filesystem. The CabInfo class + uses the CabEngine class with FileStreams to provide the more traditional file-based interface. + +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/cabwrapper.htm b/src/samples/Dtf/Documents/Guide/Content/cabwrapper.htm new file mode 100644 index 00000000..fd88437c --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/cabwrapper.htm @@ -0,0 +1,63 @@ + + + + Managed Wrapper Library for Cabinet APIs + + + + +
+
+ + + + + +
Managed Libraries for Windows Installer
+
+
+

Managed Wrapper Library for Cabinet APIs

+
+
+
+

This is a managed library that provides the ability to + create and extract cabinet files. It uses cabinet.dll (present on all versions of Windows) + to do the actual compression/decompression. It provides access to nearly all + cabinet capabilities, including spanning of multiple cab files. It even has support for + preserving directory structures and UTF8 paths.

+

There are two ways to use the library. CabinetInfo + and CabinetFileInfo + (similar to DirectoryInfo and FileInfo respectively) + provide high-level object-oriented methods for doing common file-based cabinet creation and + extraction tasks. On the other hand, the Cabinet + class provides low-level access to all + functionality, and operates completely in terms of .NET Streams. The previous two classes use + the Cabinet class to do all the actual work.

+

There are also two ways to build the library. + Compiling it normally will produce the fully functional + library in the Microsoft.Cab + namespace, while compiling it with the /D:CABMINIMAL + /D:CABEXTRACTONLY flags will create a compact assembly with only the core extraction + functionality, in the Microsoft.Cab.MiniExtract + namespace.

+

The cabinet library interops with native cabinet APIs which use the 'cdecl' + calling-convention. When building against .NET Framework versions before 2.0, + this library requires a special post-build step to process the UnmanagedFunctionPointerAttribute. + If you use this code in another assembly, don't forget to run AugmentIL + on it to fix the delegate calling-conventions, otherwise you will encounter a + NullReferenceException when attempting to call the cabinet APIs. When building against + .NET Framework version 2.0 or later, the UnmanagedFunctionPointerAttribute.cs source file + should be omitted.

+ +


+

See also:

+ +


+
+ + diff --git a/src/samples/Dtf/Documents/Guide/Content/caconfig.htm b/src/samples/Dtf/Documents/Guide/Content/caconfig.htm new file mode 100644 index 00000000..a6c97d2b --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/caconfig.htm @@ -0,0 +1,83 @@ + + + Specifying the Runtime Version + + + + + + +
+ Deployment Tools Foundation
+ Specifying the Runtime Version
+
+ + Development Guide > + Managed CAs > + Writing CAs > + CustomAction.config + +
+
+
+ +
+ +

Every managed custom action package should contain a CustomAction.config file, even though it is not required by the toolset. + Here is a sample:


+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup>
+        <supportedRuntime version="v2.0.50727"/>
+    </startup>
+</configuration>

+

The configuration file follows the standard schema for .NET Framework + configuration files documented on MSDN.

+


+

Supported Runtime Version

+

In the startup section, use supportedRuntime + tags to explicitly specify the version(s) of the .NET Framework that the custom action should run on. + If no versions are specified, the chosen version of the .NET Framework will be + the "best" match to what WixToolset.Dtf.WindowsInstaller.dll was built against.

+

Warning: leaving the version unspecified is dangerous + as it introduces a risk of compatibility problems with future versions of the .NET Framework. + It is highly recommended that you specify only the version(s) + of the .NET Framework that you have tested against.

+


+ +

Other Configuration

+

Various other kinds of configuration settings may also be added to this file, as it is a standard + .NET Framework application config file + for the custom action.

+


+ +

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/caproxy.htm b/src/samples/Dtf/Documents/Guide/Content/caproxy.htm new file mode 100644 index 00000000..2ee962d5 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/caproxy.htm @@ -0,0 +1,74 @@ + + + + Proxy Class for Managed Custom Actions + + + + +
+
+ + + + + +
Managed Libraries for Windows Installer
+
+
+

Proxy for Managed Custom Actions

+
+
+
+

The custom action proxy allows an MSI developer to write + custom actions in managed code, while maintaing all the advantages of type 1 + DLL custom actions including full access to installer state, properties, + and the session database.

+

There are generally four problems that needed to be + solved in order to create a type 1 custom action in managed code:

+
    +
  1. Exporting the CA function as a native entry point callable by + MSI: The Windows Installer engine expects to call a LoadLibrary and + GetProcAddress on the custom action DLL, so an unmanaged DLL needs to implement + the function that is initially called by MSI and ultimately returns the result. + This function acts as a proxy to relay the custom action call into the + managed custom action assembly, and relay the result back to the caller.

    +
  2. Providing supporting assemblies without + requiring them to be installed as files: If a DLL custom + action runs before the product's files are installed, then it is difficult + to provide any supporting files, because of the way the CA DLL is singly + extracted and executed from a temp file. (This can be a problem for + unmanaged CAs as well.) With managed custom actions we have already hit + that problem since both the CA assembly and the MSI wrapper assembly + need to be loaded. To solve this, the proxy DLL carries an appended + cab package. When invoked, it will extract all contents of the + cab package to a temporary working directory. This way the cab package can + carry any arbitrary dependencies the custom action may require.
  3. +
  4. Hosting and configuring the Common Language Runtime: + In order to invoke a method in a managed assembly from a previously + unmanaged process, the CLR needs to be "hosted". This involves choosing + the correct version of the .NET Framework to use out of the available + version(s) on the system, binding that version to the current process, and + configuring it to load assemblies from the temporary working directory.

    +
  5. Converting the integer session handle into a + Session object: The Session class in the managed + wrapper library has a constructor which takes an integer session handle as + its parameter. So the proxy simply instantiates this object before + calling the real CA function.

    +
+

The unmanaged CAPack module, when used in combination with the managed proxy in + the + Microsoft.WindowsInstaller assembly, accomplishes the tasks above to enable + fully-functional managed DLL custom actions.

+


+

See also:

+ +


+
+ + diff --git a/src/samples/Dtf/Documents/Guide/Content/databases.htm b/src/samples/Dtf/Documents/Guide/Content/databases.htm new file mode 100644 index 00000000..4fe1fba9 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/databases.htm @@ -0,0 +1,120 @@ + + + Working with MSI Databases + + + + + + +
+ Deployment Tools Foundation
+ Working with MSI Databases
+
+ + Development Guide > + MSI Databases + +
+
+
+ +
+ +

Querying a database

+
    using (Database db = new Database("product.msi", DatabaseOpenMode.ReadOnly))
+    {
+        string value = (string) db.ExecuteScalar(
+            "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", propName);
+    }

+

1.  Create a new Database + instance referring to the location of the .msi or .msm file.

+

2.  Execute the query:

+ +


+

Updating a binary

+
    Database db = null;
+    View view = null;
+    Record rec = null;
+    try
+    {
+        db = new Database("product.msi", DatabaseOpenMode.Direct);
+        view = db.OpenView("UPDATE `Binary` SET `Data` = ? WHERE `Name` = '{0}'", binName))
+        rec = new Record(1);
+        rec.SetStream(1, binFile);
+        view.Execute(rec);
+        db.Commit();
+    }
+    finally
+    {
+        if (rec != null) rec.Close();
+        if (view != null) view.Close();
+        if (db != null) db.Close();
+    }

+

1.  Create a new Database + instance referring to the location of the .msi or .msm file.

+

2.  Open a view by calling one of the Database.OpenView overloads.

    +
  • Parameters can be substituted in the SQL string using the String.Format syntax.
  • +
+

3.  Create a record with one field containing the new binary value.

+

4.  Execute the view by calling one of the View.Execute overloads.

    +
  • A record can be supplied for substitution of field tokens (?) in the query.
  • +
+

5.  Commit the Database.

+

6.  Close the handles.

+ +


+

About handles

+

Handle objects (Database, View, Record, SummaryInfo, Session) will remain open until + they are explicitly closed or until the objects are collected by the GC. So for the tightest + code, handle objects should be explicitly closed when they are no longer needed, + since closing them can release significant resources, and too many unnecessary + open handles can degrade performance. This is especially important within a loop + construct: for example when iterating over all the Records in a table, it is much cleaner + and faster to close each Record after it is used.

+

The handle classes in the managed library all extend the + InstallerHandle + class, which implements the IDisposable interface. This makes them easily managed with C#'s + using statement. Alternatively, they can be closed in a finally block.

+

As a general rule, methods in the library return new handle objects that should be managed + and closed by the calling code, while properties only return a reference to a prexisting handle + object.

+ +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/debuggingcas.htm b/src/samples/Dtf/Documents/Guide/Content/debuggingcas.htm new file mode 100644 index 00000000..ca1be161 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/debuggingcas.htm @@ -0,0 +1,66 @@ + + + Debugging Managed Custom Actions + + + + + + +
+ Deployment Tools Foundation
+ Debugging Managed Custom Actions
+
+ + Development Guide > + Managed CAs > + Debugging + +
+
+
+ +
+

There are two ways to attach a debugger to a managed custom action.

+

Attach to message-box: Add some temporary code to your custom action to display a + message box. Then when the message box pops up at install time, you can attch your + debugger to that process (usually identifiable by the title of the message box). + Once attached, you can ensure that symbols are loaded if necessary (they will be automatically + loaded if PDB files were embedded in the CA assembly at build time), then set breakpoints + anywhere in the custom action code.

+

MMsiBreak environment variable: When debugging managed custom actions, + you should use the MMsiBreak environment variable instead of MsiBreak. Set the MMsiBreak + variable to the custom action entrypoint name. (Remember this might be different from + the method name if it was overridden by the CustomActionAttribute.) When the CA proxy + finds a matching name, the CLR JIT-debugging dialog + will appear with text similar to "An exception 'Launch for user' has occurred + in YourCustomActionName." The debug break occurs after the custom + action assembly has been loaded, but just before custom action method is invoked. + Once attached, you can ensure that symbols are loaded if necessary, + then set breakpoints anywhere in the custom action code. Note: the MMsiBreak + environment variable can also accept a comma-separated list of action names, any of + which will cause a break when hit.

+


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/dependencies.htm b/src/samples/Dtf/Documents/Guide/Content/dependencies.htm new file mode 100644 index 00000000..cfec5880 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/dependencies.htm @@ -0,0 +1,88 @@ + + + Dependencies + + + + + + +
+ Deployment Tools Foundation
+ Dependencies
+
+ + Overview > + Dependencies + +
+
+
+ +
+ +

This page lists all the components that the DTF project depends on, at build time and at run-time.

+ +

Build-time Dependencies

+
    +
  • Visual Studio / .NET Framework - Most of DTF can be built with Visual Studio 2005 & + .NET Framework 2.0. However, the LINQ project requires VS 2008 & .NET Framework 3.5.

  • + +
  • Sandcastle - .NET documentation build engine from Microsoft, used to process all the XML doc-comments + in DTF libraries into DTFAPI.chm. + (official site)

  • + +
  • Sandcastle Builder - Sandcastle by itself is complex and difficult to use; this free tool + from Codeplex provides an easy-to-use project system around it to automate the documentation build process. + (project link)

  • + +
  • HTML Help Workshop - Tools for building HTML Help 1.x (CHM files). Used to build DTF.chm. + (download link)

  • +
+ +

Run-time Dependencies

+
    +
  • .NET Framework - Most of DTF requires .NET Framework 2.0. (.NET 1.1 is no longer supported.) + The only exception is the LINQ assembly which requires .NET Framework 3.5.

  • + +
  • Windows Installer - Windows Installer introduced new APIs and capabilities with each successive + version. Obviously, the corresponding functionality in the managed APIs is only available when the required + version of the Windows Instaler (msi.dll) is installed on the system. Use the Installer.Version property + to easily check the currently installed MSI version. Attempting to use an API not supported by the current + version will result in an EntryPointNotFoundException. To check what version is required for a particular API, + see the documentation link to the corresponding unmanaged API in MSI.chm.

    +

    In some instances when a newer version of MSI provides an "Ex" alternative to a function, only the "Ex" + function is used by the managed library. This may hide some functionality that would have otherwise been + available on a system with an older version of MSI.

  • + +
  • cabinet.dll - The DTF cabinet compression library uses cabinet.dll to implement the + low-level cabinet compression and decompression. This DLL is part of all versions of Windows, + located in the system directory.

  • + +
  • System.IO.Compression.DeflateStream - The DTF zip compression library uses this class + to implement the low-level zip compression and decompression. This class is part of .NET Framework + 2.0 and later.

  • +
+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/filepatchwrapper.htm b/src/samples/Dtf/Documents/Guide/Content/filepatchwrapper.htm new file mode 100644 index 00000000..6bab69b5 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/filepatchwrapper.htm @@ -0,0 +1,34 @@ + + + + Managed Wrapper for Binary File Patch APIs + + + + +
+
+ + + + + +
Managed Libraries for Windows Installer
+
+
+

Managed Wrapper for Binary File Patch APIs

+
+
+
+

The binary file patch creation and application APIs (supplied by MsPatchC.dll and + MsPatchA.dll) are wrapped in the Microsoft.WindowsInstaller.FilePatch.dll assembly.

+ +


+

See also:

+ +


+
+ + diff --git a/src/samples/Dtf/Documents/Guide/Content/history.htm b/src/samples/Dtf/Documents/Guide/Content/history.htm new file mode 100644 index 00000000..704ce875 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/history.htm @@ -0,0 +1,437 @@ + + + Change History + + + + + + +
+ Deployment Tools Foundation
+ Change History
+
+ + Overview > + Change History + +
+
+
+ +
+ +

2007-07-03

+See What's New?
 
 
+
+

2005-03-30

+ +
    +
  • New custom action proxy
      +
    • Managed custom actions use an XML config file to specify the CLR version.
    • +
    • New CAPack module is an unmanaged self-extracting CA DLL that can wrap both + managed and unmanaged custom actions. (The old managed CAProxy module is obsolete.)
    • +
    • Custom action build process is different but still complicated -- + see documentation for details.
    • +
    • CustomActionAttribute no longer accepts the optional NativeDependencies + parameter since it does not apply to the new proxy (all packaged files + are always extracted and available when the CA executes).
    • +
  • + +
  • 64bit support
      +
    • Various code fixes to pointer/handle types and structure alignments.
    • +
    • Cabinet and MSI libraries tested on AMD64 CLR.
    • +
    • Unmanaged and managed parts of custom action proxy tested on AMD64.
    • +
  • + +
  • MSI 3.1 APIs added:
      +
    • Installer.SetExternalUI(ExternalUIRecordHandler)
    • +
    • Installer.NotifySidChange
    • +
  • + +
  • Code builds easier with .NET Famework 2.0
      +
    • AugmentIL post-build step is no longer necessary when compiling the cabinet code + against .NET Framework 2.0, which has builtin support for cdecl delegates.
    • +
    • All C# code compiles against .NET Framework 2.0 without obsolete warnings, + when setting the NETFX2 preprocessor define.
    • +
    • Same code is still compatible with .NET Framework 1.0 + AugmentIL.
    • +
  • + +
  • Miscellaneous bugfixes/changes:
      +
    • InstallPackage.ExtractFiles could fail in some cominations of + compressed/uncompressed files - fixed.
    • +
    • Installer.DeterminePatchSequence was broken due to an incorrect interop struct - fixed.
    • +
    • CabinetInfo and CabinetFileInfo classes made serializable.
    • +
    • Added Session.FormatString method to simplify formatting a string with + property substitutions.
    • +
  • +
  • Documentation updates:
      +
    • Updated all documentation for new CA proxy.
    • +
    • Added new topic discussing InstallUtil.
    • +
  • +
+ +
+

2004-04-13

+ +
    +
  • Documentation
      +
    • Consolidated all documentation into a single CHM file.
    • +
    • Added new topics about working with MSI databases & cabinet files, + to help new users get oriented more easily.
    • +
  • + +
  • WindowsInstaller
      +
    • Removed [Beta] tags from MSI 3.0 APIs, but otherwise there + have been no changes since 3.0 Beta 1.
        +
      • Be warned these are still the least-tested parts of + the library, so early users may encounter bugs.
      • +
    • +
  • + +
  • InstallPackage
      +
    • Fixed InstallPackage.ExtractFiles() bug when directory doesn't exist.
    • +
    • Added ability to handle uncompressed files in a package marked as compressed.
    • +
  • + +
  • Cabinet
      +
    • Fixed improper handling of file attributes.
        +
      • This bug caused some packages to not be extractable by other tools.
      • +
    • +
    • Added support for UTF filenames.
        +
      • Non-ASCII filenames will automatically be stored as UTF-8. + (But note most other tools don't know how to extract them.)
      • +
    • +
  • +
+ +
+

2003-10-13

+ +
    +
  • Cab
      +
    • Fixed a bug introduced in v2.4.0 that caused files to be left in the %TEMP% + directory after creating a cab.
    • +
    • Unsealed the CabinetInfo, CabinetFileInfo, CabinetStatus classes and made a few methods + protected and virtual.
    • +
  • + +
  • AugmentIL
      +
    • Fixed a bug that sometimes caused a crash when specifying a relative output path + on the command-line.
    • +
    • Fixed a bug that sometimes caused the Win32 version to be missing from the output file.
    • +
  • + +
  • Samples\Diff: added new sample tool
      +
    • Recursively diffs directories, MSIs, MSPs, CABs, other files.
    • +
  • +
+ +
+

2003-09-23

+ +
    +
  • Cab
      +
    • Fixed a bug that caused compressing very large files/file sets to use way too + much memory. Performance on large inputs is now within a few % of native cab tools + (sometimes even a little faster!) for the same compression level.
    • +
  • + +
  • WindowsInstaller
      +
    • All the new MSI 3.0 beta APIs are wrapped, resulting in the following additions:
        +
      • New classes - Product, Patch (for accessing sourcelist and other config)
      • +
      • New methods on Install class - GetProducts, GetPatches, RemovePatches, + ApplyMultiplePatches, DetermineApplicablePatches, ExtractPatchXmlData
      • +
      • New enumerations - InstallContext, PatchStates, SourceType
      • +
      • Additional InstallProperty values
      • +
    • +
    • Note, MSI 3.0 support should be considered preliminary for now, + as APIs (both native and managed) are subject to change.
    • +
    • For MSI 2.0 compatibility, developers should not use any classes or + methods that are marked as [MSI 3.0 beta] in the documentation.
    • +
    • And unrelated to 3.0, a few additional enums have been added: + DialogAttributes, ControlAttributes, CustomActionTypes, + IniFileAction, RegistryRoot, RemoveFileInstallMode, + ServiceControlEvents, ServiceInstallFlags, TextStyles, + UpgradeAttributes, LocatorType
    • +
    • Also made a few minor non-breaking changes to keep the library FxCop-clean.
    • +
  • + +
  • AugmentIL
      +
    • Added support for strongname signing and delay-signing. AugmentIL tries to + locate the keyfile using the AssemblyKeyFileAttribute, or you may specify the + path with the new /key option.
    • +
    • All "released" assemblies will now be strongname-signed + (with an unofficial key).
    • +
  • + +
  • CAProxy
      +
    • Added support for NativeDependencies property on CustomActionAttribute. This enables + custom actions to P/Invoke into native DLLs that are carried with them.
    • +
  • + +
  • Samples\SampleCAs
      +
    • In SampleCA2, changed MessageBox.Show("") to session.Message(User,""), + because generally it is a bad practice for CAs to show independent UI.
    • +
    • Added test of CustomActionAttribute.NativeDependencies functionality.
    • +
  • + +
  • Samples\CabPack: added new sample
      +
    • Demonstrates & tests the cab library by creating self-extracting packages
    • +
  • + +
  • Samples\Inventory: added new sample
      +
    • Shows a hierarchical, relational, searchable view of all of the product, + feature, component, file, and patch data managed by MSI, for all products + installed on the system.
    • +
  • +
+ +
+

2003-09-12

+ +
    +
  • Cab:
      +
    • Added CabinetInfo.CompressDirectory method, capable of compressing an + entire directory tree structure.
    • +
    • Updated documentation of various methods concerning support of directory + structure inside cabs.
    • +
    • CabinetInfo case-sensitivity was inconsistent - + now it is case-insensitive by default, though case is still preserved
    • +
    • Separated assembly attributes into assembly.cs
    • +
  • +
  • Msi:
      +
    • InstallerException and subclasses automatically get extended error data + from MSI's last-error-record when available. The data is stored + in the exception and made available through the GetErrorRecord() + method, and the exception's Message includes the formatted error + message and data. This makes most exceptions extremely informative!
    • +
    • Added View.GetValidationErrors() method, and supporting ValidationErrorInfo + struct and ValidationError enum. This wrapper for the MsiViewGetError + API had been accidentally left out.
    • +
    • Session.Message() now supports message-box flags to specify buttons & icon
    • +
    • Added doc remarks to various methods about closing handles.
    • +
    • Separated assembly attributes into assembly.cs
    • +
  • +
  • AugmentIL:
      +
    • Recent builds of ildasm v2.0.* have a slightly different output format, + which could break AugmentIL in some cases - fixed
    • +
  • +
  • SampleCAs:
      +
    • Removed 'using' clause from SampleCA1 -- there's no need to close the session's active database handle
    • +
  • +
  • Documentation:
      +
    • Added note to ReadMe about compiling the cab source into another assembly
    • +
  • +
+ +
+

2003-08-07

+ +
    +
  • Cab:
      +
    • CabinetInfo.IsValid() usually returned false even for valid cabs - fixed
    • +
    • Extracting cab files with null timestamps generated exception - fixed
    • +
  • +
  • Msi:
      +
    • Added InstallCanceledException, subclass of InstallerException; + Methods which may be canceled by the user can throw this exception
    • +
    • Added MessageResult enumeration; + Used by Session.Message() and ExternalUIHandler delegate
    • +
    • Installer.EnableLog() now supports extended attributes correctly: + Append mode and flush-every-line
    • +
    • Added Session.DoActionSequence() - + This wrapper for the MsiSequence API had been accidentally left out
    • +
  • +
  • CAProxy:
      +
    • Catches InstallCanceledException, returns ERROR_INSTALL_USEREXIT + so CA developer doesn't necessarily have to handle the exception
    • +
  • +
  • Msi\Package:
      +
    • Added TransformInfo class: metadata about an individual patch transform
    • +
    • Added PatchPackage.GetTransform*() methods which return TransformInfo
    • +
  • +
  • Documentation:
      +
    • Added section to ReadMe.htm about building managed custom actions
    • +
  • +
+ +
+

2003-06-02

+ +
    +
  • Msi:
      +
    • Validation didn't work on merge modules - fixed
    • +
  • +
  • CAProxy:
      +
    • Was broken in 2.1 - fixed
    • +
  • +
+ +
+

2003-05-14

+ +
    +
  • Msi:
      +
    • External UI handler didn't survive a garbage collection - fixed
    • +
    • Validation engine was completely broken - now it should work + at least for MSIs which are already mostly valid
    • +
    • Added DynamicLoad property to CustomActionAttribute
      + Usage: set DynamicLoad=false when using XmlSerialization; default is true
    • +
  • +
  • Msi\Package:
      +
    • File extraction and update methods didn't work on merge modules - fixed
    • +
    • Made file update code slightly more robust
    • +
    • Removed hard-reference to the FilePatch assembly - now it is only + loaded if working with binary file patches
    • +
  • +
  • AugmentIL:
      +
    • AugmentIL would crash if some input files had read-only attr - fixed
    • +
    • Made /verbose switch slightly more verbose
    • +
  • +
  • CAProxy:
      +
    • Added support for the DynamicLoad property of CustomActionAttribute
    • +
    • Added MMsiBreak debugging functionality - see doc
    • +
  • +
  • Samples\WiFile:
      +
    • Added /l (list files) switch
    • +
  • +
  • Samples\SampleCAs:
      +
    • In the makefile the comments about debug builds had an error; + Now the sample builds debug packages (correctly) by default.
    • +
  • +
  • Documentation:
      +
    • Wrote AugmentIL.htm describing the AugmentIL tool and its options.
    • +
    • Wrote WiFile.htm describing the WiFile sample tool.
    • +
    • Added section to ReadMe.htm about debugging managed custom actions.
    • +
  • +
+ +
+

2003-03-31

+ +
    +
  • Msi: Implemented the remaining APIs, also minor improvements and bugfixes
      +
    • All published APIs are wrapped, with the exception of four: + MsiGetFileSignatureInformation (because I don't know of a .NET analog + for the returned certificate structure), and 3 APIs for previewing UI
    • +
    • Database.OpenView and Database.Execute* now take String.Format style params
    • +
    • Database.ApplyTransform can optionally use the error-suppression flags + stored in the transform summary info
    • +
    • Added a few supporting enumerations and structures for the remaining APIs
    • +
    • InstallerException gets a descriptive message for any MSI or system error
    • +
    • Fixed a bug in InstallerException which would usually report "error 0"
    • +
    • Added optimization for setting a Record field to a MemoryStream
    • +
    • Record.GetStream is capable of extracting substorages
    • +
    • Moved InstallPath class to Microsoft.WindowsInstaller.Package assembly
    • +
  • +
  • Msi\FilePatch: added new project
      +
    • Binary file patch API wrapper
    • +
  • +
  • Msi\Package: added new project
      +
    • Helper classes for working with MSI and MSP packages
    • +
  • +
  • Cab: some minor bugfixes
      +
    • Cabinet.Extract(stream, name) threw a NullReferenceException if the file + didn't exist in the cab -- now it returns null
    • +
    • CabinetInfo.CompressFileSet() was broken -- fixed
    • +
    • If a Cabinet callback throws an exception, it is propogated as the + inner-exception of the CabinetException
    • +
  • +
  • Samples\WiFile: added new sample
      +
    • Demonstrates some features of InstallPackage class in Msi\Package project
    • +
  • +
+ +
+

2003-03-20

+ +Documentation!
    +
  • Msi and Cab sources include complete C# XML documentation.
  • +
  • Msi and Cab makefiles generate XML documentation files.
  • +
  • Reference CHM compiled from XML documentation with NDoc.
  • +
+ +

I am aware that exceptions are still not documented in most areas. + Other than that, feel free to send me a note if it's still not clear + how to use parts of the API after reading the documentation.

+ +

Version is still 1.1 because there are no code changes in this release.

+ +
+

2003-03-13

+ +
    +
  • Msi: lots of small improvements for usability and consistency
      +
    • Reworked ExternalUIHandler support
    • +
    • Added Installer properties/methods:
        +
      • Components
      • +
      • ComponentClients()
      • +
      • ComponentState()
      • +
      • ComponentPath()
      • +
      • EnableLog()
      • +
    • +
    • Added Session.EvaluateCondition() method
    • +
    • Improved exception-handling in many methods in Installer, Database, + & Session classes
    • +
    • Added extensive XML doc-comments to Installer, Database, View, + & Session classes
    • +
    • A few breaking changes:
        +
      • View.ModifyMode enumeration moved outside View and + renamed ViewModifyMode
      • +
      • InstallLogMode enumeration renamed to InstallLogModes + (naming convention for bitfields)
      • +
      • Record constructor takes arbitrary number of parameters
      • +
    • +
  • +
  • AugmentIL: almost completely rewritten
      +
    • Ildasm/ilasm steps are built-in
        +
      • The round-trip can be done in one step
      • +
      • IL source input/output is still supported
      • +
    • +
    • Never throws an unhandled exception
    • +
    • Organized command-line options, consistent with other .NET tools
    • +
    • Uses a plugin architecture to allow additional augmentations
    • +
  • +
  • CAProxy: Added AIL_CAProxy.cs - AugmentIL plugin generates CA proxy methods
  • + +
  • SampleCAs: Updated makefile for new AugmentIL usage
  • +
+ +
+

2003-01-16

+ +
    +
  • ReadMe.htm: Added section on writing managed CAs
  • +
  • SampleCAs: Added missing reference to System.Windows.Forms.dll to the makefile
  • +
  • AugmentIL: Added specific warning messages for when CA method has wrong signature
  • +
  • Put sources in Toolbox-hosted Source Depot.
  • +
+ +
+

2003-01-14

+Initial posting to http://toolbox + +

 

+
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/installutil.htm b/src/samples/Dtf/Documents/Guide/Content/installutil.htm new file mode 100644 index 00000000..e235a7b6 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/installutil.htm @@ -0,0 +1,94 @@ + + + About InstallUtil + + + + + + +
+ Deployment Tools Foundation
+ About InstallUtil
+
+ + Development Guide > + Managed CAs > + InstallUtil + +
+
+
+ +
+

+ InstallUtil is often considered as another option for executing MSI custom actions + written in managed code. But in most cases it is not the best solution, for a number + of reasons.

+

+ InstallUtil (in either InstallUtil.exe or InstallUtilLib.dll form) is a .NET Framework + tool for executing the System.Configuration.Installer classes that are implemented + in an assembly. That way the assembly can contain any special code required to install + itself and uninstall itself. Essentially it is the .NET replacement for COM self-registration + aka DllRegisterServer.

+

+ Self-reg or System.Configuration.Installer is convenient for development use in + order to test code without creating an actual setup package, or for an IDE which + wants to generate self-installing code. But experienced setup developers and the + Windows Installer documentation + all agree that self-reg is a bad practice for a + production-quality setup. The current theory of state-of-the-art setup is that it + should be as data-driven as possible. That is, the setup package describes as fully + as possible the desired state of the system, and then the installer engine calculates + the necessary actions to install, uninstall, patch, etc.

+

+ S.C.I encourages developers to write code for things such as registering services + or registering COM classes or other things which are more appropriately done using + built-in MSI functionality (the ServiceInstall and Registry tables). The Visual + Studio .NET wizards also tend to generate this kind of install code. Again, that + is nice for development but not good for real installations. You end up with similar + but slightly different code in many places for doing the same thing. And that code + is a black-box to the installer engine.

+

+ An ideal MSI custom action is a logical extension of the setup engine, meaning it + is data-driven and written in a very generic way to read from existing or custom + tables in the MSI database, following a very similar pattern to the built-in actions. + This makes the CA re-usable, and makes the installation more transparent. S.C.I + custom actions invoked by InstallUtil cannot be data-driven because they don't have + full access to the install session or database. They also cannot write to the install + session's regular MSI log, but instead use a separate log which is bad for supportability.

+

+ InstallUtil also requires that the assembly be installed before the CA is able to + execute. This is a problem for CAs that need to execute during the UI phase, or + gather information before installation. For that purpose MSI allows custom action + binaries to be embedded as non-installed files, but InstallUtil cannot make use + of those.

+

+ Custom actions developed with DTF have none of the limitations of InstallUtil, + giving a setup developer full capabilities to write well-designed custom actions, + only now in managed code. +

+ +

 

+
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/inventory.htm b/src/samples/Dtf/Documents/Guide/Content/inventory.htm new file mode 100644 index 00000000..40a6ef74 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/inventory.htm @@ -0,0 +1,78 @@ + + + Windows Installer System Inventory Viewer + + + + + + +
+ Deployment Tools Foundation
+ Windows Installer System Inventory Viewer
+
+ + Development Guide > + Samples > + Inventory + +
+
+
+ +
+

This application shows a hierarchical, relational, searchable + view of all of the product, feature, component, file, and patch + data managed by MSI, for all products installed on the system.

+


+ +

Navigation

+
    +
  1. The tree on the left is self-explanatory.

  2. +
  3. Click on a row-header (grey box on the left side of the + grid) to jump to a table with more details about the item referred + to by that row. For example, clicking on a row-header of a + table that lists components will take you to a table that lists + the files in that component. Not every table has this ability, + but the cursor will turn to a hand shape to indicate when this is + possible.

  4. +
  5. Also you can navigate back and forward through your history + using the buttons in the application or mouse buttons 4 and 5.

  6. +
+


+ +

Searching

+

The search feature is not hard to find. By default, searches + are limited to the current table. However, if you choose to find + "In Subtree" by checking the box, the search will include + the current table as well as all tables under the current location in + the tree. While this can take a long time if there is a lot of + data under the current node, you can stop the search at any time with + the stop button. The search pauses when a match is found, but + clicking "Find" again will continue the same search from that + point (unless you uncheck the "Continue" checkbox or change + the search string).

+ +


+
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/managedcas.htm b/src/samples/Dtf/Documents/Guide/Content/managedcas.htm new file mode 100644 index 00000000..9cce0432 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/managedcas.htm @@ -0,0 +1,53 @@ + + + Managed Custom Actions + + + + + + +
+ Deployment Tools Foundation
+ Managed Custom Actions
+
+ + Development Guide > + Managed CAs + +
+
+
+ + + + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/msihelper.htm b/src/samples/Dtf/Documents/Guide/Content/msihelper.htm new file mode 100644 index 00000000..c1493117 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/msihelper.htm @@ -0,0 +1,59 @@ + + + + Included Components + + + + +
+
+ + + + + +
Managed Libraries for Windows Installer
+
+
+

Helper Classes for Windows Installer Packages

+
+
+
+

Included are some useful helper classes for working with + MSI and MSP packages:

+
    +
  • InstallPackage - extends the Database class to provide powerful + package-based operations such as:

    +
      +
    • direct extraction of files to uncompressed source + path +
    • updating files from uncompressed source path back + into the compressed source for the package (including updating file + metadata) +
    • applying a patch directly to the package +
    • consolidating a package with uncompressed source files or multiple msm-cabs + into a package with a single compressed cabinet
    • +
    +

    +
  • InstallPathMap, InstallPath - represent the directory structure + of an installation package, including file, component, and directory source and target + install paths. Accessible by file, component, or directory keys; searchable by + filename.

    +
  • PatchPackage - allows convenient access to patch properties, + and analysis and extraction of transforms

  • +
+


+

These classes are in the Microsoft.WindowsInstaller.Package.dll assembly.

+


+

See also:

+

The WiFile sample tool demonstrates some usage of the + InstallPackage class.

+


+
+ + diff --git a/src/samples/Dtf/Documents/Guide/Content/msiwrapper.htm b/src/samples/Dtf/Documents/Guide/Content/msiwrapper.htm new file mode 100644 index 00000000..70190ac4 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/msiwrapper.htm @@ -0,0 +1,80 @@ + + + + Included Components + + + + +
+
+ + + + + +
Managed Libraries for Windows Installer
+
+
+

Managed wrapper library for Windows Installer APIs

+
+
+
+

Microsoft.WindowsInstaller.dll is a complete .NET wrapper for the + Windows Installer APIs. It provides a convenient object model that is + comfortable to .NET developers and still familiar to anyone who has used + the MSI scripting object model.

+

Notes

+
    +
  • All published MSI APIs are wrapped, with the exception of four: + MsiGetFileSignatureInformation (because I don't know of a .NET analog for the + returned certificate structure), and three APIs for previewing UI dialogs. + Other than that, you should be able to do everything that you can + do via the C APIs or the COM automation interface.

    +
  • Some parts of this code have never had a formal test + pass, so use at your own risk. But much of the code is exercised daily, used + by the Developer Division Sustaining Engineering team's BRIQS system to build + and test patches. And it has been in use by many other teams for over two + years now with only a few minor fixes, so it can be considered very stable.

    +
  • Despite its official-sounding namespace, this assembly is not officially + sanctioned by the Windows Installer team. But currently there are not any + plans for an official set of managed even in Longhorn, so I don't see a + conflict for now.

  • +
+

Why rewrite it?

+

It is possible to access MSI's COM Automation interfaces via C# and VB.NET. + So why create yet another wrapper? Here are some of my reasons:

+
    +
  • One of the primary things I wanted to be able to do + was write custom actions in C#. The automation interface was not usable in + that case, because there is no way to convert the integer session handle + (received as a parameter to the type 1 custom action function) into a Session + automation object.

    +
  • The automation interface does not provide a way to + specify an external UI handler. Besides external UI, this is also needed + to do validation.

    +
  • The automation interface does not provide a way to + explicitly close handles (other than Views). I ran into this problem when I + wanted to programmatically delete a database that I'd just finished using, but + couldn't because it was still open!

    +
  • Finally, COM Automation is somewhat slower than invoking + the APIs directly.

  • +
+ +


+

See also:

+ +


+
+ + diff --git a/src/samples/Dtf/Documents/Guide/Content/packages.htm b/src/samples/Dtf/Documents/Guide/Content/packages.htm new file mode 100644 index 00000000..aa521685 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/packages.htm @@ -0,0 +1,86 @@ + + + Working with Install Packages + + + + + + +
+ Deployment Tools Foundation
+ Working with Install Packages
+
+ + Development Guide > + Install Packages + +
+
+
+ +
+ +

Updating files in a product layout

+

The InstallPackage class makes it easy to work with files and cabinets + in the context of a compressed or uncompressed product layout.

+

This hypothetical example takes an IDictionary 'files' which maps file keys to file paths. Each + file is to be updated in the package layout; cabinets are even recompressed if necessary to include the new files.

+
    using (InstallPackage pkg = new InstallPackage("d:\builds\product.msi",
+        DatabaseOpenMode.Transact))
+    {
+        pkg.WorkingDirectory = Path.Combine(Path.GetTempFolder(), "pkgtmp");
+        foreach (string fileKey in files.Keys)
+        {
+            string sourceFilePath = files[fileKey];
+            string destFilePath = pkg.Files[fileKey].SourcePath;
+            destFilePath = Path.Combine(pkg.WorkingDirectory, destFilePath);
+            File.Copy(sourceFilePath, destFilePath, true);
+        }
+        pkg.UpdateFiles(new ArrayList(files.Keys));
+        pkg.Commit();
+        Directory.Delete(pkg.WorkingDirectory, true);
+    }

+

1.  Create a new InstallPackage + instance referring to the location of the .msi. This is actually just a specialized subclass of a Database.

+

2.  Set the WorkingDirectory. + This is the root directory where the package expects to find the new source files.

+

3.  Copy each file to its proper location in the working directory. The + InstallPackage.Files + property is used to look up the relative source path of each file.

+

4.  Call InstallPackage.UpdateFiles + with the list of file keys. This will re-compress and package the files if necessary, and also update the + following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart*.

+

5.  Commit the database changes and cleanup the working directory.

+ + +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/powerdiff.htm b/src/samples/Dtf/Documents/Guide/Content/powerdiff.htm new file mode 100644 index 00000000..f420b47e --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/powerdiff.htm @@ -0,0 +1,71 @@ + + + MSI, MSP, CAB Diff Tool + + + + + + +
+ Deployment Tools Foundation
+ MSI, MSP, CAB Diff Tool
+
+ + Development Guide > + Samples > + DDiff + +
+
+
+ +
+

MSI, MSP, CAB Diff Tool

+ +

Usage: DDiff target1 target2 [options]
+Example: DDiff d:\dir1 d:\dir2
+Example: DDiff setup1.msi setup2.msi
+Example: DDiff patch1.msp patch2.msp -patchtarget target.msi
+Example: DDiff package1.cab package2.cab
+
+Options:
+  /o [filename]    Output results to text file (UTF8)
+  /p [package.msi] Diff patches relative to target MSI
+

+


+ +

The following types of inputs can be diffed: +

    +
  • Directories: files and subdirectories are compared.
  • +
  • Cab files: internal file list and files are compared.
  • +
  • MSI/MSM database files: summary info, tables, and embedded binary and cab streams are compared.
  • +
  • MSP files: summary info and embedded file cab are compared. When a patch target MSI is provided, the MSP's tables are also compared.
  • +
  • Versioned files: Win32 file version is compared.
  • +
  • Text files: if diff.exe is in the path, it is used to get a line-by-line diff.
  • +
  • Other files: file size and bytes are compared.
  • +
+ All processing is done recursively. So a versioned file within a cab within an MSI within a directory will have meaningful diff results.

+ +


+
+ +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/samplecas.htm b/src/samples/Dtf/Documents/Guide/Content/samplecas.htm new file mode 100644 index 00000000..4dfed6f0 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/samplecas.htm @@ -0,0 +1,84 @@ + + + Sample C# Custom Action + + + + + + +
+ Deployment Tools Foundation
+ Sample C# Custom Action
+
+ + Development Guide > + Managed CAs > + Writing CAs > + C# Sample + +
+
+
+ +
+ +

MSI custom actions are MUCH easier to write in C# than + in C++!

    [CustomAction]
+    public static ActionResult SampleCustomAction1(Session session)
+    {
+        session.Log("Hello from SampleCA1");
+        
+        string testProp = session["SampleCATest"];
+        string testProp2;
+        testProp2 = (string) session.Database.ExecuteScalar(
+            "SELECT `Value` FROM `Property` WHERE `Property` = 'SampleCATest'");
+        
+        if(testProp == testProp2)
+        {
+            session.Log("Simple property test passed.");
+            return ActionResult.Success;
+        }
+        else
+            return ActionResult.Failure;
+    }
+            
+

A sample CA project with two CAs is included in the + Samples\ManagedCA directory.  Running the CustomActionTest project will package the CA and insert + it into a test MSI. The MSI will invoke the custom actions, but it will not install anything + since the second sample CA returns ActionResult.UserExit. +

+ +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/samples.htm b/src/samples/Dtf/Documents/Guide/Content/samples.htm new file mode 100644 index 00000000..3bcd379a --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/samples.htm @@ -0,0 +1,59 @@ + + + Sample Applications + + + + + + +
+ Deployment Tools Foundation
+ Sample Applications
+
+ + Development Guide > + Samples + +
+
+
+ +
+

Besides the simple managed custom action sample, there are three functional + and useful sample tools included in this distribution:

+

MSI Inventory
+ Shows a hierarchical, relational, searchable view of all of the product, + feature, component, file, and patch data managed by MSI, for all products + installed on the system.

+

WiFile
+ Extracts and updates cabbed files in an MSI setup.

+

CabPack
+ Creates simple self-extracting cab packages. OK, so this one isn't + especially useful as a tool, but the code should be helpful.

+

DDiff
+ Recursively diffs MSI, MSP, CAB, and other files and directories. + Much more thorough than widiffdb.vbs.

+


+
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/support.htm b/src/samples/Dtf/Documents/Guide/Content/support.htm new file mode 100644 index 00000000..89acbadf --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/support.htm @@ -0,0 +1,52 @@ + + + Support/Bugs + + + + + + +
+ Deployment Tools Foundation
+ Support/Bugs
+
+ + Overview > + Support/Bugs + +
+
+
+ +
+

Please send general support questions or comments to the + wix-users discussion list.

+ +

Bugs, suggestions, or feature requests can be submitted at the + WiX project + on Sourceforge.net.

+ +


+
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/using.htm b/src/samples/Dtf/Documents/Guide/Content/using.htm new file mode 100644 index 00000000..6fe960e8 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/using.htm @@ -0,0 +1,50 @@ + + + Deployment Tools Foundation Development Guide + + + + + + +
+ Deployment Tools Foundation
+ Deployment Tools Foundation Development Guide
+
+ + Development Guide + +
+
+
+ + + + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/whatsnew.htm b/src/samples/Dtf/Documents/Guide/Content/whatsnew.htm new file mode 100644 index 00000000..3efe67bd --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/whatsnew.htm @@ -0,0 +1,257 @@ + + + What's New? + + + + + + +
+ Deployment Tools Foundation
+ What's New?
+
+ + Overview > + What's New? + + 2007-07-03 +
+
+
+ +
+ +

Highlights

+
    +
  • New project name name "Deployment Tools Foundation", and + new namespaces WixToolset.Dtf.*

  • +
  • Added ZIP compression library

  • +
  • Added library for reading/writing Win32 resources including file versions

  • +
  • Managed custom action improvements:

      +
    • Simplified authoring and building -- new MakeSfxCA tool + automatically maps DLL entrypoints to CA methods.

    • +
    • Managed custom action DLLs now run in a separate process for + better reliability with respect to CLR versions, but still have + full access to the MSI session.

    • +
  • +
  • Found and fixed many bugs with extensive unit test suite

  • +
  • LINQ to MSI ! (preview)

  • +
+ +

Unfortunately, all these changes do mean that migrating tools and applications from + the previous release can be a moderate amount of work.

+ +

Breaking Changes

+

For the first time since v1.0, this release contains some major breaking + changes, due to a significant redesign and cleanup effort that has been a + long time coming. The overall purpose of the changes is to bring the class + libraries much closer to ship-quality.

+
    +
  • All libraries use a new namespace hierarchy + under WixToolset.Dtf. + The new namespace aligns with the new project name, gives all the various + libraries an identity that makes them obviously related to the DTF project, + and mostly avoids "taking over" a namespace that might be rightfully owned + by the platform technology owner.

  • + +
  • Assemblies are also renamed to follow namespaces.

  • + +
  • A new unified compression framework forms the basis for the new ZIP + library and a redesigned CAB library. Additional archive formats can + be plugged into the framework. The stream-based compression APIs have + been redesigned to be more object-oriented and easier to use. The file-based + APIs are mostly unchanged from the old cabinet library, although some names + have changed in order to fit into the new unified framework.

  • + +
  • Large parts of the WindowsInstaller library have been redesigned + to be more object-oriented and to better follow .NET Framework design + guidelines. And various APIs throughout the library have naming or other + changes for better consistency and to follow conventions and best + pratices as enforced by FxCop.

  • + +
  • The WindowsInstaller APIs no longer make any attempt to mimic the + MSI COM automation interfaces. The naming and object patterns in the + automation interfaces often conflicted with with best practices for + .NET Framework class libraries. Since most people start using DTF + without having ever experienced MSI scripting, there is little + reason to match the scripting object model. Making the APIs more + consistent with .NET conventions will make them much easier to use + for people already experienced with the .NET Framework.

  • + +
  • APIs in all class libraries use generics where appropriate, especially + the generic collection interfaces. This means .NET Framework 2.0 or later + is required.

  • + +
  • The FilePatch library is missing from this release. An updated + and redesigned file-delta library is in development.

  • +
+ +

Other Changes

+
    +
  • New MakeSfxCA tool for building managed custom action packages: In addition to + packaging the CA DLL and dependencies, it automatically detects managed CA methods + and generates corresponding unmanaged DLL entrypoints in the CA host DLL (SfxCA.dll), + where they are called by MSI. Previously it was necessary to either provide this + mapping in a CustomAction.config file, or live with the generic "ManagedCustomActionN" + names when authoring the CustomAction table in the MSI. For more info, see the + help topic on building managed custom actions.

  • + +
  • Out-of-proc managed custom action DLLs: + When a managed custom action runs, it normally requests a specific major + version of the CLR via CustomAction.config. However in the previous implementation, + the request could be ignored if there was already a different version of the CLR + loaded into the MSI process, either from a previous custom action or by some other + means. (The CLR doesn't allow side-by-side versions within the same process.) + While there have been no reports of this issue causing setup failures in practice, + it may be only a matter of time, as new CLR versions keep coming out.

    + +

    The redesigned native host for managed custom actions, SfxCA.dll, re-launches + itself in a separate process before loading the CLR and invoking the managed CA. + This ensures that the desired CLR version is always loaded, assuming it is available + on the system. It also sets up a named-pipe remoting channel between the two processes. + All session-related MSI API calls are routed through that channel, so that the + custom action has full access to the installer session just as if it were + running in-process.

  • + +
  • The new zip compression library supports nearly all features of the zip + file format. This includes the ZIP64 extensions for archives greater than 4GB, + as well as disk-spanning capabilities. Zip encryption is not supported. The zip + library has been tested against a variety of third-party zip tools; please + report any issues with incompatible packages.

    + +

    Currently only the basic DEFLATE compression algorithm is supported + (via System.IO.Compression.DeflateStream), and the compression level is not adjustable + when packing an archive. The zip file format has a mechanism for plugging in arbitrary + compression algorithms, and that capability is exposed: you can provide a Stream object + capable of compressing and decompressing bytes as an alternative to DeflateStream.

  • + +
  • Added support for the few APIs new in MSI 4.0:

    +
      +
    • Installer.GetPatchFileList()
    • +
    • InstallLogModes.RMFilesInUse
    • +
    • ComponentAttributes.DisableRegistryReflection
    • +
    • ControlAttributes.ElevationShield
    • +
     
  • + +
  • The documentation is now built with the + Sandcastle doc build engine, + with help from the Sandcastle + Help File Builder. (The old CHM was built with NDoc.)

  • + +
  • The documentation includes detailed class diagrams for the + WindowsInstaller and Compression namespaces.

  • + +
  • WindowsInstaller API doc topics now link straight to the corresponding + unmanaged MSI API topics in MSDN. If you know an unmanaged MSI API you want to + use but don't know the managed equivalent, you can search for it and find what + managed APIs link to it.

  • + +
  • Unit tests cover about 90% of the Compression, Compression.Zip, and + Compression.Cab assemblies -- basically everything except some rare + error-handling cases.

  • + +
  • Unit tests along with samples cover over 50% of the WindowsInstaller and + WindowsInstaller.Package assemblies (including custom action functionality). More + test cases are still being added.

  • +
+ +

Bugfixes

+

In addition to the extensive cleanup due to redesigns and unit tests, the following + reported bugs have been fixed:

+
    +
  • Managed custom actions could in rare instances fail to load with error 183 + (directory already exists)

  • +
  • Timestamps of files in a cabinet could be incorrectly offset based on the timezone. + (This was due to a behavior change in the DateTime class between .NET 1.1 and 2.0.)

  • +
  • Unicode file paths for cabbed files could be handled incorrectly in some cases

  • +
  • Installer.DetermineApplicablePatches just didn't work

  • +
  • InstallPackage.ApplyPatch couldn't handle applying multiple patches to the same layout

  • +
+ +

LINQ to MSI

+

You'll never want to write MSI SQL again!

+

Language INtegrated Query is a new feature in .NET Framework 3.5 and C# 3.0. Through + a combination of intuitive language syntax and powerful query operations, LINQ provides + a whole new level of productivity for working with data in your code. While the .NET + Framework 3.5 provides LINQ capability for SQL databases and XML data, now you + can write LINQ queries to fetch and even update data in MSI databases!

+ +

Look at the following example:
+ +

    var actions = from a in db.InstallExecuteSequences
+                  join ca in db.CustomActions on a.Action equals ca.Action
+                  where ca.Type == CustomActionTypes.Dll
+                  orderby a.Sequence
+                  select new {
+                      Name = a.Action,
+                      Target = ca.Target,
+                      Sequence = a.Sequence };
+          
+    foreach (var a in actions)
+    {
+        Console.WriteLine(a);
+    }
+    
+ + The query above gets automatically translated to MSI SQL:

+ +

    SELECT `InstallExecuteSequence`.`Action`, + `CustomAction`.`Target`, `InstallExecuteSequence`.`Sequence` FROM `InstallExecuteSequence`, `CustomAction` + WHERE `InstallExecuteSequence`.Action` = `CustomAction`.`Action` ORDER BY `InstallExecuteSequence`.`Sequence`

+ +

But the query is not executed until the foreach + enumeration. Then records are fetched from the results incrementally as the enumeration progresses. + The objects fetched are actually of an anonymous type created there in the query with exactly + the desired fields. So the result of this code will be to print the Action, Target, and Sequence + of all Type 1 custom actions.

+ +

The query functionality is currently limited by the capabilities of the MSI SQL engine. For + example, a query can't use where (ca.Type & + CustomActionTypes.Dll) != 0 because the bitwise-and operator is not supported by + MSI SQL. The preview version of LINQ to MSI will throw an exception for cases like that, but + the eventual goal is to have it automatically move the data and operation outside of MSI when + necessary, so that any arbitrary expressions are supported in the query.

+ +

Note there are no MSI handles (or IDisposables) + to worry about! Handles are all managed internally and closed deterministically. Also, + with the entity object model for common tables, the compiler will tell you if you get a + column name wrong or misspelled. The entity objects even support easy inserting, updating, + and deleting (not shown here).

+ +

For more examples, see the LinqTest project in the source. More documentation + is being written.

+ +

Obviously, LINQ to MSI requires .NET Framework 3.5. Everything else + in DTF requires only .NET Framework 2.0.

+ +

Note: The LINQ functionality in this DTF release is of preview quality only + and should not be used in production. While there are unit tests covering a wide + variety of queries, using advanced queries outside what is covered by the tests + is likely to result in unexpected exceptions, and retrieved data might possibly be + incorrect or incomplete. An updated LINQ to MSI library is in development.

+ +

 

+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/wifile.htm b/src/samples/Dtf/Documents/Guide/Content/wifile.htm new file mode 100644 index 00000000..20998b73 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/wifile.htm @@ -0,0 +1,73 @@ + + + Windows Installer Package File Manipulation Tool + + + + + + +
+ Deployment Tools Foundation
+ Windows Installer Package File Manipulation Tool
+
+ + Development Guide > + Samples > + WiFile + +
+
+
+ +
+

Usage: WiFile.exe package.msi /l [filename,filename2,...]
+Usage: WiFile.exe package.msi /x [filename,filename2,...]
+Usage: WiFile.exe package.msi /u [filename,filename2,...]
+
+Lists (/l), extracts (/x) or updates (/u) files in an MSI or MSM.
+Files are extracted using their source path relative to the package.
+Specified filenames do not include paths.
+Filenames may be a pattern such as *.exe or file?.dll
+

+


+ +

Example

+

The most powerful use of WiFile.exe is to do a round-trip update of files in a + compressed MSI/MSM package. It works like this:

    +
  1. Extract specific file(s) or all files from the package: + WiFile.exe package.msi /x *
  2. +
  3. The files are now expanded into their directory structure. You can edit/update + the files however you like.
  4. +
  5. Update the package with the changed files: WiFile.exe package.msi /u *  + This also updates the file metadata in the MSI including the file version, size, and hash.
  6. +

+


+ +

Notes

+
    +
  • Also works with packages that have multiple and/or external cab(s).

  • +
+ +


+
+ +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/Content/writingcas.htm b/src/samples/Dtf/Documents/Guide/Content/writingcas.htm new file mode 100644 index 00000000..6beccf5f --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/Content/writingcas.htm @@ -0,0 +1,114 @@ + + + Writing Managed Custom Actions + + + + + + +
+ Deployment Tools Foundation
+ Writing Managed Custom Actions
+
+ + Development Guide > + Managed CAs > + Writing CAs + +
+
+
+ +
+

Caveats

+

Before choosing to write a custom action in managed code instead of + traditional native C++ code, you should carefully consider the following:

+
    +
  • Obviously, it introduces a dependency on the .NET Framework. Your + MSI package should probably have a LaunchCondition to check for the presence + of the correct version of the .NET Framework before anything else happens.

  • +
  • If the custom action runs at uninstall time, then even the uninstall of + your product may fail if the .NET Framework is not present. This means a + user could run into a problem if they uninstall the .NET Framework before + your product.

  • +
  • A managed custom action should be configured to run against a specific + version of the .NET Framework, and that version should match the version your + actual product runs against. Allowing the version to "float" to the latest + installed .NET Framework is likely to lead to compatibility problems with + future versions. The .NET Framework provides side-by-side functionality for + good reason -- use it.

  • + +
+


+

How To

+
    +
  • A custom action function needs to be declared as + public static (aka Public Shared in VB.NET). It takes one parameter which is + a Session object, and returns a + ActionResult enumeration.

    +
        [CustomAction]
    +    public static ActionResult MyCustomAction(Session session)

    +
  • The function must have a + CustomActionAttribute, which enables it to be + linked to a proxy function. The attribute can take an optional + "name" parameter, which is the name of the entrypoint + that is exported from the custom action DLL.

    +
  • Fill in MSI CustomAction table entries just like you + would for a normal type 1 native-DLL CA. Managed CAs can also work just + as well in deferred, rollback, and commit modes.

    +
  • If the custom action function throws any kind of + Exception that isn't handled internally, then it will be caught by the proxy + function. The Exception message and stack trace will be printed to the + MSI log if logging is enabled, and the CA will return a failure code.

    +
  • To be technically correct, any MSI handles obtained should be + closed before a custom action function exits -- otherwise a warning + gets printed to the log. The handle classes in the managed library + (Database, + View, + Record, + SummaryInfo) + all implement the IDisposable interface, + which makes them easily managed with C#'s using + statement. Alternatively, they can be closed in a finally block. + As a general rule, methods return new handle objects that should be + managed and closed by the user code, while properties only return a reference + to a prexisting handle object.

  • +
  • Don't forget to use a CustomAction.config file to + specify what version of the .NET Framework the custom action should run against.

  • +
+ +


+

See also:

+ +


+ +
+ + +
+ + + diff --git a/src/samples/Dtf/Documents/Guide/DTF.hhc b/src/samples/Dtf/Documents/Guide/DTF.hhc new file mode 100644 index 00000000..bf43e447 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/DTF.hhc @@ -0,0 +1,132 @@ + + + + + +
    +
  • + + + +
      +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    +
  • +
  • + + + +
      +
    • + + + +
        +
      • + + + +
          +
        • + + + +
        • +
        • + + + +
        • +
        +
      • +
      • + + + +
      • +
      • + + + +
      • +
      • + + + +
      • +
      +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
        +
      • + + + +
      • +
      • + + + +
      • +
      • + + + +
      • +
      • + + + +
      • +
      +
    • +
    +
  • +
  • + + + + + + +
  • +
+ + diff --git a/src/samples/Dtf/Documents/Guide/DTF.hhk b/src/samples/Dtf/Documents/Guide/DTF.hhk new file mode 100644 index 00000000..bc6e49b3 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/DTF.hhk @@ -0,0 +1,126 @@ + + + + + +
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
      +
    • + + + +
    • +
    • + + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
+ diff --git a/src/samples/Dtf/Documents/Guide/DTF.hhp b/src/samples/Dtf/Documents/Guide/DTF.hhp new file mode 100644 index 00000000..e9b8ad90 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/DTF.hhp @@ -0,0 +1,49 @@ +[OPTIONS] +Auto Index=Yes +Compatibility=1.1 or later +Compiled file=DTF.chm +Contents file=DTF.hhc +Default Window=DTF +Default topic=Content\about.htm +Display compile progress=Yes +Error log file=DTFhelp.log +Full-text search=Yes +Index file=DTF.hhk +Language=0x409 English (United States) +Title=Deployment Tools Foundation + +[WINDOWS] +DTF="Deployment Tools Foundation","DTF.hhc","DTF.hhk","Content\about.htm","Content\about.htm",,,,,0x22520,,0x384e,[143,72,937,601],,,,,,,0 + + +[FILES] +styles\presentation.css +Content\about.htm +Content\whatsnew.htm +Content\dependencies.htm +Content\using.htm +DTF.hhc +DTF.hhk +Content\samples.htm +Content\support.htm +Content\history.htm +Content\inventory.htm +Content\wifile.htm +Content\cabpack.htm +Content\powerdiff.htm +Content\cabs.htm +Content\databases.htm +Content\packages.htm +Content\managedcas.htm +Content\samplecas.htm +Content\writingcas.htm +Content\buildingcas.htm +Content\debuggingcas.htm +Content\caproxy.htm +Content\caconfig.htm +Content\installutil.htm + +[MERGE FILES] +DTFAPI.chm + +[INFOTYPES] diff --git a/src/samples/Dtf/Documents/Guide/dtfguide.helpproj b/src/samples/Dtf/Documents/Guide/dtfguide.helpproj new file mode 100644 index 00000000..4df2765d --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/dtfguide.helpproj @@ -0,0 +1,29 @@ + + + + + + + {3CFD8620-B41C-470C-ABEF-9D38076A2A8D} + dtf + + + + + + + + + + $(OutputPath)DTFAPI.chm + + + + + + false + + + + + diff --git a/src/samples/Dtf/Documents/Guide/styles/presentation.css b/src/samples/Dtf/Documents/Guide/styles/presentation.css new file mode 100644 index 00000000..b71c8582 --- /dev/null +++ b/src/samples/Dtf/Documents/Guide/styles/presentation.css @@ -0,0 +1,394 @@ + +/* page style */ + +body { + margin: 0; + background-color: #FFFFFF; + padding: 0; + font-size: 80%; + font-family: verdana, sans-serif; + color: #000000; +} + +table { + /* this is a trick to force tables to inherit the body font size */ + font-size: 100%; +} + +/* non-scrolling (control) region style */ + +div#control { + margin: 0; + background-color: #D4DFFF; + padding: 4px; + width: 100%; + border-bottom-color: #C8CDDE; + border-bottom-style: solid; + border-bottom-width: 1; +} + +span.productTitle { + font-size: 80%; +} + +span.topicTitle { + font-size: 140%; + font-weight: bold; + color: #003399; +} + +span#chickenFeet { + float: left; +} + +span#languageFilter { + float: right; +} + +/* scrolling (content) region style */ + +div#main { + margin: 0; + padding: 1em; + width: 100%; + clear: both; +} + +/* sections */ + +div#header { + font-size: 70%; + color: #666666; + margin-bottom: 0.5em; +} + +div.section { + margin-bottom: 1em; +} + +div.sectionTitle { + display: inline; + font-size: 120%; + font-weight: bold; + color: #003399; +} + +div.sectionContent { + margin-top: 0.2em; +} + +span.subsectionTitle { + font-weight: bold; +} + +div#footer { + margin-top: 1em; + border-top: thin solid #003399; + padding-top: 0.5em; +} + +/* authored content (block) */ + +p { + margin-top: 0; + margin-bottom: 1em; +} + +dl { + margin-top: 0; + margin-bottom: 1em; +} + +div.code { + clear: both; + width: 100%; + background: #F7F7FF; + padding: 0.4em; + font-family: "Andale Mono"; + /* font-family: "Courier New"; */ + /* font-family: "This is not a monospace font", monospace; */ + font-size: inherit; + margin-bottom: 1em; +} + +pre { + margin: 0; + padding: 0; +} + +table.authoredTable { + table-layout: fixed; + width: 100%; + margin-bottom: 1em; +} + +table.authoredTable th { + border-bottom-color: #C8CDDE; + border-bottom-style: solid; + border-bottom-width: 1; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; +} + +table.authoredTable td { + border-bottom-style: solid; + border-bottom-color: #C8CDDE; + border-bottom-width: 1px; + background: #F7F7FF; + padding: 0.2em; + vertical-align: top; +} + +div.alert { + border: 1px solid #C8CDDE; + background: #F7F7FF; +} + +div.media { + text-align: center; + margin-bottom: 1em; +} + + +/* authored content (inline) */ + +span.keyword { + font-weight: bold; +} + +span.code { + font-family: "Andale Mono", "Courier New", Courier, monospace; +} + +/* auto-generated controls */ + +div.langTabs { + width: 100%; +} + +div.langTab { + float: left; + width: 16%; + border-top: 1px solid #C8CDDE; + border-left: 1px solid #C8CDDE; + border-right: 1px solid #C8CDDE; + background: #F7F7FF; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: normal; +} + +div.activeLangTab { + float: left; + width: 16%; + border-top: 1px solid #C8CDDE; + border-left: 1px solid #C8CDDE; + border-right: 1px solid #C8CDDE; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; +} + +table.members { + table-layout: fixed; + width: 100%; +} + +table.members th.iconColumn { + width: 60px; +} + +table.members th.nameColumn { + width: 33%; +} + +table.members th.descriptionColumn { + width: 66%; +} + +table.members th { + border-bottom-color: #C8CDDE; + border-bottom-style: solid; + border-bottom-width: 1; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; +} + +table.members td { + border-bottom-style: solid; + border-bottom-color: #C8CDDE; + border-bottom-width: 1px; + background: #F7F7FF; + padding: 0.2em; + vertical-align: top; + overflow: hidden; +} + +table.exceptions { + table-layout: fixed; + width: 100%; +} + + +table.exceptions th.exceptionNameColumn { + width: 33%; +} + +table.exceptions th.exceptionConditionColumn { + width: 66%; +} + +table.exceptions th { + border-bottom-color: #C8CDDE; + border-bottom-style: solid; + border-bottom-width: 1; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; +} + +table.exceptions td { + border-bottom-style: solid; + border-bottom-color: #C8CDDE; + border-bottom-width: 1px; + background: #F7F7FF; + padding: 0.2em; + vertical-align: top; +} + +table.permissions { + table-layout: fixed; + width: 100%; +} + + +table.permissions th.permissionNameColumn { + width: 33%; +} + +table.permissions th.permissionConditionColumn { + width: 66%; +} + +table.permissions th { + border-bottom-color: #C8CDDE; + border-bottom-style: solid; + border-bottom-width: 1; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; +} + +table.permissions td { + border-bottom-style: solid; + border-bottom-color: #C8CDDE; + border-bottom-width: 1px; + background: #F7F7FF; + padding: 0.2em; + vertical-align: top; +} + +span.obsolete { + color: red; +} + +span.cs { + display: inline; +} + +span.vb { + display: none; +} + +span.cpp { + display: none; +} +/* syntax styling */ + +div.code span.identifier { + font-size: 120%; + font-weight: bold; +} + +div.code span.keyword { + color: green; +} + +div.code span.parameter { + font-style: italic; + color: purple; +} + +div.code span.literal { + color: purple; +} + +div.code span.comment { + color: red; +} + +span.foreignPhrase { + font-style: italic; +} + +span.placeholder { + font-style: italic; +} + +a { + color: blue; + font-weight: bold; + text-decoration: none; +} + +MSHelp\:link { + color: blue; + font-weight: bold; + hoverColor: #3366ff; +} + +span.nolink { + font-weight: bold; +} + +table.filter { + table-layout: fixed; +} + +tr.tabs td.tab { + width: 10em; + background: #F7F7FF; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: normal; + overflow: hidden; + cursor: pointer; +} + +tr.tabs td.activeTab { + width: 10em; + background: #EFEFF7; + padding: 0.2em; + text-align: left; + color: #000066; + font-weight: bold; + overflow: hidden; +} + +td.line { + background: #EFEFF7; +} diff --git a/src/samples/Dtf/Documents/Reference/Compression.htm b/src/samples/Dtf/Documents/Reference/Compression.htm new file mode 100644 index 00000000..7782bea1 --- /dev/null +++ b/src/samples/Dtf/Documents/Reference/Compression.htm @@ -0,0 +1,13 @@ + + + Class Diagram: WixToolset.Dtf.Compression + + + +

WixToolset.Dtf.Compression Namespace

+ + + + + + diff --git a/src/samples/Dtf/Documents/Reference/Compression1.png b/src/samples/Dtf/Documents/Reference/Compression1.png new file mode 100644 index 00000000..5b2e177f Binary files /dev/null and b/src/samples/Dtf/Documents/Reference/Compression1.png differ diff --git a/src/samples/Dtf/Documents/Reference/Compression2.png b/src/samples/Dtf/Documents/Reference/Compression2.png new file mode 100644 index 00000000..394a5f18 Binary files /dev/null and b/src/samples/Dtf/Documents/Reference/Compression2.png differ diff --git a/src/samples/Dtf/Documents/Reference/WindowsInstaller.htm b/src/samples/Dtf/Documents/Reference/WindowsInstaller.htm new file mode 100644 index 00000000..28990ce4 --- /dev/null +++ b/src/samples/Dtf/Documents/Reference/WindowsInstaller.htm @@ -0,0 +1,14 @@ + + + Class Diagram: WixToolset.Dtf.WindowsInstaller + + + +

WixToolset.Dtf.WindowsInstaller Namespace

+ + + + + + + diff --git a/src/samples/Dtf/Documents/Reference/WindowsInstaller1.png b/src/samples/Dtf/Documents/Reference/WindowsInstaller1.png new file mode 100644 index 00000000..cc769cc7 Binary files /dev/null and b/src/samples/Dtf/Documents/Reference/WindowsInstaller1.png differ diff --git a/src/samples/Dtf/Documents/Reference/WindowsInstaller2.png b/src/samples/Dtf/Documents/Reference/WindowsInstaller2.png new file mode 100644 index 00000000..0c11e501 Binary files /dev/null and b/src/samples/Dtf/Documents/Reference/WindowsInstaller2.png differ diff --git a/src/samples/Dtf/Documents/Reference/WindowsInstaller3.png b/src/samples/Dtf/Documents/Reference/WindowsInstaller3.png new file mode 100644 index 00000000..68acd7d8 Binary files /dev/null and b/src/samples/Dtf/Documents/Reference/WindowsInstaller3.png differ diff --git a/src/samples/Dtf/Documents/Reference/dtfref.shfbproj b/src/samples/Dtf/Documents/Reference/dtfref.shfbproj new file mode 100644 index 00000000..e45d2a07 --- /dev/null +++ b/src/samples/Dtf/Documents/Reference/dtfref.shfbproj @@ -0,0 +1,75 @@ + + + + + + + {27C20359-3910-423D-8058-6403935B98C6} + + Documentation + + + 1.9.9.0 + DTFAPI + Namespace, TypeParameter + InheritedMembers, InheritedFrameworkMembers, Protected, ProtectedInternalAsProtected, SealedProtected + + Deployment Tools Foundation Namespaces + Deployment Tools Foundation + wix-users%40lists.sourceforge.net + &lt%3bscript src=&quot%3bhelplink.js&quot%3b&gt%3b&lt%3b/script&gt%3b + Prototype + MemberName + .NET Framework 3.5 + + + + + + + Framework for archive packing and unpacking. + Implements cabinet archive packing and unpacking. + Implements zip archive packing and unpacking. + Classes for reading and writing resource data in executable files. + Complete class library for the Windows Installer APIs. + LINQ extensions for querying Windows Installer databases (experimental). + + Extended classes for working with Windows Installer installation and patch packages. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/Documents/Reference/helplink.js b/src/samples/Dtf/Documents/Reference/helplink.js new file mode 100644 index 00000000..a4989824 --- /dev/null +++ b/src/samples/Dtf/Documents/Reference/helplink.js @@ -0,0 +1,184 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +FixHelpLinks(); + +function GetHelpCode(apiName) +{ + switch (apiName.toLowerCase()) + { + case "msiadvertiseproduct": return 370056; + case "msiadvertiseproductex": return 370057; + case "msiapplymultiplepatches": return 370059; + case "msiapplypatch": return 370060; + case "msibegintransaction": return 736312; + case "msiclosehandle": return 370067; + case "msicollectuserinfo": return 370068; + case "msiconfigurefeature": return 370069; + case "msiconfigureproduct": return 370070; + case "msiconfigureproductex": return 370071; + case "msicreaterecord": return 370072; + case "msicreatetransformsummaryinfo": return 370073; + case "msidatabaseapplytransform": return 370074; + case "msidatabasecommit": return 370075; + case "msidatabaseexport": return 370076; + case "msidatabasegeneratetransform": return 370077; + case "msidatabasegetprimarykeys": return 370078; + case "msidatabaseimport": return 370079; + case "msidatabaseistablepersistent": return 370080; + case "msidatabasemerge": return 370081; + case "msidatabaseopenview": return 370082; + case "msidetermineapplicablepatches": return 370084; + case "msideterminepatchsequence": return 370085; + case "msidoaction": return 370090; + case "msienablelog": return 370091; + case "msiendtransaction": return 736318; + case "msienumclients": return 370094; + case "msienumcomponentcosts": return 370095; + case "msienumcomponentqualifiers": return 370096; + case "msienumcomponents": return 370097; + case "msienumfeatures": return 370098; + case "msienumpatches": return 370099; + case "msienumpatchesex": return 370100; + case "msienumproducts": return 370101; + case "msienumproductsex": return 370102; + case "msienumrelatedproducts": return 370103; + case "msievaluatecondition": return 370104; + case "msiextractpatchxmldata": return 370105; + case "msiformatrecord": return 370109; + case "msigetactivedatabase": return 370110; + case "msigetcomponentpath": return 370112; + case "msigetcomponentstate": return 370113; + case "msigetdatabasestate": return 370114; + case "msigetfeaturecost": return 370115; + case "msigetfeatureinfo": return 370116; + case "msigetfeaturestate": return 370117; + case "msigetfeatureusage": return 370118; + case "msigetfeaturevalidstates": return 370119; + case "msigetfilehash": return 370120; + case "msigetfileversion": return 370122; + case "msigetlanguage": return 370123; + case "msigetlasterrorrecord": return 370124; + case "msigetmode": return 370125; + case "msigetpatchfilelist": return 370126; + case "msigetpatchinfo": return 370127; + case "msigetpatchinfoex": return 370128; + case "msigetproductcode": return 370129; + case "msigetproductinfo": return 370130; + case "msigetproductinfoex": return 370131; + case "msigetproductinfofromscript": return 370132; + case "msigetproductproperty": return 370133; + case "msigetproperty": return 370134; + case "msigetshortcuttarget": return 370299; + case "msigetsourcepath": return 370300; + case "msigetsummaryinformation": return 370301; + case "msigettargetpath": return 370303; + case "msiinstallmissingcomponent": return 370311; + case "msiinstallmissingfile": return 370313; + case "msiinstallproduct": return 370315; + case "msijointransaction": return 736319; + case "msilocatecomponent": return 370320; + case "msinotifysidchange": return 370328; + case "msiopendatabase": return 370338; + case "msiopenpackage": return 370339; + case "msiopenpackageex": return 370340; + case "msiopenproduct": return 370341; + case "msiprocessadvertisescript": return 370353; + case "msiprocessmessage": return 370354; + case "msiprovideassembly": return 370355; + case "msiprovidecomponent": return 370356; + case "msiprovidequalifiedcomponent": return 370357; + case "msiprovidequalifiedcomponentex":return 370358; + case "msiquerycomponnetstate": return 370360; + case "msiqueryfeaturestate": return 370361; + case "msiqueryfeaturestateex": return 370362; + case "msiqueryproductstate": return 370363; + case "msirecordcleardata": return 370364; + case "msirecorddatasize": return 370365; + case "msirecordgetfieldcount": return 370366; + case "msirecordgetinteger": return 370367; + case "msirecordgetstring": return 370368; + case "msirecordisnull": return 370369; + case "msirecordreadstream": return 370370; + case "msirecordsetinteger": return 370371; + case "msirecordsetstream": return 370372; + case "msirecordsetstring": return 370373; + case "msireinstallfeature": return 370374; + case "msireinstallproduct": return 370375; + case "msiremovepatches": return 370376; + case "msisequence": return 370382; + case "msisetcomponentstate": return 370383; + case "msisetexternalui": return 370384; + case "msisetexternaluirecord": return 370385; + case "msisetfeatureattributes": return 370386; + case "msisetfeaturestate": return 370387; + case "msisetinstalllevel": return 370388; + case "msisetinternalui": return 370389; + case "msisetmode": return 370390; + case "msisetproperty": return 370391; + case "msisettargetpath": return 370392; + case "msisourcelistaddmediadisk": return 370394; + case "msisourcelistaddsource": return 370395; + case "msisourcelistaddsourceex": return 370396; + case "msisourcelistclearall": return 370397; + case "msisourcelistclearallex": return 370398; + case "msisourcelistclearmediadisk": return 370399; + case "msisourcelistclearsource": return 370401; + case "msisourcelistenummediadisks": return 370402; + case "msisourcelistenumsources": return 370403; + case "msisourcelistforceresolution": return 370404; + case "msisourcelistforceresolutionex":return 370405; + case "msisourcelistgetinfo": return 370406; + case "msisourcelistsetinfo": return 370407; + case "msisummaryinfogetproperty": return 370409; + case "msisummaryinfopersist": return 370490; + case "msisummaryinfosetproperty": return 370491; + case "msiusefeature": return 370502; + case "msiusefeatureex": return 370503; + case "msiverifydiskspace": return 370506; + case "msiverifypackage": return 370508; + case "msiviewexecute": return 370513; + case "msiviewfetch": return 370514; + case "msiviewgetcolumninfo": return 370516; + case "msiviewgeterror": return 370518; + case "msiviewmodify": return 370519; + case "productid": return 370855; + default: + return 0; + } +} + +function GetHelpLink(apiName) +{ + var helpCode = GetHelpCode(apiName); + if (helpCode != 0) + { + // Found a direct link! + var prefix = (helpCode < 500000 ? "aa" : "bb"); + return "http://msdn2.microsoft.com/en-us/library/" + prefix + helpCode + ".aspx"; + } + else + { + // This link works, but goes through an annoying 5-sec redirect page. + return "http://msdn.microsoft.com/library/en-us/msi/setup/" + apiName.toLowerCase() + ".asp"; + } +} + +// Change any MSI API help links from indirect MSDN references to direct references. +function FixHelpLinks() +{ + var msiLinkRegex = /msdn\.microsoft\.com\/library\/en-us\/msi\/setup\/([a-z]+)\.asp/i; + var links = document.body.all.tags("a"); + var i; + for (i = 0; i < links.length; i++) + { + var linkElem = links(i); + var match = msiLinkRegex.exec(linkElem.href); + if (match) + { + var apiName = match[1]; + linkElem.href = GetHelpLink(apiName); + linkElem.target = "_blank"; + linkElem.title = "MSDN Library"; + } + } +} diff --git a/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs b/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs new file mode 100644 index 00000000..7a2fa039 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System.Reflection; + +[assembly: AssemblyDescription("Sample managed embedded external UI")] diff --git a/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj b/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj new file mode 100644 index 00000000..e4c52a26 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/EmbeddedUI.csproj @@ -0,0 +1,56 @@ + + + + + {864B8C50-7895-4485-AC89-900D86FD8C0D} + Library + WixToolset.Dtf.Samples.EmbeddedUI + WixToolset.Dtf.Samples.EmbeddedUI + v3.5 + 512 + + + + + + + SetupWizard.xaml + + + + + MSBuild:Compile + Designer + + + + + 3.0 + + + 3.0 + + + + 3.5 + + + + 3.0 + + + + + {24121677-0ed0-41b5-833f-1b9a18e87bf4} + WixToolset.Dtf.WindowsInstaller + + + + + + + diff --git a/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs b/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs new file mode 100644 index 00000000..df77e106 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/InstallProgressCounter.cs @@ -0,0 +1,176 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Samples.EmbeddedUI +{ + using System; + using WixToolset.Dtf.WindowsInstaller; + + /// + /// Tracks MSI progress messages and converts them to usable progress. + /// + public class InstallProgressCounter + { + private int total; + private int completed; + private int step; + private bool moveForward; + private bool enableActionData; + private int progressPhase; + private double scriptPhaseWeight; + + public InstallProgressCounter() : this(0.3) + { + } + + public InstallProgressCounter(double scriptPhaseWeight) + { + if (!(0 <= scriptPhaseWeight && scriptPhaseWeight <= 1)) + { + throw new ArgumentOutOfRangeException("scriptPhaseWeight"); + } + + this.scriptPhaseWeight = scriptPhaseWeight; + } + + /// + /// Gets a number between 0 and 1 that indicates the overall installation progress. + /// + public double Progress { get; private set; } + + public void ProcessMessage(InstallMessage messageType, Record messageRecord) + { + // This MSI progress-handling code was mostly borrowed from burn and translated from C++ to C#. + + switch (messageType) + { + case InstallMessage.ActionStart: + if (this.enableActionData) + { + this.enableActionData = false; + } + break; + + case InstallMessage.ActionData: + if (this.enableActionData) + { + if (this.moveForward) + { + this.completed += this.step; + } + else + { + this.completed -= this.step; + } + + this.UpdateProgress(); + } + break; + + case InstallMessage.Progress: + this.ProcessProgressMessage(messageRecord); + break; + } + } + + private void ProcessProgressMessage(Record progressRecord) + { + // This MSI progress-handling code was mostly borrowed from burn and translated from C++ to C#. + + if (progressRecord == null || progressRecord.FieldCount == 0) + { + return; + } + + int fieldCount = progressRecord.FieldCount; + int progressType = progressRecord.GetInteger(1); + string progressTypeString = String.Empty; + switch (progressType) + { + case 0: // Master progress reset + if (fieldCount < 4) + { + return; + } + + this.progressPhase++; + + this.total = progressRecord.GetInteger(2); + if (this.progressPhase == 1) + { + // HACK!!! this is a hack courtesy of the Windows Installer team. It seems the script planning phase + // is always off by "about 50". So we'll toss an extra 50 ticks on so that the standard progress + // doesn't go over 100%. If there are any custom actions, they may blow the total so we'll call this + // "close" and deal with the rest. + this.total += 50; + } + + this.moveForward = (progressRecord.GetInteger(3) == 0); + this.completed = (this.moveForward ? 0 : this.total); // if forward start at 0, if backwards start at max + this.enableActionData = false; + + this.UpdateProgress(); + break; + + case 1: // Action info + if (fieldCount < 3) + { + return; + } + + if (progressRecord.GetInteger(3) == 0) + { + this.enableActionData = false; + } + else + { + this.enableActionData = true; + this.step = progressRecord.GetInteger(2); + } + break; + + case 2: // Progress report + if (fieldCount < 2 || this.total == 0 || this.progressPhase == 0) + { + return; + } + + if (this.moveForward) + { + this.completed += progressRecord.GetInteger(2); + } + else + { + this.completed -= progressRecord.GetInteger(2); + } + + this.UpdateProgress(); + break; + + case 3: // Progress total addition + this.total += progressRecord.GetInteger(2); + break; + } + } + + private void UpdateProgress() + { + if (this.progressPhase < 1 || this.total == 0) + { + this.Progress = 0; + } + else if (this.progressPhase == 1) + { + this.Progress = this.scriptPhaseWeight * Math.Min(this.completed, this.total) / this.total; + } + else if (this.progressPhase == 2) + { + this.Progress = this.scriptPhaseWeight + + (1 - this.scriptPhaseWeight) * Math.Min(this.completed, this.total) / this.total; + } + else + { + this.Progress = 1; + } + } + } +} diff --git a/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs b/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs new file mode 100644 index 00000000..9b26bef5 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SampleEmbeddedUI.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Samples.EmbeddedUI +{ + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Threading; + using System.Windows; + using System.Windows.Threading; + using WixToolset.Dtf.WindowsInstaller; + using Application = System.Windows.Application; + + public class SampleEmbeddedUI : IEmbeddedUI + { + private Thread appThread; + private Application app; + private SetupWizard setupWizard; + private ManualResetEvent installStartEvent; + private ManualResetEvent installExitEvent; + + /// + /// Initializes the embedded UI. + /// + /// Handle to the installer which can be used to get and set properties. + /// The handle is only valid for the duration of this method call. + /// Path to the directory that contains all the files from the MsiEmbeddedUI table. + /// On entry, contains the current UI level for the installation. After this + /// method returns, the installer resets the UI level to the returned value of this parameter. + /// True if the embedded UI was successfully initialized; false if the installation + /// should continue without the embedded UI. + /// The installation was canceled by the user. + /// The embedded UI failed to initialize and + /// causes the installation to fail. + public bool Initialize(Session session, string resourcePath, ref InstallUIOptions internalUILevel) + { + if (session != null) + { + if ((internalUILevel & InstallUIOptions.Full) != InstallUIOptions.Full) + { + // Don't show custom UI when the UI level is set to basic. + return false; + + // An embedded UI could display an alternate dialog sequence for reduced or + // basic modes, but it's not implemented here. We'll just fall back to the + // built-in MSI basic UI. + } + + if (String.Equals(session["REMOVE"], "All", StringComparison.OrdinalIgnoreCase)) + { + // Don't show custom UI when uninstalling. + return false; + + // An embedded UI could display an uninstall wizard, it's just not imlemented here. + } + } + + // Start the setup wizard on a separate thread. + this.installStartEvent = new ManualResetEvent(false); + this.installExitEvent = new ManualResetEvent(false); + this.appThread = new Thread(this.Run); + this.appThread.SetApartmentState(ApartmentState.STA); + this.appThread.Start(); + + // Wait for the setup wizard to either kickoff the install or prematurely exit. + int waitResult = WaitHandle.WaitAny(new WaitHandle[] { this.installStartEvent, this.installExitEvent }); + if (waitResult == 1) + { + // The setup wizard set the exit event instead of the start event. Cancel the installation. + throw new InstallCanceledException(); + } + else + { + // Start the installation with a silenced internal UI. + // This "embedded external UI" will handle message types except for source resolution. + internalUILevel = InstallUIOptions.NoChange | InstallUIOptions.SourceResolutionOnly; + return true; + } + } + + /// + /// Processes information and progress messages sent to the user interface. + /// + /// Message type. + /// Record that contains message data. + /// Message box buttons. + /// Message box icon. + /// Message box default button. + /// Result of processing the message. + public MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, + MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton) + { + // Synchronously send the message to the setup wizard window on its thread. + object result = this.setupWizard.Dispatcher.Invoke(DispatcherPriority.Send, + new Func(delegate() + { + return this.setupWizard.ProcessMessage(messageType, messageRecord, buttons, icon, defaultButton); + })); + return (MessageResult) result; + } + + /// + /// Shuts down the embedded UI at the end of the installation. + /// + /// + /// If the installation was canceled during initialization, this method will not be called. + /// If the installation was canceled or failed at any later point, this method will be called at the end. + /// + public void Shutdown() + { + // Wait for the user to exit the setup wizard. + this.setupWizard.Dispatcher.BeginInvoke(DispatcherPriority.Normal, + new Action(delegate() + { + this.setupWizard.EnableExit(); + })); + this.appThread.Join(); + } + + /// + /// Creates the setup wizard and runs the application thread. + /// + private void Run() + { + this.app = new Application(); + this.setupWizard = new SetupWizard(this.installStartEvent); + this.setupWizard.InitializeComponent(); + this.app.Run(this.setupWizard); + this.installExitEvent.Set(); + } + } +} diff --git a/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml new file mode 100644 index 00000000..a43059e8 --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs new file mode 100644 index 00000000..b25b8a9e --- /dev/null +++ b/src/samples/Dtf/EmbeddedUI/SetupWizard.xaml.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Samples.EmbeddedUI +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Windows; + using System.Windows.Controls; + using System.Windows.Data; + using System.Windows.Documents; + using System.Windows.Input; + using System.Windows.Media; + using System.Windows.Media.Imaging; + using System.Windows.Navigation; + using System.Windows.Shapes; + using WixToolset.Dtf.WindowsInstaller; + + /// + /// Interaction logic for SetupWizard.xaml + /// + public partial class SetupWizard : Window + { + private ManualResetEvent installStartEvent; + private InstallProgressCounter progressCounter; + private bool canceled; + + public SetupWizard(ManualResetEvent installStartEvent) + { + this.installStartEvent = installStartEvent; + this.progressCounter = new InstallProgressCounter(0.5); + } + + public MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, + MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton) + { + try + { + this.progressCounter.ProcessMessage(messageType, messageRecord); + this.progressBar.Value = this.progressBar.Minimum + + this.progressCounter.Progress * (this.progressBar.Maximum - this.progressBar.Minimum); + this.progressLabel.Content = "" + (int) Math.Round(100 * this.progressCounter.Progress) + "%"; + + switch (messageType) + { + case InstallMessage.Error: + case InstallMessage.Warning: + case InstallMessage.Info: + string message = String.Format("{0}: {1}", messageType, messageRecord); + this.LogMessage(message); + break; + } + + if (this.canceled) + { + this.canceled = false; + return MessageResult.Cancel; + } + } + catch (Exception ex) + { + this.LogMessage(ex.ToString()); + this.LogMessage(ex.StackTrace); + } + + return MessageResult.OK; + } + + private void LogMessage(string message) + { + this.messagesTextBox.Text += Environment.NewLine + message; + this.messagesTextBox.ScrollToEnd(); + } + + internal void EnableExit() + { + this.progressBar.Visibility = Visibility.Hidden; + this.progressLabel.Visibility = Visibility.Hidden; + this.cancelButton.Visibility = Visibility.Hidden; + this.exitButton.Visibility = Visibility.Visible; + } + + private void installButton_Click(object sender, RoutedEventArgs e) + { + this.installButton.Visibility = Visibility.Hidden; + this.progressBar.Visibility = Visibility.Visible; + this.progressLabel.Visibility = Visibility.Visible; + this.installStartEvent.Set(); + } + + private void exitButton_Click(object sender, RoutedEventArgs e) + { + this.Close(); + } + + private void cancelButton_Click(object sender, RoutedEventArgs e) + { + if (this.installButton.Visibility == Visibility.Visible) + { + this.Close(); + } + else + { + this.canceled = true; + this.cancelButton.IsEnabled = false; + } + } + } +} diff --git a/src/samples/Dtf/Inventory/Columns.resx b/src/samples/Dtf/Inventory/Columns.resx new file mode 100644 index 00000000..cfeb11e3 --- /dev/null +++ b/src/samples/Dtf/Inventory/Columns.resx @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Product Name,250 + + + Product Code,250 + + + Property,100 + + + Value,300 + + + Feature Title,230 + + + Feature,200 + + + Install State,70 + + + Component,250 + + + Component ID,250 + + + Component,180 + + + Component ID,250 + + + Install State,70 + + + Product Name,250 + + + Product Code,250 + + + Component Path,300 + + + Key,35 + + + Name,250 + + + Install Path,350 + + + Exists,40 + + + Version in Database,100 + + + Version Installed,100 + + + Match,40 + + + Key,35 + + + Name,250 + + + Install Path,350 + + + Exists,40 + + + Version in Database,120 + + + Version Installed,120 + + + Match,40 + + + Component ID,250 + + + Key,35 + + + Name,250 + + + Install Path,350 + + + Exists,40 + + + Value in Database,120 + + + Value Installed,120 + + + Match,40 + + + Component ID,250 + + + Patch Code,250 + + + Patch Code,250 + + + Property,130 + + + Value,360 + + + Product Name,360 + + + Product Code,360 + + \ No newline at end of file diff --git a/src/samples/Dtf/Inventory/Features.cs b/src/samples/Dtf/Inventory/Features.cs new file mode 100644 index 00000000..c114da86 --- /dev/null +++ b/src/samples/Dtf/Inventory/Features.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Data; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Windows.Forms; +using WixToolset.Dtf.WindowsInstaller; + +namespace WixToolset.Dtf.Samples.Inventory +{ + /// + /// Provides inventory data about features of products installed on the system. + /// + public class FeaturesInventory : IInventoryDataProvider + { + private static object syncRoot = new object(); + + public FeaturesInventory() + { + } + + public string Description + { + get { return "Features of installed products"; } + } + + public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) + { + statusCallback(0, @"Products\...\Features"); + ArrayList nodes = new ArrayList(); + foreach (ProductInstallation product in ProductInstallation.AllProducts) + { + nodes.Add(String.Format(@"Products\{0}\Features", MsiUtils.GetProductName(product.ProductCode))); + } + statusCallback(nodes.Count, String.Empty); + return (string[]) nodes.ToArray(typeof(string)); + } + + public bool IsNodeSearchable(string searchRoot, string searchNode) + { + return true; + } + + public DataView GetData(string nodePath) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 3 && path[0] == "Products" && path[2] == "Features") + { + return GetProductFeaturesData(MsiUtils.GetProductCode(path[1])); + } + return null; + } + + public DataView GetProductFeaturesData(string productCode) + { + DataTable table = new DataTable("ProductFeatures"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductFeaturesFeatureTitle", typeof(string)); + table.Columns.Add("ProductFeaturesFeatureName", typeof(string)); + table.Columns.Add("ProductFeaturesInstallState", typeof(string)); + + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + session.DoAction("CostInitialize"); + session.DoAction("FileCost"); + session.DoAction("CostFinalize"); + + IList featuresAndTitles = session.Database.ExecuteStringQuery( + "SELECT `Title`, `Feature` FROM `Feature`"); + + for(int i = 0; i < featuresAndTitles.Count; i += 2) + { + InstallState featureState = session.Features[featuresAndTitles[i + 1]].CurrentState; + table.Rows.Add(new object[] { featuresAndTitles[i], featuresAndTitles[i+1], + (featureState == InstallState.Advertised ? "Advertised" : featureState.ToString()) }); + } + } + } + return new DataView(table, "", "ProductFeaturesFeatureTitle ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + catch(IOException) { } + return null; + } + + public string GetLink(string nodePath, DataRow row) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 3 && path[0] == "Products" && path[2] == "Features") + { + return String.Format(@"Products\{0}\Features\{1}", path[1], row["ProductFeaturesFeatureName"]); + } + return null; + } + } +} diff --git a/src/samples/Dtf/Inventory/IInventoryDataProvider.cs b/src/samples/Dtf/Inventory/IInventoryDataProvider.cs new file mode 100644 index 00000000..23f2c187 --- /dev/null +++ b/src/samples/Dtf/Inventory/IInventoryDataProvider.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.Data; + +namespace WixToolset.Dtf.Samples.Inventory +{ + /// + /// Reports the total number of items loaded so far by . + /// + public delegate void InventoryDataLoadStatusCallback(int itemsLoaded, string currentNode); + + /// + /// Inventory data providers implement this interface to provide a particular type of data. + /// Implementors must provide a parameterless constructor. + /// + public interface IInventoryDataProvider + { + /// + /// Gets a description of the data provided. This description allows + /// the user to choose what type of data to gather. + /// + string Description { get; } + + /// + /// Gets the paths of all nodes for which this object provides data. + /// + /// Callback for reporting status. + /// The callback should not necessarily be invoked for every individual + /// node loaded, rather only every significant chunk. + /// An array of node paths. The parts of the node paths + /// are delimited by backslashes (\). + string[] GetNodes(InventoryDataLoadStatusCallback statusCallback); + + /// + /// When related nodes of a tree consist of duplicate data, it's + /// inefficient to search them all. This method indicates which + /// nodes should be search and which should be ignored. + /// + /// Root node of the subtree-search. + /// Node which may or may not be searched. + /// True if the node should be searched, false otherwise. + bool IsNodeSearchable(string searchRoot, string searchNode); + + /// + /// Gets the data for a particular node. + /// + /// Path of the node for which data is requested. + /// This is one of the paths returned by . + /// DataView of a table filled with data, or null if data is + /// not available. + DataView GetData(string nodePath); + + /// + /// Gets the path of another node which provides more details about + /// a particular data row. + /// + /// Path of the node containing the data + /// row being queried. + /// Data row being queried. + /// Path to another node. This is not necessarily + /// one of the nodes returned by . If the + /// node path is unknown, it will be ignored. This method may + /// return null if there is no detail node for the row. + string GetLink(string nodePath, DataRow row); + } +} diff --git a/src/samples/Dtf/Inventory/Inventory.cs b/src/samples/Dtf/Inventory/Inventory.cs new file mode 100644 index 00000000..02793be8 --- /dev/null +++ b/src/samples/Dtf/Inventory/Inventory.cs @@ -0,0 +1,1231 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Drawing; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Forms; +using System.Globalization; +using System.Reflection; +using System.Resources; +using System.Threading; +using System.Security.Permissions; +using System.Data; + + +[assembly: AssemblyDescription("Shows a hierarchical, relational, searchable " + + " view of all of the product, feature, component, file, and patch data managed " + + "by MSI, for all products installed on the system.")] + +[assembly: SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode=true)] + + +namespace WixToolset.Dtf.Samples.Inventory +{ + public class Inventory : System.Windows.Forms.Form + { + [STAThread] + public static void Main() + { + if (WixToolset.Dtf.WindowsInstaller.Installer.Version < new Version(3, 0)) + { + MessageBox.Show("This application requires Windows Installer version 3.0 or later.", + "Inventory", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + Application.Run(new Inventory()); + } + + private IInventoryDataProvider[] dataProviders; + private Hashtable dataProviderMap; + private Hashtable data; + private ArrayList tablesLoading; + private bool searching; + private bool stopSearch; + private bool navigating; + private string continueSearchRoot; + private string continueSearchPath; + private DataGridCell continueSearchCell; + private DataGridCell continueSearchEndCell; + private bool mouseOverGridLink = false; + private Stack historyBack; + private Stack historyForward; + private Stack cellHistoryBack; + private Stack cellHistoryForward; + private static readonly DataGridCell anyCell = new DataGridCell(-1,-1); + private static readonly DataGridCell zeroCell = new DataGridCell(0,0); + private static object syncRoot = new object(); + + private System.Windows.Forms.DataGrid dataGrid; + private System.Windows.Forms.TreeView treeView; + private System.Windows.Forms.Panel toolPanel; + private System.Windows.Forms.Splitter splitter; + private System.Windows.Forms.Panel dataPanel; + private System.Windows.Forms.Button backButton; + private System.Windows.Forms.Button forwardButton; + private System.Windows.Forms.Button findButton; + private System.Windows.Forms.TextBox findTextBox; + private System.Windows.Forms.Button refreshButton; + private System.Windows.Forms.Button findStopButton; + private System.Windows.Forms.CheckBox searchTreeCheckBox; + private System.Windows.Forms.ToolTip gridLinkTip; + private System.ComponentModel.IContainer components; + + public Inventory() + { + InitializeComponent(); + + this.gridLinkTip.InitialDelay = 0; + this.gridLinkTip.ReshowDelay = 0; + + this.dataProviderMap = new Hashtable(); + this.data = new Hashtable(); + this.tablesLoading = new ArrayList(); + this.historyBack = new Stack(); + this.historyForward = new Stack(); + this.cellHistoryBack = new Stack(); + this.cellHistoryForward = new Stack(); + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + if(components != null) + { + components.Dispose(); + } + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.dataGrid = new System.Windows.Forms.DataGrid(); + this.treeView = new System.Windows.Forms.TreeView(); + this.toolPanel = new System.Windows.Forms.Panel(); + this.findStopButton = new System.Windows.Forms.Button(); + this.findButton = new System.Windows.Forms.Button(); + this.searchTreeCheckBox = new System.Windows.Forms.CheckBox(); + this.findTextBox = new System.Windows.Forms.TextBox(); + this.refreshButton = new System.Windows.Forms.Button(); + this.forwardButton = new System.Windows.Forms.Button(); + this.backButton = new System.Windows.Forms.Button(); + this.dataPanel = new System.Windows.Forms.Panel(); + this.splitter = new System.Windows.Forms.Splitter(); + this.gridLinkTip = new System.Windows.Forms.ToolTip(this.components); + ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).BeginInit(); + this.toolPanel.SuspendLayout(); + this.dataPanel.SuspendLayout(); + this.SuspendLayout(); + // + // dataGrid + // + this.dataGrid.DataMember = ""; + this.dataGrid.Dock = System.Windows.Forms.DockStyle.Fill; + this.dataGrid.HeaderForeColor = System.Drawing.SystemColors.ControlText; + this.dataGrid.Location = new System.Drawing.Point(230, 0); + this.dataGrid.Name = "dataGrid"; + this.dataGrid.ReadOnly = true; + this.dataGrid.SelectionBackColor = System.Drawing.SystemColors.Highlight; + this.dataGrid.Size = new System.Drawing.Size(562, 432); + this.dataGrid.TabIndex = 1; + this.dataGrid.KeyDown += new System.Windows.Forms.KeyEventHandler(this.dataGrid_KeyDown); + this.dataGrid.MouseDown += new System.Windows.Forms.MouseEventHandler(this.dataGrid_MouseDown); + this.dataGrid.KeyUp += new System.Windows.Forms.KeyEventHandler(this.dataGrid_KeyUp); + this.dataGrid.MouseMove += new System.Windows.Forms.MouseEventHandler(this.dataGrid_MouseMove); + this.dataGrid.MouseLeave += new System.EventHandler(this.dataGrid_MouseLeave); + // + // treeView + // + this.treeView.Dock = System.Windows.Forms.DockStyle.Left; + this.treeView.HideSelection = false; + this.treeView.ImageIndex = -1; + this.treeView.Location = new System.Drawing.Point(0, 0); + this.treeView.Name = "treeView"; + this.treeView.SelectedImageIndex = -1; + this.treeView.Size = new System.Drawing.Size(224, 432); + this.treeView.TabIndex = 0; + this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyDown); + this.treeView.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Inventory_MouseDown); + this.treeView.KeyUp += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyUp); + this.treeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.treeView_AfterSelect); + // + // toolPanel + // + this.toolPanel.Controls.Add(this.findStopButton); + this.toolPanel.Controls.Add(this.findButton); + this.toolPanel.Controls.Add(this.searchTreeCheckBox); + this.toolPanel.Controls.Add(this.findTextBox); + this.toolPanel.Controls.Add(this.refreshButton); + this.toolPanel.Controls.Add(this.forwardButton); + this.toolPanel.Controls.Add(this.backButton); + this.toolPanel.Dock = System.Windows.Forms.DockStyle.Top; + this.toolPanel.Location = new System.Drawing.Point(0, 0); + this.toolPanel.Name = "toolPanel"; + this.toolPanel.Size = new System.Drawing.Size(792, 40); + this.toolPanel.TabIndex = 2; + // + // findStopButton + // + this.findStopButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.findStopButton.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.findStopButton.Location = new System.Drawing.Point(704, 8); + this.findStopButton.Name = "findStopButton"; + this.findStopButton.Size = new System.Drawing.Size(72, 25); + this.findStopButton.TabIndex = 6; + this.findStopButton.Text = "Stop"; + this.findStopButton.Visible = false; + this.findStopButton.Click += new System.EventHandler(this.findStopButton_Click); + // + // findButton + // + this.findButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.findButton.Enabled = false; + this.findButton.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.findButton.Location = new System.Drawing.Point(624, 8); + this.findButton.Name = "findButton"; + this.findButton.Size = new System.Drawing.Size(72, 25); + this.findButton.TabIndex = 4; + this.findButton.Text = "Find"; + this.findButton.Click += new System.EventHandler(this.findButton_Click); + // + // searchTreeCheckBox + // + this.searchTreeCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.searchTreeCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.searchTreeCheckBox.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((System.Byte)(0))); + this.searchTreeCheckBox.Location = new System.Drawing.Point(704, 10); + this.searchTreeCheckBox.Name = "searchTreeCheckBox"; + this.searchTreeCheckBox.Size = new System.Drawing.Size(80, 22); + this.searchTreeCheckBox.TabIndex = 5; + this.searchTreeCheckBox.Text = "In Subtree"; + this.searchTreeCheckBox.CheckedChanged += new System.EventHandler(this.searchTreeCheckBox_CheckedChanged); + // + // findTextBox + // + this.findTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.findTextBox.Location = new System.Drawing.Point(344, 10); + this.findTextBox.Name = "findTextBox"; + this.findTextBox.Size = new System.Drawing.Size(272, 20); + this.findTextBox.TabIndex = 3; + this.findTextBox.Text = ""; + this.findTextBox.TextChanged += new System.EventHandler(this.findTextBox_TextChanged); + this.findTextBox.Enter += new System.EventHandler(this.findTextBox_Enter); + // + // refreshButton + // + this.refreshButton.Enabled = false; + this.refreshButton.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.refreshButton.Location = new System.Drawing.Point(160, 8); + this.refreshButton.Name = "refreshButton"; + this.refreshButton.Size = new System.Drawing.Size(72, 25); + this.refreshButton.TabIndex = 2; + this.refreshButton.Text = "Refresh"; + this.refreshButton.Click += new System.EventHandler(this.refreshButton_Click); + // + // forwardButton + // + this.forwardButton.Enabled = false; + this.forwardButton.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.forwardButton.Location = new System.Drawing.Point(80, 8); + this.forwardButton.Name = "forwardButton"; + this.forwardButton.Size = new System.Drawing.Size(72, 25); + this.forwardButton.TabIndex = 1; + this.forwardButton.Text = "Forward"; + this.forwardButton.Click += new System.EventHandler(this.forwardButton_Click); + // + // backButton + // + this.backButton.Enabled = false; + this.backButton.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.backButton.Location = new System.Drawing.Point(8, 8); + this.backButton.Name = "backButton"; + this.backButton.Size = new System.Drawing.Size(72, 25); + this.backButton.TabIndex = 0; + this.backButton.Text = "Back"; + this.backButton.Click += new System.EventHandler(this.backButton_Click); + // + // dataPanel + // + this.dataPanel.Controls.Add(this.dataGrid); + this.dataPanel.Controls.Add(this.splitter); + this.dataPanel.Controls.Add(this.treeView); + this.dataPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.dataPanel.Location = new System.Drawing.Point(0, 40); + this.dataPanel.Name = "dataPanel"; + this.dataPanel.Size = new System.Drawing.Size(792, 432); + this.dataPanel.TabIndex = 1; + // + // splitter + // + this.splitter.Location = new System.Drawing.Point(224, 0); + this.splitter.Name = "splitter"; + this.splitter.Size = new System.Drawing.Size(6, 432); + this.splitter.TabIndex = 2; + this.splitter.TabStop = false; + // + // Inventory + // + this.AcceptButton = this.findButton; + this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); + this.ClientSize = new System.Drawing.Size(792, 472); + this.Controls.Add(this.dataPanel); + this.Controls.Add(this.toolPanel); + this.MinimumSize = new System.Drawing.Size(700, 0); + this.Name = "Inventory"; + this.Text = "MSI Inventory"; + this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Inventory_KeyDown); + this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Inventory_MouseDown); + this.Load += new System.EventHandler(this.Inventory_Load); + this.KeyUp += new System.Windows.Forms.KeyEventHandler(this.Inventory_KeyUp); + ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).EndInit(); + this.toolPanel.ResumeLayout(false); + this.dataPanel.ResumeLayout(false); + this.ResumeLayout(false); + + } + #endregion + + + #region DataProviders + + private IInventoryDataProvider[] DataProviders + { + get + { + if(this.dataProviders == null) + { + ArrayList providerList = new ArrayList(); + providerList.AddRange(FindDataProviders(Assembly.GetExecutingAssembly())); + + Uri codebase = new Uri(Assembly.GetExecutingAssembly().CodeBase); + if(codebase.IsFile) + { + foreach(string module in Directory.GetFiles(Path.GetDirectoryName(codebase.LocalPath), "*Inventory.dll")) + { + try + { + providerList.AddRange(FindDataProviders(Assembly.LoadFrom(module))); + } + catch(Exception) { } + } + } + + this.dataProviders = (IInventoryDataProvider[]) providerList.ToArray(typeof(IInventoryDataProvider)); + } + return this.dataProviders; + } + } + + private static IList FindDataProviders(Assembly assembly) + { + ArrayList providerList = new ArrayList(); + foreach(Type type in assembly.GetTypes()) + { + if(type.IsClass) + { + foreach(Type implementedInterface in type.GetInterfaces()) + { + if(implementedInterface.Equals(typeof(IInventoryDataProvider))) + { + try + { + providerList.Add(assembly.CreateInstance(type.FullName)); + } + catch(Exception) + { + // Data provider's constructor threw an exception for some reason. + // Well, now we can't get any data from that one. + } + } + } + } + } + return providerList; + } + + #endregion + + private void GoTo(string nodePath, DataGridCell cell) + { + lock(syncRoot) + { + if(this.tablesLoading == null) return; // The tree is being loaded + if(this.navigating) return; // This method is already on the callstack + + DataView table = (DataView) this.data[nodePath]; + if(table != null && table == this.dataGrid.DataSource) + { + // Grid is already in view + if(!cell.Equals(anyCell)) this.dataGrid.CurrentCell = cell; + return; + } + if(cell.Equals(anyCell)) cell = zeroCell; + + if(this.historyBack.Count == 0 || nodePath != (string) this.historyBack.Peek()) + { + this.historyBack.Push(nodePath); + if(this.cellHistoryBack.Count > 0 && this.historyForward != null) + { + this.cellHistoryBack.Pop(); + this.cellHistoryBack.Push(this.dataGrid.CurrentCell); + } + this.cellHistoryBack.Push(cell); + } + if(this.historyForward != null) + { + this.historyForward.Clear(); + this.cellHistoryForward.Clear(); + } + + if(table != null || nodePath.Length == 0 || this.dataProviderMap[nodePath] == null) + { + this.dataGrid.CaptionText = nodePath; + this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; + this.dataGrid.DataSource = table; + this.dataGrid.CurrentCell = cell; + this.dataGrid.Focus(); + } + else + { + this.dataGrid.CaptionText = nodePath + " (loading...)"; + this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; + this.dataGrid.DataSource = table; + if(!this.tablesLoading.Contains(nodePath)) + { + this.tablesLoading.Add(nodePath); + this.SetCursor(); + #if SINGLETHREAD + this.LoadTable(nodePath); + #else + new WaitCallback(this.LoadTable).BeginInvoke(nodePath, null, null); + #endif + } + } + + this.findButton.Enabled = this.findTextBox.Text.Length > 0 && !searching; + + TreeNode treeNode = this.FindNode(nodePath); + if(treeNode != this.treeView.SelectedNode) + { + this.navigating = true; + this.treeView.SelectedNode = treeNode; + this.navigating = false; + } + } + } + + private void LoadTable(object nodePathObj) + { + string nodePath = (string) nodePathObj; + IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; + DataView table = null; + if(dataProvider != null) + { + try + { + table = dataProvider.GetData(nodePath); + } + catch(Exception) + { + // Data provider threw an exception for some reason. + // Treat it like it returned no data. + } + } + + lock(syncRoot) + { + if(this.tablesLoading == null || !tablesLoading.Contains(nodePath)) return; + if(table == null) + { + this.dataProviderMap.Remove(nodePath); + } + else + { + this.data[nodePath] = table; + } + this.tablesLoading.Remove(nodePath); + } + #if SINGLETHREAD + this.TableLoaded(nodePath); + #else + this.Invoke(new WaitCallback(this.TableLoaded), new object[] { nodePath }); + #endif + } + + private void TableLoaded(object nodePathObj) + { + string nodePath = (string) nodePathObj; + lock(syncRoot) + { + this.LoadTableStyle(nodePath); + if(nodePath == this.CurrentNodePath) + { + this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; + this.dataGrid.CaptionText = nodePath; + this.dataGrid.DataSource = this.CurrentTable; + this.dataGrid.CurrentCell = (DataGridCell) this.cellHistoryBack.Peek(); + this.dataGrid.Focus(); + } + this.SetCursor(); + } + } + + private void RefreshData() + { + lock(syncRoot) + { + this.GoTo("", zeroCell); + this.treeView.Nodes.Clear(); + this.dataGrid.TableStyles.Clear(); + this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; + this.SetControlsEnabled(false); + this.treeView.BeginUpdate(); + #if SINGLETHREAD + this.LoadTree(); + #else + new ThreadStart(this.LoadTree).BeginInvoke(null, null); + #endif + } + } + + private void SetControlsEnabled(bool enabled) + { + this.backButton.Enabled = enabled && this.historyBack.Count > 1; + this.forwardButton.Enabled = enabled && this.historyForward.Count > 0; + this.refreshButton.Enabled = enabled; + this.findButton.Enabled = enabled && this.findTextBox.Text.Length > 0 && !searching; + } + + private WaitCallback treeStatusCallback; + private int treeNodesLoaded; + private int treeNodesLoadedBase; + private string treeNodesLoading; + private void TreeLoadDataProviderStatus(int status, string currentNode) + { + if (currentNode != null) + { + this.treeNodesLoading = currentNode; + } + + this.treeNodesLoaded = treeNodesLoadedBase + status; + string statusString = String.Format("Loading tree... " + this.treeNodesLoaded); + if (!String.IsNullOrEmpty(this.treeNodesLoading)) + { + statusString += ": " + treeNodesLoading; + } + + #if SINGLETHREAD + treeStatusCallback(statusString); + #else + this.Invoke(treeStatusCallback, new object[] { statusString }); + #endif + } + + private void UpdateTreeLoadStatus(object status) + { + if(status == null) + { + // Loading is complete. + this.treeView.EndUpdate(); + this.SetCursor(); + this.GoTo("Products", new DataGridCell(0, 0)); + this.SetControlsEnabled(true); + } + else + { + this.dataGrid.CaptionText = (string) status; + } + } + + private void LoadTree() + { + lock(syncRoot) + { + if(this.tablesLoading == null) return; + this.tablesLoading = null; + this.dataProviderMap.Clear(); + this.data.Clear(); + this.Invoke(new ThreadStart(this.SetCursor)); + } + + this.treeStatusCallback = new WaitCallback(UpdateTreeLoadStatus); + this.LoadTreeNodes(); + this.RenderTreeNodes(); + + lock(syncRoot) + { + this.tablesLoading = new ArrayList(); + } + // Use a status of null to signal loading complete. + #if SINGLETHREAD + this.UpdateTreeLoadStatus(null); + #else + this.Invoke(new WaitCallback(this.UpdateTreeLoadStatus), new object[] { null }); + #endif + } + + private void LoadTreeNodes() + { + #if SINGLETHREAD + this.treeStatusCallback("Loading tree... "); + #else + this.Invoke(this.treeStatusCallback, new object[] { "Loading tree... " }); + #endif + this.treeNodesLoaded = 0; + this.treeNodesLoading = null; + foreach(IInventoryDataProvider dataProvider in this.DataProviders) + { + this.treeNodesLoadedBase = this.treeNodesLoaded; + string[] nodePaths = null; + try + { + nodePaths = dataProvider.GetNodes(new InventoryDataLoadStatusCallback(this.TreeLoadDataProviderStatus)); + } + catch(Exception) + { + // Data provider threw an exception for some reason. + // Treat it like it returned no data. + } + if(nodePaths != null) + { + foreach(string nodePath in nodePaths) + { + if(!this.dataProviderMap.Contains(nodePath)) + { + this.dataProviderMap.Add(nodePath, dataProvider); + } + } + } + } + } + + private void RenderTreeNodes() + { + #if SINGLETHREAD + this.treeStatusCallback("Rendering tree... "); + #else + this.Invoke(this.treeStatusCallback, new object[] { "Rendering tree... " }); + #endif + this.treeNodesLoaded = 0; + foreach(DictionaryEntry nodePathAndProvider in this.dataProviderMap) + { + string nodePath = (string) nodePathAndProvider.Key; + #if SINGLETHREAD + this.AddNode(nodePath); + #else + this.Invoke(new WaitCallback(this.AddNode), new object[] { nodePath }); + #endif + } + } + + private void LoadTableStyle(string nodePath) + { + DataView table = (DataView) this.data[nodePath]; + if(table != null) + { + DataGridTableStyle tableStyle = this.dataGrid.TableStyles[table.Table.TableName]; + if(tableStyle == null) + { + tableStyle = new DataGridTableStyle(); + tableStyle.MappingName = table.Table.TableName; + tableStyle.RowHeadersVisible = true; + this.dataGrid.TableStyles.Add(tableStyle); + } + foreach(DataColumn column in table.Table.Columns) + { + if(!tableStyle.GridColumnStyles.Contains(column.ColumnName)) + { + string colStyle = (string) ColumnResources.GetObject(column.ColumnName, CultureInfo.InvariantCulture); + if(colStyle != null) + { + string[] colStyleParts = colStyle.Split(','); + DataGridColumnStyle columnStyle = (colStyleParts.Length > 2 && colStyleParts[2] == "bool" + ? (DataGridColumnStyle) new DataGridBoolColumn() : (DataGridColumnStyle) new DataGridTextBoxColumn()); + try { if(colStyleParts.Length > 1) columnStyle.Width = Int32.Parse(colStyleParts[1]); } + catch(FormatException) { } + columnStyle.HeaderText = colStyleParts[0]; + columnStyle.MappingName = column.ColumnName; + tableStyle.GridColumnStyles.Add(columnStyle); + } + } + } + } + } + + private static ResourceManager ColumnResources + { + get + { + if(columnResources == null) + { + columnResources = new ResourceManager(typeof(Inventory).Name + ".Columns", typeof(Inventory).Assembly); + } + return columnResources; + } + } + private static ResourceManager columnResources; + + private void AddNode(object nodePathObj) + { + string nodePath = (string) nodePathObj; + string[] path = nodePath.Split('\\'); + TreeNodeCollection nodes = this.treeView.Nodes; + TreeNode node = null; + foreach(string pathPart in path) + { + node = null; + for(int i = 0; i < nodes.Count; i++) + { + int c = string.CompareOrdinal(nodes[i].Text, pathPart); + if(c == 0) + { + node = nodes[i]; + break; + } + else if(c > 0) + { + node = new TreeNode(pathPart); + nodes.Insert(i, node); + break; + } + } + if(node == null) + { + node = new TreeNode(pathPart); + nodes.Add(node); + } + nodes = node.Nodes; + } + if(++this.treeNodesLoaded % 1000 == 0) + { + this.UpdateTreeLoadStatus("Rendering tree... " + + (100 * this.treeNodesLoaded / this.dataProviderMap.Count) + "%"); + } + } + + public string CurrentNodePath + { + get + { + TreeNode currentNode = this.treeView.SelectedNode; + return currentNode != null ? currentNode.FullPath : null; + } + } + + public DataView CurrentTable + { + get + { + string currentNodePath = this.CurrentNodePath; + return currentNodePath != null ? (DataView) this.data[this.CurrentNodePath] : null; + } + } + + private TreeNode FindNode(string nodePath) + { + if(nodePath == null) return null; + string[] path = nodePath.Split('\\'); + TreeNodeCollection nodes = this.treeView.Nodes; + TreeNode node = null; + foreach(string pathPart in path) + { + node = null; + for(int i = 0; i < nodes.Count; i++) + { + if(nodes[i].Text == pathPart) + { + node = nodes[i]; + break; + } + } + if(node != null) + { + nodes = node.Nodes; + } + } + return node; + } + + private void dataGrid_MouseDown(object sender, MouseEventArgs e) + { + Keys modKeys = Control.ModifierKeys; + if(e.Button == MouseButtons.Left && (modKeys & (Keys.Shift | Keys.Control)) == 0) + { + DataGrid.HitTestInfo hit = this.dataGrid.HitTest(e.X, e.Y); + string link = this.GetLinkForGridHit(hit); + if(link != null) + { + TreeNode node = this.FindNode(link); + if(node != null) + { + this.treeView.SelectedNode = node; + node.Expand(); + } + } + } + this.Inventory_MouseDown(sender, e); + } + + private void dataGrid_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) + { + //this.gridLinkTip.SetToolTip(this.dataGrid, null); + DataGrid.HitTestInfo hit = this.dataGrid.HitTest(e.X, e.Y); + if(hit.Type == DataGrid.HitTestType.RowHeader) + { + string link = this.GetLinkForGridHit(hit); + if(link != null) + { + this.mouseOverGridLink = true; + this.SetCursor(); + return; + } + } + else if(this.mouseOverGridLink) + { + this.mouseOverGridLink = false; + this.SetCursor(); + } + } + + private void dataGrid_MouseLeave(object sender, System.EventArgs e) + { + this.mouseOverGridLink = false; + this.SetCursor(); + } + + private string GetLinkForGridHit(DataGrid.HitTestInfo hit) + { + if(hit.Type == DataGrid.HitTestType.RowHeader && this.tablesLoading != null) + { + string nodePath = this.CurrentNodePath; + DataView table = (DataView) this.data[nodePath]; + if(table != null) + { + DataRow row = table[hit.Row].Row; + IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; + return dataProvider.GetLink(nodePath, table[hit.Row].Row); + } + } + return null; + } + + private void HistoryBack() + { + lock(syncRoot) + { + if(this.historyBack.Count > 1) + { + string nodePath = (string) this.historyBack.Pop(); + this.cellHistoryBack.Pop(); + DataGridCell cell = this.dataGrid.CurrentCell; + Stack saveForward = this.historyForward; + this.historyForward = null; + this.GoTo((string) this.historyBack.Pop(), (DataGridCell) this.cellHistoryBack.Pop()); + this.historyForward = saveForward; + this.historyForward.Push(nodePath); + this.cellHistoryForward.Push(cell); + this.backButton.Enabled = this.historyBack.Count > 1; + this.forwardButton.Enabled = this.historyForward.Count > 0; + } + } + } + + private void HistoryForward() + { + lock(syncRoot) + { + if(this.historyForward.Count > 0) + { + string nodePath = (string) this.historyForward.Pop(); + DataGridCell cell = (DataGridCell) this.cellHistoryForward.Pop(); + Stack saveForward = this.historyForward; + this.historyForward = null; + this.GoTo(nodePath, cell); + this.historyForward = saveForward; + this.backButton.Enabled = this.historyBack.Count > 1; + this.forwardButton.Enabled = this.historyForward.Count > 0; + } + } + } + + #region Find + + private void Find() + { + this.BeginFind(); + object[] findNextArgs = new object[] { this.CurrentNodePath, this.dataGrid.CurrentCell, this.treeView.SelectedNode }; + #if SINGLETHREAD + this.FindNext(findNextArgs); + #else + new WaitCallback(this.FindNext).BeginInvoke(findNextArgs, null, null); + #endif + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + private void FindNext(object start) + { + string nodePath = (string) ((object[]) start)[0]; + DataGridCell startCell = (DataGridCell) ((object[]) start)[1]; + TreeNode searchNode = (TreeNode) ((object[]) start)[2]; + DataGridCell endCell = startCell; + + string searchString = this.findTextBox.Text; + if(searchString.Length == 0) return; + + bool ignoreCase = true; // TODO: make this a configurable option? + if(ignoreCase) searchString = searchString.ToLowerInvariant(); + + if(!this.searchTreeCheckBox.Checked) + { + DataGridCell foundCell; + startCell.ColumnNumber++; + if(FindInTable((DataView) this.data[nodePath], searchString, ignoreCase, + startCell, startCell, true, out foundCell)) + { + #if SINGLETHREAD + this.EndFind(new object[] { nodePath, foundCell }); + #else + this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { nodePath, foundCell } }); + #endif + return; + } + } + else + { + if(this.continueSearchRoot != null) + { + searchNode = this.FindNode(this.continueSearchRoot); + startCell = this.continueSearchCell; + endCell = this.continueSearchEndCell; + } + else + { + this.continueSearchRoot = searchNode.FullPath; + this.continueSearchPath = this.continueSearchRoot; + this.continueSearchEndCell = endCell; + } + //if(searchNode == null) return; + ArrayList nodesList = new ArrayList(); + nodesList.Add(searchNode); + this.GetFlatTreeNodes(searchNode.Nodes, nodesList, true, this.continueSearchRoot); + TreeNode[] nodes = (TreeNode[]) nodesList.ToArray(typeof(TreeNode)); + int startNode = nodesList.IndexOf(this.FindNode(this.continueSearchPath)); + DataGridCell foundCell; + startCell.ColumnNumber++; + for(int i = startNode; i < nodes.Length; i++) + { + if(this.stopSearch) break; + DataGridCell startCellOnThisNode = zeroCell; + if(i == startNode) startCellOnThisNode = startCell; + DataView table = this.GetTableForSearch(nodes[i].FullPath); + if(table != null) + { + if(FindInTable(table, searchString, ignoreCase, startCellOnThisNode, zeroCell, false, out foundCell)) + { + #if SINGLETHREAD + this.EndFind(new object[] { nodes[i].FullPath, foundCell }); + #else + this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { nodes[i].FullPath, foundCell } }); + #endif + return; + } + } + } + if(!this.stopSearch) + { + DataView table = this.GetTableForSearch(searchNode.FullPath); + if(table != null) + { + if(FindInTable(table, searchString, ignoreCase, zeroCell, endCell, false, out foundCell)) + { + #if SINGLETHREAD + this.EndFind(new object[] { searchNode.FullPath, foundCell }); + #else + this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { searchNode.FullPath, foundCell } }); + #endif + return; + } + } + } + } + #if SINGLETHREAD + this.EndFind(null); + #else + this.Invoke(new WaitCallback(this.EndFind), new object[] { null }); + #endif + } + + private DataView GetTableForSearch(string nodePath) + { + DataView table = (DataView) this.data[nodePath]; + string status = nodePath; + if(table == null) status = status + " (loading)"; + #if SINGLETHREAD + this.FindStatus(nodePath); + #else + this.Invoke(new WaitCallback(this.FindStatus), new object[] { status }); + #endif + if(table == null) + { + this.tablesLoading.Add(nodePath); + this.Invoke(new ThreadStart(this.SetCursor)); + this.LoadTable(nodePath); + table = (DataView) this.data[nodePath]; + } + return table; + } + + private void GetFlatTreeNodes(TreeNodeCollection nodes, IList resultsList, bool searchable, string searchRoot) + { + foreach(TreeNode node in nodes) + { + string nodePath = node.FullPath; + IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; + if(!searchable || (dataProvider != null && dataProvider.IsNodeSearchable(searchRoot, nodePath))) + { + resultsList.Add(node); + } + GetFlatTreeNodes(node.Nodes, resultsList, searchable, searchRoot); + } + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + private bool FindInTable(DataView table, string searchString, bool lowerCase, + DataGridCell startCell, DataGridCell endCell, bool wrap, out DataGridCell foundCell) + { + foundCell = new DataGridCell(-1, -1); + if(table == null) return false; + if(startCell.RowNumber < 0) startCell.RowNumber = 0; + if(startCell.ColumnNumber < 0) startCell.ColumnNumber = 0; + for(int searchRow = startCell.RowNumber; searchRow < table.Count; searchRow++) + { + if(this.stopSearch) break; + if(endCell.RowNumber > startCell.RowNumber && searchRow > endCell.RowNumber) break; + + DataRowView tableRow = table[searchRow]; + for(int searchCol = (searchRow == startCell.RowNumber + ? startCell.ColumnNumber : 0); searchCol < table.Table.Columns.Count; searchCol++) + { + if(this.stopSearch) break; + if(endCell.RowNumber > startCell.RowNumber && searchRow == endCell.RowNumber + && searchCol >= endCell.ColumnNumber) break; + + string value = tableRow[searchCol].ToString(); + if(lowerCase) value = value.ToLowerInvariant(); + if(value.IndexOf(searchString, StringComparison.Ordinal) >= 0) + { + foundCell.RowNumber = searchRow; + foundCell.ColumnNumber = searchCol; + return true; + } + } + } + if(wrap) + { + for(int searchRow = 0; searchRow <= endCell.RowNumber; searchRow++) + { + if(this.stopSearch) break; + DataRowView tableRow = table[searchRow]; + for(int searchCol = 0; searchCol < (searchRow == endCell.RowNumber + ? endCell.ColumnNumber : table.Table.Columns.Count); searchCol++) + { + if(this.stopSearch) break; + string value = tableRow[searchCol].ToString(); + if(lowerCase) value = value.ToLowerInvariant(); + if(value.IndexOf(searchString, StringComparison.Ordinal) >= 0) + { + foundCell.RowNumber = searchRow; + foundCell.ColumnNumber = searchCol; + return true; + } + } + } + } + return false; + } + + private void BeginFind() + { + lock(syncRoot) + { + this.findButton.Enabled = false; + this.findButton.Text = "Searching..."; + this.findTextBox.Enabled = false; + this.searchTreeCheckBox.Visible = false; + this.findStopButton.Visible = true; + this.refreshButton.Enabled = false; + this.searching = true; + this.stopSearch = false; + this.SetCursor(); + } + } + + private void FindStatus(object status) + { + lock(syncRoot) + { + this.dataGrid.CaptionText = "Searching... " + (string) status; + this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; + } + } + + private void EndFind(object result) + { + lock(syncRoot) + { + this.searching = false; + this.refreshButton.Enabled = true; + this.findStopButton.Visible = false; + this.searchTreeCheckBox.Visible = true; + this.findTextBox.Enabled = true; + this.findButton.Text = "Find"; + this.findButton.Enabled = true; + this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; + this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; + this.dataGrid.CaptionText = this.CurrentNodePath; + if(result != null) + { + string nodePath = (string) ((object[]) result)[0]; + DataGridCell foundCell = (DataGridCell) ((object[]) result)[1]; + this.GoTo(nodePath, foundCell); + this.dataGrid.Focus(); + this.continueSearchPath = nodePath; + this.continueSearchCell = foundCell; + if(this.searchTreeCheckBox.Checked) this.searchTreeCheckBox.Text = "Continue"; + } + else + { + this.continueSearchRoot = null; + this.continueSearchPath = null; + this.searchTreeCheckBox.Text = "In Subtree"; + } + this.SetCursor(); + } + } + + private void SetCursor() + { + if(this.mouseOverGridLink) + { + Keys modKeys = Control.ModifierKeys; + if((modKeys & (Keys.Shift | Keys.Control)) == 0) + { + this.Cursor = Cursors.Hand; + return; + } + } + if(this.tablesLoading == null || this.tablesLoading.Count > 0 || this.searching) + { + this.Cursor = Cursors.AppStarting; + return; + } + this.Cursor = Cursors.Arrow; + } + + #endregion + + #region EventHandlers + + private void Inventory_Load(object sender, System.EventArgs e) + { + this.RefreshData(); + } + private void refreshButton_Click(object sender, System.EventArgs e) + { + this.RefreshData(); + } + private void Inventory_MouseDown(object sender, MouseEventArgs e) + { + if(e.Button == MouseButtons.XButton1) this.HistoryBack(); + else if(e.Button == MouseButtons.XButton2) this.HistoryForward(); + } + private void Inventory_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.SetCursor(); + if(e.KeyCode == Keys.F3) this.Find(); + else if(e.KeyCode == Keys.F && (e.Modifiers | Keys.Control) != 0) this.findTextBox.Focus(); + else if(e.KeyCode == Keys.BrowserBack) this.HistoryBack(); + else if(e.KeyCode == Keys.BrowserForward) this.HistoryForward(); + else return; + e.Handled = true; + } + private void treeView_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.Inventory_KeyDown(sender, e); + } + private void dataGrid_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.Inventory_KeyDown(sender, e); + } + private void Inventory_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.SetCursor(); + } + private void treeView_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.Inventory_KeyDown(sender, e); + } + private void dataGrid_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) + { + this.Inventory_KeyDown(sender, e); + } + private void treeView_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e) + { + this.GoTo(e.Node.FullPath, anyCell); + } + private void backButton_Click(object sender, System.EventArgs e) + { + this.HistoryBack(); + } + private void forwardButton_Click(object sender, System.EventArgs e) + { + this.HistoryForward(); + } + private void findTextBox_TextChanged(object sender, System.EventArgs e) + { + this.findButton.Enabled = this.findTextBox.Text.Length > 0 && + this.tablesLoading != null && this.treeView.SelectedNode != null && !searching; + this.searchTreeCheckBox.Text = "In Subtree"; + this.continueSearchRoot = null; + } + private void findButton_Click(object sender, System.EventArgs e) + { + this.Find(); + } + private void findTextBox_Enter(object sender, System.EventArgs e) + { + findTextBox.SelectAll(); + } + private void findStopButton_Click(object sender, System.EventArgs e) + { + this.stopSearch = true; + } + + private void searchTreeCheckBox_CheckedChanged(object sender, System.EventArgs e) + { + if(!searchTreeCheckBox.Checked && searchTreeCheckBox.Text == "Continue") + { + this.searchTreeCheckBox.Text = "In Subtree"; + } + } + + #endregion + + } +} diff --git a/src/samples/Dtf/Inventory/Inventory.csproj b/src/samples/Dtf/Inventory/Inventory.csproj new file mode 100644 index 00000000..6dc1cfd3 --- /dev/null +++ b/src/samples/Dtf/Inventory/Inventory.csproj @@ -0,0 +1,42 @@ + + + + + + + {51480F8E-B80F-42DC-91E7-3542C1F12F8C} + WinExe + WixToolset.Dtf.Samples.Inventory + Inventory + v2.0 + Inventory.ico + OnOutputUpdated + + + + + + + + Form + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/Inventory/Inventory.ico b/src/samples/Dtf/Inventory/Inventory.ico new file mode 100644 index 00000000..d5757f7a Binary files /dev/null and b/src/samples/Dtf/Inventory/Inventory.ico differ diff --git a/src/samples/Dtf/Inventory/Inventory.resx b/src/samples/Dtf/Inventory/Inventory.resx new file mode 100644 index 00000000..9aeb4d2c --- /dev/null +++ b/src/samples/Dtf/Inventory/Inventory.resx @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + Private + + + Private + + + Private + + + Private + + + False + + + False + + + True + + + True + + + Private + + + Private + + + 8, 8 + + + False + + + Private + + + Private + + + False + + + Private + + + Private + + + False + + + Private + + + Private + + + Private + + + False + + + Private + + + False + + + Private + + + Private + + + False + + + Private + + + Private + + + False + + + Private + + + Private + + + False + + + True + + + True + + + Private + + + Private + + + 8, 8 + + + False + + + Private + + + Private + + + Private + + + 17, 17 + + + Private + + + False + + + (Default) + + + False + + + False + + + 8, 8 + + + True + + + 80 + + + True + + + Inventory + + + Private + + \ No newline at end of file diff --git a/src/samples/Dtf/Inventory/components.cs b/src/samples/Dtf/Inventory/components.cs new file mode 100644 index 00000000..c5147084 --- /dev/null +++ b/src/samples/Dtf/Inventory/components.cs @@ -0,0 +1,626 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Data; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Windows.Forms; +using Microsoft.Win32; +using WixToolset.Dtf.WindowsInstaller; +using View = WixToolset.Dtf.WindowsInstaller.View; + +namespace WixToolset.Dtf.Samples.Inventory +{ + /// + /// Provides inventory data about components of products installed on the system. + /// + public class ComponentsInventory : IInventoryDataProvider + { + private static object syncRoot = new object(); + + public ComponentsInventory() + { + } + + public string Description + { + get { return "Components of installed products"; } + } + + private Hashtable componentProductsMap; + + public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) + { + ArrayList nodes = new ArrayList(); + componentProductsMap = new Hashtable(); + foreach(ProductInstallation product in ProductInstallation.AllProducts) + { + string productName = MsiUtils.GetProductName(product.ProductCode); + statusCallback(nodes.Count, String.Format(@"Products\{0}", productName)); + + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using (Session session = Installer.OpenProduct(product.ProductCode)) + { + statusCallback(nodes.Count, String.Format(@"Products\{0}\Features", productName)); + IList features = session.Database.ExecuteStringQuery("SELECT `Feature` FROM `Feature`"); + string[] featuresArray = new string[features.Count]; + features.CopyTo(featuresArray, 0); + Array.Sort(featuresArray, 0, featuresArray.Length, StringComparer.OrdinalIgnoreCase); + foreach (string feature in featuresArray) + { + nodes.Add(String.Format(@"Products\{0}\Features\{1}", productName, feature)); + } + statusCallback(nodes.Count, String.Format(@"Products\{0}\Components", productName)); + nodes.Add(String.Format(@"Products\{0}\Components", productName)); + IList components = session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`"); + for (int i = 0; i < components.Count; i++) + { + string component = components[i]; + if (component.Length > 0) + { + nodes.Add(String.Format(@"Products\{0}\Components\{1}", productName, component)); + ArrayList sharingProducts = (ArrayList) componentProductsMap[component]; + if (sharingProducts == null) + { + sharingProducts = new ArrayList(); + componentProductsMap[component] = sharingProducts; + } + sharingProducts.Add(product.ProductCode); + } + if (i % 100 == 0) statusCallback(nodes.Count, null); + } + nodes.Add(String.Format(@"Products\{0}\Files", productName)); + nodes.Add(String.Format(@"Products\{0}\Registry", productName)); + statusCallback(nodes.Count, String.Empty); + } + } + } + catch(InstallerException) { } + } + statusCallback(nodes.Count, @"Products\...\Components\...\Sharing"); + foreach (DictionaryEntry componentProducts in componentProductsMap) + { + string component = (string) componentProducts.Key; + ArrayList products = (ArrayList) componentProducts.Value; + if(products.Count > 1) + { + foreach(string productCode in products) + { + nodes.Add(String.Format(@"Products\{0}\Components\{1}\Sharing", MsiUtils.GetProductName(productCode), component)); + } + } + } + statusCallback(nodes.Count, String.Empty); + return (string[]) nodes.ToArray(typeof(string)); + } + + public bool IsNodeSearchable(string searchRoot, string searchNode) + { + string[] rootPath = searchRoot.Split('\\'); + string[] nodePath = searchNode.Split('\\'); + if(rootPath.Length < 3 && nodePath.Length >= 3 && nodePath[0] == "Products" && nodePath[2] == "Components") + { + // When searching an entire product, don't search the "Components" subtree -- + // it just has duplicate data from the Files and Registry table. And if you + // really want to know about the component, it's only a click away from + // those other tables. + return false; + } + return true; + } + + public DataView GetData(string nodePath) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 4 && path[0] == "Products" && path[2] == "Features") + { + return GetFeatureComponentsData(MsiUtils.GetProductCode(path[1]), path[3]); + } + else if(path.Length == 3 && path[0] == "Products" && path[2] == "Components") + { + return GetProductComponentsData(MsiUtils.GetProductCode(path[1])); + } + else if(path.Length == 4 && path[0] == "Products" && path[2] == "Components") + { + return GetComponentData(MsiUtils.GetProductCode(path[1]), path[3]); + } + else if(path.Length == 5 && path[0] == "Products" && path[2] == "Components" && path[4] == "Sharing") + { + return GetComponentProductsData(path[3]); + } + else if(path.Length == 3 && path[0] == "Products" && path[2] == "Files") + { + return GetProductFilesData(MsiUtils.GetProductCode(path[1])); + } + else if(path.Length == 3 && path[0] == "Products" && path[2] == "Registry") + { + return GetProductRegistryData(MsiUtils.GetProductCode(path[1])); + } + return null; + } + + public DataView GetComponentData(string productCode, string componentCode) + { + DataTable table = new DataTable("ProductComponentItems"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductComponentItemsIsKey", typeof(bool)); + table.Columns.Add("ProductComponentItemsKey", typeof(string)); + table.Columns.Add("ProductComponentItemsPath", typeof(string)); + table.Columns.Add("ProductComponentItemsExists", typeof(bool)); + table.Columns.Add("ProductComponentItemsDbVersion", typeof(string)); + table.Columns.Add("ProductComponentItemsInstalledVersion", typeof(string)); + table.Columns.Add("ProductComponentItemsInstalledMatch", typeof(bool)); + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + session.DoAction("CostInitialize"); + session.DoAction("FileCost"); + session.DoAction("CostFinalize"); + + foreach(object[] row in this.GetComponentFilesRows(productCode, componentCode, session, false)) + { + table.Rows.Add(row); + } + foreach(object[] row in this.GetComponentRegistryRows(productCode, componentCode, session, false)) + { + table.Rows.Add(row); + } + } + } + return new DataView(table, "", "ProductComponentItemsPath ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + return null; + } + + private object[][] GetComponentFilesRows(string productCode, string componentCode, Session session, bool includeComponent) + { + ArrayList rows = new ArrayList(); + string componentPath = new ComponentInstallation(componentCode, productCode).Path; + + string componentKey = (string) session.Database.ExecuteScalar( + "SELECT `Component` FROM `Component` WHERE `ComponentId` = '{0}'", componentCode); + if(componentKey == null) return null; + int attributes = Convert.ToInt32(session.Database.ExecuteScalar( + "SELECT `Attributes` FROM `Component` WHERE `Component` = '{0}'", componentKey)); + bool registryKeyPath = (attributes & (int) ComponentAttributes.RegistryKeyPath) != 0; + if(!registryKeyPath && componentPath.Length > 0) componentPath = Path.GetDirectoryName(componentPath); + string keyPath = (string) session.Database.ExecuteScalar( + "SELECT `KeyPath` FROM `Component` WHERE `Component` = '{0}'", componentKey); + + using (View view = session.Database.OpenView("SELECT `File`, `FileName`, `Version`, `Language`, " + + "`Attributes` FROM `File` WHERE `Component_` = '{0}'", componentKey)) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + string fileKey = (string) rec["File"]; + bool isKey = !registryKeyPath && keyPath == fileKey; + + string dbVersion = (string) rec["Version"]; + bool versionedFile = dbVersion.Length != 0; + if(versionedFile) + { + string language = (string) rec["Language"]; + if(language.Length > 0) + { + dbVersion = dbVersion + " (" + language + ")"; + } + } + else if(session.Database.Tables.Contains("MsiFileHash")) + { + IList hash = session.Database.ExecuteIntegerQuery("SELECT `HashPart1`, `HashPart2`, " + + "`HashPart3`, `HashPart4` FROM `MsiFileHash` WHERE `File_` = '{0}'", fileKey); + if(hash != null && hash.Count == 4) + { + dbVersion = this.GetFileHashString(hash); + } + } + + string filePath = GetLongFileName((string) rec["FileName"]); + bool exists = false; + bool installedMatch = false; + string installedVersion = ""; + if(!registryKeyPath && componentPath.Length > 0) + { + filePath = Path.Combine(componentPath, filePath); + + if(File.Exists(filePath)) + { + exists = true; + if(versionedFile) + { + installedVersion = Installer.GetFileVersion(filePath); + string language = Installer.GetFileLanguage(filePath); + if(language.Length > 0) + { + installedVersion = installedVersion + " (" + language + ")"; + } + } + else + { + int[] hash = new int[4]; + Installer.GetFileHash(filePath, hash); + installedVersion = this.GetFileHashString(hash); + } + installedMatch = installedVersion == dbVersion; + } + } + + object[] row; + if(includeComponent) row = new object[] { isKey, fileKey, filePath, exists, dbVersion, installedVersion, installedMatch, componentCode }; + else row = new object[] { isKey, fileKey, filePath, exists, dbVersion, installedVersion, installedMatch }; + rows.Add(row); + } + } + + return (object[][]) rows.ToArray(typeof(object[])); + } + + private string GetLongFileName(string fileName) + { + string[] fileNames = fileName.Split('|'); + return fileNames.Length == 1? fileNames[0] : fileNames[1]; + } + + private string GetFileHashString(IList hash) + { + return String.Format("{0:X8}{1:X8}{2:X8}{3:X8}", (uint) hash[0], (uint) hash[1], (uint) hash[2], (uint) hash[3]); + } + + private object[][] GetComponentRegistryRows(string productCode, string componentCode, Session session, bool includeComponent) + { + ArrayList rows = new ArrayList(); + string componentPath = new ComponentInstallation(componentCode, productCode).Path; + + string componentKey = (string) session.Database.ExecuteScalar( + "SELECT `Component` FROM `Component` WHERE `ComponentId` = '{0}'", componentCode); + if(componentKey == null) return null; + int attributes = Convert.ToInt32(session.Database.ExecuteScalar( + "SELECT `Attributes` FROM `Component` WHERE `Component` = '{0}'", componentKey)); + bool registryKeyPath = (attributes & (int) ComponentAttributes.RegistryKeyPath) != 0; + if(!registryKeyPath && componentPath.Length > 0) componentPath = Path.GetDirectoryName(componentPath); + string keyPath = (string) session.Database.ExecuteScalar( + "SELECT `KeyPath` FROM `Component` WHERE `Component` = '{0}'", componentKey); + + using (View view = session.Database.OpenView("SELECT `Registry`, `Root`, `Key`, `Name`, " + + "`Value` FROM `Registry` WHERE `Component_` = '{0}'", componentKey)) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + string regName = (string) rec["Name"]; + if(regName == "-") continue; // Don't list deleted keys + + string regTableKey = (string) rec["Registry"]; + bool isKey = registryKeyPath && keyPath == regTableKey; + string regPath = this.GetRegistryPath(session, (RegistryRoot) Convert.ToInt32(rec["Root"]), + (string) rec["Key"], (string) rec["Name"]); + + string dbValue; + using(Record formatRec = new Record(0)) + { + formatRec[0] = rec["Value"]; + dbValue = session.FormatRecord(formatRec); + } + + string installedValue = this.GetRegistryValue(regPath); + bool exists = installedValue != null; + if(!exists) installedValue = ""; + bool match = installedValue == dbValue; + + object[] row; + if(includeComponent) row = new object[] { isKey, regTableKey, regPath, exists, dbValue, installedValue, match, componentCode }; + else row = new object[] { isKey, regTableKey, regPath, exists, dbValue, installedValue, match }; + rows.Add(row); + } + } + + return (object[][]) rows.ToArray(typeof(object[])); + } + + private string GetRegistryPath(Session session, RegistryRoot root, string key, string name) + { + bool allUsers = session.EvaluateCondition("ALLUSERS = 1", true); + string rootName = "????"; + switch(root) + { + case RegistryRoot.LocalMachine : rootName = "HKLM"; break; + case RegistryRoot.CurrentUser : rootName = "HKCU"; break; + case RegistryRoot.Users : rootName = "HKU"; break; + case RegistryRoot.UserOrMachine: rootName = (allUsers ? "HKLM" : "HKCU"); break; + case RegistryRoot.ClassesRoot : rootName = (allUsers ? @"HKLM\Software\Classes" : @"HKCU\Software\Classes"); break; + // TODO: Technically, RegistryRoot.ClassesRoot should be under HKLM on NT4. + } + if(name.Length == 0) name = "(Default)"; + if(name == "+" || name == "*") name = ""; + else name = " : " + name; + using(Record formatRec = new Record(0)) + { + formatRec[0] = String.Format(@"{0}\{1}{2}", rootName, key, name); + return session.FormatRecord(formatRec); + } + } + + private string GetRegistryValue(string regPath) + { + string valueName = null; + int iColon = regPath.IndexOf(" : ", StringComparison.Ordinal) + 1; + if(iColon > 0) + { + valueName = regPath.Substring(iColon + 2); + regPath = regPath.Substring(0, iColon - 1); + } + if(valueName == "(Default)") valueName = ""; + + RegistryKey root; + if(regPath.StartsWith(@"HKLM\", StringComparison.Ordinal)) + { + root = Registry.LocalMachine; + regPath = regPath.Substring(5); + } + else if(regPath.StartsWith(@"HKCU\", StringComparison.Ordinal)) + { + root = Registry.CurrentUser; + regPath = regPath.Substring(5); + } + else if(regPath.StartsWith(@"HKU\", StringComparison.Ordinal)) + { + root = Registry.Users; + regPath = regPath.Substring(4); + } + else return null; + + using(RegistryKey regKey = root.OpenSubKey(regPath)) + { + if(regKey != null) + { + if(valueName == null) + { + // Just checking for the existence of the key. + return ""; + } + object value = regKey.GetValue(valueName); + if(value is string[]) + { + value = String.Join("[~]", (string[]) value); + } + else if(value is int) + { + value = "#" + value.ToString(); + } + else if(value is byte[]) + { + byte[] valueBytes = (byte[]) value; + StringBuilder byteString = new StringBuilder("#x"); + for(int i = 0; i < valueBytes.Length; i++) + { + byteString.Append(valueBytes[i].ToString("x2")); + } + value = byteString.ToString(); + } + return (value != null ? value.ToString() : null); + } + } + return null; + } + + public DataView GetProductFilesData(string productCode) + { + DataTable table = new DataTable("ProductFiles"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductFilesIsKey", typeof(bool)); + table.Columns.Add("ProductFilesKey", typeof(string)); + table.Columns.Add("ProductFilesPath", typeof(string)); + table.Columns.Add("ProductFilesExists", typeof(bool)); + table.Columns.Add("ProductFilesDbVersion", typeof(string)); + table.Columns.Add("ProductFilesInstalledVersion", typeof(string)); + table.Columns.Add("ProductFilesInstalledMatch", typeof(bool)); + table.Columns.Add("ProductFilesComponentID", typeof(string)); + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + session.DoAction("CostInitialize"); + session.DoAction("FileCost"); + session.DoAction("CostFinalize"); + + foreach(string componentCode in session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`")) + { + foreach(object[] row in this.GetComponentFilesRows(productCode, componentCode, session, true)) + { + table.Rows.Add(row); + } + } + } + } + return new DataView(table, "", "ProductFilesPath ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + return null; + } + + public DataView GetProductRegistryData(string productCode) + { + DataTable table = new DataTable("ProductRegistry"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductRegistryIsKey", typeof(bool)); + table.Columns.Add("ProductRegistryKey", typeof(string)); + table.Columns.Add("ProductRegistryPath", typeof(string)); + table.Columns.Add("ProductRegistryExists", typeof(bool)); + table.Columns.Add("ProductRegistryDbVersion", typeof(string)); + table.Columns.Add("ProductRegistryInstalledVersion", typeof(string)); + table.Columns.Add("ProductRegistryInstalledMatch", typeof(bool)); + table.Columns.Add("ProductRegistryComponentID", typeof(string)); + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + session.DoAction("CostInitialize"); + session.DoAction("FileCost"); + session.DoAction("CostFinalize"); + + foreach(string componentCode in session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`")) + { + foreach(object[] row in this.GetComponentRegistryRows(productCode, componentCode, session, true)) + { + table.Rows.Add(row); + } + } + } + } + return new DataView(table, "", "ProductRegistryPath ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + return null; + } + + public DataView GetComponentProductsData(string componentCode) + { + DataTable table = new DataTable("ComponentProducts"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ComponentProductsProductName", typeof(string)); + table.Columns.Add("ComponentProductsProductCode", typeof(string)); + table.Columns.Add("ComponentProductsComponentPath", typeof(string)); + + if(this.componentProductsMap != null) + { + ArrayList componentProducts = (ArrayList) this.componentProductsMap[componentCode]; + foreach(string productCode in componentProducts) + { + string productName = MsiUtils.GetProductName(productCode); + string componentPath = new ComponentInstallation(componentCode, productCode).Path; + table.Rows.Add(new object[] { productName, productCode, componentPath }); + } + return new DataView(table, "", "ComponentProductsProductName ASC", DataViewRowState.CurrentRows); + } + return null; + } + + public DataView GetProductComponentsData(string productCode) + { + DataTable table = new DataTable("ProductComponents"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductComponentsComponentName", typeof(string)); + table.Columns.Add("ProductComponentsComponentID", typeof(string)); + table.Columns.Add("ProductComponentsInstallState", typeof(string)); + + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + session.DoAction("CostInitialize"); + session.DoAction("FileCost"); + session.DoAction("CostFinalize"); + + IList componentsAndIds = session.Database.ExecuteStringQuery( + "SELECT `Component`, `ComponentId` FROM `Component`"); + + for (int i = 0; i < componentsAndIds.Count; i += 2) + { + if(componentsAndIds[i+1] == "Temporary Id") continue; + InstallState compState = session.Components[componentsAndIds[i]].CurrentState; + table.Rows.Add(new object[] { componentsAndIds[i], componentsAndIds[i+1], + (compState == InstallState.Advertised ? "Advertised" : compState.ToString())}); + } + } + } + return new DataView(table, "", "ProductComponentsComponentName ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + return null; + } + + public DataView GetFeatureComponentsData(string productCode, string feature) + { + DataTable table = new DataTable("ProductFeatureComponents"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductFeatureComponentsComponentName", typeof(string)); + table.Columns.Add("ProductFeatureComponentsComponentID", typeof(string)); + + try + { + IntPtr hWnd = IntPtr.Zero; + Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); + lock(syncRoot) // Only one Installer session can be active at a time + { + using(Session session = Installer.OpenProduct(productCode)) + { + IList componentsAndIds = session.Database.ExecuteStringQuery( + "SELECT `FeatureComponents`.`Component_`, " + + "`Component`.`ComponentId` FROM `FeatureComponents`, `Component` " + + "WHERE `FeatureComponents`.`Component_` = `Component`.`Component` " + + "AND `FeatureComponents`.`Feature_` = '{0}'", feature); + for (int i = 0; i < componentsAndIds.Count; i += 2) + { + table.Rows.Add(new object[] { componentsAndIds[i], componentsAndIds[i+1] }); + } + } + } + return new DataView(table, "", "ProductFeatureComponentsComponentName ASC", DataViewRowState.CurrentRows); + } + catch(InstallerException) { } + return null; + } + + public string GetLink(string nodePath, DataRow row) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 3 && path[0] == "Products" && path[2] == "Components") + { + string component = (string) row["ProductComponentsComponentID"]; + return String.Format(@"Products\{0}\Components\{1}", path[1], component); + } + else if(path.Length == 4 && path[0] == "Products" && path[2] == "Features") + { + string component = (string) row["ProductFeatureComponentsComponentID"]; + return String.Format(@"Products\{0}\Components\{1}", path[1], component); + } + else if(path.Length == 3 && path[0] == "Products" && path[2] == "Files") + { + string component = (string) row["ProductFilesComponentID"]; + return String.Format(@"Products\{0}\Components\{1}", path[1], component); + } + else if(path.Length == 3 && path[0] == "Products" && path[2] == "Registry") + { + string component = (string) row["ProductRegistryComponentID"]; + return String.Format(@"Products\{0}\Components\{1}", path[1], component); + } + else if(path.Length == 5 && path[0] == "Products" && path[2] == "Components" && path[4] == "Sharing") + { + string product = (string) row["ComponentProductsProductCode"]; + return String.Format(@"Products\{0}\Components\{1}", MsiUtils.GetProductName(product), path[3]); + } + return null; + } + } +} diff --git a/src/samples/Dtf/Inventory/msiutils.cs b/src/samples/Dtf/Inventory/msiutils.cs new file mode 100644 index 00000000..a345e194 --- /dev/null +++ b/src/samples/Dtf/Inventory/msiutils.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.Collections; +using WixToolset.Dtf.WindowsInstaller; + + +namespace WixToolset.Dtf.Samples.Inventory +{ + public class MsiUtils + { + private static Hashtable productCodesToNames = new Hashtable(); + private static Hashtable productNamesToCodes = new Hashtable(); + + public static string GetProductName(string productCode) + { + string productName = (string) productCodesToNames[productCode]; + if(productName == null) + { + productName = new ProductInstallation(productCode).ProductName; + productName = productName.Replace('\\', ' '); + if(productNamesToCodes.Contains(productName)) + { + string modifiedProductName = null; + for(int i = 2; i < Int32.MaxValue; i++) + { + modifiedProductName = productName + " [" + i + "]"; + if(!productNamesToCodes.Contains(modifiedProductName)) break; + } + productName = modifiedProductName; + } + productCodesToNames[productCode] = productName; + productNamesToCodes[productName] = productCode; + } + return productName; + } + + // Assumes GetProductName() has already been called for this product. + public static string GetProductCode(string productName) + { + return (string) productNamesToCodes[productName]; + } + + private MsiUtils() { } + } +} diff --git a/src/samples/Dtf/Inventory/patches.cs b/src/samples/Dtf/Inventory/patches.cs new file mode 100644 index 00000000..f01a4798 --- /dev/null +++ b/src/samples/Dtf/Inventory/patches.cs @@ -0,0 +1,227 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Security; +using System.Data; +using System.Collections; +using System.Globalization; +using System.Windows.Forms; +using WixToolset.Dtf.WindowsInstaller; + +namespace WixToolset.Dtf.Samples.Inventory +{ + /// + /// Provides inventory data about patches installed on the system. + /// + public class PatchesInventory : IInventoryDataProvider + { + public PatchesInventory() + { + } + + public string Description + { + get { return "Installed patches"; } + } + + public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) + { + ArrayList nodes = new ArrayList(); + statusCallback(nodes.Count, @"Products\...\Patches"); + foreach (ProductInstallation product in ProductInstallation.AllProducts) + { + string productName = MsiUtils.GetProductName(product.ProductCode); + + bool addedRoot = false; + foreach (PatchInstallation productPatch in PatchInstallation.GetPatches(null, product.ProductCode, null, UserContexts.All, PatchStates.Applied)) + { + if (!addedRoot) nodes.Add(String.Format(@"Products\{0}\Patches", productName)); + nodes.Add(String.Format(@"Products\{0}\Patches\{1}", productName, productPatch.PatchCode)); + } + } + + statusCallback(nodes.Count, "Patches"); + + string[] allPatches = GetAllPatchesList(); + if(allPatches.Length > 0) + { + nodes.Add("Patches"); + foreach(string patchCode in allPatches) + { + nodes.Add(String.Format(@"Patches\{0}", patchCode)); + nodes.Add(String.Format(@"Patches\{0}\Patched Products", patchCode)); + } + statusCallback(nodes.Count, String.Empty); + } + return (string[]) nodes.ToArray(typeof(string)); + } + + public bool IsNodeSearchable(string searchRoot, string searchNode) + { + return true; + } + + public DataView GetData(string nodePath) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 3 && path[0] == "Products" && path[2] == "Patches") + { + return this.GetProductPatchData(path[1]); + } + else if(path.Length == 4 && path[0] == "Products" && path[2] == "Patches") + { + return this.GetPatchData(path[3]); + } + else if(path.Length == 1 && path[0] == "Patches") + { + return this.GetAllPatchesData(); + } + else if(path.Length == 2 && path[0] == "Patches") + { + return this.GetPatchData(path[1]); + } + else if(path.Length == 3 && path[0] == "Patches" && path[2] == "Patched Products") + { + return this.GetPatchTargetData(path[1]); + } + return null; + } + + private string[] GetAllPatchesList() + { + ArrayList patchList = new ArrayList(); + foreach(PatchInstallation patch in PatchInstallation.AllPatches) + { + if(!patchList.Contains(patch.PatchCode)) + { + patchList.Add(patch.PatchCode); + } + } + string[] patchArray = (string[]) patchList.ToArray(typeof(string)); + Array.Sort(patchArray, 0, patchArray.Length, StringComparer.Ordinal); + return patchArray; + } + + private DataView GetAllPatchesData() + { + DataTable table = new DataTable("Patches"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("PatchesPatchCode", typeof(string)); + + foreach(string patchCode in GetAllPatchesList()) + { + table.Rows.Add(new object[] { patchCode }); + } + return new DataView(table, "", "PatchesPatchCode ASC", DataViewRowState.CurrentRows); + } + + private DataView GetProductPatchData(string productCode) + { + DataTable table = new DataTable("ProductPatches"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductPatchesPatchCode", typeof(string)); + + foreach(PatchInstallation patch in PatchInstallation.GetPatches(null, productCode, null, UserContexts.All, PatchStates.Applied)) + { + table.Rows.Add(new object[] { patch.PatchCode }); + } + return new DataView(table, "", "ProductPatchesPatchCode ASC", DataViewRowState.CurrentRows); + } + + private DataView GetPatchData(string patchCode) + { + DataTable table = new DataTable("PatchProperties"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("PatchPropertiesProperty", typeof(string)); + table.Columns.Add("PatchPropertiesValue", typeof(string)); + + table.Rows.Add(new object[] { "PatchCode", patchCode }); + + PatchInstallation patch = new PatchInstallation(patchCode, null); + + string localPackage = null; + foreach(string property in new string[] + { + "InstallDate", + "LocalPackage", + "State", + "Transforms", + "Uninstallable", + }) + { + try + { + string value = patch[property]; + table.Rows.Add(new object[] { property, (value != null ? value : "") }); + if(property == "LocalPackage") localPackage = value; + } + catch(InstallerException iex) + { + table.Rows.Add(new object[] { property, iex.Message }); + } + catch(ArgumentException) { } + } + + if(localPackage != null) + { + try + { + using(SummaryInfo patchSummaryInfo = new SummaryInfo(localPackage, false)) + { + table.Rows.Add(new object[] { "Title", patchSummaryInfo.Title }); + table.Rows.Add(new object[] { "Subject", patchSummaryInfo.Subject }); + table.Rows.Add(new object[] { "Author", patchSummaryInfo.Author }); + table.Rows.Add(new object[] { "Comments", patchSummaryInfo.Comments }); + table.Rows.Add(new object[] { "TargetProductCodes", patchSummaryInfo.Template }); + string obsoletedPatchCodes = patchSummaryInfo.RevisionNumber.Substring(patchSummaryInfo.RevisionNumber.IndexOf('}') + 1); + table.Rows.Add(new object[] { "ObsoletedPatchCodes", obsoletedPatchCodes }); + table.Rows.Add(new object[] { "TransformNames", patchSummaryInfo.LastSavedBy }); + } + } + catch(InstallerException) { } + catch(IOException) { } + catch(SecurityException) { } + } + return new DataView(table, "", "PatchPropertiesProperty ASC", DataViewRowState.CurrentRows); + } + + private DataView GetPatchTargetData(string patchCode) + { + DataTable table = new DataTable("PatchTargets"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("PatchTargetsProductName", typeof(string)); + table.Columns.Add("PatchTargetsProductCode", typeof(string)); + + foreach (PatchInstallation patch in PatchInstallation.GetPatches(patchCode, null, null, UserContexts.All, PatchStates.Applied)) + { + if(patch.PatchCode == patchCode) + { + string productName = MsiUtils.GetProductName(patch.ProductCode); + table.Rows.Add(new object[] { productName, patch.ProductCode }); + } + } + return new DataView(table, "", "PatchTargetsProductName ASC", DataViewRowState.CurrentRows); + } + + public string GetLink(string nodePath, DataRow row) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 3 && path[0] == "Products" && path[2] == "Patches") + { + return String.Format(@"Patches\{0}", row["ProductPatchesPatchCode"]); + } + else if(path.Length == 1 && path[0] == "Patches") + { + return String.Format(@"Patches\{0}", row["PatchesPatchCode"]); + } + else if(path.Length == 3 && path[0] == "Patches" && path[2] == "Patched Products") + { + return String.Format(@"Products\{0}", MsiUtils.GetProductCode((string) row["PatchTargetsProductCode"])); + } + return null; + } + } +} diff --git a/src/samples/Dtf/Inventory/products.cs b/src/samples/Dtf/Inventory/products.cs new file mode 100644 index 00000000..872c56c3 --- /dev/null +++ b/src/samples/Dtf/Inventory/products.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.Data; +using System.Globalization; +using System.Collections; +using System.Windows.Forms; +using WixToolset.Dtf.WindowsInstaller; + +namespace WixToolset.Dtf.Samples.Inventory +{ + /// + /// Provides inventory data about products installed or advertised on the system. + /// + public class ProductsInventory : IInventoryDataProvider + { + public ProductsInventory() + { + } + + public string Description + { + get { return "Installed products"; } + } + + public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) + { + statusCallback(0, "Products"); + ArrayList nodes = new ArrayList(); + nodes.Add("Products"); + foreach(ProductInstallation product in ProductInstallation.AllProducts) + { + nodes.Add("Products\\" + MsiUtils.GetProductName(product.ProductCode)); + } + statusCallback(nodes.Count, String.Empty); + return (string[]) nodes.ToArray(typeof(string)); + } + + public bool IsNodeSearchable(string searchRoot, string searchNode) + { + return true; + } + + public DataView GetData(string nodePath) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 1 && path[0] == "Products") + { + return this.GetAllProductsData(); + } + else if(path.Length == 2 && path[0] == "Products") + { + return this.GetProductData(MsiUtils.GetProductCode(path[1])); + } + return null; + } + + private DataView GetAllProductsData() + { + DataTable table = new DataTable("Products"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductsProductName", typeof(string)); + table.Columns.Add("ProductsProductCode", typeof(string)); + + foreach (ProductInstallation product in ProductInstallation.AllProducts) + { + string productName = MsiUtils.GetProductName(product.ProductCode); + table.Rows.Add(new object[] { productName, product.ProductCode }); + } + return new DataView(table, "", "ProductsProductName ASC", DataViewRowState.CurrentRows); + } + + private DataView GetProductData(string productCode) + { + DataTable table = new DataTable("ProductProperties"); + table.Locale = CultureInfo.InvariantCulture; + table.Columns.Add("ProductPropertiesProperty", typeof(string)); + table.Columns.Add("ProductPropertiesValue", typeof(string)); + + // Add a fake "ProductCode" install property, just for display convenience. + table.Rows.Add(new object[] { "ProductCode", productCode }); + + ProductInstallation product = new ProductInstallation(productCode); + + foreach(string property in new string[] + { + "AssignmentType", + "DiskPrompt", + "HelpLink", + "HelpTelephone", + "InstalledProductName", + "InstallDate", + "InstallLocation", + "InstallSource", + "Language", + "LastUsedSource", + "LastUsedType", + "LocalPackage", + "MediaPackagePath", + "PackageCode", + "PackageName", + "ProductIcon", + "ProductID", + "ProductName", + "Publisher", + "RegCompany", + "RegOwner", + "State", + "transforms", + "Uninstallable", + "UrlInfoAbout", + "UrlUpdateInfo", + "Version", + "VersionMinor", + "VersionMajor", + "VersionString" + }) + { + try + { + string value = product[property]; + table.Rows.Add(new object[] { property, (value != null ? value : "") }); + } + catch(InstallerException iex) + { + table.Rows.Add(new object[] { property, iex.Message }); + } + catch(ArgumentException) { } + } + return new DataView(table, "", "ProductPropertiesProperty ASC", DataViewRowState.CurrentRows); + } + + public string GetLink(string nodePath, DataRow row) + { + string[] path = nodePath.Split('\\'); + + if(path.Length == 1 && path[0] == "Products") + { + return String.Format(@"Products\{0}", MsiUtils.GetProductName((string) row["ProductsProductCode"])); + } + return null; + } + } +} diff --git a/src/samples/Dtf/Inventory/xp.manifest b/src/samples/Dtf/Inventory/xp.manifest new file mode 100644 index 00000000..34d61fea --- /dev/null +++ b/src/samples/Dtf/Inventory/xp.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/samples/Dtf/ManagedCA/AssemblyInfo.cs b/src/samples/Dtf/ManagedCA/AssemblyInfo.cs new file mode 100644 index 00000000..75be36b2 --- /dev/null +++ b/src/samples/Dtf/ManagedCA/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System.Reflection; + +[assembly: AssemblyDescription("Sample managed custom actions")] diff --git a/src/samples/Dtf/ManagedCA/ManagedCA.csproj b/src/samples/Dtf/ManagedCA/ManagedCA.csproj new file mode 100644 index 00000000..7fb32ad4 --- /dev/null +++ b/src/samples/Dtf/ManagedCA/ManagedCA.csproj @@ -0,0 +1,33 @@ + + + + {DB9E5F02-8241-440A-9B60-980EB5B42B13} + Library + WixToolset.Dtf.Samples.ManagedCA + WixToolset.Dtf.Samples.ManagedCA + v2.0 + OnBuildSuccess + + + + + + + + + + + {24121677-0ed0-41b5-833f-1b9a18e87bf4} + WixToolset.Dtf.WindowsInstaller + + + + + + + + diff --git a/src/samples/Dtf/ManagedCA/SampleCAs.cs b/src/samples/Dtf/ManagedCA/SampleCAs.cs new file mode 100644 index 00000000..645131c8 --- /dev/null +++ b/src/samples/Dtf/ManagedCA/SampleCAs.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Samples.ManagedCA +{ + using System; + using System.Collections.Generic; + using System.IO; + using WixToolset.Dtf.WindowsInstaller; + + public class SampleCAs + { + [CustomAction] + public static ActionResult SampleCA1(Session session) + { + using (Record msgRec = new Record(0)) + { + msgRec[0] = "Hello from SampleCA1!" + + "\r\nCLR version is v" + Environment.Version; + session.Message(InstallMessage.Info, msgRec); + session.Message(InstallMessage.User, msgRec); + } + + session.Log("Testing summary info..."); + SummaryInfo summInfo = session.Database.SummaryInfo; + session.Log("MSI PackageCode = {0}", summInfo.RevisionNumber); + session.Log("MSI ModifyDate = {0}", summInfo.LastSaveTime); + + string testProp = session["SampleCATest"]; + session.Log("Simple property test: [SampleCATest]={0}.", testProp); + + session.Log("Testing subdirectory extraction..."); + string testFilePath = "testsub\\SampleCAs.cs"; + if (!File.Exists(testFilePath)) + { + session.Log("Subdirectory extraction failed. File not found: " + testFilePath); + return ActionResult.Failure; + } + else + { + session.Log("Found file extracted in subdirectory."); + } + + session.Log("Testing record stream extraction..."); + string tempFile = null; + try + { + tempFile = Path.GetTempFileName(); + using (View binView = session.Database.OpenView( + "SELECT `Binary`.`Data` FROM `Binary`, `CustomAction` " + + "WHERE `CustomAction`.`Target` = 'SampleCA1' AND " + + "`CustomAction`.`Source` = `Binary`.`Name`")) + { + binView.Execute(); + using (Record binRec = binView.Fetch()) + { + binRec.GetStream(1, tempFile); + } + } + + session.Log("CA binary file size: {0}", new FileInfo(tempFile).Length); + string binFileVersion = Installer.GetFileVersion(tempFile); + session.Log("CA binary file version: {0}", binFileVersion); + } + finally + { + if (tempFile != null && File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + + session.Log("Testing record stream reading..."); + using (View binView2 = session.Database.OpenView("SELECT `Data` FROM `Binary` WHERE `Name` = 'TestData'")) + { + binView2.Execute(); + using (Record binRec2 = binView2.Fetch()) + { + Stream stream = binRec2.GetStream("Data"); + string testData = new StreamReader(stream, System.Text.Encoding.UTF8).ReadToEnd(); + session.Log("Test data: " + testData); + } + } + + session.Log("Listing components"); + using (View compView = session.Database.OpenView( + "SELECT `Component` FROM `Component`")) + { + compView.Execute(); + foreach (Record compRec in compView) + { + using (compRec) + { + session.Log("\t{0}", compRec["Component"]); + } + } + } + + session.Log("Testing the ability to access an external MSI database..."); + string tempDbFile = Path.GetTempFileName(); + using (Database tempDb = new Database(tempDbFile, DatabaseOpenMode.CreateDirect)) + { + // Just create an empty database. + } + using (Database tempDb2 = new Database(tempDbFile)) + { + // See if we can open and query the database. + IList tables = tempDb2.ExecuteStringQuery("SELECT `Name` FROM `_Tables`"); + session.Log("Found " + tables.Count + " tables in the newly created database."); + } + File.Delete(tempDbFile); + + return ActionResult.Success; + } + + [CustomAction("SampleCA2")] + public static ActionResult SampleCustomAction2(Session session) + { + using (Record msgRec = new Record(0)) + { + msgRec[0] = "Hello from SampleCA2!"; + session.Message(InstallMessage.Info, msgRec); + session.Message(InstallMessage.User, msgRec); + } + return ActionResult.UserExit; + } + } +} diff --git a/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.cs b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.cs new file mode 100644 index 00000000..76ff79b3 --- /dev/null +++ b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.cs @@ -0,0 +1,711 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Tools.MakeSfxCA +{ + using System; + using System.IO; + using System.Collections.Generic; + using System.Security; + using System.Text; + using System.Reflection; + using Compression; + using Compression.Cab; + using Resources; + using ResourceCollection = Resources.ResourceCollection; + + /// + /// Command-line tool for building self-extracting custom action packages. + /// Appends cabbed CA binaries to SfxCA.dll and fixes up the result's + /// entry-points and file version to look like the CA module. + /// + public static class MakeSfxCA + { + private const string REQUIRED_WI_ASSEMBLY = "WixToolset.Dtf.WindowsInstaller.dll"; + + private static TextWriter log; + + /// + /// Prints usage text for the tool. + /// + /// Console text writer. + public static void Usage(TextWriter w) + { + w.WriteLine("Deployment Tools Foundation custom action packager version {0}", + Assembly.GetExecutingAssembly().GetName().Version); + w.WriteLine("Copyright (C) .NET Foundation and contributors. All rights reserved."); + w.WriteLine(); + w.WriteLine("Usage: MakeSfxCA SfxCA.dll [support files ...]"); + w.WriteLine(); + w.WriteLine("Makes a self-extracting managed MSI CA or UI DLL package."); + w.WriteLine("Support files must include " + MakeSfxCA.REQUIRED_WI_ASSEMBLY); + w.WriteLine("Support files optionally include CustomAction.config/EmbeddedUI.config"); + } + + /// + /// Runs the MakeSfxCA command-line tool. + /// + /// Command-line arguments. + /// 0 on success, nonzero on failure. + public static int Main(string[] args) + { + if (args.Length < 3) + { + Usage(Console.Out); + return 1; + } + + var output = args[0]; + var sfxDll = args[1]; + var inputs = new string[args.Length - 2]; + Array.Copy(args, 2, inputs, 0, inputs.Length); + + try + { + Build(output, sfxDll, inputs, Console.Out); + return 0; + } + catch (ArgumentException ex) + { + Console.Error.WriteLine("Error: Invalid argument: " + ex.Message); + return 1; + } + catch (FileNotFoundException ex) + { + Console.Error.WriteLine("Error: Cannot find file: " + ex.Message); + return 1; + } + catch (Exception ex) + { + Console.Error.WriteLine("Error: Unexpected error: " + ex); + return 1; + } + } + + /// + /// Packages up all the inputs to the output location. + /// + /// Various exceptions are thrown + /// if things go wrong. + public static void Build(string output, string sfxDll, IList inputs, TextWriter log) + { + MakeSfxCA.log = log; + + if (string.IsNullOrEmpty(output)) + { + throw new ArgumentNullException("output"); + } + + if (string.IsNullOrEmpty(sfxDll)) + { + throw new ArgumentNullException("sfxDll"); + } + + if (inputs == null || inputs.Count == 0) + { + throw new ArgumentNullException("inputs"); + } + + if (!File.Exists(sfxDll)) + { + throw new FileNotFoundException(sfxDll); + } + + var customActionAssembly = inputs[0]; + if (!File.Exists(customActionAssembly)) + { + throw new FileNotFoundException(customActionAssembly); + } + + inputs = MakeSfxCA.SplitList(inputs); + + var inputsMap = MakeSfxCA.GetPackFileMap(inputs); + + var foundWIAssembly = false; + foreach (var input in inputsMap.Keys) + { + if (string.Compare(input, MakeSfxCA.REQUIRED_WI_ASSEMBLY, + StringComparison.OrdinalIgnoreCase) == 0) + { + foundWIAssembly = true; + } + } + + if (!foundWIAssembly) + { + throw new ArgumentException(MakeSfxCA.REQUIRED_WI_ASSEMBLY + + " must be included in the list of support files. " + + "If using the MSBuild targets, make sure the assembly reference " + + "has the Private (Copy Local) flag set."); + } + + MakeSfxCA.ResolveDependentAssemblies(inputsMap, Path.GetDirectoryName(customActionAssembly)); + + var entryPoints = MakeSfxCA.FindEntryPoints(customActionAssembly); + var uiClass = MakeSfxCA.FindEmbeddedUIClass(customActionAssembly); + + if (entryPoints.Count == 0 && uiClass == null) + { + throw new ArgumentException( + "No CA or UI entry points found in module: " + customActionAssembly); + } + else if (entryPoints.Count > 0 && uiClass != null) + { + throw new NotSupportedException( + "CA and UI entry points cannot be in the same assembly: " + customActionAssembly); + } + + var dir = Path.GetDirectoryName(output); + if (dir.Length > 0 && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + using (Stream outputStream = File.Create(output)) + { + MakeSfxCA.WriteEntryModule(sfxDll, outputStream, entryPoints, uiClass); + } + + MakeSfxCA.CopyVersionResource(customActionAssembly, output); + + MakeSfxCA.PackInputFiles(output, inputsMap); + + log.WriteLine("MakeSfxCA finished: " + new FileInfo(output).FullName); + } + + /// + /// Splits any list items delimited by semicolons into separate items. + /// + /// Read-only input list. + /// New list with resulting split items. + private static IList SplitList(IList list) + { + var newList = new List(list.Count); + + foreach (var item in list) + { + if (!string.IsNullOrEmpty(item)) + { + foreach (var splitItem in item.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + newList.Add(splitItem); + } + } + } + + return newList; + } + + /// + /// Sets up a reflection-only assembly-resolve-handler to handle loading dependent assemblies during reflection. + /// + /// List of input files which include non-GAC dependent assemblies. + /// Directory to auto-locate additional dependent assemblies. + /// + /// Also searches the assembly's directory for unspecified dependent assemblies, and adds them + /// to the list of input files if found. + /// + private static void ResolveDependentAssemblies(IDictionary inputFiles, string inputDir) + { + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += delegate(object sender, ResolveEventArgs args) + { + AssemblyName resolveName = new AssemblyName(args.Name); + Assembly assembly = null; + + // First, try to find the assembly in the list of input files. + foreach (var inputFile in inputFiles.Values) + { + var inputName = Path.GetFileNameWithoutExtension(inputFile); + var inputExtension = Path.GetExtension(inputFile); + if (string.Equals(inputName, resolveName.Name, StringComparison.OrdinalIgnoreCase) && + (string.Equals(inputExtension, ".dll", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputExtension, ".exe", StringComparison.OrdinalIgnoreCase))) + { + assembly = MakeSfxCA.TryLoadDependentAssembly(inputFile); + + if (assembly != null) + { + break; + } + } + } + + // Second, try to find the assembly in the input directory. + if (assembly == null && inputDir != null) + { + string assemblyPath = null; + if (File.Exists(Path.Combine(inputDir, resolveName.Name) + ".dll")) + { + assemblyPath = Path.Combine(inputDir, resolveName.Name) + ".dll"; + } + else if (File.Exists(Path.Combine(inputDir, resolveName.Name) + ".exe")) + { + assemblyPath = Path.Combine(inputDir, resolveName.Name) + ".exe"; + } + + if (assemblyPath != null) + { + assembly = MakeSfxCA.TryLoadDependentAssembly(assemblyPath); + + if (assembly != null) + { + // Add this detected dependency to the list of files to be packed. + inputFiles.Add(Path.GetFileName(assemblyPath), assemblyPath); + } + } + } + + // Third, try to load the assembly from the GAC. + if (assembly == null) + { + try + { + assembly = Assembly.ReflectionOnlyLoad(args.Name); + } + catch (FileNotFoundException) + { + } + } + + if (assembly != null) + { + if (string.Equals(assembly.GetName().ToString(), resolveName.ToString())) + { + log.WriteLine(" Loaded dependent assembly: " + assembly.Location); + return assembly; + } + + log.WriteLine(" Warning: Loaded mismatched dependent assembly: " + assembly.Location); + log.WriteLine(" Loaded assembly : " + assembly.GetName()); + log.WriteLine(" Reference assembly: " + resolveName); + } + else + { + log.WriteLine(" Error: Dependent assembly not supplied: " + resolveName); + } + + return null; + }; + } + + /// + /// Attempts a reflection-only load of a dependent assembly, logging the error if the load fails. + /// + /// Path of the assembly file to laod. + /// Loaded assembly, or null if the load failed. + private static Assembly TryLoadDependentAssembly(string assemblyPath) + { + Assembly assembly = null; + try + { + assembly = Assembly.ReflectionOnlyLoadFrom(assemblyPath); + } + catch (IOException ex) + { + log.WriteLine(" Error: Failed to load dependent assembly: {0}. {1}", assemblyPath, ex.Message); + } + catch (BadImageFormatException ex) + { + log.WriteLine(" Error: Failed to load dependent assembly: {0}. {1}", assemblyPath, ex.Message); + } + catch (SecurityException ex) + { + log.WriteLine(" Error: Failed to load dependent assembly: {0}. {1}", assemblyPath, ex.Message); + } + + return assembly; + } + + /// + /// Searches the types in the input assembly for a type that implements IEmbeddedUI. + /// + /// + /// + private static string FindEmbeddedUIClass(string module) + { + log.WriteLine("Searching for an embedded UI class in {0}", Path.GetFileName(module)); + + string uiClass = null; + + var assembly = Assembly.ReflectionOnlyLoadFrom(module); + + foreach (var type in assembly.GetExportedTypes()) + { + if (!type.IsAbstract) + { + foreach (var interfaceType in type.GetInterfaces()) + { + if (interfaceType.FullName == "WixToolset.Dtf.WindowsInstaller.IEmbeddedUI") + { + if (uiClass == null) + { + uiClass = assembly.GetName().Name + "!" + type.FullName; + } + else + { + throw new ArgumentException("Multiple IEmbeddedUI implementations found."); + } + } + } + } + } + + return uiClass; + } + + /// + /// Reflects on an input CA module to locate custom action entry-points. + /// + /// Assembly module with CA entry-points. + /// Mapping from entry-point names to assembly!class.method paths. + private static IDictionary FindEntryPoints(string module) + { + log.WriteLine("Searching for custom action entry points " + + "in {0}", Path.GetFileName(module)); + + var entryPoints = new Dictionary(); + + var assembly = Assembly.ReflectionOnlyLoadFrom(module); + + foreach (var type in assembly.GetExportedTypes()) + { + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + var entryPointName = MakeSfxCA.GetEntryPoint(method); + if (entryPointName != null) + { + var entryPointPath = string.Format( + "{0}!{1}.{2}", + Path.GetFileNameWithoutExtension(module), + type.FullName, + method.Name); + entryPoints.Add(entryPointName, entryPointPath); + + log.WriteLine(" {0}={1}", entryPointName, entryPointPath); + } + } + } + + return entryPoints; + } + + /// + /// Check for a CustomActionAttribute and return the entrypoint name for the method if it is a CA method. + /// + /// A public static method. + /// Entrypoint name for the method as specified by the custom action attribute or just the method name, + /// or null if the method is not a custom action method. + private static string GetEntryPoint(MethodInfo method) + { + IList attributes; + try + { + attributes = CustomAttributeData.GetCustomAttributes(method); + } + catch (FileLoadException) + { + // Already logged load failures in the assembly-resolve-handler. + return null; + } + + foreach (CustomAttributeData attribute in attributes) + { + if (attribute.ToString().StartsWith( + "[WixToolset.Dtf.WindowsInstaller.CustomActionAttribute(", + StringComparison.Ordinal)) + { + string entryPointName = null; + foreach (var argument in attribute.ConstructorArguments) + { + // The entry point name is the first positional argument, if specified. + entryPointName = (string) argument.Value; + break; + } + + if (string.IsNullOrEmpty(entryPointName)) + { + entryPointName = method.Name; + } + + return entryPointName; + } + } + + return null; + } + + /// + /// Counts the number of template entrypoints in SfxCA.dll. + /// + /// + /// Depending on the requirements, SfxCA.dll might be built with + /// more entrypoints than the default. + /// + private static int GetEntryPointSlotCount(byte[] fileBytes, string entryPointFormat) + { + for (var count = 0; ; count++) + { + var templateName = string.Format(entryPointFormat, count); + var templateAsciiBytes = Encoding.ASCII.GetBytes(templateName); + + var nameOffset = FindBytes(fileBytes, templateAsciiBytes); + if (nameOffset < 0) + { + return count; + } + } + } + + /// + /// Writes a modified version of SfxCA.dll to the output stream, + /// with the template entry-points mapped to the CA entry-points. + /// + /// + /// To avoid having to recompile SfxCA.dll for every different set of CAs, + /// this method looks for a preset number of template entry-points in the + /// binary file and overwrites their entrypoint name and string data with + /// CA-specific values. + /// + private static void WriteEntryModule( + string sfxDll, Stream outputStream, IDictionary entryPoints, string uiClass) + { + log.WriteLine("Modifying SfxCA.dll stub"); + + byte[] fileBytes; + using (var readStream = File.OpenRead(sfxDll)) + { + fileBytes = new byte[(int) readStream.Length]; + readStream.Read(fileBytes, 0, fileBytes.Length); + } + + const string ENTRYPOINT_FORMAT = "CustomActionEntryPoint{0:d03}"; + const int MAX_ENTRYPOINT_NAME = 72; + const int MAX_ENTRYPOINT_PATH = 160; + //var emptyBytes = new byte[0]; + + var slotCount = MakeSfxCA.GetEntryPointSlotCount(fileBytes, ENTRYPOINT_FORMAT); + + if (slotCount == 0) + { + throw new ArgumentException("Invalid SfxCA.dll file."); + } + + if (entryPoints.Count > slotCount) + { + throw new ArgumentException(string.Format( + "The custom action assembly has {0} entrypoints, which is more than the maximum ({1}). " + + "Refactor the custom actions or add more entrypoint slots in SfxCA\\EntryPoints.h.", + entryPoints.Count, slotCount)); + } + + var slotSort = new string[slotCount]; + for (var i = 0; i < slotCount - entryPoints.Count; i++) + { + slotSort[i] = string.Empty; + } + + entryPoints.Keys.CopyTo(slotSort, slotCount - entryPoints.Count); + Array.Sort(slotSort, slotCount - entryPoints.Count, entryPoints.Count, StringComparer.Ordinal); + + for (var i = 0; ; i++) + { + var templateName = string.Format(ENTRYPOINT_FORMAT, i); + var templateAsciiBytes = Encoding.ASCII.GetBytes(templateName); + var templateUniBytes = Encoding.Unicode.GetBytes(templateName); + + var nameOffset = MakeSfxCA.FindBytes(fileBytes, templateAsciiBytes); + if (nameOffset < 0) + { + break; + } + + var pathOffset = MakeSfxCA.FindBytes(fileBytes, templateUniBytes); + if (pathOffset < 0) + { + break; + } + + var entryPointName = slotSort[i]; + var entryPointPath = entryPointName.Length > 0 ? + entryPoints[entryPointName] : string.Empty; + + if (entryPointName.Length > MAX_ENTRYPOINT_NAME) + { + throw new ArgumentException(string.Format( + "Entry point name exceeds limit of {0} characters: {1}", + MAX_ENTRYPOINT_NAME, + entryPointName)); + } + + if (entryPointPath.Length > MAX_ENTRYPOINT_PATH) + { + throw new ArgumentException(string.Format( + "Entry point path exceeds limit of {0} characters: {1}", + MAX_ENTRYPOINT_PATH, + entryPointPath)); + } + + var replaceNameBytes = Encoding.ASCII.GetBytes(entryPointName); + var replacePathBytes = Encoding.Unicode.GetBytes(entryPointPath); + + MakeSfxCA.ReplaceBytes(fileBytes, nameOffset, MAX_ENTRYPOINT_NAME, replaceNameBytes); + MakeSfxCA.ReplaceBytes(fileBytes, pathOffset, MAX_ENTRYPOINT_PATH * 2, replacePathBytes); + } + + if (entryPoints.Count == 0 && uiClass != null) + { + // Remove the zzz prefix from exported EmbeddedUI entry-points. + foreach (var export in new string[] { "InitializeEmbeddedUI", "EmbeddedUIHandler", "ShutdownEmbeddedUI" }) + { + var exportNameBytes = Encoding.ASCII.GetBytes("zzz" + export); + + var exportOffset = MakeSfxCA.FindBytes(fileBytes, exportNameBytes); + if (exportOffset < 0) + { + throw new ArgumentException("Input SfxCA.dll does not contain exported entry-point: " + export); + } + + var replaceNameBytes = Encoding.ASCII.GetBytes(export); + MakeSfxCA.ReplaceBytes(fileBytes, exportOffset, exportNameBytes.Length, replaceNameBytes); + } + + if (uiClass.Length > MAX_ENTRYPOINT_PATH) + { + throw new ArgumentException(string.Format( + "UI class full name exceeds limit of {0} characters: {1}", + MAX_ENTRYPOINT_PATH, + uiClass)); + } + + var templateBytes = Encoding.Unicode.GetBytes("InitializeEmbeddedUI_FullClassName"); + var replaceBytes = Encoding.Unicode.GetBytes(uiClass); + + // Fill in the embedded UI implementor class so the proxy knows which one to load. + var replaceOffset = MakeSfxCA.FindBytes(fileBytes, templateBytes); + if (replaceOffset >= 0) + { + MakeSfxCA.ReplaceBytes(fileBytes, replaceOffset, MAX_ENTRYPOINT_PATH * 2, replaceBytes); + } + } + + outputStream.Write(fileBytes, 0, fileBytes.Length); + } + + /// + /// Searches for a sub-array of bytes within a larger array of bytes. + /// + private static int FindBytes(byte[] source, byte[] find) + { + for (var i = 0; i < source.Length; i++) + { + int j; + for (j = 0; j < find.Length; j++) + { + if (source[i + j] != find[j]) + { + break; + } + } + + if (j == find.Length) + { + return i; + } + } + + return -1; + } + + /// + /// Replaces a range of bytes with new bytes, padding any extra part + /// of the range with zeroes. + /// + private static void ReplaceBytes( + byte[] source, int offset, int length, byte[] replace) + { + for (var i = 0; i < length; i++) + { + if (i < replace.Length) + { + source[offset + i] = replace[i]; + } + else + { + source[offset + i] = 0; + } + } + } + + /// + /// Print the name of one file as it is being packed into the cab. + /// + private static void PackProgress(object source, ArchiveProgressEventArgs e) + { + if (e.ProgressType == ArchiveProgressType.StartFile && log != null) + { + log.WriteLine(" {0}", e.CurrentFileName); + } + } + + /// + /// Gets a mapping from filenames as they will be in the cab to filenames + /// as they are currently on disk. + /// + /// + /// By default, all files will be placed in the root of the cab. But inputs may + /// optionally include an alternate inside-cab file path before an equals sign. + /// + private static IDictionary GetPackFileMap(IList inputs) + { + var fileMap = new Dictionary(); + foreach (var inputFile in inputs) + { + if (inputFile.IndexOf('=') > 0) + { + var parse = inputFile.Split('='); + if (!fileMap.ContainsKey(parse[0])) + { + fileMap.Add(parse[0], parse[1]); + } + } + else + { + var fileName = Path.GetFileName(inputFile); + if (!fileMap.ContainsKey(fileName)) + { + fileMap.Add(fileName, inputFile); + } + } + } + return fileMap; + } + + /// + /// Packs the input files into a cab that is appended to the + /// output SfxCA.dll. + /// + private static void PackInputFiles(string outputFile, IDictionary fileMap) + { + log.WriteLine("Packaging files"); + + var cabInfo = new CabInfo(outputFile); + cabInfo.PackFileSet(null, fileMap, CompressionLevel.Max, PackProgress); + } + + /// + /// Copies the version resource information from the CA module to + /// the CA package. This gives the package the file version and + /// description of the CA module, instead of the version and + /// description of SfxCA.dll. + /// + private static void CopyVersionResource(string sourceFile, string destFile) + { + log.WriteLine("Copying file version info from {0} to {1}", + sourceFile, destFile); + + var rc = new ResourceCollection(); + rc.Find(sourceFile, ResourceType.Version); + rc.Load(sourceFile); + rc.Save(destFile); + } + } +} diff --git a/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.csproj b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.csproj new file mode 100644 index 00000000..c6982532 --- /dev/null +++ b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.csproj @@ -0,0 +1,33 @@ + + + + + + netcoreapp3.1;net461 + Exe + WixToolset.Dtf.Tools.MakeSfxCA + MakeSfxCA + embedded + app.config + MakeSfxCA.exe.manifest + Major + win-x86 + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + diff --git a/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.exe.manifest b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.exe.manifest new file mode 100644 index 00000000..49b074e0 --- /dev/null +++ b/src/samples/Dtf/Tools/MakeSfxCA/MakeSfxCA.exe.manifest @@ -0,0 +1,20 @@ + + + + + + + WiX Toolset Compiler + + + + + + + + + + true + + + diff --git a/src/samples/Dtf/Tools/MakeSfxCA/app.config b/src/samples/Dtf/Tools/MakeSfxCA/app.config new file mode 100644 index 00000000..65d3d6c3 --- /dev/null +++ b/src/samples/Dtf/Tools/MakeSfxCA/app.config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/samples/Dtf/Tools/SfxCA/ClrHost.cpp b/src/samples/Dtf/Tools/SfxCA/ClrHost.cpp new file mode 100644 index 00000000..1988fb2a --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/ClrHost.cpp @@ -0,0 +1,262 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" + +void Log(MSIHANDLE hSession, const wchar_t* szMessage, ...); + +//--------------------------------------------------------------------- +// CLR HOSTING +//--------------------------------------------------------------------- + +/// +/// Binds to the CLR after determining the appropriate version. +/// +/// Handle to the installer session, +/// used just for logging. +/// Specific version of the CLR to load. +/// If null, then the config file and/or primary assembly are +/// used to determine the version. +/// XML .config file which may contain +/// a startup section to direct which version of the CLR to use. +/// May be NULL. +/// Assembly to be used to determine +/// the version of the CLR in the absence of other configuration. +/// May be NULL. +/// Returned runtime host interface. +/// True if the CLR was loaded successfully, false if +/// there was some error. +/// +/// If szPrimaryAssembly is NULL and szConfigFile is also NULL or +/// does not contain any version configuration, the CLR will not be loaded. +/// +bool LoadCLR(MSIHANDLE hSession, const wchar_t* szVersion, const wchar_t* szConfigFile, + const wchar_t* szPrimaryAssembly, ICorRuntimeHost** ppHost) +{ + typedef HRESULT (__stdcall *PGetRequestedRuntimeInfo)(LPCWSTR pExe, LPCWSTR pwszVersion, + LPCWSTR pConfigurationFile, DWORD startupFlags, DWORD runtimeInfoFlags, + LPWSTR pDirectory, DWORD dwDirectory, DWORD *dwDirectoryLength, + LPWSTR pVersion, DWORD cchBuffer, DWORD* dwlength); + typedef HRESULT (__stdcall *PCorBindToRuntimeEx)(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, + DWORD startupFlags, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv); + + HMODULE hmodMscoree = LoadLibrary(L"mscoree.dll"); + if (hmodMscoree == NULL) + { + Log(hSession, L"Failed to load mscoree.dll (Error code %d). This custom action " + L"requires the .NET Framework to be installed.", GetLastError()); + return false; + } + PGetRequestedRuntimeInfo pGetRequestedRuntimeInfo = (PGetRequestedRuntimeInfo) + GetProcAddress(hmodMscoree, "GetRequestedRuntimeInfo"); + PCorBindToRuntimeEx pCorBindToRuntimeEx = (PCorBindToRuntimeEx) + GetProcAddress(hmodMscoree, "CorBindToRuntimeEx"); + if (pGetRequestedRuntimeInfo == NULL || pCorBindToRuntimeEx == NULL) + { + Log(hSession, L"Failed to locate functions in mscoree.dll (Error code %d). This custom action " + L"requires the .NET Framework to be installed.", GetLastError()); + FreeLibrary(hmodMscoree); + return false; + } + + wchar_t szClrVersion[20]; + HRESULT hr; + + if (szVersion != NULL && szVersion[0] != L'\0') + { + wcsncpy_s(szClrVersion, 20, szVersion, 20); + } + else + { + wchar_t szVersionDir[MAX_PATH]; + hr = pGetRequestedRuntimeInfo(szPrimaryAssembly, NULL, + szConfigFile, 0, 0, szVersionDir, MAX_PATH, NULL, szClrVersion, 20, NULL); + if (FAILED(hr)) + { + Log(hSession, L"Failed to get requested CLR info. Error code 0x%x", hr); + Log(hSession, L"Ensure that the proper version of the .NET Framework is installed, or " + L"that there is a matching supportedRuntime element in CustomAction.config. " + L"If you are binding to .NET 4 or greater add " + L"useLegacyV2RuntimeActivationPolicy=true to the element."); + FreeLibrary(hmodMscoree); + return false; + } + } + + Log(hSession, L"Binding to CLR version %s", szClrVersion); + + ICorRuntimeHost* pHost; + hr = pCorBindToRuntimeEx(szClrVersion, NULL, + STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN, + CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void**) &pHost); + if (FAILED(hr)) + { + Log(hSession, L"Failed to bind to the CLR. Error code 0x%X", hr); + FreeLibrary(hmodMscoree); + return false; + } + hr = pHost->Start(); + if (FAILED(hr)) + { + Log(hSession, L"Failed to start the CLR. Error code 0x%X", hr); + pHost->Release(); + FreeLibrary(hmodMscoree); + return false; + } + *ppHost = pHost; + FreeLibrary(hmodMscoree); + return true; +} + +/// +/// Creates a new CLR application domain. +/// +/// Handle to the installer session, +/// used just for logging +/// Interface to the runtime host where the +/// app domain will be created. +/// Name of the app domain to create. +/// Application base directory path, where +/// the app domain will look first to load its assemblies. +/// Optional XML .config file containing any +/// configuration for thae app domain. +/// Returned app domain interface. +/// True if the app domain was created successfully, false if +/// there was some error. +bool CreateAppDomain(MSIHANDLE hSession, ICorRuntimeHost* pHost, + const wchar_t* szName, const wchar_t* szAppBase, + const wchar_t* szConfigFile, _AppDomain** ppAppDomain) +{ + IUnknown* punkAppDomainSetup = NULL; + IAppDomainSetup* pAppDomainSetup = NULL; + HRESULT hr = pHost->CreateDomainSetup(&punkAppDomainSetup); + if (SUCCEEDED(hr)) + { + hr = punkAppDomainSetup->QueryInterface(__uuidof(IAppDomainSetup), (void**) &pAppDomainSetup); + punkAppDomainSetup->Release(); + } + if (FAILED(hr)) + { + Log(hSession, L"Failed to create app domain setup. Error code 0x%X", hr); + return false; + } + + const wchar_t* szUrlPrefix = L"file:///"; + size_t cchApplicationBase = wcslen(szUrlPrefix) + wcslen(szAppBase); + wchar_t* szApplicationBase = (wchar_t*) _alloca((cchApplicationBase + 1) * sizeof(wchar_t)); + if (szApplicationBase == NULL) hr = E_OUTOFMEMORY; + else + { + StringCchCopy(szApplicationBase, cchApplicationBase + 1, szUrlPrefix); + StringCchCat(szApplicationBase, cchApplicationBase + 1, szAppBase); + BSTR bstrApplicationBase = SysAllocString(szApplicationBase); + if (bstrApplicationBase == NULL) hr = E_OUTOFMEMORY; + else + { + hr = pAppDomainSetup->put_ApplicationBase(bstrApplicationBase); + SysFreeString(bstrApplicationBase); + } + } + + if (SUCCEEDED(hr) && szConfigFile != NULL) + { + BSTR bstrConfigFile = SysAllocString(szConfigFile); + if (bstrConfigFile == NULL) hr = E_OUTOFMEMORY; + else + { + hr = pAppDomainSetup->put_ConfigurationFile(bstrConfigFile); + SysFreeString(bstrConfigFile); + } + } + + if (FAILED(hr)) + { + Log(hSession, L"Failed to configure app domain setup. Error code 0x%X", hr); + pAppDomainSetup->Release(); + return false; + } + + IUnknown* punkAppDomain; + hr = pHost->CreateDomainEx(szName, pAppDomainSetup, NULL, &punkAppDomain); + pAppDomainSetup->Release(); + if (SUCCEEDED(hr)) + { + hr = punkAppDomain->QueryInterface(__uuidof(_AppDomain), (void**) ppAppDomain); + punkAppDomain->Release(); + } + + if (FAILED(hr)) + { + Log(hSession, L"Failed to create app domain. Error code 0x%X", hr); + return false; + } + + return true; +} + +/// +/// Locates a specific method in a specific class and assembly. +/// +/// Handle to the installer session, +/// used just for logging +/// Application domain in which to +/// load assemblies. +/// Display name of the assembly +/// containing the method. +/// Fully-qualified name of the class +/// containing the method. +/// Name of the method. +/// Returned method interface. +/// True if the method was located, otherwise false. +/// Only public static methods are searched. Method +/// parameter types are not considered; if there are multiple +/// matching methods with different parameters, an error results. +bool GetMethod(MSIHANDLE hSession, _AppDomain* pAppDomain, + const wchar_t* szAssembly, const wchar_t* szClass, + const wchar_t* szMethod, _MethodInfo** ppMethod) +{ + HRESULT hr; + _Assembly* pAssembly = NULL; + BSTR bstrAssemblyName = SysAllocString(szAssembly); + if (bstrAssemblyName == NULL) hr = E_OUTOFMEMORY; + else + { + hr = pAppDomain->Load_2(bstrAssemblyName, &pAssembly); + SysFreeString(bstrAssemblyName); + } + if (FAILED(hr)) + { + Log(hSession, L"Failed to load assembly %s. Error code 0x%X", szAssembly, hr); + return false; + } + + _Type* pType = NULL; + BSTR bstrClass = SysAllocString(szClass); + if (bstrClass == NULL) hr = E_OUTOFMEMORY; + else + { + hr = pAssembly->GetType_2(bstrClass, &pType); + SysFreeString(bstrClass); + } + pAssembly->Release(); + if (FAILED(hr) || pType == NULL) + { + Log(hSession, L"Failed to load class %s. Error code 0x%X", szClass, hr); + return false; + } + + BSTR bstrMethod = SysAllocString(szMethod); + if (bstrMethod == NULL) hr = E_OUTOFMEMORY; + else + { + hr = pType->GetMethod_2(bstrMethod, + (BindingFlags) (BindingFlags_Public | BindingFlags_Static), ppMethod); + SysFreeString(bstrMethod); + } + pType->Release(); + if (FAILED(hr) || *ppMethod == NULL) + { + Log(hSession, L"Failed to get method %s. Error code 0x%X", szMethod, hr); + return false; + } + return true; +} diff --git a/src/samples/Dtf/Tools/SfxCA/EmbeddedUI.cpp b/src/samples/Dtf/Tools/SfxCA/EmbeddedUI.cpp new file mode 100644 index 00000000..a49cdeec --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/EmbeddedUI.cpp @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "SfxUtil.h" + +// Globals for keeping track of things across UI messages. +static const wchar_t* g_szWorkingDir; +static ICorRuntimeHost* g_pClrHost; +static _AppDomain* g_pAppDomain; +static _MethodInfo* g_pProcessMessageMethod; +static _MethodInfo* g_pShutdownMethod; + +// Reserve extra space for strings to be replaced at build time. +#define NULLSPACE \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + +// Prototypes for local functions. +// See the function definitions for comments. + +bool InvokeInitializeMethod(_MethodInfo* pInitMethod, MSIHANDLE hSession, + const wchar_t* szClassName, LPDWORD pdwInternalUILevel, UINT* puiResult); + +/// +/// First entry-point for the UI DLL when loaded and called by MSI. +/// Extracts the payload, hosts the CLR, and invokes the managed +/// initialize method. +/// +/// Handle to the installer session, +/// used for logging errors and to be passed on to the managed initialize method. +/// Path the directory where resources from the MsiEmbeddedUI table +/// have been extracted, and where additional payload from this package will be extracted. +/// MSI install UI level passed to and returned from +/// the managed initialize method. +extern "C" +UINT __stdcall InitializeEmbeddedUI(MSIHANDLE hSession, LPCWSTR szResourcePath, LPDWORD pdwInternalUILevel) +{ + // If the managed initialize method cannot be called, continue the installation in BASIC UI mode. + UINT uiResult = INSTALLUILEVEL_BASIC; + + const wchar_t* szClassName = L"InitializeEmbeddedUI_FullClassName" NULLSPACE; + + g_szWorkingDir = szResourcePath; + + wchar_t szModule[MAX_PATH]; + DWORD cchCopied = GetModuleFileName(g_hModule, szModule, MAX_PATH - 1); + if (cchCopied == 0) + { + Log(hSession, L"Failed to get module path. Error code %d.", GetLastError()); + return uiResult; + } + else if (cchCopied == MAX_PATH - 1) + { + Log(hSession, L"Failed to get module path -- path is too long."); + return uiResult; + } + + Log(hSession, L"Extracting embedded UI to temporary directory: %s", g_szWorkingDir); + int err = ExtractCabinet(szModule, g_szWorkingDir); + if (err != 0) + { + Log(hSession, L"Failed to extract to temporary directory. Cabinet error code %d.", err); + Log(hSession, L"Ensure that no MsiEmbeddedUI.FileName values are the same as " + L"any file contained in the embedded UI package."); + return uiResult; + } + + wchar_t szConfigFilePath[MAX_PATH + 20]; + StringCchCopy(szConfigFilePath, MAX_PATH + 20, g_szWorkingDir); + StringCchCat(szConfigFilePath, MAX_PATH + 20, L"\\EmbeddedUI.config"); + + const wchar_t* szConfigFile = szConfigFilePath; + if (!PathFileExists(szConfigFilePath)) + { + szConfigFile = NULL; + } + + wchar_t szWIAssembly[MAX_PATH + 50]; + StringCchCopy(szWIAssembly, MAX_PATH + 50, g_szWorkingDir); + StringCchCat(szWIAssembly, MAX_PATH + 50, L"\\WixToolset.Dtf.WindowsInstaller.dll"); + + if (LoadCLR(hSession, NULL, szConfigFile, szWIAssembly, &g_pClrHost)) + { + if (CreateAppDomain(hSession, g_pClrHost, L"EmbeddedUI", g_szWorkingDir, + szConfigFile, &g_pAppDomain)) + { + const wchar_t* szMsiAssemblyName = L"WixToolset.Dtf.WindowsInstaller"; + const wchar_t* szProxyClass = L"WixToolset.Dtf.WindowsInstaller.EmbeddedUIProxy"; + const wchar_t* szInitMethod = L"Initialize"; + const wchar_t* szProcessMessageMethod = L"ProcessMessage"; + const wchar_t* szShutdownMethod = L"Shutdown"; + + if (GetMethod(hSession, g_pAppDomain, szMsiAssemblyName, + szProxyClass, szProcessMessageMethod, &g_pProcessMessageMethod) && + GetMethod(hSession, g_pAppDomain, szMsiAssemblyName, + szProxyClass, szShutdownMethod, &g_pShutdownMethod)) + { + _MethodInfo* pInitMethod; + if (GetMethod(hSession, g_pAppDomain, szMsiAssemblyName, + szProxyClass, szInitMethod, &pInitMethod)) + { + bool invokeSuccess = InvokeInitializeMethod(pInitMethod, hSession, szClassName, pdwInternalUILevel, &uiResult); + pInitMethod->Release(); + if (invokeSuccess) + { + if (uiResult == 0) + { + return ERROR_SUCCESS; + } + else if (uiResult == ERROR_INSTALL_USEREXIT) + { + // InitializeEmbeddedUI is not allowed to return ERROR_INSTALL_USEREXIT. + // So return success here and then IDCANCEL on the next progress message. + uiResult = 0; + *pdwInternalUILevel = INSTALLUILEVEL_NONE; + Log(hSession, L"Initialization canceled by user."); + } + } + } + } + + g_pProcessMessageMethod->Release(); + g_pProcessMessageMethod = NULL; + g_pShutdownMethod->Release(); + g_pShutdownMethod = NULL; + + g_pClrHost->UnloadDomain(g_pAppDomain); + g_pAppDomain->Release(); + g_pAppDomain = NULL; + } + g_pClrHost->Stop(); + g_pClrHost->Release(); + g_pClrHost = NULL; + } + + return uiResult; +} + +/// +/// Entry-point for UI progress messages received from the MSI engine during an active installation. +/// Forwards the progress messages to the managed handler method and returns its result. +/// +extern "C" +INT __stdcall EmbeddedUIHandler(UINT uiMessageType, MSIHANDLE hRecord) +{ + if (g_pProcessMessageMethod == NULL) + { + // Initialization was canceled. + return IDCANCEL; + } + + VARIANT vResult; + VariantInit(&vResult); + + VARIANT vNull; + vNull.vt = VT_EMPTY; + + SAFEARRAY* saArgs = SafeArrayCreateVector(VT_VARIANT, 0, 2); + VARIANT vMessageType; + vMessageType.vt = VT_I4; + vMessageType.lVal = (LONG) uiMessageType; + LONG index = 0; + HRESULT hr = SafeArrayPutElement(saArgs, &index, &vMessageType); + if (FAILED(hr)) goto LExit; + VARIANT vRecord; + vRecord.vt = VT_I4; + vRecord.lVal = (LONG) hRecord; + index = 1; + hr = SafeArrayPutElement(saArgs, &index, &vRecord); + if (FAILED(hr)) goto LExit; + + hr = g_pProcessMessageMethod->Invoke_3(vNull, saArgs, &vResult); + +LExit: + SafeArrayDestroy(saArgs); + if (SUCCEEDED(hr)) + { + return vResult.intVal; + } + else + { + return -1; + } +} + +/// +/// Entry-point for the UI shutdown message received from the MSI engine after installation has completed. +/// Forwards the shutdown message to the managed shutdown method, then shuts down the CLR. +/// +extern "C" +DWORD __stdcall ShutdownEmbeddedUI() +{ + if (g_pShutdownMethod != NULL) + { + VARIANT vNull; + vNull.vt = VT_EMPTY; + SAFEARRAY* saArgs = SafeArrayCreateVector(VT_VARIANT, 0, 0); + g_pShutdownMethod->Invoke_3(vNull, saArgs, NULL); + SafeArrayDestroy(saArgs); + + g_pClrHost->UnloadDomain(g_pAppDomain); + g_pAppDomain->Release(); + g_pClrHost->Stop(); + g_pClrHost->Release(); + } + + return 0; +} + +/// +/// Loads and invokes the managed portion of the proxy. +/// +/// Managed initialize method to be invoked. +/// Handle to the installer session, +/// used for logging errors and to be passed on to the managed initialize method. +/// Name of the UI class to be loaded. +/// This must be of the form: AssemblyName!Namespace.Class +/// MSI install UI level passed to and returned from +/// the managed initialize method. +/// Return value of the invoked initialize method. +/// True if the managed proxy was invoked successfully, or an +/// error code if there was some error. Note the initialize method itself may +/// return an error via puiResult while this method still returns true +/// since the invocation was successful. +bool InvokeInitializeMethod(_MethodInfo* pInitMethod, MSIHANDLE hSession, const wchar_t* szClassName, LPDWORD pdwInternalUILevel, UINT* puiResult) +{ + VARIANT vResult; + VariantInit(&vResult); + + VARIANT vNull; + vNull.vt = VT_EMPTY; + + SAFEARRAY* saArgs = SafeArrayCreateVector(VT_VARIANT, 0, 3); + VARIANT vSessionHandle; + vSessionHandle.vt = VT_I4; + vSessionHandle.lVal = (LONG) hSession; + LONG index = 0; + HRESULT hr = SafeArrayPutElement(saArgs, &index, &vSessionHandle); + if (FAILED(hr)) goto LExit; + VARIANT vEntryPoint; + vEntryPoint.vt = VT_BSTR; + vEntryPoint.bstrVal = SysAllocString(szClassName); + if (vEntryPoint.bstrVal == NULL) + { + hr = E_OUTOFMEMORY; + goto LExit; + } + index = 1; + hr = SafeArrayPutElement(saArgs, &index, &vEntryPoint); + if (FAILED(hr)) goto LExit; + VARIANT vUILevel; + vUILevel.vt = VT_I4; + vUILevel.ulVal = *pdwInternalUILevel; + index = 2; + hr = SafeArrayPutElement(saArgs, &index, &vUILevel); + if (FAILED(hr)) goto LExit; + + hr = pInitMethod->Invoke_3(vNull, saArgs, &vResult); + +LExit: + SafeArrayDestroy(saArgs); + if (SUCCEEDED(hr)) + { + *puiResult = (UINT) vResult.lVal; + if ((*puiResult & 0xFFFF) == 0) + { + // Due to interop limitations, the successful resulting UILevel is returned + // as the high-word of the return value instead of via a ref parameter. + *pdwInternalUILevel = *puiResult >> 16; + *puiResult = 0; + } + return true; + } + else + { + Log(hSession, L"Failed to invoke EmbeddedUI Initialize method. Error code 0x%X", hr); + return false; + } +} diff --git a/src/samples/Dtf/Tools/SfxCA/EntryPoints.def b/src/samples/Dtf/Tools/SfxCA/EntryPoints.def new file mode 100644 index 00000000..dd28b920 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/EntryPoints.def @@ -0,0 +1,140 @@ +; Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + + +LIBRARY "SfxCA" + +EXPORTS + +CustomActionEntryPoint000________________________________________________=CustomActionEntryPoint000 +CustomActionEntryPoint001________________________________________________=CustomActionEntryPoint001 +CustomActionEntryPoint002________________________________________________=CustomActionEntryPoint002 +CustomActionEntryPoint003________________________________________________=CustomActionEntryPoint003 +CustomActionEntryPoint004________________________________________________=CustomActionEntryPoint004 +CustomActionEntryPoint005________________________________________________=CustomActionEntryPoint005 +CustomActionEntryPoint006________________________________________________=CustomActionEntryPoint006 +CustomActionEntryPoint007________________________________________________=CustomActionEntryPoint007 +CustomActionEntryPoint008________________________________________________=CustomActionEntryPoint008 +CustomActionEntryPoint009________________________________________________=CustomActionEntryPoint009 +CustomActionEntryPoint010________________________________________________=CustomActionEntryPoint010 +CustomActionEntryPoint011________________________________________________=CustomActionEntryPoint011 +CustomActionEntryPoint012________________________________________________=CustomActionEntryPoint012 +CustomActionEntryPoint013________________________________________________=CustomActionEntryPoint013 +CustomActionEntryPoint014________________________________________________=CustomActionEntryPoint014 +CustomActionEntryPoint015________________________________________________=CustomActionEntryPoint015 +CustomActionEntryPoint016________________________________________________=CustomActionEntryPoint016 +CustomActionEntryPoint017________________________________________________=CustomActionEntryPoint017 +CustomActionEntryPoint018________________________________________________=CustomActionEntryPoint018 +CustomActionEntryPoint019________________________________________________=CustomActionEntryPoint019 +CustomActionEntryPoint020________________________________________________=CustomActionEntryPoint020 +CustomActionEntryPoint021________________________________________________=CustomActionEntryPoint021 +CustomActionEntryPoint022________________________________________________=CustomActionEntryPoint022 +CustomActionEntryPoint023________________________________________________=CustomActionEntryPoint023 +CustomActionEntryPoint024________________________________________________=CustomActionEntryPoint024 +CustomActionEntryPoint025________________________________________________=CustomActionEntryPoint025 +CustomActionEntryPoint026________________________________________________=CustomActionEntryPoint026 +CustomActionEntryPoint027________________________________________________=CustomActionEntryPoint027 +CustomActionEntryPoint028________________________________________________=CustomActionEntryPoint028 +CustomActionEntryPoint029________________________________________________=CustomActionEntryPoint029 +CustomActionEntryPoint030________________________________________________=CustomActionEntryPoint030 +CustomActionEntryPoint031________________________________________________=CustomActionEntryPoint031 +CustomActionEntryPoint032________________________________________________=CustomActionEntryPoint032 +CustomActionEntryPoint033________________________________________________=CustomActionEntryPoint033 +CustomActionEntryPoint034________________________________________________=CustomActionEntryPoint034 +CustomActionEntryPoint035________________________________________________=CustomActionEntryPoint035 +CustomActionEntryPoint036________________________________________________=CustomActionEntryPoint036 +CustomActionEntryPoint037________________________________________________=CustomActionEntryPoint037 +CustomActionEntryPoint038________________________________________________=CustomActionEntryPoint038 +CustomActionEntryPoint039________________________________________________=CustomActionEntryPoint039 +CustomActionEntryPoint040________________________________________________=CustomActionEntryPoint040 +CustomActionEntryPoint041________________________________________________=CustomActionEntryPoint041 +CustomActionEntryPoint042________________________________________________=CustomActionEntryPoint042 +CustomActionEntryPoint043________________________________________________=CustomActionEntryPoint043 +CustomActionEntryPoint044________________________________________________=CustomActionEntryPoint044 +CustomActionEntryPoint045________________________________________________=CustomActionEntryPoint045 +CustomActionEntryPoint046________________________________________________=CustomActionEntryPoint046 +CustomActionEntryPoint047________________________________________________=CustomActionEntryPoint047 +CustomActionEntryPoint048________________________________________________=CustomActionEntryPoint048 +CustomActionEntryPoint049________________________________________________=CustomActionEntryPoint049 +CustomActionEntryPoint050________________________________________________=CustomActionEntryPoint050 +CustomActionEntryPoint051________________________________________________=CustomActionEntryPoint051 +CustomActionEntryPoint052________________________________________________=CustomActionEntryPoint052 +CustomActionEntryPoint053________________________________________________=CustomActionEntryPoint053 +CustomActionEntryPoint054________________________________________________=CustomActionEntryPoint054 +CustomActionEntryPoint055________________________________________________=CustomActionEntryPoint055 +CustomActionEntryPoint056________________________________________________=CustomActionEntryPoint056 +CustomActionEntryPoint057________________________________________________=CustomActionEntryPoint057 +CustomActionEntryPoint058________________________________________________=CustomActionEntryPoint058 +CustomActionEntryPoint059________________________________________________=CustomActionEntryPoint059 +CustomActionEntryPoint060________________________________________________=CustomActionEntryPoint060 +CustomActionEntryPoint061________________________________________________=CustomActionEntryPoint061 +CustomActionEntryPoint062________________________________________________=CustomActionEntryPoint062 +CustomActionEntryPoint063________________________________________________=CustomActionEntryPoint063 +CustomActionEntryPoint064________________________________________________=CustomActionEntryPoint064 +CustomActionEntryPoint065________________________________________________=CustomActionEntryPoint065 +CustomActionEntryPoint066________________________________________________=CustomActionEntryPoint066 +CustomActionEntryPoint067________________________________________________=CustomActionEntryPoint067 +CustomActionEntryPoint068________________________________________________=CustomActionEntryPoint068 +CustomActionEntryPoint069________________________________________________=CustomActionEntryPoint069 +CustomActionEntryPoint070________________________________________________=CustomActionEntryPoint070 +CustomActionEntryPoint071________________________________________________=CustomActionEntryPoint071 +CustomActionEntryPoint072________________________________________________=CustomActionEntryPoint072 +CustomActionEntryPoint073________________________________________________=CustomActionEntryPoint073 +CustomActionEntryPoint074________________________________________________=CustomActionEntryPoint074 +CustomActionEntryPoint075________________________________________________=CustomActionEntryPoint075 +CustomActionEntryPoint076________________________________________________=CustomActionEntryPoint076 +CustomActionEntryPoint077________________________________________________=CustomActionEntryPoint077 +CustomActionEntryPoint078________________________________________________=CustomActionEntryPoint078 +CustomActionEntryPoint079________________________________________________=CustomActionEntryPoint079 +CustomActionEntryPoint080________________________________________________=CustomActionEntryPoint080 +CustomActionEntryPoint081________________________________________________=CustomActionEntryPoint081 +CustomActionEntryPoint082________________________________________________=CustomActionEntryPoint082 +CustomActionEntryPoint083________________________________________________=CustomActionEntryPoint083 +CustomActionEntryPoint084________________________________________________=CustomActionEntryPoint084 +CustomActionEntryPoint085________________________________________________=CustomActionEntryPoint085 +CustomActionEntryPoint086________________________________________________=CustomActionEntryPoint086 +CustomActionEntryPoint087________________________________________________=CustomActionEntryPoint087 +CustomActionEntryPoint088________________________________________________=CustomActionEntryPoint088 +CustomActionEntryPoint089________________________________________________=CustomActionEntryPoint089 +CustomActionEntryPoint090________________________________________________=CustomActionEntryPoint090 +CustomActionEntryPoint091________________________________________________=CustomActionEntryPoint091 +CustomActionEntryPoint092________________________________________________=CustomActionEntryPoint092 +CustomActionEntryPoint093________________________________________________=CustomActionEntryPoint093 +CustomActionEntryPoint094________________________________________________=CustomActionEntryPoint094 +CustomActionEntryPoint095________________________________________________=CustomActionEntryPoint095 +CustomActionEntryPoint096________________________________________________=CustomActionEntryPoint096 +CustomActionEntryPoint097________________________________________________=CustomActionEntryPoint097 +CustomActionEntryPoint098________________________________________________=CustomActionEntryPoint098 +CustomActionEntryPoint099________________________________________________=CustomActionEntryPoint099 +CustomActionEntryPoint100________________________________________________=CustomActionEntryPoint100 +CustomActionEntryPoint101________________________________________________=CustomActionEntryPoint101 +CustomActionEntryPoint102________________________________________________=CustomActionEntryPoint102 +CustomActionEntryPoint103________________________________________________=CustomActionEntryPoint103 +CustomActionEntryPoint104________________________________________________=CustomActionEntryPoint104 +CustomActionEntryPoint105________________________________________________=CustomActionEntryPoint105 +CustomActionEntryPoint106________________________________________________=CustomActionEntryPoint106 +CustomActionEntryPoint107________________________________________________=CustomActionEntryPoint107 +CustomActionEntryPoint108________________________________________________=CustomActionEntryPoint108 +CustomActionEntryPoint109________________________________________________=CustomActionEntryPoint109 +CustomActionEntryPoint110________________________________________________=CustomActionEntryPoint110 +CustomActionEntryPoint111________________________________________________=CustomActionEntryPoint111 +CustomActionEntryPoint112________________________________________________=CustomActionEntryPoint112 +CustomActionEntryPoint113________________________________________________=CustomActionEntryPoint113 +CustomActionEntryPoint114________________________________________________=CustomActionEntryPoint114 +CustomActionEntryPoint115________________________________________________=CustomActionEntryPoint115 +CustomActionEntryPoint116________________________________________________=CustomActionEntryPoint116 +CustomActionEntryPoint117________________________________________________=CustomActionEntryPoint117 +CustomActionEntryPoint118________________________________________________=CustomActionEntryPoint118 +CustomActionEntryPoint119________________________________________________=CustomActionEntryPoint119 +CustomActionEntryPoint120________________________________________________=CustomActionEntryPoint120 +CustomActionEntryPoint121________________________________________________=CustomActionEntryPoint121 +CustomActionEntryPoint122________________________________________________=CustomActionEntryPoint122 +CustomActionEntryPoint123________________________________________________=CustomActionEntryPoint123 +CustomActionEntryPoint124________________________________________________=CustomActionEntryPoint124 +CustomActionEntryPoint125________________________________________________=CustomActionEntryPoint125 +CustomActionEntryPoint126________________________________________________=CustomActionEntryPoint126 +CustomActionEntryPoint127________________________________________________=CustomActionEntryPoint127 + +zzzzInvokeManagedCustomActionOutOfProcW=InvokeManagedCustomActionOutOfProc +zzzInitializeEmbeddedUI=InitializeEmbeddedUI +zzzEmbeddedUIHandler=EmbeddedUIHandler +zzzShutdownEmbeddedUI=ShutdownEmbeddedUI diff --git a/src/samples/Dtf/Tools/SfxCA/EntryPoints.h b/src/samples/Dtf/Tools/SfxCA/EntryPoints.h new file mode 100644 index 00000000..bd2fa970 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/EntryPoints.h @@ -0,0 +1,162 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +int InvokeCustomAction(MSIHANDLE hSession, + const wchar_t* szWorkingDir, const wchar_t* szEntryPoint); + +/// +/// Macro for defining and exporting a custom action entrypoint. +/// +/// Name of the entrypoint as exported from +/// the DLL. +/// Path to the managed custom action method, +/// in the form: "AssemblyName!Namespace.Class.Method" +/// +/// To prevent the exported name from being decorated, add +/// /EXPORT:name to the linker options for every entrypoint. +/// +#define CUSTOMACTION_ENTRYPOINT(name,method) extern "C" int __stdcall \ + name(MSIHANDLE hSession) { return InvokeCustomAction(hSession, NULL, method); } + +// TEMPLATE ENTRYPOINTS +// To be edited by the MakeSfxCA tool. + +#define NULLSPACE \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" \ +L"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + +#define TEMPLATE_CA_ENTRYPOINT(id,sid) CUSTOMACTION_ENTRYPOINT( \ + CustomActionEntryPoint##id##, \ + L"CustomActionEntryPoint" sid NULLSPACE) + +TEMPLATE_CA_ENTRYPOINT(000,L"000"); +TEMPLATE_CA_ENTRYPOINT(001,L"001"); +TEMPLATE_CA_ENTRYPOINT(002,L"002"); +TEMPLATE_CA_ENTRYPOINT(003,L"003"); +TEMPLATE_CA_ENTRYPOINT(004,L"004"); +TEMPLATE_CA_ENTRYPOINT(005,L"005"); +TEMPLATE_CA_ENTRYPOINT(006,L"006"); +TEMPLATE_CA_ENTRYPOINT(007,L"007"); +TEMPLATE_CA_ENTRYPOINT(008,L"008"); +TEMPLATE_CA_ENTRYPOINT(009,L"009"); +TEMPLATE_CA_ENTRYPOINT(010,L"010"); +TEMPLATE_CA_ENTRYPOINT(011,L"011"); +TEMPLATE_CA_ENTRYPOINT(012,L"012"); +TEMPLATE_CA_ENTRYPOINT(013,L"013"); +TEMPLATE_CA_ENTRYPOINT(014,L"014"); +TEMPLATE_CA_ENTRYPOINT(015,L"015"); +TEMPLATE_CA_ENTRYPOINT(016,L"016"); +TEMPLATE_CA_ENTRYPOINT(017,L"017"); +TEMPLATE_CA_ENTRYPOINT(018,L"018"); +TEMPLATE_CA_ENTRYPOINT(019,L"019"); +TEMPLATE_CA_ENTRYPOINT(020,L"020"); +TEMPLATE_CA_ENTRYPOINT(021,L"021"); +TEMPLATE_CA_ENTRYPOINT(022,L"022"); +TEMPLATE_CA_ENTRYPOINT(023,L"023"); +TEMPLATE_CA_ENTRYPOINT(024,L"024"); +TEMPLATE_CA_ENTRYPOINT(025,L"025"); +TEMPLATE_CA_ENTRYPOINT(026,L"026"); +TEMPLATE_CA_ENTRYPOINT(027,L"027"); +TEMPLATE_CA_ENTRYPOINT(028,L"028"); +TEMPLATE_CA_ENTRYPOINT(029,L"029"); +TEMPLATE_CA_ENTRYPOINT(030,L"030"); +TEMPLATE_CA_ENTRYPOINT(031,L"031"); +TEMPLATE_CA_ENTRYPOINT(032,L"032"); +TEMPLATE_CA_ENTRYPOINT(033,L"033"); +TEMPLATE_CA_ENTRYPOINT(034,L"034"); +TEMPLATE_CA_ENTRYPOINT(035,L"035"); +TEMPLATE_CA_ENTRYPOINT(036,L"036"); +TEMPLATE_CA_ENTRYPOINT(037,L"037"); +TEMPLATE_CA_ENTRYPOINT(038,L"038"); +TEMPLATE_CA_ENTRYPOINT(039,L"039"); +TEMPLATE_CA_ENTRYPOINT(040,L"040"); +TEMPLATE_CA_ENTRYPOINT(041,L"041"); +TEMPLATE_CA_ENTRYPOINT(042,L"042"); +TEMPLATE_CA_ENTRYPOINT(043,L"043"); +TEMPLATE_CA_ENTRYPOINT(044,L"044"); +TEMPLATE_CA_ENTRYPOINT(045,L"045"); +TEMPLATE_CA_ENTRYPOINT(046,L"046"); +TEMPLATE_CA_ENTRYPOINT(047,L"047"); +TEMPLATE_CA_ENTRYPOINT(048,L"048"); +TEMPLATE_CA_ENTRYPOINT(049,L"049"); +TEMPLATE_CA_ENTRYPOINT(050,L"050"); +TEMPLATE_CA_ENTRYPOINT(051,L"051"); +TEMPLATE_CA_ENTRYPOINT(052,L"052"); +TEMPLATE_CA_ENTRYPOINT(053,L"053"); +TEMPLATE_CA_ENTRYPOINT(054,L"054"); +TEMPLATE_CA_ENTRYPOINT(055,L"055"); +TEMPLATE_CA_ENTRYPOINT(056,L"056"); +TEMPLATE_CA_ENTRYPOINT(057,L"057"); +TEMPLATE_CA_ENTRYPOINT(058,L"058"); +TEMPLATE_CA_ENTRYPOINT(059,L"059"); +TEMPLATE_CA_ENTRYPOINT(060,L"060"); +TEMPLATE_CA_ENTRYPOINT(061,L"061"); +TEMPLATE_CA_ENTRYPOINT(062,L"062"); +TEMPLATE_CA_ENTRYPOINT(063,L"063"); +TEMPLATE_CA_ENTRYPOINT(064,L"064"); +TEMPLATE_CA_ENTRYPOINT(065,L"065"); +TEMPLATE_CA_ENTRYPOINT(066,L"066"); +TEMPLATE_CA_ENTRYPOINT(067,L"067"); +TEMPLATE_CA_ENTRYPOINT(068,L"068"); +TEMPLATE_CA_ENTRYPOINT(069,L"069"); +TEMPLATE_CA_ENTRYPOINT(070,L"070"); +TEMPLATE_CA_ENTRYPOINT(071,L"071"); +TEMPLATE_CA_ENTRYPOINT(072,L"072"); +TEMPLATE_CA_ENTRYPOINT(073,L"073"); +TEMPLATE_CA_ENTRYPOINT(074,L"074"); +TEMPLATE_CA_ENTRYPOINT(075,L"075"); +TEMPLATE_CA_ENTRYPOINT(076,L"076"); +TEMPLATE_CA_ENTRYPOINT(077,L"077"); +TEMPLATE_CA_ENTRYPOINT(078,L"078"); +TEMPLATE_CA_ENTRYPOINT(079,L"079"); +TEMPLATE_CA_ENTRYPOINT(080,L"080"); +TEMPLATE_CA_ENTRYPOINT(081,L"081"); +TEMPLATE_CA_ENTRYPOINT(082,L"082"); +TEMPLATE_CA_ENTRYPOINT(083,L"083"); +TEMPLATE_CA_ENTRYPOINT(084,L"084"); +TEMPLATE_CA_ENTRYPOINT(085,L"085"); +TEMPLATE_CA_ENTRYPOINT(086,L"086"); +TEMPLATE_CA_ENTRYPOINT(087,L"087"); +TEMPLATE_CA_ENTRYPOINT(088,L"088"); +TEMPLATE_CA_ENTRYPOINT(089,L"089"); +TEMPLATE_CA_ENTRYPOINT(090,L"090"); +TEMPLATE_CA_ENTRYPOINT(091,L"091"); +TEMPLATE_CA_ENTRYPOINT(092,L"092"); +TEMPLATE_CA_ENTRYPOINT(093,L"093"); +TEMPLATE_CA_ENTRYPOINT(094,L"094"); +TEMPLATE_CA_ENTRYPOINT(095,L"095"); +TEMPLATE_CA_ENTRYPOINT(096,L"096"); +TEMPLATE_CA_ENTRYPOINT(097,L"097"); +TEMPLATE_CA_ENTRYPOINT(098,L"098"); +TEMPLATE_CA_ENTRYPOINT(099,L"099"); +TEMPLATE_CA_ENTRYPOINT(100,L"100"); +TEMPLATE_CA_ENTRYPOINT(101,L"101"); +TEMPLATE_CA_ENTRYPOINT(102,L"102"); +TEMPLATE_CA_ENTRYPOINT(103,L"103"); +TEMPLATE_CA_ENTRYPOINT(104,L"104"); +TEMPLATE_CA_ENTRYPOINT(105,L"105"); +TEMPLATE_CA_ENTRYPOINT(106,L"106"); +TEMPLATE_CA_ENTRYPOINT(107,L"107"); +TEMPLATE_CA_ENTRYPOINT(108,L"108"); +TEMPLATE_CA_ENTRYPOINT(109,L"109"); +TEMPLATE_CA_ENTRYPOINT(110,L"110"); +TEMPLATE_CA_ENTRYPOINT(111,L"111"); +TEMPLATE_CA_ENTRYPOINT(112,L"112"); +TEMPLATE_CA_ENTRYPOINT(113,L"113"); +TEMPLATE_CA_ENTRYPOINT(114,L"114"); +TEMPLATE_CA_ENTRYPOINT(115,L"115"); +TEMPLATE_CA_ENTRYPOINT(116,L"116"); +TEMPLATE_CA_ENTRYPOINT(117,L"117"); +TEMPLATE_CA_ENTRYPOINT(118,L"118"); +TEMPLATE_CA_ENTRYPOINT(119,L"119"); +TEMPLATE_CA_ENTRYPOINT(120,L"120"); +TEMPLATE_CA_ENTRYPOINT(121,L"121"); +TEMPLATE_CA_ENTRYPOINT(122,L"122"); +TEMPLATE_CA_ENTRYPOINT(123,L"123"); +TEMPLATE_CA_ENTRYPOINT(124,L"124"); +TEMPLATE_CA_ENTRYPOINT(125,L"125"); +TEMPLATE_CA_ENTRYPOINT(126,L"126"); +TEMPLATE_CA_ENTRYPOINT(127,L"127"); + +// Note: Keep in sync with EntryPoints.def diff --git a/src/samples/Dtf/Tools/SfxCA/Extract.cpp b/src/samples/Dtf/Tools/SfxCA/Extract.cpp new file mode 100644 index 00000000..171cf52f --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/Extract.cpp @@ -0,0 +1,282 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" + +//--------------------------------------------------------------------- +// CABINET EXTRACTION +//--------------------------------------------------------------------- + +// Globals make this code unsuited for multhreaded use, +// but FDI doesn't provide any other way to pass context. + +// Handle to the FDI (cab extraction) engine. Need access to this in a callback. +static HFDI g_hfdi; + +// FDI is not unicode-aware, so avoid passing these paths through the callbacks. +static const wchar_t* g_szExtractDir; +static const wchar_t* g_szCabFile; + +// Offset into the source file where the cabinet really starts. +// Used to trick FDI into extracting from a concatenated cabinet. +static int g_lCabOffset; + +// Use the secure CRT version of _wsopen if available. +#ifdef __GOT_SECURE_LIB__ +#define _wsopen__s(hf,file,oflag,shflag,pmode) _wsopen_s(&hf,file,oflag,shflag,pmode) +#else +#define _wsopen__s(hf,file,oflag,shflag,pmode) hf = _wsopen(file,oflag,shflag,pmode) +#endif + +/// +/// FDI callback to open a cabinet file. +/// +/// Name of the file to be opened. This parameter +/// is ignored since with our limited use this method is only ever called +/// to open the main cabinet file. +/// Type of operations allowed. +/// Permission setting. +/// Integer file handle, or -1 if the file could not be opened. +/// +/// To support reading from a cabinet that is concatenated onto +/// another file, this function first searches for the offset of the cabinet, +/// then saves that offset for use in recalculating later seeks. +/// +static FNOPEN(CabOpen) +{ + UNREFERENCED_PARAMETER(pszFile); + int hf; + _wsopen__s(hf, g_szCabFile, oflag, _SH_DENYWR, pmode); + if (hf != -1) + { + FDICABINETINFO cabInfo; + int length = _lseek(hf, 0, SEEK_END); + for(int offset = 0; offset < length; offset += 256) + { + if (_lseek(hf, offset, SEEK_SET) != offset) break; + if (FDIIsCabinet(g_hfdi, hf, &cabInfo)) + { + g_lCabOffset = offset; + _lseek(hf, offset, SEEK_SET); + return hf; + } + } + _close(hf); + } + return -1; +} + +/// +/// FDI callback to seek within a file. +/// +/// File handle. +/// Seek distance +/// Whether to seek relative to the +/// beginning, current position, or end of the file. +/// Resultant position within the cabinet. +/// +/// To support reading from a cabinet that is concatenated onto +/// another file, this function recalculates seeks based on the +/// offset that was determined when the cabinet was opened. +/// +static FNSEEK(CabSeek) +{ + if (seektype == SEEK_SET) dist += g_lCabOffset; + int pos = _lseek((int) hf, dist, seektype); + pos -= g_lCabOffset; + return pos; +} + +/// +/// Ensures a directory and its parent directory path exists. +/// +/// Directory path, not including file name. +/// 0 if the directory exists or was successfully created, else nonzero. +/// +/// This function modifies characters in szDirPath, but always restores them +/// regardless of error condition. +/// +static int EnsureDirectoryExists(__inout_z wchar_t* szDirPath) +{ + int ret = 0; + if (!::CreateDirectoryW(szDirPath, NULL)) + { + UINT err = ::GetLastError(); + if (err != ERROR_ALREADY_EXISTS) + { + // Directory creation failed for some reason other than already existing. + // Try to create the parent directory first. + wchar_t* szLastSlash = NULL; + for (wchar_t* sz = szDirPath; *sz; sz++) + { + if (*sz == L'\\') + { + szLastSlash = sz; + } + } + if (szLastSlash) + { + // Temporarily take one directory off the path and recurse. + *szLastSlash = L'\0'; + ret = EnsureDirectoryExists(szDirPath); + *szLastSlash = L'\\'; + + // Try to create the directory if all parents are created. + if (ret == 0 && !::CreateDirectoryW(szDirPath, NULL)) + { + err = ::GetLastError(); + if (err != ERROR_ALREADY_EXISTS) + { + ret = -1; + } + } + } + else + { + ret = -1; + } + } + } + return ret; +} + +/// +/// Ensures a file's directory and its parent directory path exists. +/// +/// Path including file name. +/// 0 if the file's directory exists or was successfully created, else nonzero. +/// +/// This function modifies characters in szFilePath, but always restores them +/// regardless of error condition. +/// +static int EnsureFileDirectoryExists(__inout_z wchar_t* szFilePath) +{ + int ret = 0; + wchar_t* szLastSlash = NULL; + for (wchar_t* sz = szFilePath; *sz; sz++) + { + if (*sz == L'\\') + { + szLastSlash = sz; + } + } + if (szLastSlash) + { + *szLastSlash = L'\0'; + ret = EnsureDirectoryExists(szFilePath); + *szLastSlash = L'\\'; + } + return ret; +} + +/// +/// FDI callback for handling files in the cabinet. +/// +/// Type of notification. +/// Structure containing data about the notification. +/// +/// Refer to fdi.h for more comments on this notification callback. +/// +static FNFDINOTIFY(CabNotification) +{ + // fdintCOPY_FILE: + // Called for each file that *starts* in the current cabinet, giving + // the client the opportunity to request that the file be copied or + // skipped. + // Entry: + // pfdin->psz1 = file name in cabinet + // pfdin->cb = uncompressed size of file + // pfdin->date = file date + // pfdin->time = file time + // pfdin->attribs = file attributes + // pfdin->iFolder = file's folder index + // Exit-Success: + // Return non-zero file handle for destination file; FDI writes + // data to this file use the PFNWRITE function supplied to FDICreate, + // and then calls fdintCLOSE_FILE_INFO to close the file and set + // the date, time, and attributes. + // Exit-Failure: + // Returns 0 => Skip file, do not copy + // Returns -1 => Abort FDICopy() call + if (fdint == fdintCOPY_FILE) + { + size_t cchFile = MultiByteToWideChar(CP_UTF8, 0, pfdin->psz1, -1, NULL, 0); + size_t cchFilePath = wcslen(g_szExtractDir) + 1 + cchFile; + wchar_t* szFilePath = (wchar_t*) _alloca((cchFilePath + 1) * sizeof(wchar_t)); + if (szFilePath == NULL) return -1; + StringCchCopyW(szFilePath, cchFilePath + 1, g_szExtractDir); + StringCchCatW(szFilePath, cchFilePath + 1, L"\\"); + MultiByteToWideChar(CP_UTF8, 0, pfdin->psz1, -1, + szFilePath + cchFilePath - cchFile, (int) cchFile + 1); + int hf = -1; + if (EnsureFileDirectoryExists(szFilePath) == 0) + { + _wsopen__s(hf, szFilePath, + _O_BINARY | _O_CREAT | _O_WRONLY | _O_SEQUENTIAL, + _SH_DENYWR, _S_IREAD | _S_IWRITE); + } + return hf; + } + + // fdintCLOSE_FILE_INFO: + // Called after all of the data has been written to a target file. + // This function must close the file and set the file date, time, + // and attributes. + // Entry: + // pfdin->psz1 = file name in cabinet + // pfdin->hf = file handle + // pfdin->date = file date + // pfdin->time = file time + // pfdin->attribs = file attributes + // pfdin->iFolder = file's folder index + // pfdin->cb = Run After Extract (0 - don't run, 1 Run) + // Exit-Success: + // Returns TRUE + // Exit-Failure: + // Returns FALSE, or -1 to abort + else if (fdint == fdintCLOSE_FILE_INFO) + { + _close((int) pfdin->hf); + return TRUE; + } + return 0; +} + +/// +/// Extracts all contents of a cabinet file to a directory. +/// +/// Path to the cabinet file to be extracted. +/// The cabinet may actually start at some offset within the file, +/// as long as that offset is a multiple of 256. +/// Directory where files are to be extracted. +/// This directory must already exist, but should be empty. +/// 0 if the cabinet was extracted successfully, +/// or an error code if any error occurred. +/// +/// The extraction will not overwrite any files in the destination +/// directory; extraction will be interrupted and fail if any files +/// with the same name already exist. +/// +int ExtractCabinet(const wchar_t* szCabFile, const wchar_t* szExtractDir) +{ + ERF erf; + // Most of the FDI callbacks can be handled by existing CRT I/O functions. + // For our functionality we only need to handle the open and seek callbacks. + HFDI hfdi = FDICreate((PFNALLOC) malloc, (PFNFREE) free, CabOpen, + (PFNREAD) _read, (PFNWRITE) _write, (PFNCLOSE) _close, + CabSeek, cpu80386, &erf); + if (hfdi != NULL) + { + g_hfdi = hfdi; + g_szCabFile = szCabFile; + g_szExtractDir = szExtractDir; + char szEmpty[1] = {0}; + if (FDICopy(hfdi, szEmpty, szEmpty, 0, CabNotification, NULL, NULL)) + { + FDIDestroy(hfdi); + return 0; + } + FDIDestroy(hfdi); + } + + return erf.erfOper; +} diff --git a/src/samples/Dtf/Tools/SfxCA/RemoteMsi.cpp b/src/samples/Dtf/Tools/SfxCA/RemoteMsi.cpp new file mode 100644 index 00000000..ba59fdf7 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/RemoteMsi.cpp @@ -0,0 +1,629 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "RemoteMsiSession.h" + + +// +// Ensures that the request buffer is large enough to hold a request, +// reallocating the buffer if necessary. +// It will also reduce the buffer size if the previous allocation was very large. +// +static __success(return == 0) UINT EnsureBufSize(__deref_out_ecount(*pcchBuf) wchar_t** pszBuf, __deref_inout DWORD* pcchBuf, DWORD cchRequired) +{ + // It will also reduce the buffer size if the previous allocation was very large. + if (*pcchBuf < cchRequired || (LARGE_BUFFER_THRESHOLD/2 < *pcchBuf && cchRequired < *pcchBuf)) + { + if (*pszBuf != NULL) + { + SecureZeroMemory(*pszBuf, *pcchBuf); + delete[] *pszBuf; + } + + *pcchBuf = max(MIN_BUFFER_STRING_SIZE, cchRequired); + *pszBuf = new wchar_t[*pcchBuf]; + + if (*pszBuf == NULL) + { + return ERROR_OUTOFMEMORY; + } + } + + return ERROR_SUCCESS; +} + +typedef int (WINAPI *PMsiFunc_I_I)(int in1, __out int* out1); +typedef int (WINAPI *PMsiFunc_II_I)(int in1, int in2, __out int* out1); +typedef int (WINAPI *PMsiFunc_IS_I)(int in1, __in_z wchar_t* in2, __out int* out1); +typedef int (WINAPI *PMsiFunc_ISI_I)(int in1, __in_z wchar_t* in2, int in3, __out int* out1); +typedef int (WINAPI *PMsiFunc_ISII_I)(int in1, __in_z wchar_t* in2, int in3, int in4, __out int* out1); +typedef int (WINAPI *PMsiFunc_IS_II)(int in1, __in_z wchar_t* in2, __out int* out1, __out int* out2); +typedef MSIDBERROR (WINAPI *PMsiEFunc_I_S)(int in1, __out_ecount_full(*cchOut1) wchar_t* out1, __inout DWORD* cchOut1); +typedef int (WINAPI *PMsiFunc_I_S)(int in1, __out_ecount_full(*cchOut1) wchar_t* out1, __inout DWORD* cchOut1); +typedef int (WINAPI *PMsiFunc_II_S)(int in1, int in2, __out_ecount_full(*cchOut1) wchar_t* out1, __inout DWORD* cchOut1); +typedef int (WINAPI *PMsiFunc_IS_S)(int in1, __in_z wchar_t* in2, __out_ecount_full(*cchOut1) wchar_t* out1, __inout DWORD* cchOut1); +typedef int (WINAPI *PMsiFunc_ISII_SII)(int in1, __in_z wchar_t* in2, int in3, int in4, __out_ecount_full(*cchOut1) wchar_t* out1, __inout DWORD* cchOut1, __out int* out2, __out int* out3); + +UINT MsiFunc_I_I(PMsiFunc_I_I func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + int out1; + UINT ret = (UINT) func(in1, &out1); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + } + return ret; +} + +UINT MsiFunc_II_I(PMsiFunc_II_I func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + int in2 = pReq->fields[1].iValue; + int out1; + UINT ret = (UINT) func(in1, in2, &out1); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + } + return ret; +} + +UINT MsiFunc_IS_I(PMsiFunc_IS_I func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + int out1; + UINT ret = (UINT) func(in1, in2, &out1); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + } + return ret; +} + +UINT MsiFunc_ISI_I(PMsiFunc_ISI_I func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + int in3 = pReq->fields[2].iValue; + int out1; + UINT ret = (UINT) func(in1, in2, in3, &out1); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + } + return ret; +} + +UINT MsiFunc_ISII_I(PMsiFunc_ISII_I func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + int in3 = pReq->fields[2].iValue; + int in4 = pReq->fields[3].iValue; + int out1; + UINT ret = (UINT) func(in1, in2, in3, in4, &out1); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + } + return ret; +} + +UINT MsiFunc_IS_II(PMsiFunc_IS_II func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + int out1, out2; + UINT ret = (UINT) func(in1, in2, &out1, &out2); + if (ret == 0) + { + pResp->fields[1].vt = VT_I4; + pResp->fields[1].iValue = out1; + pResp->fields[2].vt = VT_I4; + pResp->fields[2].iValue = out2; + } + return ret; +} + +UINT MsiFunc_I_S(PMsiFunc_I_S func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp, __deref_inout_ecount(cchBuf) wchar_t*& szBuf, __inout DWORD& cchBuf) +{ + int in1 = pReq->fields[0].iValue; + szBuf[0] = L'\0'; + DWORD cchValue = cchBuf; + UINT ret = (UINT) func(in1, szBuf, &cchValue); + if (ret == ERROR_MORE_DATA) + { + ret = EnsureBufSize(&szBuf, &cchBuf, ++cchValue); + if (ret == 0) + { + ret = (UINT) func(in1, szBuf, &cchValue); + } + } + if (ret == 0) + { + pResp->fields[1].vt = VT_LPWSTR; + pResp->fields[1].szValue = szBuf; + } + return ret; +} + +MSIDBERROR MsiEFunc_I_S(PMsiEFunc_I_S func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp, __deref_inout_ecount(cchBuf) wchar_t*& szBuf, __inout DWORD& cchBuf) +{ + int in1 = pReq->fields[0].iValue; + szBuf[0] = L'\0'; + DWORD cchValue = cchBuf; + MSIDBERROR ret = func(in1, szBuf, &cchValue); + if (ret == MSIDBERROR_MOREDATA) + { + if (0 == EnsureBufSize(&szBuf, &cchBuf, ++cchValue)) + { + ret = func(in1, szBuf, &cchValue); + } + } + if (ret != MSIDBERROR_MOREDATA) + { + pResp->fields[1].vt = VT_LPWSTR; + pResp->fields[1].szValue = szBuf; + } + return ret; +} + +UINT MsiFunc_II_S(PMsiFunc_II_S func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp, __deref_inout_ecount(cchBuf) wchar_t*& szBuf, __inout DWORD& cchBuf) +{ + int in1 = pReq->fields[0].iValue; + int in2 = pReq->fields[1].iValue; + szBuf[0] = L'\0'; + DWORD cchValue = cchBuf; + UINT ret = (UINT) func(in1, in2, szBuf, &cchValue); + if (ret == ERROR_MORE_DATA) + { + ret = EnsureBufSize(&szBuf, &cchBuf, ++cchValue); + if (ret == 0) + { + ret = (UINT) func(in1, in2, szBuf, &cchValue); + } + } + if (ret == 0) + { + pResp->fields[1].vt = VT_LPWSTR; + pResp->fields[1].szValue = szBuf; + } + return ret; +} + +UINT MsiFunc_IS_S(PMsiFunc_IS_S func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp, __deref_inout_ecount(cchBuf) wchar_t*& szBuf, __inout DWORD& cchBuf) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + szBuf[0] = L'\0'; + DWORD cchValue = cchBuf; + UINT ret = (UINT) func(in1, in2, szBuf, &cchValue); + if (ret == ERROR_MORE_DATA) + { + ret = EnsureBufSize(&szBuf, &cchBuf, ++cchValue); + if (ret == 0) + { + ret = (UINT) func(in1, in2, szBuf, &cchValue); + } + } + if (ret == 0) + { + pResp->fields[1].vt = VT_LPWSTR; + pResp->fields[1].szValue = szBuf; + } + return ret; +} + +UINT MsiFunc_ISII_SII(PMsiFunc_ISII_SII func, const RemoteMsiSession::RequestData* pReq, RemoteMsiSession::RequestData* pResp, __deref_inout_ecount(cchBuf) wchar_t*& szBuf, __inout DWORD& cchBuf) +{ + int in1 = pReq->fields[0].iValue; + wchar_t* in2 = pReq->fields[1].szValue; + int in3 = pReq->fields[2].iValue; + int in4 = pReq->fields[3].iValue; + szBuf[0] = L'\0'; + DWORD cchValue = cchBuf; + int out2, out3; + UINT ret = (UINT) func(in1, in2, in3, in4, szBuf, &cchValue, &out2, &out3); + if (ret == ERROR_MORE_DATA) + { + ret = EnsureBufSize(&szBuf, &cchBuf, ++cchValue); + if (ret == 0) + { + ret = (UINT) func(in1, in2, in3, in4, szBuf, &cchValue, &out2, &out3); + } + } + if (ret == 0) + { + pResp->fields[1].vt = VT_LPWSTR; + pResp->fields[1].szValue = szBuf; + pResp->fields[2].vt = VT_I4; + pResp->fields[2].iValue = out2; + pResp->fields[3].vt = VT_I4; + pResp->fields[3].iValue = out3; + } + return ret; +} + +void RemoteMsiSession::ProcessRequest(RequestId id, const RequestData* pReq, RequestData* pResp) +{ + SecureZeroMemory(pResp, sizeof(RequestData)); + + UINT ret = EnsureBufSize(&m_pBufSend, &m_cbBufSend, 1024); + + if (0 == ret) + { + switch (id) + { + case RemoteMsiSession::EndSession: + { + this->ExitCode = pReq->fields[0].iValue; + } + break; + case RemoteMsiSession::MsiCloseHandle: + { + MSIHANDLE h = (MSIHANDLE) pReq->fields[0].iValue; + ret = ::MsiCloseHandle(h); + } + break; + case RemoteMsiSession::MsiProcessMessage: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + INSTALLMESSAGE eMessageType = (INSTALLMESSAGE) pReq->fields[1].iValue; + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[2].iValue; + ret = ::MsiProcessMessage(hInstall, eMessageType, hRecord); + } + break; + case RemoteMsiSession::MsiGetProperty: + { + ret = MsiFunc_IS_S((PMsiFunc_IS_S) ::MsiGetProperty, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiSetProperty: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szName = pReq->fields[1].szValue; + const wchar_t* szValue = pReq->fields[2].szValue; + ret = ::MsiSetProperty(hInstall, szName, szValue); + } + break; + case RemoteMsiSession::MsiCreateRecord: + { + UINT cParams = pReq->fields[0].uiValue; + ret = ::MsiCreateRecord(cParams); + } + break; + case RemoteMsiSession::MsiRecordGetFieldCount: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + ret = ::MsiRecordGetFieldCount(hRecord); + } + break; + case RemoteMsiSession::MsiRecordGetInteger: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + ret = ::MsiRecordGetInteger(hRecord, iField); + } + break; + case RemoteMsiSession::MsiRecordSetInteger: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + int iValue = pReq->fields[2].iValue; + ret = ::MsiRecordSetInteger(hRecord, iField, iValue); + } + break; + case RemoteMsiSession::MsiRecordGetString: + { + ret = MsiFunc_II_S((PMsiFunc_II_S) ::MsiRecordGetString, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiRecordSetString: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + const wchar_t* szValue = pReq->fields[2].szValue; + ret = ::MsiRecordSetString(hRecord, iField, szValue); + } + break; + case RemoteMsiSession::MsiRecordClearData: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + ret = ::MsiRecordClearData(hRecord); + } + break; + case RemoteMsiSession::MsiRecordIsNull: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + ret = ::MsiRecordIsNull(hRecord, iField); + } + break; + case RemoteMsiSession::MsiFormatRecord: + { + ret = MsiFunc_II_S((PMsiFunc_II_S) ::MsiFormatRecord, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiGetActiveDatabase: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + ret = (UINT) ::MsiGetActiveDatabase(hInstall); + } + break; + case RemoteMsiSession::MsiDatabaseOpenView: + { + ret = MsiFunc_IS_I((PMsiFunc_IS_I) ::MsiDatabaseOpenView, pReq, pResp); + } + break; + case RemoteMsiSession::MsiViewExecute: + { + MSIHANDLE hView = (MSIHANDLE) pReq->fields[0].iValue; + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[1].iValue; + ret = ::MsiViewExecute(hView, hRecord); + } + break; + case RemoteMsiSession::MsiViewFetch: + { + ret = MsiFunc_I_I((PMsiFunc_I_I) ::MsiViewFetch, pReq, pResp); + } + break; + case RemoteMsiSession::MsiViewModify: + { + MSIHANDLE hView = (MSIHANDLE) pReq->fields[0].iValue; + MSIMODIFY eModifyMode = (MSIMODIFY) pReq->fields[1].iValue; + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[2].iValue; + ret = ::MsiViewModify(hView, eModifyMode, hRecord); + } + break; + case RemoteMsiSession::MsiViewGetError: + { + ret = MsiEFunc_I_S((PMsiEFunc_I_S) ::MsiViewGetError, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiViewGetColumnInfo: + { + ret = MsiFunc_II_I((PMsiFunc_II_I) ::MsiViewGetColumnInfo, pReq, pResp); + } + break; + case RemoteMsiSession::MsiDatabaseGetPrimaryKeys: + { + ret = MsiFunc_IS_I((PMsiFunc_IS_I) ::MsiDatabaseGetPrimaryKeys, pReq, pResp); + } + break; + case RemoteMsiSession::MsiDatabaseIsTablePersistent: + { + MSIHANDLE hDb = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szTable = pReq->fields[1].szValue; + ret = ::MsiDatabaseIsTablePersistent(hDb, szTable); + } + break; + case RemoteMsiSession::MsiDoAction: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szAction = pReq->fields[1].szValue; + ret = ::MsiDoAction(hInstall, szAction); + } + break; + case RemoteMsiSession::MsiEnumComponentCosts: + { + ret = MsiFunc_ISII_SII((PMsiFunc_ISII_SII) ::MsiEnumComponentCosts, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiEvaluateCondition: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szCondition = pReq->fields[1].szValue; + ret = ::MsiEvaluateCondition(hInstall, szCondition); + } + break; + case RemoteMsiSession::MsiGetComponentState: + { + ret = MsiFunc_IS_II((PMsiFunc_IS_II) ::MsiGetComponentState, pReq, pResp); + } + break; + case RemoteMsiSession::MsiGetFeatureCost: + { + ret = MsiFunc_ISII_I((PMsiFunc_ISII_I) ::MsiGetFeatureCost, pReq, pResp); + } + break; + case RemoteMsiSession::MsiGetFeatureState: + { + ret = MsiFunc_IS_II((PMsiFunc_IS_II) ::MsiGetFeatureState, pReq, pResp); + } + break; + case RemoteMsiSession::MsiGetFeatureValidStates: + { + ret = MsiFunc_IS_I((PMsiFunc_IS_I) ::MsiGetFeatureValidStates, pReq, pResp); + } + break; + case RemoteMsiSession::MsiGetLanguage: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + ret = ::MsiGetLanguage(hInstall); + } + break; + case RemoteMsiSession::MsiGetLastErrorRecord: + { + ret = ::MsiGetLastErrorRecord(); + } + break; + case RemoteMsiSession::MsiGetMode: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + MSIRUNMODE iRunMode = (MSIRUNMODE) pReq->fields[1].iValue; + ret = ::MsiGetMode(hInstall, iRunMode); + } + break; + case RemoteMsiSession::MsiGetSourcePath: + { + ret = MsiFunc_IS_S((PMsiFunc_IS_S) ::MsiGetSourcePath, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiGetSummaryInformation: + { + ret = MsiFunc_ISI_I((PMsiFunc_ISI_I) ::MsiGetSummaryInformation, pReq, pResp); + } + break; + case RemoteMsiSession::MsiGetTargetPath: + { + ret = MsiFunc_IS_S((PMsiFunc_IS_S) ::MsiGetTargetPath, pReq, pResp, m_pBufSend, m_cbBufSend); + } + break; + case RemoteMsiSession::MsiRecordDataSize: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + ret = ::MsiRecordDataSize(hRecord, iField); + } + break; + case RemoteMsiSession::MsiRecordReadStream: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + DWORD cbRead = (DWORD) pReq->fields[2].uiValue; + ret = EnsureBufSize(&m_pBufSend, &m_cbBufSend, (cbRead + 1) / 2); + if (ret == 0) + { + ret = ::MsiRecordReadStream(hRecord, iField, (char*) m_pBufSend, &cbRead); + if (ret == 0) + { + pResp->fields[1].vt = VT_STREAM; + pResp->fields[1].szValue = m_pBufSend; + pResp->fields[2].vt = VT_I4; + pResp->fields[2].uiValue = (UINT) cbRead; + } + } + } + break; + case RemoteMsiSession::MsiRecordSetStream: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + UINT iField = pReq->fields[1].uiValue; + const wchar_t* szFilePath = pReq->fields[2].szValue; + ret = ::MsiRecordSetStream(hRecord, iField, szFilePath); + } + break; + case RemoteMsiSession::MsiSequence: + { + MSIHANDLE hRecord = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szTable = pReq->fields[1].szValue; + UINT iSequenceMode = pReq->fields[2].uiValue; + ret = ::MsiSequence(hRecord, szTable, iSequenceMode); + } + break; + case RemoteMsiSession::MsiSetComponentState: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szComponent = pReq->fields[1].szValue; + INSTALLSTATE iState = (INSTALLSTATE) pReq->fields[2].iValue; + ret = ::MsiSetComponentState(hInstall, szComponent, iState); + } + break; + case RemoteMsiSession::MsiSetFeatureAttributes: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szFeature = pReq->fields[1].szValue; + DWORD dwAttrs = (DWORD) pReq->fields[2].uiValue; + ret = ::MsiSetFeatureAttributes(hInstall, szFeature, dwAttrs); + } + break; + case RemoteMsiSession::MsiSetFeatureState: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szFeature = pReq->fields[1].szValue; + INSTALLSTATE iState = (INSTALLSTATE) pReq->fields[2].iValue; + ret = ::MsiSetFeatureState(hInstall, szFeature, iState); + } + break; + case RemoteMsiSession::MsiSetInstallLevel: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + int iInstallLevel = pReq->fields[1].iValue; + ret = ::MsiSetInstallLevel(hInstall, iInstallLevel); + } + break; + case RemoteMsiSession::MsiSetMode: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + MSIRUNMODE iRunMode = (MSIRUNMODE) pReq->fields[1].uiValue; + BOOL fState = (BOOL) pReq->fields[2].iValue; + ret = ::MsiSetMode(hInstall, iRunMode, fState); + } + break; + case RemoteMsiSession::MsiSetTargetPath: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + const wchar_t* szFolder = pReq->fields[1].szValue; + const wchar_t* szFolderPath = pReq->fields[2].szValue; + ret = ::MsiSetTargetPath(hInstall, szFolder, szFolderPath); + } + break; + case RemoteMsiSession::MsiSummaryInfoGetProperty: + { + MSIHANDLE hSummaryInfo = (MSIHANDLE) pReq->fields[0].iValue; + UINT uiProperty = pReq->fields[1].uiValue; + UINT uiDataType; + int iValue; + FILETIME ftValue; + m_pBufSend[0] = L'\0'; + DWORD cchValue = m_cbBufSend; + ret = ::MsiSummaryInfoGetProperty(hSummaryInfo, uiProperty, &uiDataType, &iValue, &ftValue, m_pBufSend, &cchValue); + if (ret == ERROR_MORE_DATA) + { + ret = EnsureBufSize(&m_pBufSend, &m_cbBufSend, ++cchValue); + if (ret == 0) + { + ret = ::MsiSummaryInfoGetProperty(hSummaryInfo, uiProperty, &uiDataType, &iValue, &ftValue, m_pBufSend, &cchValue); + } + } + if (ret == 0) + { + pResp->fields[1].vt = VT_UI4; + pResp->fields[1].uiValue = uiDataType; + + switch (uiDataType) + { + case VT_I2: + case VT_I4: + pResp->fields[2].vt = VT_I4; + pResp->fields[2].iValue = iValue; + break; + case VT_FILETIME: + pResp->fields[2].vt = VT_UI4; + pResp->fields[2].iValue = ftValue.dwHighDateTime; + pResp->fields[3].vt = VT_UI4; + pResp->fields[3].iValue = ftValue.dwLowDateTime; + break; + case VT_LPSTR: + pResp->fields[2].vt = VT_LPWSTR; + pResp->fields[2].szValue = m_pBufSend; + break; + } + } + } + break; + case RemoteMsiSession::MsiVerifyDiskSpace: + { + MSIHANDLE hInstall = (MSIHANDLE) pReq->fields[0].iValue; + ret = ::MsiVerifyDiskSpace(hInstall); + } + break; + + default: + { + ret = ERROR_INVALID_FUNCTION; + } + break; + } + } + + pResp->fields[0].vt = VT_UI4; + pResp->fields[0].uiValue = ret; +} diff --git a/src/samples/Dtf/Tools/SfxCA/RemoteMsiSession.h b/src/samples/Dtf/Tools/SfxCA/RemoteMsiSession.h new file mode 100644 index 00000000..90c7c01f --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/RemoteMsiSession.h @@ -0,0 +1,898 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#define LARGE_BUFFER_THRESHOLD 65536 // bytes +#define MIN_BUFFER_STRING_SIZE 1024 // wchar_ts + +/////////////////////////////////////////////////////////////////////////////////////// +// RemoteMsiSession // +////////////////////// +// +// Allows accessing MSI APIs from another process using named pipes. +// +class RemoteMsiSession +{ +public: + + // This enumeration MUST stay in sync with the + // managed equivalent in RemotableNativeMethods.cs! + enum RequestId + { + EndSession = 0, + MsiCloseHandle, + MsiCreateRecord, + MsiDatabaseGetPrimaryKeys, + MsiDatabaseIsTablePersistent, + MsiDatabaseOpenView, + MsiDoAction, + MsiEnumComponentCosts, + MsiEvaluateCondition, + MsiFormatRecord, + MsiGetActiveDatabase, + MsiGetComponentState, + MsiGetFeatureCost, + MsiGetFeatureState, + MsiGetFeatureValidStates, + MsiGetLanguage, + MsiGetLastErrorRecord, + MsiGetMode, + MsiGetProperty, + MsiGetSourcePath, + MsiGetSummaryInformation, + MsiGetTargetPath, + MsiProcessMessage, + MsiRecordClearData, + MsiRecordDataSize, + MsiRecordGetFieldCount, + MsiRecordGetInteger, + MsiRecordGetString, + MsiRecordIsNull, + MsiRecordReadStream, + MsiRecordSetInteger, + MsiRecordSetStream, + MsiRecordSetString, + MsiSequence, + MsiSetComponentState, + MsiSetFeatureAttributes, + MsiSetFeatureState, + MsiSetInstallLevel, + MsiSetMode, + MsiSetProperty, + MsiSetTargetPath, + MsiSummaryInfoGetProperty, + MsiVerifyDiskSpace, + MsiViewExecute, + MsiViewFetch, + MsiViewGetError, + MsiViewGetColumnInfo, + MsiViewModify, + }; + + static const int MAX_REQUEST_FIELDS = 4; + + // Used to pass data back and forth for remote API calls, + // including in & out params & return values. + // Only strings and ints are supported. + struct RequestData + { + struct + { + VARENUM vt; + union { + int iValue; + UINT uiValue; + DWORD cchValue; + LPWSTR szValue; + BYTE* sValue; + DWORD cbValue; + }; + } fields[MAX_REQUEST_FIELDS]; + }; + +public: + + // This value is set from the single data parameter in the EndSession request. + // It saves the exit code of the out-of-proc custom action. + int ExitCode; + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession constructor + // + // Creates a new remote session instance, for use either by the server + // or client process. + // + // szName - Identifies the session instance being remoted. The server and + // the client must use the same name. The name should be unique + // enough to avoid conflicting with other instances on the system. + // + // fServer - True if the calling process is the server process, false if the + // calling process is the client process. + // + RemoteMsiSession(const wchar_t* szName, bool fServer=true) + : m_fServer(fServer), + m_szName(szName != NULL && szName[0] != L'\0' ? szName : L"RemoteMsiSession"), + m_szPipeName(NULL), + m_hPipe(NULL), + m_fConnecting(false), + m_fConnected(false), + m_hReceiveThread(NULL), + m_hReceiveStopEvent(NULL), + m_pBufReceive(NULL), + m_cbBufReceive(0), + m_pBufSend(NULL), + m_cbBufSend(0), + ExitCode(ERROR_INSTALL_FAILURE) + { + SecureZeroMemory(&m_overlapped, sizeof(OVERLAPPED)); + m_overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession destructor + // + // Closes any open handles and frees any allocated memory. + // + ~RemoteMsiSession() + { + WaitExitCode(); + if (m_hPipe != NULL) + { + CloseHandle(m_hPipe); + m_hPipe = NULL; + } + if (m_overlapped.hEvent != NULL) + { + CloseHandle(m_overlapped.hEvent); + m_overlapped.hEvent = NULL; + } + if (m_szPipeName != NULL) + { + delete[] m_szPipeName; + m_szPipeName = NULL; + } + if (m_pBufReceive != NULL) + { + SecureZeroMemory(m_pBufReceive, m_cbBufReceive); + delete[] m_pBufReceive; + m_pBufReceive = NULL; + } + if (m_pBufSend != NULL) + { + SecureZeroMemory(m_pBufSend, m_cbBufSend); + delete[] m_pBufSend; + m_pBufSend = NULL; + } + m_fConnecting = false; + m_fConnected = false; + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession::WaitExitCode() + // + // Waits for the server processing thread to complete. + // + void WaitExitCode() + { + if (m_hReceiveThread != NULL) + { + SetEvent(m_hReceiveStopEvent); + WaitForSingleObject(m_hReceiveThread, INFINITE); + CloseHandle(m_hReceiveThread); + m_hReceiveThread = NULL; + } + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession::Connect() + // + // Connects the inter-process communication channel. + // (Currently implemented as a named pipe.) + // + // This method must be called first by the server process, then by the client + // process. The method does not block; the server will asynchronously wait + // for the client process to make the connection. + // + // Returns: 0 on success, Win32 error code on failure. + // + virtual DWORD Connect() + { + const wchar_t* szPipePrefix = L"\\\\.\\pipe\\"; + size_t cchPipeNameBuf = wcslen(szPipePrefix) + wcslen(m_szName) + 1; + m_szPipeName = new wchar_t[cchPipeNameBuf]; + + if (m_szPipeName == NULL) + { + return ERROR_OUTOFMEMORY; + } + else + { + wcscpy_s(m_szPipeName, cchPipeNameBuf, szPipePrefix); + wcscat_s(m_szPipeName, cchPipeNameBuf, m_szName); + + if (m_fServer) + { + return this->ConnectPipeServer(); + } + else + { + return this->ConnectPipeClient(); + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession::IsConnected() + // + // Checks if the server process and client process are currently connected. + // + virtual bool IsConnected() const + { + return m_fConnected; + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession::ProcessRequests() + // + // For use by the service process. Watches for requests in the input buffer and calls + // the callback for each one. + // + // This method does not block; it spawns a separate thread to do the work. + // + // Returns: 0 on success, Win32 error code on failure. + // + virtual DWORD ProcessRequests() + { + return this->StartProcessingReqests(); + } + + ///////////////////////////////////////////////////////////////////////////////////// + // RemoteMsiSession::SendRequest() + // + // For use by the client process. Sends a request to the server and + // synchronously waits on a response, up to the timeout value. + // + // id - ID code of the MSI API call being requested. + // + // pRequest - Pointer to a data structure containing request parameters. + // + // ppResponse - [OUT] Pointer to a location that receives the response parameters. + // + // Returns: 0 on success, Win32 error code on failure. + // Returns WAIT_TIMEOUT if no response was received in time. + // + virtual DWORD SendRequest(RequestId id, const RequestData* pRequest, RequestData** ppResponse) + { + if (m_fServer) + { + return ERROR_INVALID_OPERATION; + } + + if (!m_fConnected) + { + *ppResponse = NULL; + return 0; + } + + DWORD dwRet = this->SendRequest(id, pRequest); + if (dwRet != 0) + { + return dwRet; + } + + if (id != EndSession) + { + static RequestData response; + if (ppResponse != NULL) + { + *ppResponse = &response; + } + + return this->ReceiveResponse(id, &response); + } + else + { + CloseHandle(m_hPipe); + m_hPipe = NULL; + m_fConnected = false; + return 0; + } + } + +private: + + // + // Do not allow assignment. + // + RemoteMsiSession& operator=(const RemoteMsiSession&); + + // + // Called only by the server process. + // Create a new thread to handle receiving requests. + // + DWORD StartProcessingReqests() + { + if (!m_fServer || m_hReceiveStopEvent != NULL) + { + return ERROR_INVALID_OPERATION; + } + + DWORD dwRet = 0; + + m_hReceiveStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (m_hReceiveStopEvent == NULL) + { + dwRet = GetLastError(); + } + else + { + if (m_hReceiveThread != NULL) + { + CloseHandle(m_hReceiveThread); + } + + m_hReceiveThread = CreateThread(NULL, 0, + RemoteMsiSession::ProcessRequestsThreadStatic, this, 0, NULL); + + if (m_hReceiveThread == NULL) + { + dwRet = GetLastError(); + CloseHandle(m_hReceiveStopEvent); + m_hReceiveStopEvent = NULL; + } + } + + return dwRet; + } + + // + // Called only by the watcher process. + // First verify the connection is complete. Then continually read and parse messages, + // invoke the callback, and send the replies. + // + static DWORD WINAPI ProcessRequestsThreadStatic(void* pv) + { + return reinterpret_cast(pv)->ProcessRequestsThread(); + } + + DWORD ProcessRequestsThread() + { + DWORD dwRet; + + dwRet = CompleteConnection(); + if (dwRet != 0) + { + if (dwRet == ERROR_OPERATION_ABORTED) dwRet = 0; + } + + while (m_fConnected) + { + RequestId id; + RequestData req; + dwRet = ReceiveRequest(&id, &req); + if (dwRet != 0) + { + if (dwRet == ERROR_OPERATION_ABORTED || + dwRet == ERROR_BROKEN_PIPE || dwRet == ERROR_NO_DATA) + { + dwRet = 0; + } + } + else + { + RequestData resp; + ProcessRequest(id, &req, &resp); + + if (id == EndSession) + { + break; + } + + dwRet = SendResponse(id, &resp); + if (dwRet != 0 && dwRet != ERROR_BROKEN_PIPE && dwRet != ERROR_NO_DATA) + { + dwRet = 0; + } + } + } + + CloseHandle(m_hReceiveStopEvent); + m_hReceiveStopEvent = NULL; + return dwRet; + } + + // + // Called only by the server process's receive thread. + // Read one request into a RequestData object. + // + DWORD ReceiveRequest(RequestId* pId, RequestData* pReq) + { + DWORD dwRet = this->ReadPipe((BYTE*) pId, sizeof(RequestId)); + + if (dwRet == 0) + { + dwRet = this->ReadRequestData(pReq); + } + + return dwRet; + } + + // + // Called by the server process's receive thread or the client's request call + // to read the response. Read data from the pipe, allowing interruption by the + // stop event if on the server. + // + DWORD ReadPipe(__out_bcount(cbRead) BYTE* pBuf, DWORD cbRead) + { + DWORD dwRet = 0; + DWORD dwTotalBytesRead = 0; + + while (dwRet == 0 && dwTotalBytesRead < cbRead) + { + DWORD dwBytesReadThisTime; + ResetEvent(m_overlapped.hEvent); + if (!ReadFile(m_hPipe, pBuf + dwTotalBytesRead, cbRead - dwTotalBytesRead, &dwBytesReadThisTime, &m_overlapped)) + { + dwRet = GetLastError(); + if (dwRet == ERROR_IO_PENDING) + { + if (m_fServer) + { + HANDLE hWaitHandles[] = { m_overlapped.hEvent, m_hReceiveStopEvent }; + dwRet = WaitForMultipleObjects(2, hWaitHandles, FALSE, INFINITE); + } + else + { + dwRet = WaitForSingleObject(m_overlapped.hEvent, INFINITE); + } + + if (dwRet == WAIT_OBJECT_0) + { + if (!GetOverlappedResult(m_hPipe, &m_overlapped, &dwBytesReadThisTime, FALSE)) + { + dwRet = GetLastError(); + } + } + else if (dwRet == WAIT_FAILED) + { + dwRet = GetLastError(); + } + else + { + dwRet = ERROR_OPERATION_ABORTED; + } + } + } + + dwTotalBytesRead += dwBytesReadThisTime; + } + + if (dwRet != 0) + { + if (m_fServer) + { + CancelIo(m_hPipe); + DisconnectNamedPipe(m_hPipe); + } + else + { + CloseHandle(m_hPipe); + m_hPipe = NULL; + } + m_fConnected = false; + } + + return dwRet; + } + + // + // Called only by the server process. + // Given a request, invoke the MSI API and return the response. + // This is implemented in RemoteMsi.cpp. + // + void ProcessRequest(RequestId id, const RequestData* pReq, RequestData* pResp); + + // + // Called only by the client process. + // Send request data over the pipe. + // + DWORD SendRequest(RequestId id, const RequestData* pRequest) + { + DWORD dwRet = WriteRequestData(id, pRequest); + + if (dwRet != 0) + { + m_fConnected = false; + CloseHandle(m_hPipe); + m_hPipe = NULL; + } + + return dwRet; + } + + // + // Called only by the server process. + // Just send a response over the pipe. + // + DWORD SendResponse(RequestId id, const RequestData* pResp) + { + DWORD dwRet = WriteRequestData(id, pResp); + + if (dwRet != 0) + { + DisconnectNamedPipe(m_hPipe); + m_fConnected = false; + } + + return dwRet; + } + + // + // Called either by the client or server process. + // Writes data to the pipe for a request or response. + // + DWORD WriteRequestData(RequestId id, const RequestData* pReq) + { + DWORD dwRet = 0; + + RequestData req = *pReq; // Make a copy because the const data can't be changed. + + dwRet = this->WritePipe((const BYTE *)&id, sizeof(RequestId)); + if (dwRet != 0) + { + return dwRet; + } + + BYTE* sValues[MAX_REQUEST_FIELDS] = {0}; + for (int i = 0; i < MAX_REQUEST_FIELDS; i++) + { + if (req.fields[i].vt == VT_LPWSTR) + { + sValues[i] = (BYTE*) req.fields[i].szValue; + req.fields[i].cchValue = (DWORD) wcslen(req.fields[i].szValue); + } + else if (req.fields[i].vt == VT_STREAM) + { + sValues[i] = req.fields[i].sValue; + req.fields[i].cbValue = (DWORD) req.fields[i + 1].uiValue; + } + } + + dwRet = this->WritePipe((const BYTE *)&req, sizeof(RequestData)); + if (dwRet != 0) + { + return dwRet; + } + + for (int i = 0; i < MAX_REQUEST_FIELDS; i++) + { + if (sValues[i] != NULL) + { + DWORD cbValue; + if (req.fields[i].vt == VT_LPWSTR) + { + cbValue = (req.fields[i].cchValue + 1) * sizeof(WCHAR); + } + else + { + cbValue = req.fields[i].cbValue; + } + + dwRet = this->WritePipe(const_cast (sValues[i]), cbValue); + if (dwRet != 0) + { + break; + } + } + } + + return dwRet; + } + + // + // Called when writing a request or response. Writes data to + // the pipe, allowing interruption by the stop event if on the server. + // + DWORD WritePipe(const BYTE* pBuf, DWORD cbWrite) + { + DWORD dwRet = 0; + DWORD dwTotalBytesWritten = 0; + + while (dwRet == 0 && dwTotalBytesWritten < cbWrite) + { + DWORD dwBytesWrittenThisTime; + ResetEvent(m_overlapped.hEvent); + if (!WriteFile(m_hPipe, pBuf + dwTotalBytesWritten, cbWrite - dwTotalBytesWritten, &dwBytesWrittenThisTime, &m_overlapped)) + { + dwRet = GetLastError(); + if (dwRet == ERROR_IO_PENDING) + { + if (m_fServer) + { + HANDLE hWaitHandles[] = { m_overlapped.hEvent, m_hReceiveStopEvent }; + dwRet = WaitForMultipleObjects(2, hWaitHandles, FALSE, INFINITE); + } + else + { + dwRet = WaitForSingleObject(m_overlapped.hEvent, INFINITE); + } + + if (dwRet == WAIT_OBJECT_0) + { + if (!GetOverlappedResult(m_hPipe, &m_overlapped, &dwBytesWrittenThisTime, FALSE)) + { + dwRet = GetLastError(); + } + } + else if (dwRet == WAIT_FAILED) + { + dwRet = GetLastError(); + } + else + { + dwRet = ERROR_OPERATION_ABORTED; + } + } + } + + dwTotalBytesWritten += dwBytesWrittenThisTime; + } + + return dwRet; + } + + // + // Called either by the client or server process. + // Reads data from the pipe for a request or response. + // + DWORD ReadRequestData(RequestData* pReq) + { + DWORD dwRet = ReadPipe((BYTE*) pReq, sizeof(RequestData)); + + if (dwRet == 0) + { + DWORD cbData = 0; + for (int i = 0; i < MAX_REQUEST_FIELDS; i++) + { + if (pReq->fields[i].vt == VT_LPWSTR) + { + cbData += (pReq->fields[i].cchValue + 1) * sizeof(WCHAR); + } + else if (pReq->fields[i].vt == VT_STREAM) + { + cbData += pReq->fields[i].cbValue; + } + } + + if (cbData > 0) + { + if (!CheckRequestDataBuf(cbData)) + { + return ERROR_OUTOFMEMORY; + } + + dwRet = this->ReadPipe((BYTE*) m_pBufReceive, cbData); + if (dwRet == 0) + { + DWORD dwOffset = 0; + for (int i = 0; i < MAX_REQUEST_FIELDS; i++) + { + if (pReq->fields[i].vt == VT_LPWSTR) + { + LPWSTR szTemp = (LPWSTR) (m_pBufReceive + dwOffset); + dwOffset += (pReq->fields[i].cchValue + 1) * sizeof(WCHAR); + pReq->fields[i].szValue = szTemp; + } + else if (pReq->fields[i].vt == VT_STREAM) + { + BYTE* sTemp = m_pBufReceive + dwOffset; + dwOffset += pReq->fields[i].cbValue; + pReq->fields[i].sValue = sTemp; + } + } + } + } + } + + return dwRet; + } + + // + // Called only by the client process. + // Wait for a response on the pipe. If no response is received before the timeout, + // then give up and close the connection. + // + DWORD ReceiveResponse(RequestId id, RequestData* pResp) + { + RequestId responseId; + DWORD dwRet = ReadPipe((BYTE*) &responseId, sizeof(RequestId)); + if (dwRet == 0 && responseId != id) + { + dwRet = ERROR_OPERATION_ABORTED; + } + + if (dwRet == 0) + { + dwRet = this->ReadRequestData(pResp); + } + + return dwRet; + } + + // + // Called only by the server process's receive thread. + // Try to complete and verify an asynchronous connection operation. + // + DWORD CompleteConnection() + { + DWORD dwRet = 0; + if (m_fConnecting) + { + HANDLE hWaitHandles[] = { m_overlapped.hEvent, m_hReceiveStopEvent }; + DWORD dwWaitRes = WaitForMultipleObjects(2, hWaitHandles, FALSE, INFINITE); + + if (dwWaitRes == WAIT_OBJECT_0) + { + m_fConnecting = false; + + DWORD dwUnused; + if (GetOverlappedResult(m_hPipe, &m_overlapped, &dwUnused, FALSE)) + { + m_fConnected = true; + } + else + { + dwRet = GetLastError(); + } + } + else if (dwWaitRes == WAIT_FAILED) + { + CancelIo(m_hPipe); + dwRet = GetLastError(); + } + else + { + CancelIo(m_hPipe); + dwRet = ERROR_OPERATION_ABORTED; + } + } + return dwRet; + } + + // + // Called only by the server process. + // Creates a named pipe instance and begins asynchronously waiting + // for a connection from the client process. + // + DWORD ConnectPipeServer() + { + DWORD dwRet = 0; + const int BUFSIZE = 1024; // Suggested pipe I/O buffer sizes + m_hPipe = CreateNamedPipe( + m_szPipeName, + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, + 1, BUFSIZE, BUFSIZE, 0, NULL); + if (m_hPipe == INVALID_HANDLE_VALUE) + { + m_hPipe = NULL; + dwRet = GetLastError(); + } + else if (ConnectNamedPipe(m_hPipe, &m_overlapped)) + { + m_fConnected = true; + } + else + { + dwRet = GetLastError(); + + if (dwRet == ERROR_PIPE_BUSY) + { + // All pipe instances are busy, so wait for a maximum of 20 seconds + dwRet = 0; + if (WaitNamedPipe(m_szPipeName, 20000)) + { + m_fConnected = true; + } + else + { + dwRet = GetLastError(); + } + } + + if (dwRet == ERROR_IO_PENDING) + { + dwRet = 0; + m_fConnecting = true; + } + } + return dwRet; + } + + // + // Called only by the client process. + // Attemps to open a connection to an existing named pipe instance + // which should have already been created by the server process. + // + DWORD ConnectPipeClient() + { + DWORD dwRet = 0; + m_hPipe = CreateFile( + m_szPipeName, GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); + if (m_hPipe != INVALID_HANDLE_VALUE) + { + m_fConnected = true; + } + else + { + m_hPipe = NULL; + dwRet = GetLastError(); + } + return dwRet; + } + + // + // Ensures that the request buffer is large enough to hold a request, + // reallocating the buffer if necessary. + // It will also reduce the buffer size if the previous allocation was very large. + // + BOOL CheckRequestDataBuf(DWORD cbBuf) + { + if (m_cbBufReceive < cbBuf || (LARGE_BUFFER_THRESHOLD < m_cbBufReceive && cbBuf < m_cbBufReceive)) + { + if (m_pBufReceive != NULL) + { + SecureZeroMemory(m_pBufReceive, m_cbBufReceive); + delete[] m_pBufReceive; + } + m_cbBufReceive = max(MIN_BUFFER_STRING_SIZE*2, cbBuf); + m_pBufReceive = new BYTE[m_cbBufReceive]; + if (m_pBufReceive == NULL) + { + m_cbBufReceive = 0; + } + } + return m_pBufReceive != NULL; + } + +private: + + // Name of this instance. + const wchar_t* m_szName; + + // "\\.\pipe\name" + wchar_t* m_szPipeName; + + // Handle to the pipe instance. + HANDLE m_hPipe; + + // Handle to the thread that receives requests. + HANDLE m_hReceiveThread; + + // Handle to the event used to signal the receive thread to exit. + HANDLE m_hReceiveStopEvent; + + // All pipe I/O is done in overlapped mode to avoid unintentional blocking. + OVERLAPPED m_overlapped; + + // Dynamically-resized buffer for receiving requests. + BYTE* m_pBufReceive; + + // Current size of the receive request buffer. + DWORD m_cbBufReceive; + + // Dynamically-resized buffer for sending requests. + wchar_t* m_pBufSend; + + // Current size of the send request buffer. + DWORD m_cbBufSend; + + // True if this is the server process, false if this is the client process. + const bool m_fServer; + + // True if an asynchronous connection operation is currently in progress. + bool m_fConnecting; + + // True if the pipe is currently connected. + bool m_fConnected; +}; diff --git a/src/samples/Dtf/Tools/SfxCA/SfxCA.cpp b/src/samples/Dtf/Tools/SfxCA/SfxCA.cpp new file mode 100644 index 00000000..06319f1e --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxCA.cpp @@ -0,0 +1,363 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "EntryPoints.h" +#include "SfxUtil.h" + +#define MANAGED_CAs_OUT_OF_PROC 1 + +HMODULE g_hModule; +bool g_fRunningOutOfProc = false; + +RemoteMsiSession* g_pRemote = NULL; + +// Prototypes for local functions. +// See the function definitions for comments. + +bool InvokeManagedCustomAction(MSIHANDLE hSession, + _AppDomain* pAppDomain, const wchar_t* szEntryPoint, int* piResult); + +/// +/// Entry-point for the CA DLL when re-launched as a separate process; +/// connects the comm channel for remote MSI APIs, then invokes the +/// managed custom action entry-point. +/// +/// +/// Do not change the parameters or calling-convention: RUNDLL32 +/// requires this exact signature. +/// +extern "C" +void __stdcall InvokeManagedCustomActionOutOfProc( + __in HWND hwnd, __in HINSTANCE hinst, __in_z wchar_t* szCmdLine, int nCmdShow) +{ + UNREFERENCED_PARAMETER(hwnd); + UNREFERENCED_PARAMETER(hinst); + UNREFERENCED_PARAMETER(nCmdShow); + + g_fRunningOutOfProc = true; + + const wchar_t* szSessionName = szCmdLine; + MSIHANDLE hSession; + const wchar_t* szEntryPoint; + + int i; + for (i = 0; szCmdLine[i] && szCmdLine[i] != L' '; i++); + if (szCmdLine[i] != L'\0') szCmdLine[i++] = L'\0'; + hSession = _wtoi(szCmdLine + i); + + for (; szCmdLine[i] && szCmdLine[i] != L' '; i++); + if (szCmdLine[i] != L'\0') szCmdLine[i++] = L'\0'; + szEntryPoint = szCmdLine + i; + + g_pRemote = new RemoteMsiSession(szSessionName, false); + g_pRemote->Connect(); + + int ret = InvokeCustomAction(hSession, NULL, szEntryPoint); + + RemoteMsiSession::RequestData requestData; + SecureZeroMemory(&requestData, sizeof(RemoteMsiSession::RequestData)); + requestData.fields[0].vt = VT_I4; + requestData.fields[0].iValue = ret; + g_pRemote->SendRequest(RemoteMsiSession::EndSession, &requestData, NULL); + delete g_pRemote; +} + +/// +/// Re-launch this CA DLL as a separate process, and setup a comm channel +/// for remote MSI API calls back to this process. +/// +int InvokeOutOfProcManagedCustomAction(MSIHANDLE hSession, const wchar_t* szEntryPoint) +{ + wchar_t szSessionName[100] = {0}; + swprintf_s(szSessionName, 100, L"SfxCA_%d", ::GetTickCount()); + + RemoteMsiSession remote(szSessionName, true); + + DWORD ret = remote.Connect(); + if (ret != 0) + { + Log(hSession, L"Failed to create communication pipe for new CA process. Error code: %d", ret); + return ERROR_INSTALL_FAILURE; + } + + ret = remote.ProcessRequests(); + if (ret != 0) + { + Log(hSession, L"Failed to open communication pipe for new CA process. Error code: %d", ret); + return ERROR_INSTALL_FAILURE; + } + + wchar_t szModule[MAX_PATH] = {0}; + GetModuleFileName(g_hModule, szModule, MAX_PATH); + + const wchar_t* rundll32 = L"rundll32.exe"; + wchar_t szRunDll32Path[MAX_PATH] = {0}; + GetSystemDirectory(szRunDll32Path, MAX_PATH); + wcscat_s(szRunDll32Path, MAX_PATH, L"\\"); + wcscat_s(szRunDll32Path, MAX_PATH, rundll32); + + const wchar_t* entry = L"zzzzInvokeManagedCustomActionOutOfProc"; + wchar_t szCommandLine[1024] = {0}; + swprintf_s(szCommandLine, 1024, L"%s \"%s\",%s %s %d %s", + rundll32, szModule, entry, szSessionName, hSession, szEntryPoint); + + STARTUPINFO si; + SecureZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + + PROCESS_INFORMATION pi; + SecureZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + if (!CreateProcess(szRunDll32Path, szCommandLine, NULL, NULL, FALSE, + 0, NULL, NULL, &si, &pi)) + { + DWORD err = GetLastError(); + Log(hSession, L"Failed to create new CA process via RUNDLL32. Error code: %d", err); + return ERROR_INSTALL_FAILURE; + } + + DWORD dwWait = WaitForSingleObject(pi.hProcess, INFINITE); + if (dwWait != WAIT_OBJECT_0) + { + DWORD err = GetLastError(); + Log(hSession, L"Failed to wait for CA process. Error code: %d", err); + return ERROR_INSTALL_FAILURE; + } + + DWORD dwExitCode; + BOOL bRet = GetExitCodeProcess(pi.hProcess, &dwExitCode); + if (!bRet) + { + DWORD err = GetLastError(); + Log(hSession, L"Failed to get exit code of CA process. Error code: %d", err); + return ERROR_INSTALL_FAILURE; + } + else if (dwExitCode != 0) + { + Log(hSession, L"RUNDLL32 returned error code: %d", dwExitCode); + return ERROR_INSTALL_FAILURE; + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + remote.WaitExitCode(); + return remote.ExitCode; +} + +/// +/// Entrypoint for the managed CA proxy (RemotableNativeMethods) to +/// call MSI APIs remotely. +/// +void __stdcall MsiRemoteInvoke(RemoteMsiSession::RequestId id, RemoteMsiSession::RequestData* pRequest, RemoteMsiSession::RequestData** ppResponse) +{ + if (g_fRunningOutOfProc) + { + g_pRemote->SendRequest(id, pRequest, ppResponse); + } + else + { + *ppResponse = NULL; + } +} + +/// +/// Invokes a managed custom action from native code by +/// extracting the package to a temporary working directory +/// then hosting the CLR and locating and calling the entrypoint. +/// +/// Handle to the installation session. +/// Passed to custom action entrypoints by the installer engine. +/// Directory containing the CA binaries +/// and the CustomAction.config file defining the entrypoints. +/// This may be NULL, in which case the current module must have +/// a concatenated cabinet containing those files, which will be +/// extracted to a temporary directory. +/// Name of the CA entrypoint to be invoked. +/// This must be either an explicit "AssemblyName!Namespace.Class.Method" +/// string, or a simple name that maps to a full entrypoint definition +/// in CustomAction.config. +/// The value returned by the managed custom action method, +/// or ERROR_INSTALL_FAILURE if the CA could not be invoked. +int InvokeCustomAction(MSIHANDLE hSession, + const wchar_t* szWorkingDir, const wchar_t* szEntryPoint) +{ +#ifdef MANAGED_CAs_OUT_OF_PROC + if (!g_fRunningOutOfProc && szWorkingDir == NULL) + { + return InvokeOutOfProcManagedCustomAction(hSession, szEntryPoint); + } +#endif + + wchar_t szTempDir[MAX_PATH]; + bool fDeleteTemp = false; + if (szWorkingDir == NULL) + { + if (!ExtractToTempDirectory(hSession, g_hModule, szTempDir, MAX_PATH)) + { + return ERROR_INSTALL_FAILURE; + } + szWorkingDir = szTempDir; + fDeleteTemp = true; + } + + wchar_t szConfigFilePath[MAX_PATH + 20]; + StringCchCopy(szConfigFilePath, MAX_PATH + 20, szWorkingDir); + StringCchCat(szConfigFilePath, MAX_PATH + 20, L"\\CustomAction.config"); + + const wchar_t* szConfigFile = szConfigFilePath; + if (!::PathFileExists(szConfigFilePath)) + { + szConfigFile = NULL; + } + + wchar_t szWIAssembly[MAX_PATH + 50]; + StringCchCopy(szWIAssembly, MAX_PATH + 50, szWorkingDir); + StringCchCat(szWIAssembly, MAX_PATH + 50, L"\\WixToolset.Dtf.WindowsInstaller.dll"); + + int iResult = ERROR_INSTALL_FAILURE; + ICorRuntimeHost* pHost; + if (LoadCLR(hSession, NULL, szConfigFile, szWIAssembly, &pHost)) + { + _AppDomain* pAppDomain; + if (CreateAppDomain(hSession, pHost, L"CustomAction", szWorkingDir, + szConfigFile, &pAppDomain)) + { + if (!InvokeManagedCustomAction(hSession, pAppDomain, szEntryPoint, &iResult)) + { + iResult = ERROR_INSTALL_FAILURE; + } + HRESULT hr = pHost->UnloadDomain(pAppDomain); + if (FAILED(hr)) + { + Log(hSession, L"Failed to unload app domain. Error code 0x%X", hr); + } + pAppDomain->Release(); + } + + pHost->Stop(); + pHost->Release(); + } + + if (fDeleteTemp) + { + DeleteDirectory(szTempDir); + } + return iResult; +} + +/// +/// Called by the system when the DLL is loaded. +/// Saves the module handle for later use. +/// +BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, void* pReserved) +{ + UNREFERENCED_PARAMETER(pReserved); + + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + g_hModule = hModule; + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +/// +/// Loads and invokes the managed portion of the proxy. +/// +/// Handle to the installer session, +/// used for logging errors and to be passed on to the custom action. +/// AppDomain which has its application +/// base set to the CA working directory. +/// Name of the CA entrypoint to be invoked. +/// This must be either an explicit "AssemblyName!Namespace.Class.Method" +/// string, or a simple name that maps to a full entrypoint definition +/// in CustomAction.config. +/// Return value of the invoked custom +/// action method. +/// True if the managed proxy was invoked successfully, +/// false if there was some error. Note the custom action itself may +/// return an error via piResult while this method still returns true +/// since the invocation was successful. +bool InvokeManagedCustomAction(MSIHANDLE hSession, _AppDomain* pAppDomain, + const wchar_t* szEntryPoint, int* piResult) +{ + VARIANT vResult; + ::VariantInit(&vResult); + + const bool f64bit = (sizeof(void*) == sizeof(LONGLONG)); + const wchar_t* szMsiAssemblyName = L"WixToolset.Dtf.WindowsInstaller"; + const wchar_t* szMsiCAProxyClass = L"WixToolset.Dtf.WindowsInstaller.CustomActionProxy"; + const wchar_t* szMsiCAInvokeMethod = (f64bit ? L"InvokeCustomAction64" : L"InvokeCustomAction32"); + + _MethodInfo* pCAInvokeMethod; + if (!GetMethod(hSession, pAppDomain, szMsiAssemblyName, + szMsiCAProxyClass, szMsiCAInvokeMethod, &pCAInvokeMethod)) + { + return false; + } + + HRESULT hr; + VARIANT vNull; + vNull.vt = VT_EMPTY; + SAFEARRAY* saArgs = SafeArrayCreateVector(VT_VARIANT, 0, 3); + VARIANT vSessionHandle; + vSessionHandle.vt = VT_I4; + vSessionHandle.intVal = hSession; + LONG index = 0; + hr = SafeArrayPutElement(saArgs, &index, &vSessionHandle); + if (FAILED(hr)) goto LExit; + VARIANT vEntryPoint; + vEntryPoint.vt = VT_BSTR; + vEntryPoint.bstrVal = SysAllocString(szEntryPoint); + if (vEntryPoint.bstrVal == NULL) + { + hr = E_OUTOFMEMORY; + goto LExit; + } + index = 1; + hr = SafeArrayPutElement(saArgs, &index, &vEntryPoint); + if (FAILED(hr)) goto LExit; + VARIANT vRemotingFunctionPtr; +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant + if (f64bit) +#pragma warning(pop) + { + vRemotingFunctionPtr.vt = VT_I8; + vRemotingFunctionPtr.llVal = (LONGLONG) (g_fRunningOutOfProc ? MsiRemoteInvoke : NULL); + } + else + { + vRemotingFunctionPtr.vt = VT_I4; +#pragma warning(push) +#pragma warning(disable:4302) // truncation +#pragma warning(disable:4311) // pointer truncation + vRemotingFunctionPtr.lVal = (LONG) (g_fRunningOutOfProc ? MsiRemoteInvoke : NULL); +#pragma warning(pop) + } + index = 2; + hr = SafeArrayPutElement(saArgs, &index, &vRemotingFunctionPtr); + if (FAILED(hr)) goto LExit; + + hr = pCAInvokeMethod->Invoke_3(vNull, saArgs, &vResult); + +LExit: + SafeArrayDestroy(saArgs); + pCAInvokeMethod->Release(); + + if (FAILED(hr)) + { + Log(hSession, L"Failed to invoke custom action method. Error code 0x%X", hr); + return false; + } + + *piResult = vResult.intVal; + return true; +} + diff --git a/src/samples/Dtf/Tools/SfxCA/SfxCA.rc b/src/samples/Dtf/Tools/SfxCA/SfxCA.rc new file mode 100644 index 00000000..4d78194b --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxCA.rc @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#define VER_DLL +#define VER_LANG_NEUTRAL +#define VER_ORIGINAL_FILENAME "SfxCA.dll" +#define VER_INTERNAL_NAME "SfxCA" +#define VER_FILE_DESCRIPTION "DTF Self-Extracting Custom Action" + +// Additional resources here + diff --git a/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj b/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj new file mode 100644 index 00000000..aeaaa776 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj @@ -0,0 +1,68 @@ + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + {55D5BA28-D427-4F53-80C2-FE9EF23C1553} + DynamicLibrary + SfxCA + v142 + Unicode + EntryPoints.def + + + + + msi.lib;cabinet.lib;shlwapi.lib + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj.filters b/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj.filters new file mode 100644 index 00000000..a5ebf693 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxCA.vcxproj.filters @@ -0,0 +1,62 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + {81c92f68-18c2-4cd4-a588-5c3616860dd9} + + + {6cdc30ee-e14d-4679-b92e-3e080535e53b} + + + {1666a44e-4f2e-4f13-980e-d0c3dfa7cb6d} + + + + + Resource Files + + + + + Resource Files + + + + \ No newline at end of file diff --git a/src/samples/Dtf/Tools/SfxCA/SfxUtil.cpp b/src/samples/Dtf/Tools/SfxCA/SfxUtil.cpp new file mode 100644 index 00000000..1bf2c5b2 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxUtil.cpp @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "SfxUtil.h" + +/// +/// Writes a formatted message to the MSI log. +/// Does out-of-proc MSI calls if necessary. +/// +void Log(MSIHANDLE hSession, const wchar_t* szMessage, ...) +{ + const int LOG_BUFSIZE = 4096; + wchar_t szBuf[LOG_BUFSIZE]; + va_list args; + va_start(args, szMessage); + StringCchVPrintf(szBuf, LOG_BUFSIZE, szMessage, args); + + if (!g_fRunningOutOfProc || NULL == g_pRemote) + { + MSIHANDLE hRec = MsiCreateRecord(1); + MsiRecordSetString(hRec, 0, L"SFXCA: [1]"); + MsiRecordSetString(hRec, 1, szBuf); + MsiProcessMessage(hSession, INSTALLMESSAGE_INFO, hRec); + MsiCloseHandle(hRec); + } + else + { + // Logging is the only remote-MSI operation done from unmanaged code. + // It's not very convenient here because part of the infrastructure + // for remote MSI APIs is on the managed side. + + RemoteMsiSession::RequestData req; + RemoteMsiSession::RequestData* pResp = NULL; + SecureZeroMemory(&req, sizeof(RemoteMsiSession::RequestData)); + + req.fields[0].vt = VT_UI4; + req.fields[0].uiValue = 1; + g_pRemote->SendRequest(RemoteMsiSession::MsiCreateRecord, &req, &pResp); + MSIHANDLE hRec = (MSIHANDLE) pResp->fields[0].iValue; + + req.fields[0].vt = VT_I4; + req.fields[0].iValue = (int) hRec; + req.fields[1].vt = VT_UI4; + req.fields[1].uiValue = 0; + req.fields[2].vt = VT_LPWSTR; + req.fields[2].szValue = L"SFXCA: [1]"; + g_pRemote->SendRequest(RemoteMsiSession::MsiRecordSetString, &req, &pResp); + + req.fields[0].vt = VT_I4; + req.fields[0].iValue = (int) hRec; + req.fields[1].vt = VT_UI4; + req.fields[1].uiValue = 1; + req.fields[2].vt = VT_LPWSTR; + req.fields[2].szValue = szBuf; + g_pRemote->SendRequest(RemoteMsiSession::MsiRecordSetString, &req, &pResp); + + req.fields[0].vt = VT_I4; + req.fields[0].iValue = (int) hSession; + req.fields[1].vt = VT_I4; + req.fields[1].iValue = (int) INSTALLMESSAGE_INFO; + req.fields[2].vt = VT_I4; + req.fields[2].iValue = (int) hRec; + g_pRemote->SendRequest(RemoteMsiSession::MsiProcessMessage, &req, &pResp); + + req.fields[0].vt = VT_I4; + req.fields[0].iValue = (int) hRec; + req.fields[1].vt = VT_EMPTY; + req.fields[2].vt = VT_EMPTY; + g_pRemote->SendRequest(RemoteMsiSession::MsiCloseHandle, &req, &pResp); + } +} + +/// +/// Deletes a directory, including all files and subdirectories. +/// +/// Path to the directory to delete, +/// not including a trailing backslash. +/// True if the directory was successfully deleted, or false +/// if the deletion failed (most likely because some files were locked). +/// +bool DeleteDirectory(const wchar_t* szDir) +{ + size_t cchDir = wcslen(szDir); + size_t cchPathBuf = cchDir + 3 + MAX_PATH; + wchar_t* szPath = (wchar_t*) _alloca(cchPathBuf * sizeof(wchar_t)); + if (szPath == NULL) return false; + StringCchCopy(szPath, cchPathBuf, szDir); + StringCchCat(szPath, cchPathBuf, L"\\*"); + WIN32_FIND_DATA fd; + HANDLE hSearch = FindFirstFile(szPath, &fd); + while (hSearch != INVALID_HANDLE_VALUE) + { + StringCchCopy(szPath + cchDir + 1, cchPathBuf - (cchDir + 1), fd.cFileName); + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + if (wcscmp(fd.cFileName, L".") != 0 && wcscmp(fd.cFileName, L"..") != 0) + { + DeleteDirectory(szPath); + } + } + else + { + DeleteFile(szPath); + } + if (!FindNextFile(hSearch, &fd)) + { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + } + return RemoveDirectory(szDir) != 0; +} + +bool DirectoryExists(const wchar_t* szDir) +{ + if (szDir != NULL) + { + DWORD dwAttrs = GetFileAttributes(szDir); + if (dwAttrs != -1 && (dwAttrs & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + return true; + } + } + return false; +} + +/// +/// Extracts a cabinet that is concatenated to a module +/// to a new temporary directory. +/// +/// Handle to the installer session, +/// used just for logging. +/// Module that has the concatenated cabinet. +/// Buffer for returning the path of the +/// created temp directory. +/// Size in characters of the buffer. +/// True if the files were extracted, or false if the +/// buffer was too small or the directory could not be created +/// or the extraction failed for some other reason. +__success(return != false) +bool ExtractToTempDirectory(__in MSIHANDLE hSession, __in HMODULE hModule, + __out_ecount_z(cchTempDirBuf) wchar_t* szTempDir, DWORD cchTempDirBuf) +{ + wchar_t szModule[MAX_PATH]; + DWORD cchCopied = GetModuleFileName(hModule, szModule, MAX_PATH - 1); + if (cchCopied == 0) + { + Log(hSession, L"Failed to get module path. Error code %d.", GetLastError()); + return false; + } + else if (cchCopied == MAX_PATH - 1) + { + Log(hSession, L"Failed to get module path -- path is too long."); + return false; + } + + if (szTempDir == NULL || cchTempDirBuf < wcslen(szModule) + 1) + { + Log(hSession, L"Temp directory buffer is NULL or too small."); + return false; + } + StringCchCopy(szTempDir, cchTempDirBuf, szModule); + StringCchCat(szTempDir, cchTempDirBuf, L"-"); + + DWORD cchTempDir = (DWORD) wcslen(szTempDir); + for (int i = 0; DirectoryExists(szTempDir); i++) + { + swprintf_s(szTempDir + cchTempDir, cchTempDirBuf - cchTempDir, L"%d", i); + } + + if (!CreateDirectory(szTempDir, NULL)) + { + cchCopied = GetTempPath(cchTempDirBuf, szTempDir); + if (cchCopied == 0 || cchCopied >= cchTempDirBuf) + { + Log(hSession, L"Failed to get temp directory. Error code %d", GetLastError()); + return false; + } + + wchar_t* szModuleName = wcsrchr(szModule, L'\\'); + if (szModuleName == NULL) szModuleName = szModule; + else szModuleName = szModuleName + 1; + StringCchCat(szTempDir, cchTempDirBuf, szModuleName); + StringCchCat(szTempDir, cchTempDirBuf, L"-"); + + cchTempDir = (DWORD) wcslen(szTempDir); + for (int i = 0; DirectoryExists(szTempDir); i++) + { + swprintf_s(szTempDir + cchTempDir, cchTempDirBuf - cchTempDir, L"%d", i); + } + + if (!CreateDirectory(szTempDir, NULL)) + { + Log(hSession, L"Failed to create temp directory. Error code %d", GetLastError()); + return false; + } + } + + Log(hSession, L"Extracting custom action to temporary directory: %s\\", szTempDir); + int err = ExtractCabinet(szModule, szTempDir); + if (err != 0) + { + Log(hSession, L"Failed to extract to temporary directory. Cabinet error code %d.", err); + DeleteDirectory(szTempDir); + return false; + } + return true; +} + diff --git a/src/samples/Dtf/Tools/SfxCA/SfxUtil.h b/src/samples/Dtf/Tools/SfxCA/SfxUtil.h new file mode 100644 index 00000000..af12d8dd --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/SfxUtil.h @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "RemoteMsiSession.h" + +void Log(MSIHANDLE hSession, const wchar_t* szMessage, ...); + +int ExtractCabinet(const wchar_t* szCabFile, const wchar_t* szExtractDir); + +bool DeleteDirectory(const wchar_t* szDir); + +__success(return != false) +bool ExtractToTempDirectory(__in MSIHANDLE hSession, __in HMODULE hModule, + __out_ecount_z(cchTempDirBuf) wchar_t* szTempDir, DWORD cchTempDirBuf); + +bool LoadCLR(MSIHANDLE hSession, const wchar_t* szVersion, const wchar_t* szConfigFile, + const wchar_t* szPrimaryAssembly, ICorRuntimeHost** ppHost); + +bool CreateAppDomain(MSIHANDLE hSession, ICorRuntimeHost* pHost, + const wchar_t* szName, const wchar_t* szAppBase, + const wchar_t* szConfigFile, _AppDomain** ppAppDomain); + +bool GetMethod(MSIHANDLE hSession, _AppDomain* pAppDomain, + const wchar_t* szAssembly, const wchar_t* szClass, + const wchar_t* szMethod, _MethodInfo** ppCAMethod); + +extern HMODULE g_hModule; +extern bool g_fRunningOutOfProc; + +extern RemoteMsiSession* g_pRemote; + + diff --git a/src/samples/Dtf/Tools/SfxCA/packages.config b/src/samples/Dtf/Tools/SfxCA/packages.config new file mode 100644 index 00000000..1ffaa8df --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/samples/Dtf/Tools/SfxCA/precomp.cpp b/src/samples/Dtf/Tools/SfxCA/precomp.cpp new file mode 100644 index 00000000..ce82c1d7 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/precomp.cpp @@ -0,0 +1,3 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" \ No newline at end of file diff --git a/src/samples/Dtf/Tools/SfxCA/precomp.h b/src/samples/Dtf/Tools/SfxCA/precomp.h new file mode 100644 index 00000000..48d4f011 --- /dev/null +++ b/src/samples/Dtf/Tools/SfxCA/precomp.h @@ -0,0 +1,18 @@ +#pragma once +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#import raw_interfaces_only rename("ReportEvent", "CorReportEvent") +using namespace mscorlib; diff --git a/src/samples/Dtf/Tools/Tools.proj b/src/samples/Dtf/Tools/Tools.proj new file mode 100644 index 00000000..751247dc --- /dev/null +++ b/src/samples/Dtf/Tools/Tools.proj @@ -0,0 +1,15 @@ + + + + + + + + + + Platform=x64 + + + + + diff --git a/src/samples/Dtf/WiFile/WiFile.cs b/src/samples/Dtf/WiFile/WiFile.cs new file mode 100644 index 00000000..1e5c80df --- /dev/null +++ b/src/samples/Dtf/WiFile/WiFile.cs @@ -0,0 +1,147 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.RegularExpressions; +using WixToolset.Dtf.WindowsInstaller; +using WixToolset.Dtf.WindowsInstaller.Package; + +[assembly: AssemblyDescription("Windows Installer package file extraction and update tool")] + + +/// +/// Shows sample use of the InstallPackage class. +/// +public class WiFile +{ + public static void Usage(TextWriter w) + { + w.WriteLine("Usage: WiFile.exe package.msi /l [filename,filename2,...]"); + w.WriteLine("Usage: WiFile.exe package.msi /x [filename,filename2,...]"); + w.WriteLine("Usage: WiFile.exe package.msi /u [filename,filename2,...]"); + w.WriteLine(); + w.WriteLine("Lists (/l), extracts (/x) or updates (/u) files in an MSI or MSM."); + w.WriteLine("Files are extracted using their source path relative to the package."); + w.WriteLine("Specified filenames do not include paths."); + w.WriteLine("Filenames may be a pattern such as *.exe or file?.dll"); + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + public static int Main(string[] args) + { + if(!(args.Length == 2 || args.Length == 3)) + { + Usage(Console.Out); + return -1; + } + + string msiFile = args[0]; + + string option = args[1].ToLowerInvariant(); + if(option.StartsWith("-", StringComparison.Ordinal)) option = "/" + option.Substring(1); + + string[] fileNames = null; + if(args.Length == 3) + { + fileNames = args[2].Split(','); + } + + try + { + switch(option) + { + case "/l": + using(InstallPackage pkg = new InstallPackage(msiFile, DatabaseOpenMode.ReadOnly)) + { + pkg.Message += new InstallPackageMessageHandler(Console.WriteLine); + IEnumerable fileKeys = (fileNames != null ? FindFileKeys(pkg, fileNames) : pkg.Files.Keys); + + foreach(string fileKey in fileKeys) + { + Console.WriteLine(pkg.Files[fileKey]); + } + } + break; + + case "/x": + using(InstallPackage pkg = new InstallPackage(msiFile, DatabaseOpenMode.ReadOnly)) + { + pkg.Message += new InstallPackageMessageHandler(Console.WriteLine); + ICollection fileKeys = FindFileKeys(pkg, fileNames); + + pkg.ExtractFiles(fileKeys); + } + break; + + case "/u": + using(InstallPackage pkg = new InstallPackage(msiFile, DatabaseOpenMode.Transact)) + { + pkg.Message += new InstallPackageMessageHandler(Console.WriteLine); + ICollection fileKeys = FindFileKeys(pkg, fileNames); + + pkg.UpdateFiles(fileKeys); + pkg.Commit(); + } + break; + + default: + Usage(Console.Out); + return -1; + } + } + catch(InstallerException iex) + { + Console.WriteLine("Error: " + iex.Message); + return iex.ErrorCode != 0 ? iex.ErrorCode : 1; + } + catch(FileNotFoundException fnfex) + { + Console.WriteLine(fnfex.Message); + return 2; + } + catch(Exception ex) + { + Console.WriteLine("Error: " + ex.Message); + return 1; + } + return 0; + } + + static ICollection FindFileKeys(InstallPackage pkg, ICollection fileNames) + { + List fileKeys = null; + if(fileNames != null) + { + fileKeys = new List(); + foreach(string fileName in fileNames) + { + string[] foundFileKeys = null; + if(fileName.IndexOfAny(new char[] { '*', '?' }) >= 0) + { + foundFileKeys = pkg.FindFiles(FilePatternToRegex(fileName)); + } + else + { + foundFileKeys = pkg.FindFiles(fileName); + } + fileKeys.AddRange(foundFileKeys); + } + if(fileKeys.Count == 0) + { + throw new FileNotFoundException("Files not found in package."); + } + } + return fileKeys; + } + + static Regex FilePatternToRegex(string pattern) + { + return new Regex("^" + Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".") + "$", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } +} diff --git a/src/samples/Dtf/WiFile/WiFile.csproj b/src/samples/Dtf/WiFile/WiFile.csproj new file mode 100644 index 00000000..b5a95481 --- /dev/null +++ b/src/samples/Dtf/WiFile/WiFile.csproj @@ -0,0 +1,27 @@ + + + + + + + {AE562F7F-EE33-41D6-A962-DA488FEFBD08} + Exe + WixToolset.Dtf.Samples.WiFile + WiFile + v2.0 + + + + + + + + + + + + + + + + diff --git a/src/samples/Dtf/XPack/AssemblyInfo.cs b/src/samples/Dtf/XPack/AssemblyInfo.cs new file mode 100644 index 00000000..6dfb9437 --- /dev/null +++ b/src/samples/Dtf/XPack/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +using System.Reflection; + +[assembly: AssemblyDescription("Simple command-line CAB/ZIP packing and unpacking tool.")] diff --git a/src/samples/Dtf/XPack/XPack.cs b/src/samples/Dtf/XPack/XPack.cs new file mode 100644 index 00000000..36543a73 --- /dev/null +++ b/src/samples/Dtf/XPack/XPack.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Dtf.Samples.XPack +{ + using System; + using System.IO; + using System.Collections.Generic; + using System.Text; + using WixToolset.Dtf.Compression; + + public class XPack + { + public static void Usage(TextWriter writer) + { + writer.WriteLine("Usage: XPack /P "); + writer.WriteLine("Usage: XPack /P "); + writer.WriteLine(); + writer.WriteLine("Packs all files in a directory tree into an archive,"); + writer.WriteLine("using either the cab or zip format. Any existing archive"); + writer.WriteLine("with the same name will be overwritten."); + writer.WriteLine(); + writer.WriteLine("Usage: XPack /U "); + writer.WriteLine("Usage: XPack /U "); + writer.WriteLine(); + writer.WriteLine("Unpacks all files from a cab or zip archive to the"); + writer.WriteLine("specified directory. Any existing files with the same"); + writer.WriteLine("names will be overwritten."); + } + + public static void Main(string[] args) + { + try + { + if (args.Length == 3 && args[0].ToUpperInvariant() == "/P") + { + ArchiveInfo a = GetArchive(args[1]); + a.Pack(args[2], true, CompressionLevel.Max, ProgressHandler); + } + else if (args.Length == 3 && args[0].ToUpperInvariant() == "/U") + { + ArchiveInfo a = GetArchive(args[1]); + a.Unpack(args[2], ProgressHandler); + } + else + { + Usage(Console.Out); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + private static void ProgressHandler(object source, ArchiveProgressEventArgs e) + { + if (e.ProgressType == ArchiveProgressType.StartFile) + { + Console.WriteLine(e.CurrentFileName); + } + } + + private static ArchiveInfo GetArchive(string name) + { + string extension = Path.GetExtension(name).ToUpperInvariant(); + if (extension == ".CAB") + { + return new WixToolset.Dtf.Compression.Cab.CabInfo(name); + } + else if (extension == ".ZIP") + { + return new WixToolset.Dtf.Compression.Zip.ZipInfo(name); + } + else + { + throw new ArgumentException("Unknown archive file extension: " + extension); + } + } + } +} diff --git a/src/samples/Dtf/XPack/XPack.csproj b/src/samples/Dtf/XPack/XPack.csproj new file mode 100644 index 00000000..778c2d94 --- /dev/null +++ b/src/samples/Dtf/XPack/XPack.csproj @@ -0,0 +1,27 @@ + + + + + + + {03E55D95-DABE-4571-9CDA-92A44F92A465} + Exe + WixToolset.Dtf.Samples.XPack + XPack + v2.0 + + + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb